From e4cb0e400915a46d751bb55bb87d4367ce252ca9 Mon Sep 17 00:00:00 2001 From: smthspec Date: Fri, 19 Jun 2026 09:07:02 +0300 Subject: [PATCH] Rework interaction layer onto ZenProxyFramework proxy API Furniture (sink/fridge/lekarnicka/vending) are house proxies, not entities, so actions are now driven by ZenProxyFramework: actions target proxies by name (GetZenProxyNameTarget), the proxy world position is synced to the server via ActionData, repair spawns an invisible ZenProxySinkTest marker (presence = repaired), and gather-water checks for it. Registry is keyed by proxy name; loot tables / cooldowns / settings JSON kept. NOTE: superseded approach - ZenProxyFramework does not compile on DayZ 1.29 (HouseType type-name collision). Next: a self-contained @YAProxy framework (based on ZenProxyFramework + ShowMeInfoV2's geometry-LOD proxy scan) that YAInteraction will depend on instead. Co-Authored-By: Claude Opus 4.8 --- config.cpp | 3 +- .../Actions/ActionYAGatherWater.c | 154 +++++----------- .../Actions/ActionYARepairSink.c | 89 +++++----- .../YAInteraction/Actions/ActionYASearch.c | 104 ++++------- .../Config/YAInteractionConfig.c | 18 +- .../Config/YAInteractionConfigLoader.c | 19 +- .../YAInteraction/Config/YAInteractionData.c | 27 ++- .../Entities/YAInteractionManager.c | 168 ++++++++++-------- .../4_World/YAInteraction/YAInteractionInit.c | 78 +++++--- 9 files changed, 309 insertions(+), 351 deletions(-) diff --git a/config.cpp b/config.cpp index a6b712e..61a7587 100644 --- a/config.cpp +++ b/config.cpp @@ -8,7 +8,8 @@ class CfgPatches requiredAddons[] = { "DZ_Data", - "DZ_Scripts" + "DZ_Scripts", + "ZenProxyFramework" // dependency: ensures ZPF loads before us (proxy API + ZenProxySinkTest) }; }; }; diff --git a/scripts/4_World/YAInteraction/Actions/ActionYAGatherWater.c b/scripts/4_World/YAInteraction/Actions/ActionYAGatherWater.c index 3606aeb..bd7bbed 100644 --- a/scripts/4_World/YAInteraction/Actions/ActionYAGatherWater.c +++ b/scripts/4_World/YAInteraction/Actions/ActionYAGatherWater.c @@ -1,15 +1,12 @@ // ============================================================================ // ActionYAGatherWater // ---------------------------------------------------------------------------- -// Stage 2 of the sink. Only usable once the sink has been repaired (state in -// YAInteractionManager). Fills a liquid container in the player's hands (or -// the first fillable container in their inventory) with a random amount of -// clean water. +// Stage 2 of the sink. Attached to the liquid container in the player's hands +// (Bottle_Base). Only usable once the sink is repaired (a ZenProxySinkTest +// entity exists at the proxy world position). Fills the held container with a +// random amount of water from config, gated by a per-player cooldown. // -// Cooldown model (v1): per-player, keyed by player UID, in a static map. -// Resets on server restart. -// TODO (v2): swap the static map for CF persistence in IsOnCooldown()/ -// StampCooldown(). +// Registered on Bottle_Base (see YAInteractionInit) - item-based. // ============================================================================ class ActionYAGatherWaterCB : ActionContinuousBaseCB @@ -22,49 +19,54 @@ class ActionYAGatherWaterCB : ActionContinuousBaseCB class ActionYAGatherWater : ActionContinuousBase { - // playerUID -> server time (ms) when the cooldown expires. - protected static ref map m_PlayerCooldowns = new map(); - void ActionYAGatherWater() { m_CallbackClass = ActionYAGatherWaterCB; - m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_FILLBOTTLEWELL; // reuse "fill from fixture" anim + m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_FILLBOTTLEWELL; m_FullBody = true; m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH; - m_Text = "Gather Water"; } - override typename GetInputType() + override string GetText() { - return ContinuousInteractActionInput; + return "Gather Water"; } - override void CreateConditionComponents() - { - m_ConditionItem = new CCINone(); - m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT); - } - - override bool HasTarget() + override bool HasZenProxyTarget() { return true; } - // Sink interactable + repaired + player carries a fillable container. + override string GetZenProxyNameTarget() + { + return YAInteractionManager.GetRepairWaterProxyNames(); + } + + override void CreateConditionComponents() + { + m_ConditionItem = new CCINonRuined(); // the container in hands + m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL); + } + override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item) { - Object targetObject = target.GetObject(); - if (!targetObject) + if (!CanFill(item)) return false; - YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType()); - if (!entry || entry.ActionType != YAInteractionConfig.ACTION_REPAIR_WATER) + if (!IsTargetingZenActionProxy(player, target, item)) return false; - if (!YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString())) - return false; + // Client-only: only available when the sink is repaired (entity exists). + if (GetGame().IsClient()) + { + vector ls, ws; + string houseType; + GetZenActionProxyTargetEx(player, target, item, ls, ws, houseType); + if (!GetProxyObject(ws, YAInteractionConfig.SINK_PROXY_ENTITY)) + return false; + } - return FindFillableContainer(player) != null; + return true; } override bool IsLockTargetOnUse() @@ -75,113 +77,53 @@ class ActionYAGatherWater : ActionContinuousBase override void OnFinishProgressServer(ActionData action_data) { PlayerBase player = action_data.m_Player; - Object targetObject = action_data.m_Target.GetObject(); - if (!player || !player.GetIdentity() || !targetObject) + if (!player || !player.GetIdentity()) return; - YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType()); - if (!entry) - return; + vector proxyWS = GetZenProxyTargetWS(action_data); - // Defensive re-check (repair state could change mid-progress). - if (!YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString())) + // Defensive: sink must be repaired. + if (!GetProxyObject(proxyWS, YAInteractionConfig.SINK_PROXY_ENTITY)) { player.MessageStatus("The sink needs to be repaired first."); return; } string uid = player.GetIdentity().GetId(); - - // --- Per-player cooldown gate --- - if (IsOnCooldown(uid)) + if (YAInteractionManager.IsWaterOnCooldown(uid)) { - int remaining = GetCooldownRemaining(uid); - Print(string.Format("[YAInteraction] GatherWater on cooldown (%1s) for %2", remaining, uid)); + int remaining = YAInteractionManager.GetWaterCooldownRemaining(uid); player.MessageStatus(string.Format("You need to wait before gathering water again. (%1s)", remaining)); return; } - ItemBase container = FindFillableContainer(player); - if (!container) + ItemBase container = ItemBase.Cast(action_data.m_MainItem); + if (!CanFill(container)) { - player.MessageStatus("You have nothing to fill with water."); + player.MessageStatus("This container can't hold any more water."); return; } float amount = Math.RandomFloatInclusive(YAInteractionConfigLoader.Settings.WaterAmountMin, YAInteractionConfigLoader.Settings.WaterAmountMax); Liquid.FillContainer(container, YAInteractionConfig.WATER_LIQUID_TYPE, amount); - StampCooldown(uid, entry.CooldownSec); + // Cooldown comes from the sink's registry entry. + YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(GetZenProxyTargetType(action_data)); + float cd = YAInteractionConfigLoader.Settings.SinkPlayerCooldownSec; + if (entry && entry.CooldownSec > 0) + cd = entry.CooldownSec; + YAInteractionManager.StampWaterCooldown(uid, cd); Print(string.Format("[YAInteraction] %1 gathered %2 water into '%3'", uid, amount, container.GetType())); player.MessageStatus(string.Format("You filled %1 with %2 ml of water.", container.GetDisplayName(), Math.Round(amount))); } - // ------------------------------------------------------------------ - // Container discovery: hands first, then inventory. - // ------------------------------------------------------------------ - protected ItemBase FindFillableContainer(PlayerBase player) - { - if (!player) - return null; - - ItemBase inHands = ItemBase.Cast(player.GetItemInHands()); - if (CanFill(inHands)) - return inHands; - - array items = new array(); - player.GetInventory().EnumerateInventory(InventoryTraversalType.PREORDER, items); - foreach (EntityAI ent : items) - { - ItemBase candidate = ItemBase.Cast(ent); - if (CanFill(candidate)) - return candidate; - } - - return null; - } - protected bool CanFill(ItemBase item) { if (!item || !item.IsLiquidContainer()) return false; - if (item.GetQuantity() >= item.GetQuantityMax()) - return false; // already full - + return false; return Liquid.CanFillContainer(item, YAInteractionConfig.WATER_LIQUID_TYPE); } - - // ------------------------------------------------------------------ - // Cooldown helpers (persistence-aware surface area). - // ------------------------------------------------------------------ - - // TODO (v2): read last-gather time from CF storage instead of the static map. - protected static bool IsOnCooldown(string uid) - { - int expiry; - if (!m_PlayerCooldowns.Find(uid, expiry)) - return false; - return GetGame().GetTime() < expiry; - } - - protected static int GetCooldownRemaining(string uid) - { - int expiry; - if (!m_PlayerCooldowns.Find(uid, expiry)) - return 0; - - int remainingMs = expiry - GetGame().GetTime(); - if (remainingMs < 0) - remainingMs = 0; - return remainingMs / 1000; - } - - // TODO (v2): persist this stamp via CF so it survives server restarts. - protected static void StampCooldown(string uid, float cooldownSec) - { - int expiry = GetGame().GetTime() + (int)(cooldownSec * 1000); - m_PlayerCooldowns.Set(uid, expiry); - Print(string.Format("[YAInteraction] Water cooldown stamped for %1 (%2s)", uid, cooldownSec)); - } } diff --git a/scripts/4_World/YAInteraction/Actions/ActionYARepairSink.c b/scripts/4_World/YAInteraction/Actions/ActionYARepairSink.c index db38d77..5d1d845 100644 --- a/scripts/4_World/YAInteraction/Actions/ActionYARepairSink.c +++ b/scripts/4_World/YAInteraction/Actions/ActionYARepairSink.c @@ -1,13 +1,13 @@ // ============================================================================ // ActionYARepairSink // ---------------------------------------------------------------------------- -// Stage 1 of the sink. Requires a Wrench or PipeWrench in hands and an -// un-repaired sink. On completion it flags the object as repaired in the -// per-object repair map (YAInteractionManager), which then unlocks -// ActionYAGatherWater on the same object. +// Stage 1 of the sink. Requires a wrench/pipe wrench (the item this action is +// attached to). On completion it spawns a ZenProxySinkTest entity at the sink +// proxy's world position - the entity's PRESENCE represents "repaired" and is +// networked to all clients (ZenProxyFramework pattern). // -// Repair state is in-memory and resets on server restart. -// TODO (v2): persist repair state via CF (see YAInteractionManager). +// Registered on Wrench + PipeWrench (see YAInteractionInit) - item-based, so +// it is unaffected by PlayerBase action load-order issues. // ============================================================================ class ActionYARepairSinkCB : ActionContinuousBaseCB @@ -23,43 +23,48 @@ class ActionYARepairSink : ActionContinuousBase void ActionYARepairSink() { m_CallbackClass = ActionYARepairSinkCB; - m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_CRAFTING; // kneeling craft/repair anim + m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT; m_FullBody = true; m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH; - m_Text = "Repair Sink"; } - override typename GetInputType() + override string GetText() { - return ContinuousInteractActionInput; + return "Repair Sink"; } - override void CreateConditionComponents() - { - m_ConditionItem = new CCINone(); // tool requirement checked in ActionCondition - m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT); - } - - override bool HasTarget() + override bool HasZenProxyTarget() { return true; } - // Sink interactable + a wrench in hands + not already repaired. + override string GetZenProxyNameTarget() + { + return YAInteractionManager.GetRepairWaterProxyNames(); + } + + override void CreateConditionComponents() + { + m_ConditionItem = new CCINonRuined(); // the wrench in hands + m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL); + } + override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item) { - Object targetObject = target.GetObject(); - if (!targetObject) + if (!IsTargetingZenActionProxy(player, target, item)) return false; - YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType()); - if (!entry || entry.ActionType != YAInteractionConfig.ACTION_REPAIR_WATER) - return false; + // Client-only: hide if the sink is already repaired (entity exists). + if (GetGame().IsClient()) + { + vector ls, ws; + string houseType; + GetZenActionProxyTargetEx(player, target, item, ls, ws, houseType); + if (GetProxyObject(ws, YAInteractionConfig.SINK_PROXY_ENTITY)) + return false; + } - if (!IsRepairTool(item)) - return false; - - return !YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString()); + return true; } override bool IsLockTargetOnUse() @@ -70,30 +75,24 @@ class ActionYARepairSink : ActionContinuousBase override void OnFinishProgressServer(ActionData action_data) { PlayerBase player = action_data.m_Player; - Object targetObject = action_data.m_Target.GetObject(); - if (!player || !targetObject) - return; + vector proxyWS = GetZenProxyTargetWS(action_data); - string key = targetObject.GetNetworkIDString(); - - // Re-check on the server (state may have changed during the progress). - if (YAInteractionManager.IsRepaired(key)) + // Already repaired? (entity present) + if (GetProxyObject(proxyWS, YAInteractionConfig.SINK_PROXY_ENTITY)) { - player.MessageStatus("This sink is already repaired."); + if (player) + player.MessageStatus("This sink is already repaired."); return; } - YAInteractionManager.SetRepaired(key, true); + Object marker = GetGame().CreateObjectEx(YAInteractionConfig.SINK_PROXY_ENTITY, proxyWS, ECE_FULL); + if (!marker) + { + Print("[YAInteraction] FAILED to spawn sink marker @ " + proxyWS.ToString(false)); + return; + } - Print(string.Format("[YAInteraction] %1 repaired sink %2", player.GetIdentity().GetId(), key)); + Print(string.Format("[YAInteraction] %1 repaired sink @ %2", player.GetIdentity().GetId(), proxyWS.ToString(false))); player.MessageStatus("You repaired the sink. You can now gather water."); } - - // A wrench-type tool held in hands. IsKindOf covers any modded variants. - protected bool IsRepairTool(ItemBase item) - { - if (!item) - return false; - return item.IsKindOf("Wrench") || item.IsKindOf("PipeWrench"); - } } diff --git a/scripts/4_World/YAInteraction/Actions/ActionYASearch.c b/scripts/4_World/YAInteraction/Actions/ActionYASearch.c index aa4dd42..727879c 100644 --- a/scripts/4_World/YAInteraction/Actions/ActionYASearch.c +++ b/scripts/4_World/YAInteraction/Actions/ActionYASearch.c @@ -1,14 +1,14 @@ // ============================================================================ // ActionYASearch // ---------------------------------------------------------------------------- -// Generic "Search" action. The target's registry entry (resolved by classname -// via YAInteractionManager) decides the loot table and the per-object cooldown. -// No classnames live here - everything comes from config. +// Empty-handed "Search" on a searchable furniture PROXY (fridge / med cabinet +// / vending machine). Uses ZenProxyFramework: the action targets proxies by +// name (GetZenProxyNameTarget) and the proxy world position is synced to the +// server via ActionData, so OnFinishProgressServer can roll loot + gate the +// per-object cooldown by that position. // -// Cooldown model (v1): per-object, keyed by network id, in a static map. -// Resets on server restart. -// TODO (v2): swap the static map for CF persistence in IsOnCooldown()/ -// StampCooldown() - those two methods are the only persistence surface. +// Registered on PlayerBase (see YAInteractionInit). Cooldown is in-memory. +// TODO (v2): persist the per-proxy cooldown via CF. // ============================================================================ class ActionYASearchCB : ActionContinuousBaseCB @@ -21,46 +21,41 @@ class ActionYASearchCB : ActionContinuousBaseCB class ActionYASearch : ActionContinuousBase { - // network id string -> server time (ms) when the cooldown expires. - protected static ref map m_ObjectCooldowns = new map(); - void ActionYASearch() { m_CallbackClass = ActionYASearchCB; - m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT; // generic full-body interact anim + m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT; m_FullBody = true; m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH; - m_Text = "Search"; } - override typename GetInputType() + override string GetText() { - return ContinuousInteractActionInput; + return "Search"; + } + + override bool HasZenProxyTarget() + { + return true; + } + + override string GetZenProxyNameTarget() + { + return YAInteractionManager.GetSearchProxyNames(); } override void CreateConditionComponents() { m_ConditionItem = new CCINone(); - m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT); + m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL); } - override bool HasTarget() - { - return true; - } - - // Empty-handed; target must be a registered "search" interactable. override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item) { - Object targetObject = target.GetObject(); - if (!targetObject) - return false; - if (player.GetItemInHands()) return false; - YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType()); - return entry != null && entry.ActionType == YAInteractionConfig.ACTION_SEARCH; + return IsTargetingZenActionProxy(player, target, item); } override bool IsLockTargetOnUse() @@ -71,71 +66,36 @@ class ActionYASearch : ActionContinuousBase override void OnFinishProgressServer(ActionData action_data) { PlayerBase player = action_data.m_Player; - Object targetObject = action_data.m_Target.GetObject(); - if (!player || !targetObject) + if (!player) return; - YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType()); + string proxyType = GetZenProxyTargetType(action_data); + vector proxyWS = GetZenProxyTargetWS(action_data); + + YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(proxyType); if (!entry || entry.ActionType != YAInteractionConfig.ACTION_SEARCH) return; - string key = targetObject.GetNetworkIDString(); + string posKey = proxyWS.ToString(false); - // --- Per-object cooldown gate --- - if (IsOnCooldown(key)) + if (YAInteractionManager.IsSearchOnCooldown(posKey)) { - int remaining = GetCooldownRemaining(key); - Print(string.Format("[YAInteraction] Search on cooldown (%1s left) for %2", remaining, key)); + int remaining = YAInteractionManager.GetSearchCooldownRemaining(posKey); player.MessageStatus(string.Format("You already searched this. (%1s)", remaining)); return; } - // --- Roll & spawn --- array table = YAInteractionManager.GetLootTableForEntry(entry); int rolls = Math.RandomIntInclusive(YAInteractionConfig.SEARCH_ROLLS_MIN, YAInteractionConfig.SEARCH_ROLLS_MAX); array spawned = YALootTableManager.GetInstance().SpawnFromTable(table, rolls, player, player.GetPosition()); - // Stamp cooldown even on an empty result so the object is gated either way. - StampCooldown(key, entry.CooldownSec); + YAInteractionManager.StampSearchCooldown(posKey, entry.CooldownSec); - Print(string.Format("[YAInteraction] %1 searched '%2' -> %3 item(s)", player.GetIdentity().GetId(), targetObject.GetType(), spawned.Count())); + Print(string.Format("[YAInteraction] %1 searched proxy '%2' @ %3 -> %4 item(s)", player.GetIdentity().GetId(), proxyType, posKey, spawned.Count())); if (spawned.Count() > 0) player.MessageStatus(string.Format("You found %1 item(s).", spawned.Count())); else player.MessageStatus("You found nothing useful."); } - - // ------------------------------------------------------------------ - // Cooldown helpers (the only persistence-aware surface area). - // ------------------------------------------------------------------ - - // TODO (v2): read last-search time from CF storage instead of the static map. - protected static bool IsOnCooldown(string key) - { - int expiry; - if (!m_ObjectCooldowns.Find(key, expiry)) - return false; - return GetGame().GetTime() < expiry; - } - - protected static int GetCooldownRemaining(string key) - { - int expiry; - if (!m_ObjectCooldowns.Find(key, expiry)) - return 0; - - int remainingMs = expiry - GetGame().GetTime(); - if (remainingMs < 0) - remainingMs = 0; - return remainingMs / 1000; - } - - // TODO (v2): persist this stamp via CF so it survives server restarts. - protected static void StampCooldown(string key, float cooldownSec) - { - int expiry = GetGame().GetTime() + (int)(cooldownSec * 1000); - m_ObjectCooldowns.Set(key, expiry); - Print(string.Format("[YAInteraction] Search cooldown stamped for %1 (%2s)", key, cooldownSec)); - } } diff --git a/scripts/4_World/YAInteraction/Config/YAInteractionConfig.c b/scripts/4_World/YAInteraction/Config/YAInteractionConfig.c index cf4cbaf..0225777 100644 --- a/scripts/4_World/YAInteraction/Config/YAInteractionConfig.c +++ b/scripts/4_World/YAInteraction/Config/YAInteractionConfig.c @@ -1,12 +1,9 @@ // ============================================================================ // YAInteractionConfig - compile-time constants only // ---------------------------------------------------------------------------- -// The registry, loot tables and scalar tunables have moved to JSON -// (YAInteractionSettings + YAInteractionData, loaded by -// YAInteractionConfigLoader). What remains here are the engine-side constants -// that are NOT user-tunable: action-type / loot-table id strings (which must -// match the JSON values), the water liquid type, search roll range and the -// action progress-bar durations. +// Registry, loot tables and scalar tunables live in JSON (YAInteractionSettings +// + YAInteractionData via YAInteractionConfigLoader). These are the engine-side +// constants that aren't user-tunable. // ============================================================================ class YAInteractionConfig { @@ -19,10 +16,13 @@ class YAInteractionConfig static const string TABLE_FOOD = "FoodTable"; static const string TABLE_VENDING = "VendingTable"; + // ZenProxyFramework entity reused as the sink "repaired" marker. + static const string SINK_PROXY_ENTITY = "ZenProxySinkTest"; + // Non-JSON engine constants. - static const int WATER_LIQUID_TYPE = LIQUID_CLEANWATER; - static const int SEARCH_ROLLS_MIN = 1; // weighted picks per search (min) - static const int SEARCH_ROLLS_MAX = 2; // weighted picks per search (max) + static const int WATER_LIQUID_TYPE = LIQUID_WATER; + static const int SEARCH_ROLLS_MIN = 1; + static const int SEARCH_ROLLS_MAX = 2; // Progress-bar durations (seconds). static const float SEARCH_TIME = 3.0; diff --git a/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c b/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c index a2e5ef8..1baf5ba 100644 --- a/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c +++ b/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c @@ -102,12 +102,15 @@ class YAInteractionConfigLoader YAInteractionData d = new YAInteractionData(); - // --- Interactable registry --- - // (classname, actionType, lootTableName, cooldownSec) - d.Interactables.Insert(MakeEntry("StaticObj_Furniture_case_sink_a", YAInteractionConfig.ACTION_REPAIR_WATER, "", Settings.SinkPlayerCooldownSec)); - d.Interactables.Insert(MakeEntry("StaticObj_Furniture_lekarnicka", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_MED, Settings.ObjectCooldownSec)); - d.Interactables.Insert(MakeEntry("StaticObj_Furniture_fridge", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_FOOD, Settings.ObjectCooldownSec)); - d.Interactables.Insert(MakeEntry("StaticObj_Furniture_vending_machine", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_VENDING, Settings.ObjectCooldownSec)); + // --- Interactable registry (keyed by ZenProxyFramework PROXY NAME) --- + // (proxyName, actionType, lootTableName, cooldownSec) + // "case_sink_a" is confirmed (it's in ZenProxyConstants.SINKS). The search + // names are best guesses - confirm via ZPF's proxy dump / debug F-key and + // correct them in interactables.json (or here + rebuild). + d.Interactables.Insert(MakeEntry("case_sink_a", YAInteractionConfig.ACTION_REPAIR_WATER, "", Settings.SinkPlayerCooldownSec)); + d.Interactables.Insert(MakeEntry("lekarnicka", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_MED, Settings.ObjectCooldownSec)); + d.Interactables.Insert(MakeEntry("fridge", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_FOOD, Settings.ObjectCooldownSec)); + d.Interactables.Insert(MakeEntry("vending_machine", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_VENDING, Settings.ObjectCooldownSec)); // --- Loot tables (classnames verified in P:\DZ) --- YALootTable med = MakeTable(YAInteractionConfig.TABLE_MED); @@ -142,10 +145,10 @@ class YAInteractionConfigLoader // --- Factory helpers (data classes have no parameterised ctor so the // JSON serializer can default-construct them). --- - private static YAInteractableEntry MakeEntry(string className, string actionType, string lootTableName, float cooldownSec) + private static YAInteractableEntry MakeEntry(string proxyName, string actionType, string lootTableName, float cooldownSec) { YAInteractableEntry e = new YAInteractableEntry(); - e.ClassName = className; + e.ProxyName = proxyName; e.ActionType = actionType; e.LootTableName = lootTableName; e.CooldownSec = cooldownSec; diff --git a/scripts/4_World/YAInteraction/Config/YAInteractionData.c b/scripts/4_World/YAInteraction/Config/YAInteractionData.c index 71c433f..173a1a1 100644 --- a/scripts/4_World/YAInteraction/Config/YAInteractionData.c +++ b/scripts/4_World/YAInteraction/Config/YAInteractionData.c @@ -1,41 +1,40 @@ // ============================================================================ // YAInteractionData (+ YALootEntry, YALootTable, YAInteractableEntry) // ---------------------------------------------------------------------------- -// Serializable model for /YAInteraction/interactables.json: -// the interactable registry and all loot tables. +// Serializable model for /YAInteraction/interactables.json. // -// Field names MUST match the JSON keys exactly. Arrays are initialised inline -// with `new array<...>()` so the JSON serializer can populate them and so a -// freshly-constructed instance is never null. No explicit constructors: the -// serializer instantiates each class via its default constructor. +// Interactables are keyed by PROXY NAME - the in-house furniture proxy +// selection name reported by ZenProxyFramework (e.g. "case_sink_a", "fridge"). +// Discover names with ZPF's debug mode (DebugOn + press F) or its dumped +// proxy JSON, then set them here. +// +// Field names MUST match the JSON keys. Arrays are initialised inline so the +// serializer can populate them. No explicit constructors (serializer needs the +// default ctor). // ============================================================================ -// One row of a loot table. Weight is float to round-trip JSON values like 40.0. class YALootEntry { string ClassName; // item classname - float Weight; // relative weight within its table (higher = more likely) - int MinQty; // min stack quantity (<=0 => leave item default) - int MaxQty; // max stack quantity (<=0 => leave item default) + float Weight; // relative weight within its table + int MinQty; // min stack qty (<=0 => item default) + int MaxQty; // max stack qty (<=0 => item default) } -// A named loot table: a name plus its weighted entries. class YALootTable { string Name; ref array Entries = new array(); } -// One registered interactable object. class YAInteractableEntry { - string ClassName; // target object classname + string ProxyName; // in-house proxy selection name (ZenProxyFramework) string ActionType; // "search" or "repair_water" (YAInteractionConfig.ACTION_*) string LootTableName; // named loot table (empty for repair_water) float CooldownSec; // search: per-object cooldown / water: per-player cooldown } -// Root document for interactables.json. class YAInteractionData { ref array Interactables = new array(); diff --git a/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c b/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c index b369df8..6963dd1 100644 --- a/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c +++ b/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c @@ -1,34 +1,39 @@ // ============================================================================ // YAInteractionManager // ---------------------------------------------------------------------------- -// Builds runtime lookup maps from the loaded config (YAInteractionConfigLoader -// .Data) and owns per-object repair state. Nothing here contains a hardcoded -// target classname - everything is derived from the loaded registry. +// Builds runtime lookups from the loaded config (keyed by ZenProxyFramework +// proxy name) and owns the in-memory cooldowns. // -// RebuildLookups() is called by the loader after Data is (re)loaded. Lookups -// are also lazily ensured so client-side action injection works even though -// the loader's full Init() only runs on the server. +// Repair state is NOT tracked here - it's represented by the presence of a +// ZenProxySinkTest entity spawned at the sink (ZenProxyFramework pattern), so +// it is networked to clients for free. +// +// Cooldowns (reset on restart): +// * Search - per-object, keyed by proxy world-position string. +// * Water - per-player, keyed by player UID. +// TODO (v2): persist cooldowns via CF, keyed by proxy world position. // ============================================================================ class YAInteractionManager { - // lowercased classname -> registry entry - private static ref map m_EntryByClass = new map(); - // loot table name -> table + private static ref map m_EntryByProxy = new map(); private static ref map m_TableByName = new map(); - // per-object repair state, keyed by network id string. - // TODO (v2): back this with CF persistence so repairs survive a restart. - private static ref map m_RepairState = new map(); + + private static ref map m_SearchCooldowns = new map(); // proxyWorldPos -> expiry ms + private static ref map m_WaterCooldowns = new map(); // playerUID -> expiry ms + + // Cached comma-joined proxy-name lists for the actions' GetZenProxyNameTarget(). + private static string m_SearchProxyNames = ""; + private static string m_RepairWaterProxyNames = ""; private static bool m_Ready = false; - // ------------------------------------------------------------------ - // Fold the loaded Data into case-insensitive lookup maps. - // Called by YAInteractionConfigLoader after (re)loading Data. // ------------------------------------------------------------------ static void RebuildLookups() { - m_EntryByClass.Clear(); + m_EntryByProxy.Clear(); m_TableByName.Clear(); + m_SearchProxyNames = ""; + m_RepairWaterProxyNames = ""; if (!YAInteractionConfigLoader.Data) { @@ -39,11 +44,17 @@ class YAInteractionManager foreach (YAInteractableEntry e : YAInteractionConfigLoader.Data.Interactables) { - if (!e || e.ClassName == "") + if (!e || e.ProxyName == "") continue; - string key = e.ClassName; + + string key = e.ProxyName; key.ToLower(); - m_EntryByClass.Set(key, e); + 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) @@ -54,100 +65,115 @@ class YAInteractionManager } m_Ready = true; - Print(string.Format("[YAInteraction] Lookups built: %1 interactable(s), %2 loot table(s)", m_EntryByClass.Count(), m_TableByName.Count())); + 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; } - // Ensure config + lookups exist. On the server Init() has already run; on a - // client this lazily builds in-code defaults (no file I/O). private static void EnsureReady() { if (m_Ready) return; - YAInteractionConfigLoader.EnsureDefaults(); RebuildLookups(); } // ------------------------------------------------------------------ - // Registry lookups. + // Proxy-name targeting lists for the actions. // ------------------------------------------------------------------ - static YAInteractableEntry GetEntry(string className) + static string GetSearchProxyNames() { EnsureReady(); - - if (className == "") - return null; - - string key = className; - key.ToLower(); - - YAInteractableEntry e; - if (m_EntryByClass.Find(key, e)) - return e; - return null; + return m_SearchProxyNames; } - static bool IsInteractable(string className) + static string GetRepairWaterProxyNames() { - return GetEntry(className) != null; + EnsureReady(); + return m_RepairWaterProxyNames; + } + + // Case-insensitive lookup by proxy name. + static YAInteractableEntry GetEntryByProxyName(string proxyName) + { + EnsureReady(); + if (proxyName == "") + return null; + string key = proxyName; + key.ToLower(); + YAInteractableEntry e; + if (m_EntryByProxy.Find(key, e)) + return e; + return null; } static array 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' referenced by '%2' not found", entry.LootTableName, entry.ClassName)); + Print(string.Format("[YAInteraction] Loot table '%1' for proxy '%2' not found", entry.LootTableName, entry.ProxyName)); return null; } // ------------------------------------------------------------------ - // Action injection mapping. Returns the action typenames to register - // on a classname. Keeps YAInteractionInit free of classname strings. + // Search cooldown (per object, keyed by proxy world position). // ------------------------------------------------------------------ - static array GetActionTypesFor(string className) + static bool IsSearchOnCooldown(string posKey) { - array result = new array(); + int expiry; + if (!m_SearchCooldowns.Find(posKey, expiry)) + return false; + return GetGame().GetTime() < expiry; + } - YAInteractableEntry entry = GetEntry(className); - if (!entry) - return result; + 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; + } - if (entry.ActionType == YAInteractionConfig.ACTION_SEARCH) - { - result.Insert(ActionYASearch); - } - else if (entry.ActionType == YAInteractionConfig.ACTION_REPAIR_WATER) - { - // Both always registered; their ActionCondition gates which is usable. - result.Insert(ActionYARepairSink); - result.Insert(ActionYAGatherWater); - } - - return result; + static void StampSearchCooldown(string posKey, float cooldownSec) + { + m_SearchCooldowns.Set(posKey, GetGame().GetTime() + (int)(cooldownSec * 1000)); } // ------------------------------------------------------------------ - // Per-object repair state (sinks). - // TODO (v2): read/write through CF persistence instead of the static map. + // Water cooldown (per player). // ------------------------------------------------------------------ - static bool IsRepaired(string networkKey) + static bool IsWaterOnCooldown(string uid) { - bool repaired; - if (m_RepairState.Find(networkKey, repaired)) - return repaired; - return false; + int expiry; + if (!m_WaterCooldowns.Find(uid, expiry)) + return false; + return GetGame().GetTime() < expiry; } - static void SetRepaired(string networkKey, bool value = true) + static int GetWaterCooldownRemaining(string uid) { - m_RepairState.Set(networkKey, value); - Print(string.Format("[YAInteraction] Repair state for %1 set to %2", networkKey, value)); + 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)); } } diff --git a/scripts/4_World/YAInteraction/YAInteractionInit.c b/scripts/4_World/YAInteraction/YAInteractionInit.c index 8c58d0a..4fd48c1 100644 --- a/scripts/4_World/YAInteraction/YAInteractionInit.c +++ b/scripts/4_World/YAInteraction/YAInteractionInit.c @@ -1,37 +1,65 @@ // ============================================================================ // YAInteractionInit // ---------------------------------------------------------------------------- -// Injects the mod's actions onto the target objects, driven entirely by the -// config registry. There are no classname strings and no action-type branching -// here - YAInteractionManager.GetActionTypesFor() returns exactly which actions -// to register for a given classname. +// Wires the actions into the game, following the ZenProxyFramework pattern: // -// BASE CLASS - verified against P:\scripts and P:\DZ: -// The four targets are config classes that inherit: -// StaticObj_Furniture_* -> HouseNoDestruct -> House -> Static -// There is NO script class named StaticObj or HouseNoDestruct; "House" is the -// first script-backed ancestor (House : BuildingBase). So every one of these -// objects resolves to the BuildingBase script class - that is where we hook. +// * ActionConstructor - register custom actions so GetAction() resolves them +// (otherwise AddAction() dereferences null -> crash). +// * PlayerBase - Search (empty-handed; the only player-side action). +// * Wrench / PipeWrench - Repair Sink (item-in-hands; load-order safe). +// * Bottle_Base - Gather Water (item-in-hands; load-order safe). // -// We override SetActions() rather than GetActions(): BuildingBase implements -// a GetActions()/SetActions() split where GetActions() lazily calls -// SetActions() ONCE and caches the result per object type. SetActions() is -// therefore the correct extension point. (Overriding GetActions() directly -// would re-append to the cached array on every query and duplicate actions.) -// Calling super.SetActions() first preserves any inherited/base actions. +// Furniture identification + position sync is handled by ZenProxyFramework's +// modded ActionBase (GetZenProxyNameTarget / GetZenProxyTargetWS). // ============================================================================ -modded class BuildingBase +modded class ActionConstructor +{ + override void RegisterActions(TTypenameArray actions) + { + super.RegisterActions(actions); + + actions.Insert(ActionYASearch); + actions.Insert(ActionYARepairSink); + actions.Insert(ActionYAGatherWater); + } +} + +modded class PlayerBase +{ + override void SetActions(out TInputActionMap InputActionMap) + { + super.SetActions(InputActionMap); + + AddAction(ActionYASearch, InputActionMap); + + Print("[YAInteraction] Registered player action (Search)"); + } +} + +modded class Wrench { override void SetActions() { - super.SetActions(); // preserve inherited actions (doors, fireplaces, etc.) - - array actionsToAdd = YAInteractionManager.GetActionTypesFor(GetType()); - foreach (typename actionType : actionsToAdd) - { - AddAction(actionType); - Print("[YAInteraction] Registered " + actionType + " on '" + GetType() + "'"); - } + super.SetActions(); + AddAction(ActionYARepairSink); + } +} + +modded class PipeWrench +{ + override void SetActions() + { + super.SetActions(); + AddAction(ActionYARepairSink); + } +} + +modded class Bottle_Base +{ + override void SetActions() + { + super.SetActions(); + AddAction(ActionYAGatherWater); } }