Initial commit: YAProxy house-proxy interaction framework (1.29)

Self-contained framework for attaching user actions to furniture proxies inside
house models. Detection via geometry-LOD selection scan (works on DayZ 1.29);
resolved proxy name + world position synced to server via ActionData. No
entities used for detection.

- modded House: geometry-LOD proxy position/name maps (client-side)
- YAProxyActionBase: cursor->proxy matching + sync + subclass hooks
- YAProxyActionData: client->server proxy target sync
- YAProxyDebug: validation action + per-house scan logging (DEBUG flag)

Credit: Zenarchist (ZenProxyFramework) and ShowMeInfoV2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-19 09:45:23 +03:00
commit 89f42a04b0
9 changed files with 625 additions and 0 deletions

1
$PBOPREFIX$.txt Normal file
View file

@ -0,0 +1 @@
YAProxy

66
README.md Normal file
View file

@ -0,0 +1,66 @@
# YAProxy
A lightweight DayZ framework (1.29+) for attaching user actions to **furniture
proxies** baked into house models (sinks, fridges, cabinets, vending machines,
…), which are not separate entities.
Credit: technique combines **Zenarchist's ZenProxyFramework** (action API /
client→server position sync, invisible-entity persistence pattern) and the
**ShowMeInfoV2** mod (geometry-LOD proxy enumeration that works on 1.29).
## How it works
- `modded House` reads the building's **geometry LOD selections**
(`GetLODByName(LOD.NAME_GEOMETRY).GetSelections()`), finds those containing a
proxy path, and builds `position → proxy name` maps (client-side).
- `YAProxyActionBase` resolves which proxy the cursor is on, and syncs the
resolved **name + world position** to the server via `ActionData`.
- Detection uses **no entities**. (Spawning an invisible marker entity is only
one option a consumer can use for *persistent state*, e.g. "sink is repaired
for N days" — handled by the consuming mod, not here.)
## Writing an action (consumer mods)
```c
class ActionMySearch : YAProxyActionBase
{
override string GetProxyKeywords() { return "fridge,lekarnicka"; } // substring match; "*" = any
override float GetDuration() { return 3.0; }
override string GetText() { return "Search"; }
// optional extra availability check (client)
override bool YA_ExtraCondition(PlayerBase p, ActionTarget t, ItemBase i, string name, vector ws)
{ return !p.GetItemInHands(); }
override void OnFinishProgressServer(ActionData ad)
{
string proxy = GetProxyName(ad); // synced proxy name
vector ws = GetProxyWS(ad); // synced world position
// ... your effect ...
}
}
```
Register it (required, or `AddAction` dereferences null):
```c
modded class ActionConstructor { override void RegisterActions(TTypenameArray a){ super.RegisterActions(a); a.Insert(ActionMySearch); } }
```
Attach it where it should appear:
- empty-handed → `modded PlayerBase.SetActions(out TInputActionMap m){ super...; AddAction(ActionMySearch, m); }`
- tool/container in hands → `modded Wrench / Bottle_Base .SetActions(){ super...; AddAction(ActionMySearch); }`
For an item-in-hands action, override `CreateConditionComponents()` to use
`CCINonRuined` instead of the default `CCINotPresent`.
## Debug / discovery
`YAProxyConstants.DEBUG` (default **true** during bring-up) enables:
- per-house scan logs: `[YAProxy] House 'Land_X' proxies built: map1=N map2=M`
- an empty-handed **"Proxy: \<name\>"** action on any proxy that, on execute,
dumps the resolved name, local vs world position, the full house proxy map,
and the server-synced values.
Set `DEBUG = false` for production (and remove `YAProxyDebug.c` if desired).
## Notes / limitations
- A building only scans if its script class is `House`. Many vanilla buildings
aren't `House` by default; declare them `class Land_X : House {}` (see
ZenProxyFramework's `ScriptedBuildings.c` for a curated list) as needed.
- Some proxies are missed by the LOD scan and need manual position overrides
(also per-building) — a future addition if required.

60
config.cpp Normal file
View file

@ -0,0 +1,60 @@
// ============================================================================
// @YAProxy - a lightweight DayZ house-proxy interaction framework (1.29+)
// ----------------------------------------------------------------------------
// Lets dependent mods attach user actions to furniture PROXIES baked into
// house p3d models (sinks, fridges, cabinets, ...), which are not separate
// entities. Detection uses the building's geometry-LOD selections; the
// targeted proxy's name + world position are synced to the server via the
// action's ActionData.
//
// Credit: technique adapted from Zenarchist's ZenProxyFramework (action API /
// position sync) and the ShowMeInfoV2 mod (geometry-LOD proxy enumeration that
// works on DayZ 1.29).
// ============================================================================
class CfgPatches
{
class YAProxy
{
units[] = {};
weapons[] = {};
requiredVersion = 0.1;
requiredAddons[] =
{
"DZ_Data",
"DZ_Scripts"
};
};
};
class CfgMods
{
class YAProxy
{
dir = "YAProxy";
picture = "";
action = "";
hideName = 1;
hidePicture = 1;
name = "YAProxy";
credits = "drasikas; technique from Zenarchist (ZenProxyFramework) & ShowMeInfoV2";
author = "drasikas";
authorID = "0";
version = "1.0";
extra = 0;
type = "mod";
dependencies[] = { "Game", "World" };
class defs
{
class gameScriptModule
{
value = "";
files[] = { "YAProxy/scripts/3_Game" };
};
class worldScriptModule
{
value = "";
files[] = { "YAProxy/scripts/4_World" };
};
};
};
};

13
mod.cpp Normal file
View file

@ -0,0 +1,13 @@
class CfgMods
{
class YAProxy
{
name = "YAProxy";
author = "drasikas";
authorID = "0";
version = "1.0";
tooltip = "House-proxy interaction framework (1.29+).";
overview = "Framework for attaching user actions to furniture proxies inside house models. Detects proxies via geometry-LOD selections and syncs the targeted proxy to the server. Credit: Zenarchist (ZenProxyFramework) and ShowMeInfoV2.";
dependencies[] = { "Game", "World" };
};
};

View file

@ -0,0 +1,16 @@
// ============================================================================
// YAProxyConstants
// ============================================================================
class YAProxyConstants
{
static const string TAG = "[YAProxy]";
// Matching tolerances (metres). MAIN: cursor-hit vs selection LS to decide a
// proxy is "near"; FINE: world-pos match for the fallback map.
static const float DIST_MAIN = 5.0;
static const float DIST_FINE = 1.15;
// Toggle verbose proxy-scan logging AND the debug "Proxy: <name>" player
// action (validation). Set false for production.
static bool DEBUG = true;
}

View file

@ -0,0 +1,238 @@
// ============================================================================
// YAProxyActionBase
// ----------------------------------------------------------------------------
// Base continuous action for interacting with house furniture proxies.
//
// Subclass it and override:
// GetProxyKeywords() - comma-separated keywords; the proxy matches if its
// name contains any keyword (e.g. "case_sink_a").
// GetDuration() - progress time in seconds.
// GetText() - action label.
// YA_ExtraCondition() - extra availability checks (item state, etc.).
// OnFinishProgressServer() - the effect; use GetProxyName()/GetProxyWS().
// And set the condition components (item requirement) in CreateConditionComponents.
//
// Resolution (client) uses the modded House proxy maps (geometry-LOD scan);
// the resolved proxy name + world pos are synced to the server via ActionData.
//
// Detection: uses the default ContinuousDefaultActionInput (detect-from-item +
// detect-from-player), so the action works whether it is registered on the
// player (empty-handed) or on an item (wrench / bottle). The cursor target is
// passed to ActionCondition either way.
// ============================================================================
class YAProxyActionBaseCB : ActionContinuousBaseCB
{
override void CreateActionComponent()
{
float t = 3.0;
YAProxyActionBase a = YAProxyActionBase.Cast(m_ActionData.m_Action);
if (a)
t = a.GetDuration();
m_ActionData.m_ActionComponent = new CAContinuousTime(t);
}
}
class YAProxyActionBase : ActionContinuousBase
{
// Last client-side resolution (for live labels / debug). Transient.
protected string m_LastName;
protected vector m_LastWS;
void YAProxyActionBase()
{
m_CallbackClass = YAProxyActionBaseCB;
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT;
m_FullBody = true;
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
}
// ---------------- subclass hooks ----------------
float GetDuration() { return 3.0; }
string GetProxyKeywords() { return ""; } // comma-separated; "" = match nothing
override void CreateConditionComponents()
{
m_ConditionItem = new CCINotPresent(); // no item by default (override for tools/containers)
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.DEFAULT);
}
// Extra availability check after a proxy is matched (client side).
bool YA_ExtraCondition(PlayerBase player, ActionTarget target, ItemBase item, string proxyName, vector proxyWS)
{
return true;
}
override bool IsLockTargetOnUse()
{
return false;
}
// ---------------- custom ActionData ----------------
override ActionData CreateActionData()
{
return new YAProxyActionData;
}
// ---------------- resolution (client) ----------------
// Which configured proxy is the cursor on? Returns name ("" if none), fills ws.
string YA_Resolve(ActionTarget target, out vector ws)
{
ws = vector.Zero;
if (!target || !target.GetObject())
return "";
House house = House.Cast(target.GetParent());
if (!house || !house.YA_ProxiesBuilt())
return "";
string keywords = GetProxyKeywords();
if (keywords == "")
return "";
keywords.ToLower();
TStringArray kw = new TStringArray();
keywords.Split(",", kw);
// 1) Map1: the target object's world position is the key for most proxies.
string wKey = target.GetObject().GetWorldPosition().ToString(false);
string name1;
if (house.YA_GetProxyMap1().Find(wKey, name1) && YA_KeywordMatch(name1, kw))
{
ws = target.GetObject().GetWorldPosition();
return name1;
}
// 2) Map2 fallback: nearest local-space proxy to the cursor hit point.
vector hitLS = target.GetCursorHitPos() - house.GetPosition();
TStringStringMap map2 = house.YA_GetProxyMap2();
float best = YAProxyConstants.DIST_FINE;
string bestName = "";
vector bestLS = vector.Zero;
for (int i = 0; i < map2.Count(); i++)
{
vector lsPos = map2.GetKey(i).ToVector();
float d = vector.Distance(hitLS, lsPos);
if (d < best && YA_KeywordMatch(map2.GetElement(i), kw))
{
best = d;
bestName = map2.GetElement(i);
bestLS = lsPos;
}
}
if (bestName != "")
{
ws = house.GetPosition() + bestLS;
return bestName;
}
return "";
}
protected bool YA_KeywordMatch(string proxyName, TStringArray keywords)
{
string n = proxyName;
n.ToLower();
foreach (string k : keywords)
{
if (k == "*") // wildcard: match any proxy (debug)
return true;
if (k != "" && n.Contains(k))
return true;
}
return false;
}
// ---------------- availability ----------------
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
{
// Server can't see the cursor target; let it through (validated on finish).
if (GetGame().IsDedicatedServer())
return true;
vector ws;
string name = YA_Resolve(target, ws);
if (name == "")
return false;
m_LastName = name; // remember for live labels / debug
m_LastWS = ws;
return YA_ExtraCondition(player, target, item, name, ws);
}
// ---------------- client -> server sync ----------------
override bool SetupAction(PlayerBase player, ActionTarget target, ItemBase item, out ActionData action_data, Param extra_data = NULL)
{
if (!super.SetupAction(player, target, item, action_data, extra_data))
return false;
if (GetGame().IsClient())
{
YAProxyActionData pad = YAProxyActionData.Cast(action_data);
if (pad)
{
vector ws;
pad.m_ProxyName = YA_Resolve(target, ws);
pad.m_ProxyWS = ws;
}
}
return true;
}
override void WriteToContext(ParamsWriteContext ctx, ActionData action_data)
{
super.WriteToContext(ctx, action_data);
YAProxyActionData pad = YAProxyActionData.Cast(action_data);
if (pad)
{
ctx.Write(pad.m_ProxyName);
ctx.Write(pad.m_ProxyWS);
}
}
override bool ReadFromContext(ParamsReadContext ctx, out ActionReciveData action_recive_data)
{
if (!action_recive_data)
action_recive_data = new YAProxyActionReceiveData;
super.ReadFromContext(ctx, action_recive_data);
YAProxyActionReceiveData rd = YAProxyActionReceiveData.Cast(action_recive_data);
string name;
vector ws;
if (!ctx.Read(name)) return false;
if (!ctx.Read(ws)) return false;
rd.m_ProxyName = name;
rd.m_ProxyWS = ws;
return true;
}
override void HandleReciveData(ActionReciveData action_recive_data, ActionData action_data)
{
super.HandleReciveData(action_recive_data, action_data);
YAProxyActionReceiveData rd = YAProxyActionReceiveData.Cast(action_recive_data);
YAProxyActionData pad = YAProxyActionData.Cast(action_data);
if (rd && pad)
{
pad.m_ProxyName = rd.m_ProxyName;
pad.m_ProxyWS = rd.m_ProxyWS;
}
}
// ---------------- server-side accessors ----------------
string GetProxyName(ActionData action_data)
{
YAProxyActionData pad = YAProxyActionData.Cast(action_data);
if (pad)
return pad.m_ProxyName;
return "";
}
vector GetProxyWS(ActionData action_data)
{
YAProxyActionData pad = YAProxyActionData.Cast(action_data);
if (pad)
return pad.m_ProxyWS;
return vector.Zero;
}
}

