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:
parent
7fb07aac4a
commit
4bd4a74152
6 changed files with 169 additions and 52 deletions
26
README.md
26
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
scripts/4_World/YAInteraction/Actions/ActionYASearchObj.c
Normal file
84
scripts/4_World/YAInteraction/Actions/ActionYASearchObj.c
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
// ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue