Rebuild interaction layer on @YAProxy (1.29-working)

Replace the ZenProxyFramework dependency (broken on 1.29) with @YAProxy, our
self-contained proxy framework. Actions now extend YAProxyActionBase:

- Search / RepairSink / GatherWater resolve the targeted furniture proxy by
  keyword and use the synced world position from YAProxy
- Confirmed proxy names from live test: case_sink_a, fridge, lekarnicka
  (vending defaulted to 'sodamachine' pending confirmation)
- Registry lookup matches proxy names as substrings (handles '.001' suffix)
- Sink repaired-state + search/water cooldowns tracked server-side, keyed by
  proxy world position / player UID (in-memory, reset on restart per v1 spec)
- Repair on Wrench/PipeWrench, GatherWater on Bottle_Base (item-based, load-
  order safe); Search on PlayerBase

requiredAddons: YAProxy. Loot tables / settings JSON unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-19 09:58:00 +03:00
parent e4cb0e4009
commit 5a07c85416
9 changed files with 126 additions and 196 deletions

View file

@ -3,6 +3,11 @@
A DayZ server mod that adds interactable furniture (search for loot) and a
two-stage repairable sink (repair, then gather water).
> **Requires [@YAProxy](http://192.168.88.23/edvinas/YAProxy)** — the furniture
> are proxies baked into house models, not entities. @YAProxy handles proxy
> detection + position sync; YAInteraction's actions extend `YAProxyActionBase`.
> Load `@YAProxy` before `@YAInteraction` (handled via `requiredAddons`).
## Features
- **Searchable furniture** — medical cabinet, fridge, and vending machine yield

View file

@ -9,7 +9,7 @@ class CfgPatches
{
"DZ_Data",
"DZ_Scripts",
"ZenProxyFramework" // dependency: ensures ZPF loads before us (proxy API + ZenProxySinkTest)
"YAProxy" // dependency: proxy detection framework (loads before us)
};
};
};

View file

