From 4bd4a74152ab479926d77fd4f8e7cdeb0222b8f7 Mon Sep 17 00:00:00 2001 From: smthspec Date: Fri, 19 Jun 2026 17:23:58 +0300 Subject: [PATCH] Support actions on standalone-spawned objects (by classname) Objects spawned directly in the world (init.c / CE) are real BuildingBase entities, so attach a Search to them by classname - independent of the proxy framework (which needs a parent house; CCTCursorParent rejects parentless objects). - ActionYASearchObj: object-targeted (CCTNonRuined) Search; resolves the entry from target.GetObject().GetType() (substring), cooldown keyed by network id - modded BuildingBase.SetActions: attaches it to classes matching a search entry - Shared YAInteractionManager.PerformSearch() used by both proxy + object actions - Re-add vending_machine (works for standalone-spawned vending machines) Lets you make spawned loot boxes / vending machines lootable: add the classname (or a substring) as a 'search' entry. Co-Authored-By: Claude Opus 4.8 --- README.md | 26 +++--- .../YAInteraction/Actions/ActionYASearch.c | 39 +-------- .../YAInteraction/Actions/ActionYASearchObj.c | 84 +++++++++++++++++++ .../Config/YAInteractionConfigLoader.c | 8 +- .../Entities/YAInteractionManager.c | 42 ++++++++++ .../4_World/YAInteraction/YAInteractionInit.c | 22 ++++- 6 files changed, 169 insertions(+), 52 deletions(-) create mode 100644 scripts/4_World/YAInteraction/Actions/ActionYASearchObj.c diff --git a/README.md b/README.md index a16eb94..9388678 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,25 @@ two-stage repairable sink (repair, then gather water). start and read (overriding defaults) on every start after. No target/loot classnames exist anywhere else in the codebase. -## Target objects (v1) +## Target objects -Identified by their YAProxy proxy name (substring match): +Entries are matched by name as a substring, against **either**: +- a **house proxy** name (furniture baked into a building) via @YAProxy, **or** +- a **standalone entity** classname (objects spawned in the world via init.c / + CE), targeted directly — no proxy needed. -| Proxy | Action | Loot table | -|-------|--------|-----------| -| `case_sink_a` | Repair → Gather Water | — | -| `lekarnicka` | Search | MedTable | -| `fridge` | Search | FoodTable | +| Name | Action | Loot table | Notes | +|------|--------|-----------|-------| +| `case_sink_a` | Repair → Gather Water | — | house proxy | +| `lekarnicka` | Search | MedTable | house proxy | +| `fridge` | Search | FoodTable | house proxy or standalone | +| `vending_machine` | Search | VendingTable | **standalone only** | -> Vending machines (`vending_machine`) are **deferred**: in tested buildings the -> proxy exposes no targetable object, so it can't be detected. `VendingTable` is -> still defined for when that's solved or its loot is moved to another furniture. +> House-baked vending **proxies** can't be detected (the engine exposes no +> targetable object), but a vending machine **spawned standalone** (e.g. +> `CreateObjectEx("StaticObj_Furniture_vending_machine", …)` in init.c) is a real +> entity and works. The same applies to any spawned object: add its classname (or +> a substring) to the registry as a `search` entry to make it lootable. ## Configuration files diff --git a/scripts/4_World/YAInteraction/Actions/ActionYASearch.c b/scripts/4_World/YAInteraction/Actions/ActionYASearch.c index 78f672c..7ff1613 100644 --- a/scripts/4_World/YAInteraction/Actions/ActionYASearch.c +++ b/scripts/4_World/YAInteraction/Actions/ActionYASearch.c @@ -49,42 +49,7 @@ class ActionYASearch : YAProxyActionBase if (!entry || entry.ActionType != YAInteractionConfig.ACTION_SEARCH) return; - string posKey = proxyWS.ToString(false); - - if (YAInteractionManager.IsSearchOnCooldown(posKey)) - { - player.MessageStatus(string.Format("You already searched this. (%1s)", YAInteractionManager.GetSearchCooldownRemaining(posKey))); - return; - } - - // Searching gates the object whether or not it yields anything. - YAInteractionManager.StampSearchCooldown(posKey, entry.CooldownSec); - - // Find-chance roll. - if (Math.RandomFloat(0, 100) >= entry.SearchChance) - { - Print(string.Format("[YAInteraction] %1 searched '%2' @ %3 -> nothing (%4%% chance)", player.GetIdentity().GetId(), proxyName, posKey, entry.SearchChance)); - player.MessageStatus("You found nothing useful."); - return; - } - - int count = Math.RandomIntInclusive(entry.SearchMin, entry.SearchMax); - array spawned; - if (count > 0) - { - array table = YAInteractionManager.GetLootTableForEntry(entry); - spawned = YALootTableManager.GetInstance().SpawnFromTable(table, count, player, player.GetPosition()); - } - - int found; - if (spawned) - found = spawned.Count(); - - Print(string.Format("[YAInteraction] %1 searched '%2' @ %3 -> %4 item(s)", player.GetIdentity().GetId(), proxyName, posKey, found)); - - if (found > 0) - player.MessageStatus(string.Format("You found %1 item(s).", found)); - else - player.MessageStatus("You found nothing useful."); + // State keyed by the proxy world position (per-object cooldown). + YAInteractionManager.PerformSearch(player, entry, proxyWS.ToString(false)); } } diff --git a/scripts/4_World/YAInteraction/Actions/ActionYASearchObj.c b/scripts/4_World/YAInteraction/Actions/ActionYASearchObj.c new file mode 100644 index 0000000..961615f --- /dev/null +++ b/scripts/4_World/YAInteraction/Actions/ActionYASearchObj.c @@ -0,0 +1,84 @@ +// ============================================================================ +// ActionYASearchObj +// ---------------------------------------------------------------------------- +// "Search" for STANDALONE objects (entities spawned directly in the world via +// init.c / CE, e.g. loot boxes, vending machines) - as opposed to furniture +// PROXIES baked into houses (which ActionYASearch + @YAProxy handle). +// +// A standalone object is a real entity, so it's targeted directly: the entry +// is resolved from target.GetObject().GetType() (substring-matched against the +// registry, so "vending_machine" matches "StaticObj_Furniture_vending_machine") +// and the per-object cooldown is keyed by the entity network id. +// +// Registered on BuildingBase via SetActions (see YAInteractionInit) - these +// classes are House : BuildingBase, so the action attaches to the spawned +// entity itself (no @YAProxy needed for standalone objects). +// ============================================================================ + +class ActionYASearchObjCB : ActionContinuousBaseCB +{ + override void CreateActionComponent() + { + m_ActionData.m_ActionComponent = new CAContinuousTime(YAInteractionConfig.SEARCH_TIME); + } +} + +class ActionYASearchObj : ActionContinuousBase +{ + void ActionYASearchObj() + { + m_CallbackClass = ActionYASearchObjCB; + m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT; + m_FullBody = true; + m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH; + } + + override string GetText() + { + return "Search"; + } + + override void CreateConditionComponents() + { + m_ConditionItem = new CCINone(); // empty-handed + m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT); + } + + override bool HasTarget() + { + return true; + } + + override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item) + { + if (player.GetItemInHands()) + return false; + + Object obj = target.GetObject(); + if (!obj) + return false; + + YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(obj.GetType()); + return entry != null && entry.ActionType == YAInteractionConfig.ACTION_SEARCH; + } + + override bool IsLockTargetOnUse() + { + return false; + } + + override void OnFinishProgressServer(ActionData action_data) + { + PlayerBase player = action_data.m_Player; + Object obj = action_data.m_Target.GetObject(); + if (!player || !obj) + return; + + YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(obj.GetType()); + if (!entry || entry.ActionType != YAInteractionConfig.ACTION_SEARCH) + return; + + // State keyed by the entity network id (per-object cooldown). + YAInteractionManager.PerformSearch(player, entry, obj.GetNetworkIDString()); + } +} diff --git a/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c b/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c index a74a352..98cfdf8 100644 --- a/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c +++ b/scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c @@ -114,10 +114,10 @@ class YAInteractionConfigLoader 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, 60.0, 1, 2)); d.Interactables.Insert(MakeEntry("fridge", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_FOOD, Settings.ObjectCooldownSec, 75.0, 1, 2)); - // Vending machines deferred: in tested buildings the proxy has no targetable - // object (target.GetObject()==null), so it can't be detected/overridden - // cleanly. VendingTable below is kept for when that's solved or moved to a - // detectable furniture. Re-add: MakeEntry("vending_machine", ACTION_SEARCH, TABLE_VENDING, ObjectCooldownSec) + // Works for vending machines SPAWNED STANDALONE (init.c / CE) - matched as + // a real entity by classname (StaticObj_Furniture_vending_machine). House- + // baked vending proxies still can't be detected (no targetable object). + d.Interactables.Insert(MakeEntry("vending_machine", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_VENDING, Settings.ObjectCooldownSec, 80.0, 1, 2)); // --- Loot tables (classnames verified in P:\DZ) --- YALootTable med = MakeTable(YAInteractionConfig.TABLE_MED); diff --git a/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c b/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c index bbba0a6..6cde824 100644 --- a/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c +++ b/scripts/4_World/YAInteraction/Entities/YAInteractionManager.c @@ -140,6 +140,48 @@ class YAInteractionManager return null; } + // ------------------------------------------------------------------ + // Shared server-side search: cooldown gate, find-chance, item count, + // loot spawn + feedback. Used by both the proxy action (ActionYASearch, + // stateKey = proxy world pos) and the standalone object action + // (ActionYASearchObj, stateKey = entity network id). + // ------------------------------------------------------------------ + static void PerformSearch(PlayerBase player, YAInteractableEntry entry, string stateKey) + { + if (!player || !entry) + return; + + if (IsSearchOnCooldown(stateKey)) + { + player.MessageStatus(string.Format("You already searched this. (%1s)", GetSearchCooldownRemaining(stateKey))); + return; + } + + // Searching gates the object whether or not it yields anything. + StampSearchCooldown(stateKey, entry.CooldownSec); + + if (Math.RandomFloat(0, 100) >= entry.SearchChance) + { + player.MessageStatus("You found nothing useful."); + return; + } + + int count = Math.RandomIntInclusive(entry.SearchMin, entry.SearchMax); + int found = 0; + if (count > 0) + { + array table = GetLootTableForEntry(entry); + array spawned = YALootTableManager.GetInstance().SpawnFromTable(table, count, player, player.GetPosition()); + if (spawned) + found = spawned.Count(); + } + + if (found > 0) + player.MessageStatus(string.Format("You found %1 item(s).", found)); + else + player.MessageStatus("You found nothing useful."); + } + // ------------------------------------------------------------------ // Search cooldown (per object, keyed by proxy world position). // ------------------------------------------------------------------ diff --git a/scripts/4_World/YAInteraction/YAInteractionInit.c b/scripts/4_World/YAInteraction/YAInteractionInit.c index c3ce81c..007d1e3 100644 --- a/scripts/4_World/YAInteraction/YAInteractionInit.c +++ b/scripts/4_World/YAInteraction/YAInteractionInit.c @@ -19,9 +19,29 @@ modded class ActionConstructor { super.RegisterActions(actions); - actions.Insert(ActionYASearch); + actions.Insert(ActionYASearch); // house proxy (via @YAProxy) actions.Insert(ActionYARepairSink); actions.Insert(ActionYAGatherWater); + actions.Insert(ActionYASearchObj); // standalone entity (by classname) + } +} + +// Standalone objects spawned in the world (init.c / CE) - e.g. loot boxes, +// vending machines - are real BuildingBase entities, so we attach an +// object-targeted Search directly to the class (no proxy needed). The entry is +// matched by classname substring against the registry. +modded class BuildingBase +{ + override void SetActions() + { + super.SetActions(); + + YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(GetType()); + if (entry && entry.ActionType == YAInteractionConfig.ACTION_SEARCH) + { + AddAction(ActionYASearchObj); + Print("[YAInteraction] Registered object Search on '" + GetType() + "'"); + } } }