YAInteraction/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c
smthspec 4bd4a74152 Support actions on standalone-spawned objects (by classname)
Objects spawned directly in the world (init.c / CE) are real BuildingBase
entities, so attach a Search to them by classname - independent of the proxy
framework (which needs a parent house; CCTCursorParent rejects parentless
objects).

- ActionYASearchObj: object-targeted (CCTNonRuined) Search; resolves the entry
  from target.GetObject().GetType() (substring), cooldown keyed by network id
- modded BuildingBase.SetActions: attaches it to classes matching a search entry
- Shared YAInteractionManager.PerformSearch() used by both proxy + object actions
- Re-add vending_machine (works for standalone-spawned vending machines)

Lets you make spawned loot boxes / vending machines lootable: add the classname
(or a substring) as a 'search' entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 17:23:58 +03:00

257 lines
8.1 KiB
C

// ============================================================================
// YAInteractionManager
// ----------------------------------------------------------------------------
// Builds runtime lookups from the loaded config (keyed by YAProxy proxy name)
// and owns the in-memory cooldowns + sink repaired-state.
//
// In-memory, reset on server restart (v1 spec):
// * Search cooldown - per-object, keyed by proxy world-position string.
// * Water cooldown - per-player, keyed by player UID.
// * Sink repaired flag - per-object, keyed by proxy world-position string.
// TODO (v2): persist via CF, and/or spawn a networked invisible marker so the
// client can hide the Repair action once a sink is fixed.
// ============================================================================
class YAInteractionManager
{
private static ref map<string, ref YAInteractableEntry> m_EntryByProxy = new map<string, ref YAInteractableEntry>();
private static ref map<string, ref YALootTable> m_TableByName = new map<string, ref YALootTable>();
private static ref map<string, int> m_SearchCooldowns = new map<string, int>(); // proxyWorldPos -> expiry ms
private static ref map<string, int> m_WaterCooldowns = new map<string, int>(); // playerUID -> expiry ms
private static ref map<string, bool> m_RepairedSinks = new map<string, bool>(); // proxyWorldPos -> repaired (resets on restart)
// Cached comma-joined proxy-name lists for the actions' GetProxyKeywords().
private static string m_SearchProxyNames = "";
private static string m_RepairWaterProxyNames = "";
private static bool m_Ready = false;
// ------------------------------------------------------------------
static void RebuildLookups()
{
m_EntryByProxy.Clear();
m_TableByName.Clear();
m_SearchProxyNames = "";
m_RepairWaterProxyNames = "";
if (!YAInteractionConfigLoader.Data)
{
Print("[YAInteraction] RebuildLookups: Data not loaded");
m_Ready = false;
return;
}
foreach (YAInteractableEntry e : YAInteractionConfigLoader.Data.Interactables)
{
if (!e || e.ProxyName == "")
continue;
string key = e.ProxyName;
key.ToLower();
m_EntryByProxy.Set(key, e);
if (e.ActionType == YAInteractionConfig.ACTION_SEARCH)
m_SearchProxyNames = AppendName(m_SearchProxyNames, e.ProxyName);
else if (e.ActionType == YAInteractionConfig.ACTION_REPAIR_WATER)
m_RepairWaterProxyNames = AppendName(m_RepairWaterProxyNames, e.ProxyName);
}
foreach (YALootTable t : YAInteractionConfigLoader.Data.LootTables)
{
if (!t || t.Name == "")
continue;
m_TableByName.Set(t.Name, t);
}
m_Ready = true;
Print(string.Format("[YAInteraction] Lookups built: %1 interactable(s), %2 loot table(s). search='%3' repair_water='%4'",
m_EntryByProxy.Count(), m_TableByName.Count(), m_SearchProxyNames, m_RepairWaterProxyNames));
}
private static string AppendName(string csv, string name)
{
if (csv == "")
return name;
return csv + "," + name;
}
private static void EnsureReady()
{
if (m_Ready)
return;
YAInteractionConfigLoader.EnsureDefaults();
RebuildLookups();
}
// ------------------------------------------------------------------
// Proxy-name targeting lists for the actions.
// ------------------------------------------------------------------
static string GetSearchProxyNames()
{
EnsureReady();
return m_SearchProxyNames;
}
static string GetRepairWaterProxyNames()
{
EnsureReady();
return m_RepairWaterProxyNames;
}
// Lookup by proxy name. The resolved name often carries an instance suffix
// (e.g. "fridge.001"), so we match the registry key as a substring.
static YAInteractableEntry GetEntryByProxyName(string proxyName)
{
EnsureReady();
if (proxyName == "")
return null;
string lower = proxyName;
lower.ToLower();
YAInteractableEntry exact;
if (m_EntryByProxy.Find(lower, exact))
return exact;
if (!YAInteractionConfigLoader.Data)
return null;
foreach (YAInteractableEntry entry : YAInteractionConfigLoader.Data.Interactables)
{
if (!entry || entry.ProxyName == "")
continue;
string key = entry.ProxyName;
key.ToLower();
if (lower.Contains(key))
return entry;
}
return null;
}
static array<ref YALootEntry> GetLootTableForEntry(YAInteractableEntry entry)
{
EnsureReady();
if (!entry || entry.LootTableName == "")
return null;
YALootTable t;
if (m_TableByName.Find(entry.LootTableName, t))
return t.Entries;
Print(string.Format("[YAInteraction] Loot table '%1' for proxy '%2' not found", entry.LootTableName, entry.ProxyName));
return null;
}
// ------------------------------------------------------------------
// Shared server-side search: cooldown gate, find-chance, item count,
// loot spawn + feedback. Used by both the proxy action (ActionYASearch,
// stateKey = proxy world pos) and the standalone object action
// (ActionYASearchObj, stateKey = entity network id).
// ------------------------------------------------------------------
static void PerformSearch(PlayerBase player, YAInteractableEntry entry, string stateKey)
{
if (!player || !entry)
return;
if (IsSearchOnCooldown(stateKey))
{
player.MessageStatus(string.Format("You already searched this. (%1s)", GetSearchCooldownRemaining(stateKey)));
return;
}
// Searching gates the object whether or not it yields anything.
StampSearchCooldown(stateKey, entry.CooldownSec);
if (Math.RandomFloat(0, 100) >= entry.SearchChance)
{
player.MessageStatus("You found nothing useful.");
return;
}
int count = Math.RandomIntInclusive(entry.SearchMin, entry.SearchMax);
int found = 0;
if (count > 0)
{
array<ref YALootEntry> table = GetLootTableForEntry(entry);
array<EntityAI> spawned = YALootTableManager.GetInstance().SpawnFromTable(table, count, player, player.GetPosition());
if (spawned)
found = spawned.Count();
}
if (found > 0)
player.MessageStatus(string.Format("You found %1 item(s).", found));
else
player.MessageStatus("You found nothing useful.");
}
// ------------------------------------------------------------------
// Search cooldown (per object, keyed by proxy world position).
// ------------------------------------------------------------------
static bool IsSearchOnCooldown(string posKey)
{
int expiry;
if (!m_SearchCooldowns.Find(posKey, expiry))
return false;
return GetGame().GetTime() < expiry;
}
static int GetSearchCooldownRemaining(string posKey)
{
int expiry;
if (!m_SearchCooldowns.Find(posKey, expiry))
return 0;
int rem = expiry - GetGame().GetTime();
if (rem < 0) rem = 0;
return rem / 1000;
}
static void StampSearchCooldown(string posKey, float cooldownSec)
{
m_SearchCooldowns.Set(posKey, GetGame().GetTime() + (int)(cooldownSec * 1000));
}
// ------------------------------------------------------------------
// Water cooldown (per player).
// ------------------------------------------------------------------
static bool IsWaterOnCooldown(string uid)
{
int expiry;
if (!m_WaterCooldowns.Find(uid, expiry))
return false;
return GetGame().GetTime() < expiry;
}
static int GetWaterCooldownRemaining(string uid)
{
int expiry;
if (!m_WaterCooldowns.Find(uid, expiry))
return 0;
int rem = expiry - GetGame().GetTime();
if (rem < 0) rem = 0;
return rem / 1000;
}
static void StampWaterCooldown(string uid, float cooldownSec)
{
m_WaterCooldowns.Set(uid, GetGame().GetTime() + (int)(cooldownSec * 1000));
}
// ------------------------------------------------------------------
// Sink repaired state (server-side, keyed by proxy world position).
// In-memory only -> resets on server restart (v1 spec).
// TODO (v2): spawn a networked invisible marker (so the client hides the
// Repair action once done) + CF/types.xml lifetime for cross-restart expiry.
// ------------------------------------------------------------------
static bool IsSinkRepaired(string posKey)
{
bool repaired;
if (m_RepairedSinks.Find(posKey, repaired))
return repaired;
return false;
}
static void SetSinkRepaired(string posKey, bool value = true)
{
m_RepairedSinks.Set(posKey, value);
Print(string.Format("[YAInteraction] Sink repaired state %1 = %2", posKey, value));
}
}