@ -2,29 +2,22 @@
// ActionYAGatherWater
// ----------------------------------------------------------------------------
// 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.
// (Bottle_Base). Only succeeds once the sink is repaired (server state keyed by
// proxy world pos). Fills the held container with a random amount of water from
// config, gated by a per-player cooldown.
//
// Registered on Bottle_Base (see YAInteractionInit) - item-based.
// ============================================================================
class ActionYAGatherWaterCB : ActionContinuousBaseCB
class ActionYAGatherWater : YAProxyActionBase
{
override void CreateActionComponent()
override float GetDuration()
{
m_ActionData.m_ActionComponent = new CAContinuousTime(YAInteractionConfig.GATHER_WATER_TIME);
return YAInteractionConfig.GATHER_WATER_TIME;
}
}
class ActionYAGatherWater : ActionContinuousBase
{
void ActionYAGatherWater()
override string GetProxyKeywords()
{
m_CallbackClass = ActionYAGatherWaterCB;
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_FILLBOTTLEWELL;
m_FullBody = true;
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
return YAInteractionManager.GetRepairWaterProxyNames();
}
override string GetText()
@ -32,46 +25,15 @@ class ActionYAGatherWater : ActionContinuousBase
return "Gather Water";
}
override bool HasZenProxyTarget()
{
return true;
}
override string GetZenProxyNameTarget()
{
return YAInteractionManager.GetRepairWaterProxyNames();
}
override void CreateConditionComponents()
{
m_ConditionItem = new CCINonRuined(); // the container in hands
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL);
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.DEFAULT);
}
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
override bool YA_ExtraCondition(PlayerBase player, ActionTarget target, ItemBase item, string proxyName, vector proxyWS)
{
if (!CanFill(item))
return false;
if (!IsTargetingZenActionProxy(player, target, item))
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 true;
}
override bool IsLockTargetOnUse()
{
return false;
return CanFill(item);
}
override void OnFinishProgressServer(ActionData action_data)
@ -80,10 +42,10 @@ class ActionYAGatherWater : ActionContinuousBase
if (!player || !player.GetIdentity())
return;
vector proxyWS = GetZenProxyTargetWS(action_data);
vector proxyWS = GetProxyWS(action_data);
string posKey = proxyWS.ToString(false);
// Defensive: sink must be repaired.
if (!GetProxyObject(proxyWS, YAInteractionConfig.SINK_PROXY_ENTITY))
if (!YAInteractionManager.IsSinkRepaired(posKey))
{
player.MessageStatus("The sink needs to be repaired first.");
return;
@ -92,8 +54,7 @@ class ActionYAGatherWater : ActionContinuousBase
string uid = player.GetIdentity().GetId();
if (YAInteractionManager.IsWaterOnCooldown(uid))
{
int remaining = YAInteractionManager.GetWaterCooldownRemaining(uid);
player.MessageStatus(string.Format("You need to wait before gathering water again. (%1s)", remaining));
player.MessageStatus(string.Format("You need to wait before gathering water again. (%1s)", YAInteractionManager.GetWaterCooldownRemaining(uid)));
return;
}
@ -107,9 +68,8 @@ class ActionYAGatherWater : ActionContinuousBase
float amount = Math.RandomFloatInclusive(YAInteractionConfigLoader.Settings.WaterAmountMin, YAInteractionConfigLoader.Settings.WaterAmountMax);
Liquid.FillContainer(container, YAInteractionConfig.WATER_LIQUID_TYPE, amount);
// Cooldown comes from the sink's registry entry.
YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(GetZenProxyTargetType(action_data));
float cd = YAInteractionConfigLoader.Settings.SinkPlayerCooldownSec;
YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(GetProxyName(action_data));
if (entry && entry.CooldownSec > 0)
cd = entry.CooldownSec;
YAInteractionManager.StampWaterCooldown(uid, cd);

View file

@ -1,31 +1,23 @@
// ============================================================================
// ActionYARepairSink
// ----------------------------------------------------------------------------
// 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).
// Stage 1 of the sink. Requires a wrench / pipe wrench (the item this action
// is attached to). On completion it flags the sink (keyed by proxy world pos)
// as repaired in YAInteractionManager, which unlocks ActionYAGatherWater.
//
// Registered on Wrench + PipeWrench (see YAInteractionInit) - item-based, so
// it is unaffected by PlayerBase action load-order issues.
// Registered on Wrench + PipeWrench (see YAInteractionInit) - item-based.
// Repaired state is in-memory and resets on server restart (v1).
// ============================================================================
class ActionYARepairSinkCB : ActionContinuousBaseCB
class ActionYARepairSink : YAProxyActionBase
{
override void CreateActionComponent()
override float GetDuration()
{
m_ActionData.m_ActionComponent = new CAContinuousTime(YAInteractionConfig.REPAIR_TIME);
return YAInteractionConfig.REPAIR_TIME;
}
}
class ActionYARepairSink : ActionContinuousBase
{
void ActionYARepairSink()
override string GetProxyKeywords()
{
m_CallbackClass = ActionYARepairSinkCB;
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT;
m_FullBody = true;
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
return YAInteractionManager.GetRepairWaterProxyNames();
}
override string GetText()
@ -33,66 +25,31 @@ class ActionYARepairSink : ActionContinuousBase
return "Repair Sink";
}
override bool HasZenProxyTarget()
{
return true;
}
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)
{
if (!IsTargetingZenActionProxy(player, target, item))
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;
}
return true;
}
override bool IsLockTargetOnUse()
{
return false;
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.DEFAULT);
}
override void OnFinishProgressServer(ActionData action_data)
{
PlayerBase player = action_data.m_Player;
vector proxyWS = GetZenProxyTargetWS(action_data);
vector proxyWS = GetProxyWS(action_data);
string posKey = proxyWS.ToString(false);
// Already repaired? (entity present)
if (GetProxyObject(proxyWS, YAInteractionConfig.SINK_PROXY_ENTITY))
if (YAInteractionManager.IsSinkRepaired(posKey))
{
if (player)
player.MessageStatus("This sink is already repaired.");
return;
}
Object marker = GetGame().CreateObjectEx(YAInteractionConfig.SINK_PROXY_ENTITY, proxyWS, ECE_FULL);
if (!marker)
{
Print("[YAInteraction] FAILED to spawn sink marker @ " + proxyWS.ToString(false));
return;
}
YAInteractionManager.SetSinkRepaired(posKey, true);
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.");
if (player)
{
Print(string.Format("[YAInteraction] %1 repaired sink @ %2", player.GetIdentity().GetId(), posKey));
player.MessageStatus("You repaired the sink. You can now gather water.");
}
}
}

View file

