From 89f42a04b091eb9368b227f8f13caa8c1c23f8ca Mon Sep 17 00:00:00 2001 From: smthspec Date: Fri, 19 Jun 2026 09:45:23 +0300 Subject: [PATCH] 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 --- $PBOPREFIX$.txt | 1 + README.md | 66 ++++++ config.cpp | 60 +++++ mod.cpp | 13 ++ scripts/3_Game/YAProxy/YAProxyConstants.c | 16 ++ scripts/4_World/YAProxy/YAProxyActionBase.c | 238 ++++++++++++++++++++ scripts/4_World/YAProxy/YAProxyActionData.c | 18 ++ scripts/4_World/YAProxy/YAProxyDebug.c | 103 +++++++++ scripts/4_World/YAProxy/YAProxyHouse.c | 110 +++++++++ 9 files changed, 625 insertions(+) create mode 100644 $PBOPREFIX$.txt create mode 100644 README.md create mode 100644 config.cpp create mode 100644 mod.cpp create mode 100644 scripts/3_Game/YAProxy/YAProxyConstants.c create mode 100644 scripts/4_World/YAProxy/YAProxyActionBase.c create mode 100644 scripts/4_World/YAProxy/YAProxyActionData.c create mode 100644 scripts/4_World/YAProxy/YAProxyDebug.c create mode 100644 scripts/4_World/YAProxy/YAProxyHouse.c diff --git a/$PBOPREFIX$.txt b/$PBOPREFIX$.txt new file mode 100644 index 0000000..fb027d8 --- /dev/null +++ b/$PBOPREFIX$.txt @@ -0,0 +1 @@ +YAProxy \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..51281f3 --- /dev/null +++ b/README.md @@ -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: \"** 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. diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..8a2946c --- /dev/null +++ b/config.cpp @@ -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" }; + }; + }; + }; +}; diff --git a/mod.cpp b/mod.cpp new file mode 100644 index 0000000..02113d4 --- /dev/null +++ b/mod.cpp @@ -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" }; + }; +}; diff --git a/scripts/3_Game/YAProxy/YAProxyConstants.c b/scripts/3_Game/YAProxy/YAProxyConstants.c new file mode 100644 index 0000000..cd7dee6 --- /dev/null +++ b/scripts/3_Game/YAProxy/YAProxyConstants.c @@ -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: " player + // action (validation). Set false for production. + static bool DEBUG = true; +} diff --git a/scripts/4_World/YAProxy/YAProxyActionBase.c b/scripts/4_World/YAProxy/YAProxyActionBase.c new file mode 100644 index 0000000..77dafa8 --- /dev/null +++ b/scripts/4_World/YAProxy/YAProxyActionBase.c @@ -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; + } +} diff --git a/scripts/4_World/YAProxy/YAProxyActionData.c b/scripts/4_World/YAProxy/YAProxyActionData.c new file mode 100644 index 0000000..b7dfe6c --- /dev/null +++ b/scripts/4_World/YAProxy/YAProxyActionData.c @@ -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; +} diff --git a/scripts/4_World/YAProxy/YAProxyDebug.c b/scripts/4_World/YAProxy/YAProxyDebug.c new file mode 100644 index 0000000..76247a3 --- /dev/null +++ b/scripts/4_World/YAProxy/YAProxyDebug.c @@ -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: " 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"); + } + } +} diff --git a/scripts/4_World/YAProxy/YAProxyHouse.c b/scripts/4_World/YAProxy/YAProxyHouse.c new file mode 100644 index 0000000..acf34ce --- /dev/null +++ b/scripts/4_World/YAProxy/YAProxyHouse.c @@ -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 geoSelections = new array(); + 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; + } +}