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>
257 lines
8.1 KiB
C
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));
|
|
}
|
|
}
|
|
|