@ -1,32 +1,23 @@
// ============================================================================
// ActionYASearch
// ----------------------------------------------------------------------------
// 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.
// Empty-handed "Search" on a searchable furniture proxy (fridge / med cabinet
// / vending machine). Built on YAProxy: the framework resolves which proxy the
// cursor is on and syncs its name + world position to the server. We roll our
// loot table for that proxy and gate a per-object cooldown by the world pos.
//
// Registered on PlayerBase (see YAInteractionInit). Cooldown is in-memory.
// TODO (v2): persist the per-proxy cooldown via CF.
// Registered on PlayerBase (see YAInteractionInit).
// ============================================================================
class ActionYASearchCB : ActionContinuousBaseCB
class ActionYASearch : YAProxyActionBase
{
override void CreateActionComponent()
override float GetDuration()
{
m_ActionData.m_ActionComponent = new CAContinuousTime(YAInteractionConfig.SEARCH_TIME);
return YAInteractionConfig.SEARCH_TIME;
}
}
class ActionYASearch : ActionContinuousBase
{
void ActionYASearch()
override string GetProxyKeywords()
{
m_CallbackClass = ActionYASearchCB;
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT;
m_FullBody = true;
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
return YAInteractionManager.GetSearchProxyNames();
}
override string GetText()
@ -34,33 +25,15 @@ class ActionYASearch : ActionContinuousBase
return "Search";
}
override bool HasZenProxyTarget()
{
return true;
}
override string GetZenProxyNameTarget()
{
return YAInteractionManager.GetSearchProxyNames();
}
override void CreateConditionComponents()
{
m_ConditionItem = new CCINone();
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL);
m_ConditionItem = new CCINotPresent(); // empty-handed
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.DEFAULT);
}
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
override bool YA_ExtraCondition(PlayerBase player, ActionTarget target, ItemBase item, string proxyName, vector proxyWS)
{
if (player.GetItemInHands())
return false;
return IsTargetingZenActionProxy(player, target, item);
}
override bool IsLockTargetOnUse()
{
return false;
return !player.GetItemInHands();
}
override void OnFinishProgressServer(ActionData action_data)
@ -69,10 +42,10 @@ class ActionYASearch : ActionContinuousBase
if (!player)
return;
string proxyType = GetZenProxyTargetType(action_data);
vector proxyWS = GetZenProxyTargetWS(action_data);
string proxyName = GetProxyName(action_data);
vector proxyWS = GetProxyWS(action_data);
YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(proxyType);
YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(proxyName);
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_SEARCH)
return;
@ -80,8 +53,7 @@ class ActionYASearch : ActionContinuousBase
if (YAInteractionManager.IsSearchOnCooldown(posKey))
{
int remaining = YAInteractionManager.GetSearchCooldownRemaining(posKey);
player.MessageStatus(string.Format("You already searched this. (%1s)", remaining));
player.MessageStatus(string.Format("You already searched this. (%1s)", YAInteractionManager.GetSearchCooldownRemaining(posKey)));
return;
}
@ -91,7 +63,7 @@ class ActionYASearch : ActionContinuousBase
YAInteractionManager.StampSearchCooldown(posKey, entry.CooldownSec);
Print(string.Format("[YAInteraction] %1 searched proxy '%2' @ %3 -> %4 item(s)", player.GetIdentity().GetId(), proxyType, posKey, spawned.Count()));
Print(string.Format("[YAInteraction] %1 searched '%2' @ %3 -> %4 item(s)", player.GetIdentity().GetId(), proxyName, posKey, spawned.Count()));
if (spawned.Count() > 0)
player.MessageStatus(string.Format("You found %1 item(s).", spawned.Count()));

View file

@ -16,9 +16,6 @@ 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_WATER;
static const int SEARCH_ROLLS_MIN = 1;

View file

@ -107,10 +107,13 @@ class YAInteractionConfigLoader
// "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));
// Proxy names confirmed via YAProxy debug: case_sink_a / fridge / lekarnicka.
// Vending not yet confirmed - "sodamachine" is ZenProxyFramework's name;
// adjust here (or in interactables.json) if your map differs.
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("sodamachine", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_VENDING, Settings.ObjectCooldownSec));
// --- Loot tables (classnames verified in P:\DZ) ---
YALootTable med = MakeTable(YAInteractionConfig.TABLE_MED);

View file

@ -1,27 +1,26 @@
// ============================================================================
// YAInteractionManager
// ----------------------------------------------------------------------------
// Builds runtime lookups from the loaded config (keyed by ZenProxyFramework
// proxy name) and owns the in-memory cooldowns.
// Builds runtime lookups from the loaded config (keyed by YAProxy proxy name)
// and owns the in-memory cooldowns + sink repaired-state.
//
// 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.
// 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, 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' GetZenProxyNameTarget().
// Cached comma-joined proxy-name lists for the actions' GetProxyKeywords().
private static string m_SearchProxyNames = "";
private static string m_RepairWaterProxyNames = "";
@ -99,17 +98,33 @@ class YAInteractionManager
return m_RepairWaterProxyNames;
}
// Case-insensitive lookup by proxy name.
// 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 key = proxyName;
key.ToLower();
YAInteractableEntry e;
if (m_EntryByProxy.Find(key, e))
return e;
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;
}
@ -176,4 +191,25 @@ class YAInteractionManager
{
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));
}
}

View file

@ -1,7 +1,7 @@
// ============================================================================
// YAInteractionInit
// ----------------------------------------------------------------------------
// Wires the actions into the game, following the ZenProxyFramework pattern:
// Wires the actions into the game (actions extend YAProxyActionBase):
//
// * ActionConstructor - register custom actions so GetAction() resolves them
// (otherwise AddAction() dereferences null -> crash).
@ -9,8 +9,8 @@
// * Wrench / PipeWrench - Repair Sink (item-in-hands; load-order safe).
// * Bottle_Base - Gather Water (item-in-hands; load-order safe).
//
// Furniture identification + position sync is handled by ZenProxyFramework's
// modded ActionBase (GetZenProxyNameTarget / GetZenProxyTargetWS).
// Furniture identification + position sync is handled by @YAProxy
// (YAProxyActionBase: GetProxyKeywords / GetProxyName / GetProxyWS).
// ============================================================================
modded class ActionConstructor