View file

@ -0,0 +1,18 @@
// ============================================================================
// YAProxyActionData / YAProxyActionReceiveData
// ----------------------------------------------------------------------------
// Carries the resolved proxy name + world position from client to server so
// server-side action code can act on the right proxy (the cursor target itself
// is not networked). Pattern adapted from ZenProxyFramework.
// ============================================================================
class YAProxyActionReceiveData : ActionReciveData
{
string m_ProxyName;
vector m_ProxyWS;
}
class YAProxyActionData : ActionData
{
string m_ProxyName;
vector m_ProxyWS;
}

View file

@ -0,0 +1,103 @@
// ============================================================================
// YAProxyDebug - validation / discovery action (remove or set DEBUG=false
// for production)
// ----------------------------------------------------------------------------
// Empty-handed action available on ANY recognised house proxy. The action
// prompt shows "Proxy: <name>" live; executing it dumps full diagnostics:
// * the resolved proxy name,
// * target.GetObject() local vs world position (to confirm which keying
// matches on 1.29 - ZPF uses local, ShowMeInfo uses world),
// * the whole house proxy map (every proxy name + key in that building),
// * the server-synced name + world pos.
//
// Only registered on the player when YAProxyConstants.DEBUG is true.
// ============================================================================
class ActionYAProxyDebug : YAProxyActionBase
{
override float GetDuration()
{
return 1.0;
}
override string GetProxyKeywords()
{
return "*"; // match any proxy
}
override string GetText()
{
if (m_LastName != "")
return "Proxy: " + m_LastName;
return "Proxy: ?";
}
override bool YA_ExtraCondition(PlayerBase player, ActionTarget target, ItemBase item, string proxyName, vector proxyWS)
{
return !player.GetItemInHands(); // empty-handed
}
override void OnFinishProgressClient(ActionData action_data)
{
ActionTarget t = action_data.m_Target;
if (!t || !t.GetObject())
return;
Object obj = t.GetObject();
Object parent = t.GetParent();
Print("[YAProxy][DBG] ===== proxy target dump =====");
Print("[YAProxy][DBG] resolvedName = '" + m_LastName + "'");
Print("[YAProxy][DBG] obj.GetPosition() (local?) = " + obj.GetPosition().ToString(false));
Print("[YAProxy][DBG] obj.GetWorldPosition() = " + obj.GetWorldPosition().ToString(false));
Print("[YAProxy][DBG] cursorHitPos = " + t.GetCursorHitPos().ToString(false));
House h = House.Cast(parent);
if (h)
{
Print("[YAProxy][DBG] house = " + h.GetType() + " pos = " + h.GetPosition().ToString(false) + " built = " + h.YA_ProxiesBuilt());
YA_DumpMap("map1(world)", h.YA_GetProxyMap1());
YA_DumpMap("map2(local)", h.YA_GetProxyMap2());
}
else
{
Print("[YAProxy][DBG] parent is NOT a House script class (" + parent + ") - scan won't run for it");
}
}
protected void YA_DumpMap(string label, TStringStringMap m)
{
Print("[YAProxy][DBG] --- " + label + " (" + m.Count() + ") ---");
for (int i = 0; i < m.Count(); i++)
Print("[YAProxy][DBG] " + label + "[" + m.GetKey(i) + "] = " + m.GetElement(i));
}
override void OnFinishProgressServer(ActionData action_data)
{
Print("[YAProxy][DBG] SERVER synced proxy='" + GetProxyName(action_data) + "' ws=" + GetProxyWS(action_data).ToString(false));
}
}
// --- registration (debug only) ---
modded class ActionConstructor
{
override void RegisterActions(TTypenameArray actions)
{
super.RegisterActions(actions);
actions.Insert(ActionYAProxyDebug);
}
}
modded class PlayerBase
{
override void SetActions(out TInputActionMap InputActionMap)
{
super.SetActions(InputActionMap);
if (YAProxyConstants.DEBUG)
{
AddAction(ActionYAProxyDebug, InputActionMap);
Print("[YAProxy] Debug proxy action registered on player");
}
}
}

View file

@ -0,0 +1,110 @@
// ============================================================================
// YAProxyHouse (modded House)
// ----------------------------------------------------------------------------
// Builds, shortly after each house spawns, a map of furniture-proxy world/
// local positions -> proxy name by reading the building's geometry-LOD
// selections. Dependent actions look up the targeted proxy in these maps.
//
// Adapted from the ShowMeInfoV2 mod's GetBuildingMaps() (the geometry-LOD scan
// that works on DayZ 1.29). Two maps:
// Map1 (worldish): keyed by GetSelectionPositionOld(sel) - matches the
// target object's world position for most proxies.
// Map2 (local): keyed by GetSelectionPositionLS(sel) - distance fallback
// for proxies whose component pos doesn't match Map1.
// ============================================================================
modded class House
{
protected ref TStringStringMap ya_ProxyMap1; // world-pos string -> proxy name
protected ref TStringStringMap ya_ProxyMap2; // local-pos string -> proxy name
protected bool ya_ProxiesBuilt = false;
void House()
{
ya_ProxyMap1 = new TStringStringMap;
ya_ProxyMap2 = new TStringStringMap;
// Resolution happens client-side; the server uses the synced world pos and
// dedicated servers may not even load geometry LODs. So scan only off the
// dedicated server. Geometry isn't ready at construction -> defer.
if (!GetGame().IsDedicatedServer())
GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(this.YA_BuildProxyMaps, 1000, false);
}
TStringStringMap YA_GetProxyMap1() { return ya_ProxyMap1; }
TStringStringMap YA_GetProxyMap2() { return ya_ProxyMap2; }
bool YA_ProxiesBuilt() { return ya_ProxiesBuilt; }
void YA_BuildProxyMaps()
{
string type = GetType();
type.ToLower();
if (type.Contains("contaminatedarea")) // no geometry / not relevant
return;
LOD geoLod = GetLODByName(LOD.NAME_GEOMETRY);
if (!geoLod)
return;
// ---- Map1: geometry-LOD proxy selections ----
array<Selection> geoSelections = new array<Selection>();
geoLod.GetSelections(geoSelections);
foreach (Selection sel : geoSelections)
{
if (!sel)
continue;
string selName = sel.GetName();
if (!YA_IsProxySelection(selName))
continue;
string posKey = GetSelectionPositionOld(selName).ToString(false);
string proxyName = YA_ParseProxyName(selName);
if (proxyName != "")
ya_ProxyMap1.Set(posKey, proxyName);
}
// ---- Map2: parent selection list (local-space positions) ----
TStringArray parentSelections = new TStringArray();
GetSelectionList(parentSelections);
foreach (string pSel : parentSelections)
{
if (!YA_IsProxySelection(pSel))
continue;
string lsKey = GetSelectionPositionLS(pSel).ToString(false);
string pName = YA_ParseProxyName(pSel);
if (pName != "")
ya_ProxyMap2.Set(lsKey, pName);
}
ya_ProxiesBuilt = true;
if (YAProxyConstants.DEBUG)
Print(string.Format("%1 House '%2' proxies built: map1=%3 map2=%4", YAProxyConstants.TAG, GetType(), ya_ProxyMap1.Count(), ya_ProxyMap2.Count()));
}
// A real, targetable proxy selection (not the "proxies" group or shadows).
protected bool YA_IsProxySelection(string selection)
{
string s = selection;
s.ToLower();
return s.Contains("proxy") && !s.Contains("proxies") && !s.Contains("shadow");
}
// "proxy\furniture\fridge.p3d(.001)" -> "fridge"
protected string YA_ParseProxyName(string selection)
{
string s = selection;
s.ToLower();
TStringArray parts = new TStringArray();
s.Split("\\", parts);
if (parts.Count() == 0)
return "";
string last = parts[parts.Count() - 1]; // "fridge.p3d" / "fridge.p3d.001"
int dot = last.IndexOf(".p3d");
if (dot != -1)
last = last.Substring(0, dot);
return last;
}
}