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 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-19 17:23:58 +03:00
parent 7fb07aac4a
commit 4bd4a74152
6 changed files with 169 additions and 52 deletions

View file

@ -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

View file

@ -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<EntityAI> spawned;
if (count > 0)
{
array<ref YALootEntry> 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));
}
}

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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<ref YALootEntry> table = GetLootTableForEntry(entry);
array<EntityAI> 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).
// ------------------------------------------------------------------

View file

@ -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() + "'");
}
}
}