Rework interaction layer onto ZenProxyFramework proxy API
Furniture (sink/fridge/lekarnicka/vending) are house proxies, not entities, so actions are now driven by ZenProxyFramework: actions target proxies by name (GetZenProxyNameTarget), the proxy world position is synced to the server via ActionData, repair spawns an invisible ZenProxySinkTest marker (presence = repaired), and gather-water checks for it. Registry is keyed by proxy name; loot tables / cooldowns / settings JSON kept. NOTE: superseded approach - ZenProxyFramework does not compile on DayZ 1.29 (HouseType type-name collision). Next: a self-contained @YAProxy framework (based on ZenProxyFramework + ShowMeInfoV2's geometry-LOD proxy scan) that YAInteraction will depend on instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1f63096cdb
commit
e4cb0e4009
9 changed files with 309 additions and 351 deletions
|
|
@ -8,7 +8,8 @@ class CfgPatches
|
|||
requiredAddons[] =
|
||||
{
|
||||
"DZ_Data",
|
||||
"DZ_Scripts"
|
||||
"DZ_Scripts",
|
||||
"ZenProxyFramework" // dependency: ensures ZPF loads before us (proxy API + ZenProxySinkTest)
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
// ============================================================================
|
||||
// ActionYAGatherWater
|
||||
// ----------------------------------------------------------------------------
|
||||
// Stage 2 of the sink. Only usable once the sink has been repaired (state in
|
||||
// YAInteractionManager). Fills a liquid container in the player's hands (or
|
||||
// the first fillable container in their inventory) with a random amount of
|
||||
// clean water.
|
||||
// Stage 2 of the sink. Attached to the liquid container in the player's hands
|
||||
// (Bottle_Base). Only usable once the sink is repaired (a ZenProxySinkTest
|
||||
// entity exists at the proxy world position). Fills the held container with a
|
||||
// random amount of water from config, gated by a per-player cooldown.
|
||||
//
|
||||
// Cooldown model (v1): per-player, keyed by player UID, in a static map.
|
||||
// Resets on server restart.
|
||||
// TODO (v2): swap the static map for CF persistence in IsOnCooldown()/
|
||||
// StampCooldown().
|
||||
// Registered on Bottle_Base (see YAInteractionInit) - item-based.
|
||||
// ============================================================================
|
||||
|
||||
class ActionYAGatherWaterCB : ActionContinuousBaseCB
|
||||
|
|
@ -22,49 +19,54 @@ class ActionYAGatherWaterCB : ActionContinuousBaseCB
|
|||
|
||||
class ActionYAGatherWater : ActionContinuousBase
|
||||
{
|
||||
// playerUID -> server time (ms) when the cooldown expires.
|
||||
protected static ref map<string, int> m_PlayerCooldowns = new map<string, int>();
|
||||
|
||||
void ActionYAGatherWater()
|
||||
{
|
||||
m_CallbackClass = ActionYAGatherWaterCB;
|
||||
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_FILLBOTTLEWELL; // reuse "fill from fixture" anim
|
||||
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_FILLBOTTLEWELL;
|
||||
m_FullBody = true;
|
||||
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
|
||||
m_Text = "Gather Water";
|
||||
}
|
||||
|
||||
override typename GetInputType()
|
||||
override string GetText()
|
||||
{
|
||||
return ContinuousInteractActionInput;
|
||||
return "Gather Water";
|
||||
}
|
||||
|
||||
override void CreateConditionComponents()
|
||||
{
|
||||
m_ConditionItem = new CCINone();
|
||||
m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT);
|
||||
}
|
||||
|
||||
override bool HasTarget()
|
||||
override bool HasZenProxyTarget()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sink interactable + repaired + player carries a fillable container.
|
||||
override string GetZenProxyNameTarget()
|
||||
{
|
||||
return YAInteractionManager.GetRepairWaterProxyNames();
|
||||
}
|
||||
|
||||
override void CreateConditionComponents()
|
||||
{
|
||||
m_ConditionItem = new CCINonRuined(); // the container in hands
|
||||
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL);
|
||||
}
|
||||
|
||||
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
|
||||
{
|
||||
Object targetObject = target.GetObject();
|
||||
if (!targetObject)
|
||||
if (!CanFill(item))
|
||||
return false;
|
||||
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
||||
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_REPAIR_WATER)
|
||||
if (!IsTargetingZenActionProxy(player, target, item))
|
||||
return false;
|
||||
|
||||
if (!YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString()))
|
||||
return false;
|
||||
// Client-only: only available when the sink is repaired (entity exists).
|
||||
if (GetGame().IsClient())
|
||||
{
|
||||
vector ls, ws;
|
||||
string houseType;
|
||||
GetZenActionProxyTargetEx(player, target, item, ls, ws, houseType);
|
||||
if (!GetProxyObject(ws, YAInteractionConfig.SINK_PROXY_ENTITY))
|
||||
return false;
|
||||
}
|
||||
|
||||
return FindFillableContainer(player) != null;
|
||||
return true;
|
||||
}
|
||||
|
||||
override bool IsLockTargetOnUse()
|
||||
|
|
@ -75,113 +77,53 @@ class ActionYAGatherWater : ActionContinuousBase
|
|||
override void OnFinishProgressServer(ActionData action_data)
|
||||
{
|
||||
PlayerBase player = action_data.m_Player;
|
||||
Object targetObject = action_data.m_Target.GetObject();
|
||||
if (!player || !player.GetIdentity() || !targetObject)
|
||||
if (!player || !player.GetIdentity())
|
||||
return;
|
||||
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
||||
if (!entry)
|
||||
return;
|
||||
vector proxyWS = GetZenProxyTargetWS(action_data);
|
||||
|
||||
// Defensive re-check (repair state could change mid-progress).
|
||||
if (!YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString()))
|
||||
// Defensive: sink must be repaired.
|
||||
if (!GetProxyObject(proxyWS, YAInteractionConfig.SINK_PROXY_ENTITY))
|
||||
{
|
||||
player.MessageStatus("The sink needs to be repaired first.");
|
||||
return;
|
||||
}
|
||||
|
||||
string uid = player.GetIdentity().GetId();
|
||||
|
||||
// --- Per-player cooldown gate ---
|
||||
if (IsOnCooldown(uid))
|
||||
if (YAInteractionManager.IsWaterOnCooldown(uid))
|
||||
{
|
||||
int remaining = GetCooldownRemaining(uid);
|
||||
Print(string.Format("[YAInteraction] GatherWater on cooldown (%1s) for %2", remaining, uid));
|
||||
int remaining = YAInteractionManager.GetWaterCooldownRemaining(uid);
|
||||
player.MessageStatus(string.Format("You need to wait before gathering water again. (%1s)", remaining));
|
||||
return;
|
||||
}
|
||||
|
||||
ItemBase container = FindFillableContainer(player);
|
||||
if (!container)
|
||||
ItemBase container = ItemBase.Cast(action_data.m_MainItem);
|
||||
if (!CanFill(container))
|
||||
{
|
||||
player.MessageStatus("You have nothing to fill with water.");
|
||||
player.MessageStatus("This container can't hold any more water.");
|
||||
return;
|
||||
}
|
||||
|
||||
float amount = Math.RandomFloatInclusive(YAInteractionConfigLoader.Settings.WaterAmountMin, YAInteractionConfigLoader.Settings.WaterAmountMax);
|
||||
Liquid.FillContainer(container, YAInteractionConfig.WATER_LIQUID_TYPE, amount);
|
||||
|
||||
StampCooldown(uid, entry.CooldownSec);
|
||||
// Cooldown comes from the sink's registry entry.
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(GetZenProxyTargetType(action_data));
|
||||
float cd = YAInteractionConfigLoader.Settings.SinkPlayerCooldownSec;
|
||||
if (entry && entry.CooldownSec > 0)
|
||||
cd = entry.CooldownSec;
|
||||
YAInteractionManager.StampWaterCooldown(uid, cd);
|
||||
|
||||
Print(string.Format("[YAInteraction] %1 gathered %2 water into '%3'", uid, amount, container.GetType()));
|
||||
player.MessageStatus(string.Format("You filled %1 with %2 ml of water.", container.GetDisplayName(), Math.Round(amount)));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Container discovery: hands first, then inventory.
|
||||
// ------------------------------------------------------------------
|
||||
protected ItemBase FindFillableContainer(PlayerBase player)
|
||||
{
|
||||
if (!player)
|
||||
return null;
|
||||
|
||||
ItemBase inHands = ItemBase.Cast(player.GetItemInHands());
|
||||
if (CanFill(inHands))
|
||||
return inHands;
|
||||
|
||||
array<EntityAI> items = new array<EntityAI>();
|
||||
player.GetInventory().EnumerateInventory(InventoryTraversalType.PREORDER, items);
|
||||
foreach (EntityAI ent : items)
|
||||
{
|
||||
ItemBase candidate = ItemBase.Cast(ent);
|
||||
if (CanFill(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected bool CanFill(ItemBase item)
|
||||
{
|
||||
if (!item || !item.IsLiquidContainer())
|
||||
return false;
|
||||
|
||||
if (item.GetQuantity() >= item.GetQuantityMax())
|
||||
return false; // already full
|
||||
|
||||
return false;
|
||||
return Liquid.CanFillContainer(item, YAInteractionConfig.WATER_LIQUID_TYPE);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Cooldown helpers (persistence-aware surface area).
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// TODO (v2): read last-gather time from CF storage instead of the static map.
|
||||
protected static bool IsOnCooldown(string uid)
|
||||
{
|
||||
int expiry;
|
||||
if (!m_PlayerCooldowns.Find(uid, expiry))
|
||||
return false;
|
||||
return GetGame().GetTime() < expiry;
|
||||
}
|
||||
|
||||
protected static int GetCooldownRemaining(string uid)
|
||||
{
|
||||
int expiry;
|
||||
if (!m_PlayerCooldowns.Find(uid, expiry))
|
||||
return 0;
|
||||
|
||||
int remainingMs = expiry - GetGame().GetTime();
|
||||
if (remainingMs < 0)
|
||||
remainingMs = 0;
|
||||
return remainingMs / 1000;
|
||||
}
|
||||
|
||||
// TODO (v2): persist this stamp via CF so it survives server restarts.
|
||||
protected static void StampCooldown(string uid, float cooldownSec)
|
||||
{
|
||||
int expiry = GetGame().GetTime() + (int)(cooldownSec * 1000);
|
||||
m_PlayerCooldowns.Set(uid, expiry);
|
||||
Print(string.Format("[YAInteraction] Water cooldown stamped for %1 (%2s)", uid, cooldownSec));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// ============================================================================
|
||||
// ActionYARepairSink
|
||||
// ----------------------------------------------------------------------------
|
||||
// Stage 1 of the sink. Requires a Wrench or PipeWrench in hands and an
|
||||
// un-repaired sink. On completion it flags the object as repaired in the
|
||||
// per-object repair map (YAInteractionManager), which then unlocks
|
||||
// ActionYAGatherWater on the same object.
|
||||
// Stage 1 of the sink. Requires a wrench/pipe wrench (the item this action is
|
||||
// attached to). On completion it spawns a ZenProxySinkTest entity at the sink
|
||||
// proxy's world position - the entity's PRESENCE represents "repaired" and is
|
||||
// networked to all clients (ZenProxyFramework pattern).
|
||||
//
|
||||
// Repair state is in-memory and resets on server restart.
|
||||
// TODO (v2): persist repair state via CF (see YAInteractionManager).
|
||||
// Registered on Wrench + PipeWrench (see YAInteractionInit) - item-based, so
|
||||
// it is unaffected by PlayerBase action load-order issues.
|
||||
// ============================================================================
|
||||
|
||||
class ActionYARepairSinkCB : ActionContinuousBaseCB
|
||||
|
|
@ -23,43 +23,48 @@ class ActionYARepairSink : ActionContinuousBase
|
|||
void ActionYARepairSink()
|
||||
{
|
||||
m_CallbackClass = ActionYARepairSinkCB;
|
||||
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_CRAFTING; // kneeling craft/repair anim
|
||||
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT;
|
||||
m_FullBody = true;
|
||||
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
|
||||
m_Text = "Repair Sink";
|
||||
}
|
||||
|
||||
override typename GetInputType()
|
||||
override string GetText()
|
||||
{
|
||||
return ContinuousInteractActionInput;
|
||||
return "Repair Sink";
|
||||
}
|
||||
|
||||
override void CreateConditionComponents()
|
||||
{
|
||||
m_ConditionItem = new CCINone(); // tool requirement checked in ActionCondition
|
||||
m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT);
|
||||
}
|
||||
|
||||
override bool HasTarget()
|
||||
override bool HasZenProxyTarget()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sink interactable + a wrench in hands + not already repaired.
|
||||
override string GetZenProxyNameTarget()
|
||||
{
|
||||
return YAInteractionManager.GetRepairWaterProxyNames();
|
||||
}
|
||||
|
||||
override void CreateConditionComponents()
|
||||
{
|
||||
m_ConditionItem = new CCINonRuined(); // the wrench in hands
|
||||
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL);
|
||||
}
|
||||
|
||||
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
|
||||
{
|
||||
Object targetObject = target.GetObject();
|
||||
if (!targetObject)
|
||||
if (!IsTargetingZenActionProxy(player, target, item))
|
||||
return false;
|
||||
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
||||
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_REPAIR_WATER)
|
||||
return false;
|
||||
// Client-only: hide if the sink is already repaired (entity exists).
|
||||
if (GetGame().IsClient())
|
||||
{
|
||||
vector ls, ws;
|
||||
string houseType;
|
||||
GetZenActionProxyTargetEx(player, target, item, ls, ws, houseType);
|
||||
if (GetProxyObject(ws, YAInteractionConfig.SINK_PROXY_ENTITY))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsRepairTool(item))
|
||||
return false;
|
||||
|
||||
return !YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString());
|
||||
return true;
|
||||
}
|
||||
|
||||
override bool IsLockTargetOnUse()
|
||||
|
|
@ -70,30 +75,24 @@ class ActionYARepairSink : ActionContinuousBase
|
|||
override void OnFinishProgressServer(ActionData action_data)
|
||||
{
|
||||
PlayerBase player = action_data.m_Player;
|
||||
Object targetObject = action_data.m_Target.GetObject();
|
||||
if (!player || !targetObject)
|
||||
return;
|
||||
vector proxyWS = GetZenProxyTargetWS(action_data);
|
||||
|
||||
string key = targetObject.GetNetworkIDString();
|
||||
|
||||
// Re-check on the server (state may have changed during the progress).
|
||||
if (YAInteractionManager.IsRepaired(key))
|
||||
// Already repaired? (entity present)
|
||||
if (GetProxyObject(proxyWS, YAInteractionConfig.SINK_PROXY_ENTITY))
|
||||
{
|
||||
player.MessageStatus("This sink is already repaired.");
|
||||
if (player)
|
||||
player.MessageStatus("This sink is already repaired.");
|
||||
return;
|
||||
}
|
||||
|
||||
YAInteractionManager.SetRepaired(key, true);
|
||||
Object marker = GetGame().CreateObjectEx(YAInteractionConfig.SINK_PROXY_ENTITY, proxyWS, ECE_FULL);
|
||||
if (!marker)
|
||||
{
|
||||
Print("[YAInteraction] FAILED to spawn sink marker @ " + proxyWS.ToString(false));
|
||||
return;
|
||||
}
|
||||
|
||||
Print(string.Format("[YAInteraction] %1 repaired sink %2", player.GetIdentity().GetId(), key));
|
||||
Print(string.Format("[YAInteraction] %1 repaired sink @ %2", player.GetIdentity().GetId(), proxyWS.ToString(false)));
|
||||
player.MessageStatus("You repaired the sink. You can now gather water.");
|
||||
}
|
||||
|
||||
// A wrench-type tool held in hands. IsKindOf covers any modded variants.
|
||||
protected bool IsRepairTool(ItemBase item)
|
||||
{
|
||||
if (!item)
|
||||
return false;
|
||||
return item.IsKindOf("Wrench") || item.IsKindOf("PipeWrench");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// ============================================================================
|
||||
// ActionYASearch
|
||||
// ----------------------------------------------------------------------------
|
||||
// Generic "Search" action. The target's registry entry (resolved by classname
|
||||
// via YAInteractionManager) decides the loot table and the per-object cooldown.
|
||||
// No classnames live here - everything comes from config.
|
||||
// Empty-handed "Search" on a searchable furniture PROXY (fridge / med cabinet
|
||||
// / vending machine). Uses ZenProxyFramework: the action targets proxies by
|
||||
// name (GetZenProxyNameTarget) and the proxy world position is synced to the
|
||||
// server via ActionData, so OnFinishProgressServer can roll loot + gate the
|
||||
// per-object cooldown by that position.
|
||||
//
|
||||
// Cooldown model (v1): per-object, keyed by network id, in a static map.
|
||||
// Resets on server restart.
|
||||
// TODO (v2): swap the static map for CF persistence in IsOnCooldown()/
|
||||
// StampCooldown() - those two methods are the only persistence surface.
|
||||
// Registered on PlayerBase (see YAInteractionInit). Cooldown is in-memory.
|
||||
// TODO (v2): persist the per-proxy cooldown via CF.
|
||||
// ============================================================================
|
||||
|
||||
class ActionYASearchCB : ActionContinuousBaseCB
|
||||
|
|
@ -21,46 +21,41 @@ class ActionYASearchCB : ActionContinuousBaseCB
|
|||
|
||||
class ActionYASearch : ActionContinuousBase
|
||||
{
|
||||
// network id string -> server time (ms) when the cooldown expires.
|
||||
protected static ref map<string, int> m_ObjectCooldowns = new map<string, int>();
|
||||
|
||||
void ActionYASearch()
|
||||
{
|
||||
m_CallbackClass = ActionYASearchCB;
|
||||
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT; // generic full-body interact anim
|
||||
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_INTERACT;
|
||||
m_FullBody = true;
|
||||
m_StanceMask = DayZPlayerConstants.STANCEMASK_ERECT | DayZPlayerConstants.STANCEMASK_CROUCH;
|
||||
m_Text = "Search";
|
||||
}
|
||||
|
||||
override typename GetInputType()
|
||||
override string GetText()
|
||||
{
|
||||
return ContinuousInteractActionInput;
|
||||
return "Search";
|
||||
}
|
||||
|
||||
override bool HasZenProxyTarget()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
override string GetZenProxyNameTarget()
|
||||
{
|
||||
return YAInteractionManager.GetSearchProxyNames();
|
||||
}
|
||||
|
||||
override void CreateConditionComponents()
|
||||
{
|
||||
m_ConditionItem = new CCINone();
|
||||
m_ConditionTarget = new CCTNonRuined(UAMaxDistances.DEFAULT);
|
||||
m_ConditionTarget = new CCTCursorParent(UAMaxDistances.SMALL);
|
||||
}
|
||||
|
||||
override bool HasTarget()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty-handed; target must be a registered "search" interactable.
|
||||
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
|
||||
{
|
||||
Object targetObject = target.GetObject();
|
||||
if (!targetObject)
|
||||
return false;
|
||||
|
||||
if (player.GetItemInHands())
|
||||
return false;
|
||||
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
||||
return entry != null && entry.ActionType == YAInteractionConfig.ACTION_SEARCH;
|
||||
return IsTargetingZenActionProxy(player, target, item);
|
||||
}
|
||||
|
||||
override bool IsLockTargetOnUse()
|
||||
|
|
@ -71,71 +66,36 @@ class ActionYASearch : ActionContinuousBase
|
|||
override void OnFinishProgressServer(ActionData action_data)
|
||||
{
|
||||
PlayerBase player = action_data.m_Player;
|
||||
Object targetObject = action_data.m_Target.GetObject();
|
||||
if (!player || !targetObject)
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
|
||||
string proxyType = GetZenProxyTargetType(action_data);
|
||||
vector proxyWS = GetZenProxyTargetWS(action_data);
|
||||
|
||||
YAInteractableEntry entry = YAInteractionManager.GetEntryByProxyName(proxyType);
|
||||
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_SEARCH)
|
||||
return;
|
||||
|
||||
string key = targetObject.GetNetworkIDString();
|
||||
string posKey = proxyWS.ToString(false);
|
||||
|
||||
// --- Per-object cooldown gate ---
|
||||
if (IsOnCooldown(key))
|
||||
if (YAInteractionManager.IsSearchOnCooldown(posKey))
|
||||
{
|
||||
int remaining = GetCooldownRemaining(key);
|
||||
Print(string.Format("[YAInteraction] Search on cooldown (%1s left) for %2", remaining, key));
|
||||
int remaining = YAInteractionManager.GetSearchCooldownRemaining(posKey);
|
||||
player.MessageStatus(string.Format("You already searched this. (%1s)", remaining));
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Roll & spawn ---
|
||||
array<ref YALootEntry> table = YAInteractionManager.GetLootTableForEntry(entry);
|
||||
int rolls = Math.RandomIntInclusive(YAInteractionConfig.SEARCH_ROLLS_MIN, YAInteractionConfig.SEARCH_ROLLS_MAX);
|
||||
array<EntityAI> spawned = YALootTableManager.GetInstance().SpawnFromTable(table, rolls, player, player.GetPosition());
|
||||
|
||||
// Stamp cooldown even on an empty result so the object is gated either way.
|
||||
StampCooldown(key, entry.CooldownSec);
|
||||
YAInteractionManager.StampSearchCooldown(posKey, entry.CooldownSec);
|
||||
|
||||
Print(string.Format("[YAInteraction] %1 searched '%2' -> %3 item(s)", player.GetIdentity().GetId(), targetObject.GetType(), spawned.Count()));
|
||||
Print(string.Format("[YAInteraction] %1 searched proxy '%2' @ %3 -> %4 item(s)", player.GetIdentity().GetId(), proxyType, posKey, spawned.Count()));
|
||||
|
||||
if (spawned.Count() > 0)
|
||||
player.MessageStatus(string.Format("You found %1 item(s).", spawned.Count()));
|
||||
else
|
||||
player.MessageStatus("You found nothing useful.");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Cooldown helpers (the only persistence-aware surface area).
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// TODO (v2): read last-search time from CF storage instead of the static map.
|
||||
protected static bool IsOnCooldown(string key)
|
||||
{
|
||||
int expiry;
|
||||
if (!m_ObjectCooldowns.Find(key, expiry))
|
||||
return false;
|
||||
return GetGame().GetTime() < expiry;
|
||||
}
|
||||
|
||||
protected static int GetCooldownRemaining(string key)
|
||||
{
|
||||
int expiry;
|
||||
if (!m_ObjectCooldowns.Find(key, expiry))
|
||||
return 0;
|
||||
|
||||
int remainingMs = expiry - GetGame().GetTime();
|
||||
if (remainingMs < 0)
|
||||
remainingMs = 0;
|
||||
return remainingMs / 1000;
|
||||
}
|
||||
|
||||
// TODO (v2): persist this stamp via CF so it survives server restarts.
|
||||
protected static void StampCooldown(string key, float cooldownSec)
|
||||
{
|
||||
int expiry = GetGame().GetTime() + (int)(cooldownSec * 1000);
|
||||
m_ObjectCooldowns.Set(key, expiry);
|
||||
Print(string.Format("[YAInteraction] Search cooldown stamped for %1 (%2s)", key, cooldownSec));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
// ============================================================================
|
||||
// YAInteractionConfig - compile-time constants only
|
||||
// ----------------------------------------------------------------------------
|
||||
// The registry, loot tables and scalar tunables have moved to JSON
|
||||
// (YAInteractionSettings + YAInteractionData, loaded by
|
||||
// YAInteractionConfigLoader). What remains here are the engine-side constants
|
||||
// that are NOT user-tunable: action-type / loot-table id strings (which must
|
||||
// match the JSON values), the water liquid type, search roll range and the
|
||||
// action progress-bar durations.
|
||||
// Registry, loot tables and scalar tunables live in JSON (YAInteractionSettings
|
||||
// + YAInteractionData via YAInteractionConfigLoader). These are the engine-side
|
||||
// constants that aren't user-tunable.
|
||||
// ============================================================================
|
||||
class YAInteractionConfig
|
||||
{
|
||||
|
|
@ -19,10 +16,13 @@ class YAInteractionConfig
|
|||
static const string TABLE_FOOD = "FoodTable";
|
||||
static const string TABLE_VENDING = "VendingTable";
|
||||
|
||||
// ZenProxyFramework entity reused as the sink "repaired" marker.
|
||||
static const string SINK_PROXY_ENTITY = "ZenProxySinkTest";
|
||||
|
||||
// Non-JSON engine constants.
|
||||
static const int WATER_LIQUID_TYPE = LIQUID_CLEANWATER;
|
||||
static const int SEARCH_ROLLS_MIN = 1; // weighted picks per search (min)
|
||||
static const int SEARCH_ROLLS_MAX = 2; // weighted picks per search (max)
|
||||
static const int WATER_LIQUID_TYPE = LIQUID_WATER;
|
||||
static const int SEARCH_ROLLS_MIN = 1;
|
||||
static const int SEARCH_ROLLS_MAX = 2;
|
||||
|
||||
// Progress-bar durations (seconds).
|
||||
static const float SEARCH_TIME = 3.0;
|
||||
|
|
|
|||
|
|
@ -102,12 +102,15 @@ class YAInteractionConfigLoader
|
|||
|
||||
YAInteractionData d = new YAInteractionData();
|
||||
|
||||
// --- Interactable registry ---
|
||||
// (classname, actionType, lootTableName, cooldownSec)
|
||||
d.Interactables.Insert(MakeEntry("StaticObj_Furniture_case_sink_a", YAInteractionConfig.ACTION_REPAIR_WATER, "", Settings.SinkPlayerCooldownSec));
|
||||
d.Interactables.Insert(MakeEntry("StaticObj_Furniture_lekarnicka", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_MED, Settings.ObjectCooldownSec));
|
||||
d.Interactables.Insert(MakeEntry("StaticObj_Furniture_fridge", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_FOOD, Settings.ObjectCooldownSec));
|
||||
d.Interactables.Insert(MakeEntry("StaticObj_Furniture_vending_machine", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_VENDING, Settings.ObjectCooldownSec));
|
||||
// --- Interactable registry (keyed by ZenProxyFramework PROXY NAME) ---
|
||||
// (proxyName, actionType, lootTableName, cooldownSec)
|
||||
// "case_sink_a" is confirmed (it's in ZenProxyConstants.SINKS). The search
|
||||
// names are best guesses - confirm via ZPF's proxy dump / debug F-key and
|
||||
// correct them in interactables.json (or here + rebuild).
|
||||
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));
|
||||
d.Interactables.Insert(MakeEntry("fridge", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_FOOD, Settings.ObjectCooldownSec));
|
||||
d.Interactables.Insert(MakeEntry("vending_machine", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_VENDING, Settings.ObjectCooldownSec));
|
||||
|
||||
// --- Loot tables (classnames verified in P:\DZ) ---
|
||||
YALootTable med = MakeTable(YAInteractionConfig.TABLE_MED);
|
||||
|
|
@ -142,10 +145,10 @@ class YAInteractionConfigLoader
|
|||
// --- Factory helpers (data classes have no parameterised ctor so the
|
||||
// JSON serializer can default-construct them). ---
|
||||
|
||||
private static YAInteractableEntry MakeEntry(string className, string actionType, string lootTableName, float cooldownSec)
|
||||
private static YAInteractableEntry MakeEntry(string proxyName, string actionType, string lootTableName, float cooldownSec)
|
||||
{
|
||||
YAInteractableEntry e = new YAInteractableEntry();
|
||||
e.ClassName = className;
|
||||
e.ProxyName = proxyName;
|
||||
e.ActionType = actionType;
|
||||
e.LootTableName = lootTableName;
|
||||
e.CooldownSec = cooldownSec;
|
||||
|
|
|
|||
|
|
@ -1,41 +1,40 @@
|
|||
// ============================================================================
|
||||
// YAInteractionData (+ YALootEntry, YALootTable, YAInteractableEntry)
|
||||
// ----------------------------------------------------------------------------
|
||||
// Serializable model for <ProfileFolder>/YAInteraction/interactables.json:
|
||||
// the interactable registry and all loot tables.
|
||||
// Serializable model for <ProfileFolder>/YAInteraction/interactables.json.
|
||||
//
|
||||
// Field names MUST match the JSON keys exactly. Arrays are initialised inline
|
||||
// with `new array<...>()` so the JSON serializer can populate them and so a
|
||||
// freshly-constructed instance is never null. No explicit constructors: the
|
||||
// serializer instantiates each class via its default constructor.
|
||||
// Interactables are keyed by PROXY NAME - the in-house furniture proxy
|
||||
// selection name reported by ZenProxyFramework (e.g. "case_sink_a", "fridge").
|
||||
// Discover names with ZPF's debug mode (DebugOn + press F) or its dumped
|
||||
// proxy JSON, then set them here.
|
||||
//
|
||||
// Field names MUST match the JSON keys. Arrays are initialised inline so the
|
||||
// serializer can populate them. No explicit constructors (serializer needs the
|
||||
// default ctor).
|
||||
// ============================================================================
|
||||
|
||||
// One row of a loot table. Weight is float to round-trip JSON values like 40.0.
|
||||
class YALootEntry
|
||||
{
|
||||
string ClassName; // item classname
|
||||
float Weight; // relative weight within its table (higher = more likely)
|
||||
int MinQty; // min stack quantity (<=0 => leave item default)
|
||||
int MaxQty; // max stack quantity (<=0 => leave item default)
|
||||
float Weight; // relative weight within its table
|
||||
int MinQty; // min stack qty (<=0 => item default)
|
||||
int MaxQty; // max stack qty (<=0 => item default)
|
||||
}
|
||||
|
||||
// A named loot table: a name plus its weighted entries.
|
||||
class YALootTable
|
||||
{
|
||||
string Name;
|
||||
ref array<ref YALootEntry> Entries = new array<ref YALootEntry>();
|
||||
}
|
||||
|
||||
// One registered interactable object.
|
||||
class YAInteractableEntry
|
||||
{
|
||||
string ClassName; // target object classname
|
||||
string ProxyName; // in-house proxy selection name (ZenProxyFramework)
|
||||
string ActionType; // "search" or "repair_water" (YAInteractionConfig.ACTION_*)
|
||||
string LootTableName; // named loot table (empty for repair_water)
|
||||
float CooldownSec; // search: per-object cooldown / water: per-player cooldown
|
||||
}
|
||||
|
||||
// Root document for interactables.json.
|
||||
class YAInteractionData
|
||||
{
|
||||
ref array<ref YAInteractableEntry> Interactables = new array<ref YAInteractableEntry>();
|
||||
|
|
|
|||
|
|
@ -1,34 +1,39 @@
|
|||
// ============================================================================
|
||||
// YAInteractionManager
|
||||
// ----------------------------------------------------------------------------
|
||||
// Builds runtime lookup maps from the loaded config (YAInteractionConfigLoader
|
||||
// .Data) and owns per-object repair state. Nothing here contains a hardcoded
|
||||
// target classname - everything is derived from the loaded registry.
|
||||
// Builds runtime lookups from the loaded config (keyed by ZenProxyFramework
|
||||
// proxy name) and owns the in-memory cooldowns.
|
||||
//
|
||||
// RebuildLookups() is called by the loader after Data is (re)loaded. Lookups
|
||||
// are also lazily ensured so client-side action injection works even though
|
||||
// the loader's full Init() only runs on the server.
|
||||
// Repair state is NOT tracked here - it's represented by the presence of a
|
||||
// ZenProxySinkTest entity spawned at the sink (ZenProxyFramework pattern), so
|
||||
// it is networked to clients for free.
|
||||
//
|
||||
// Cooldowns (reset on restart):
|
||||
// * Search - per-object, keyed by proxy world-position string.
|
||||
// * Water - per-player, keyed by player UID.
|
||||
// TODO (v2): persist cooldowns via CF, keyed by proxy world position.
|
||||
// ============================================================================
|
||||
class YAInteractionManager
|
||||
{
|
||||
// lowercased classname -> registry entry
|
||||
private static ref map<string, ref YAInteractableEntry> m_EntryByClass = new map<string, ref YAInteractableEntry>();
|
||||
// loot table name -> table
|
||||
private static ref map<string, ref YAInteractableEntry> m_EntryByProxy = new map<string, ref YAInteractableEntry>();
|
||||
private static ref map<string, ref YALootTable> m_TableByName = new map<string, ref YALootTable>();
|
||||
// per-object repair state, keyed by network id string.
|
||||
// TODO (v2): back this with CF persistence so repairs survive a restart.
|
||||
private static ref map<string, bool> m_RepairState = new map<string, bool>();
|
||||
|
||||
private static ref map<string, int> m_SearchCooldowns = new map<string, int>(); // proxyWorldPos -> expiry ms
|
||||
private static ref map<string, int> m_WaterCooldowns = new map<string, int>(); // playerUID -> expiry ms
|
||||
|
||||
// Cached comma-joined proxy-name lists for the actions' GetZenProxyNameTarget().
|
||||
private static string m_SearchProxyNames = "";
|
||||
private static string m_RepairWaterProxyNames = "";
|
||||
|
||||
private static bool m_Ready = false;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Fold the loaded Data into case-insensitive lookup maps.
|
||||
// Called by YAInteractionConfigLoader after (re)loading Data.
|
||||
// ------------------------------------------------------------------
|
||||
static void RebuildLookups()
|
||||
{
|
||||
m_EntryByClass.Clear();
|
||||
m_EntryByProxy.Clear();
|
||||
m_TableByName.Clear();
|
||||
m_SearchProxyNames = "";
|
||||
m_RepairWaterProxyNames = "";
|
||||
|
||||
if (!YAInteractionConfigLoader.Data)
|
||||
{
|
||||
|
|
@ -39,11 +44,17 @@ class YAInteractionManager
|
|||
|
||||
foreach (YAInteractableEntry e : YAInteractionConfigLoader.Data.Interactables)
|
||||
{
|
||||
if (!e || e.ClassName == "")
|
||||
if (!e || e.ProxyName == "")
|
||||
continue;
|
||||
string key = e.ClassName;
|
||||
|
||||
string key = e.ProxyName;
|
||||
key.ToLower();
|
||||
m_EntryByClass.Set(key, e);
|
||||
m_EntryByProxy.Set(key, e);
|
||||
|
||||
if (e.ActionType == YAInteractionConfig.ACTION_SEARCH)
|
||||
m_SearchProxyNames = AppendName(m_SearchProxyNames, e.ProxyName);
|
||||
else if (e.ActionType == YAInteractionConfig.ACTION_REPAIR_WATER)
|
||||
m_RepairWaterProxyNames = AppendName(m_RepairWaterProxyNames, e.ProxyName);
|
||||
}
|
||||
|
||||
foreach (YALootTable t : YAInteractionConfigLoader.Data.LootTables)
|
||||
|
|
@ -54,100 +65,115 @@ class YAInteractionManager
|
|||
}
|
||||
|
||||
m_Ready = true;
|
||||
Print(string.Format("[YAInteraction] Lookups built: %1 interactable(s), %2 loot table(s)", m_EntryByClass.Count(), m_TableByName.Count()));
|
||||
Print(string.Format("[YAInteraction] Lookups built: %1 interactable(s), %2 loot table(s). search='%3' repair_water='%4'",
|
||||
m_EntryByProxy.Count(), m_TableByName.Count(), m_SearchProxyNames, m_RepairWaterProxyNames));
|
||||
}
|
||||
|
||||
private static string AppendName(string csv, string name)
|
||||
{
|
||||
if (csv == "")
|
||||
return name;
|
||||
return csv + "," + name;
|
||||
}
|
||||
|
||||
// Ensure config + lookups exist. On the server Init() has already run; on a
|
||||
// client this lazily builds in-code defaults (no file I/O).
|
||||
private static void EnsureReady()
|
||||
{
|
||||
if (m_Ready)
|
||||
return;
|
||||
|
||||
YAInteractionConfigLoader.EnsureDefaults();
|
||||
RebuildLookups();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Registry lookups.
|
||||
// Proxy-name targeting lists for the actions.
|
||||
// ------------------------------------------------------------------
|
||||
static YAInteractableEntry GetEntry(string className)
|
||||
static string GetSearchProxyNames()
|
||||
{
|
||||
EnsureReady();
|
||||
|
||||
if (className == "")
|
||||
return null;
|
||||
|
||||
string key = className;
|
||||
key.ToLower();
|
||||
|
||||
YAInteractableEntry e;
|
||||
if (m_EntryByClass.Find(key, e))
|
||||
return e;
|
||||
return null;
|
||||
return m_SearchProxyNames;
|
||||
}
|
||||
|
||||
static bool IsInteractable(string className)
|
||||
static string GetRepairWaterProxyNames()
|
||||
{
|
||||
return GetEntry(className) != null;
|
||||
EnsureReady();
|
||||
return m_RepairWaterProxyNames;
|
||||
}
|
||||
|
||||
// Case-insensitive lookup by proxy name.
|
||||
static YAInteractableEntry GetEntryByProxyName(string proxyName)
|
||||
{
|
||||
EnsureReady();
|
||||
if (proxyName == "")
|
||||
return null;
|
||||
string key = proxyName;
|
||||
key.ToLower();
|
||||
YAInteractableEntry e;
|
||||
if (m_EntryByProxy.Find(key, e))
|
||||
return e;
|
||||
return null;
|
||||
}
|
||||
|
||||
static array<ref YALootEntry> GetLootTableForEntry(YAInteractableEntry entry)
|
||||
{
|
||||
EnsureReady();
|
||||
|
||||
if (!entry || entry.LootTableName == "")
|
||||
return null;
|
||||
|
||||
YALootTable t;
|
||||
if (m_TableByName.Find(entry.LootTableName, t))
|
||||
return t.Entries;
|
||||
|
||||
Print(string.Format("[YAInteraction] Loot table '%1' referenced by '%2' not found", entry.LootTableName, entry.ClassName));
|
||||
Print(string.Format("[YAInteraction] Loot table '%1' for proxy '%2' not found", entry.LootTableName, entry.ProxyName));
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Action injection mapping. Returns the action typenames to register
|
||||
// on a classname. Keeps YAInteractionInit free of classname strings.
|
||||
// Search cooldown (per object, keyed by proxy world position).
|
||||
// ------------------------------------------------------------------
|
||||
static array<typename> GetActionTypesFor(string className)
|
||||
static bool IsSearchOnCooldown(string posKey)
|
||||
{
|
||||
array<typename> result = new array<typename>();
|
||||
int expiry;
|
||||
if (!m_SearchCooldowns.Find(posKey, expiry))
|
||||
return false;
|
||||
return GetGame().GetTime() < expiry;
|
||||
}
|
||||
|
||||
YAInteractableEntry entry = GetEntry(className);
|
||||
if (!entry)
|
||||
return result;
|
||||
static int GetSearchCooldownRemaining(string posKey)
|
||||
{
|
||||
int expiry;
|
||||
if (!m_SearchCooldowns.Find(posKey, expiry))
|
||||
return 0;
|
||||
int rem = expiry - GetGame().GetTime();
|
||||
if (rem < 0) rem = 0;
|
||||
return rem / 1000;
|
||||
}
|
||||
|
||||
if (entry.ActionType == YAInteractionConfig.ACTION_SEARCH)
|
||||
{
|
||||
result.Insert(ActionYASearch);
|
||||
}
|
||||
else if (entry.ActionType == YAInteractionConfig.ACTION_REPAIR_WATER)
|
||||
{
|
||||
// Both always registered; their ActionCondition gates which is usable.
|
||||
result.Insert(ActionYARepairSink);
|
||||
result.Insert(ActionYAGatherWater);
|
||||
}
|
||||
|
||||
return result;
|
||||
static void StampSearchCooldown(string posKey, float cooldownSec)
|
||||
{
|
||||
m_SearchCooldowns.Set(posKey, GetGame().GetTime() + (int)(cooldownSec * 1000));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Per-object repair state (sinks).
|
||||
// TODO (v2): read/write through CF persistence instead of the static map.
|
||||
// Water cooldown (per player).
|
||||
// ------------------------------------------------------------------
|
||||
static bool IsRepaired(string networkKey)
|
||||
static bool IsWaterOnCooldown(string uid)
|
||||
{
|
||||
bool repaired;
|
||||
if (m_RepairState.Find(networkKey, repaired))
|
||||
return repaired;
|
||||
return false;
|
||||
int expiry;
|
||||
if (!m_WaterCooldowns.Find(uid, expiry))
|
||||
return false;
|
||||
return GetGame().GetTime() < expiry;
|
||||
}
|
||||
|
||||
static void SetRepaired(string networkKey, bool value = true)
|
||||
static int GetWaterCooldownRemaining(string uid)
|
||||
{
|
||||
m_RepairState.Set(networkKey, value);
|
||||
Print(string.Format("[YAInteraction] Repair state for %1 set to %2", networkKey, value));
|
||||
int expiry;
|
||||
if (!m_WaterCooldowns.Find(uid, expiry))
|
||||
return 0;
|
||||
int rem = expiry - GetGame().GetTime();
|
||||
if (rem < 0) rem = 0;
|
||||
return rem / 1000;
|
||||
}
|
||||
|
||||
static void StampWaterCooldown(string uid, float cooldownSec)
|
||||
{
|
||||
m_WaterCooldowns.Set(uid, GetGame().GetTime() + (int)(cooldownSec * 1000));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,65 @@
|
|||
// ============================================================================
|
||||
// YAInteractionInit
|
||||
// ----------------------------------------------------------------------------
|
||||
// Injects the mod's actions onto the target objects, driven entirely by the
|
||||
// config registry. There are no classname strings and no action-type branching
|
||||
// here - YAInteractionManager.GetActionTypesFor() returns exactly which actions
|
||||
// to register for a given classname.
|
||||
// Wires the actions into the game, following the ZenProxyFramework pattern:
|
||||
//
|
||||
// BASE CLASS - verified against P:\scripts and P:\DZ:
|
||||
// The four targets are config classes that inherit:
|
||||
// StaticObj_Furniture_* -> HouseNoDestruct -> House -> Static
|
||||
// There is NO script class named StaticObj or HouseNoDestruct; "House" is the
|
||||
// first script-backed ancestor (House : BuildingBase). So every one of these
|
||||
// objects resolves to the BuildingBase script class - that is where we hook.
|
||||
// * ActionConstructor - register custom actions so GetAction() resolves them
|
||||
// (otherwise AddAction() dereferences null -> crash).
|
||||
// * PlayerBase - Search (empty-handed; the only player-side action).
|
||||
// * Wrench / PipeWrench - Repair Sink (item-in-hands; load-order safe).
|
||||
// * Bottle_Base - Gather Water (item-in-hands; load-order safe).
|
||||
//
|
||||
// We override SetActions() rather than GetActions(): BuildingBase implements
|
||||
// a GetActions()/SetActions() split where GetActions() lazily calls
|
||||
// SetActions() ONCE and caches the result per object type. SetActions() is
|
||||
// therefore the correct extension point. (Overriding GetActions() directly
|
||||
// would re-append to the cached array on every query and duplicate actions.)
|
||||
// Calling super.SetActions() first preserves any inherited/base actions.
|
||||
// Furniture identification + position sync is handled by ZenProxyFramework's
|
||||
// modded ActionBase (GetZenProxyNameTarget / GetZenProxyTargetWS).
|
||||
// ============================================================================
|
||||
|
||||
modded class BuildingBase
|
||||
modded class ActionConstructor
|
||||
{
|
||||
override void RegisterActions(TTypenameArray actions)
|
||||
{
|
||||
super.RegisterActions(actions);
|
||||
|
||||
actions.Insert(ActionYASearch);
|
||||
actions.Insert(ActionYARepairSink);
|
||||
actions.Insert(ActionYAGatherWater);
|
||||
}
|
||||
}
|
||||
|
||||
modded class PlayerBase
|
||||
{
|
||||
override void SetActions(out TInputActionMap InputActionMap)
|
||||
{
|
||||
super.SetActions(InputActionMap);
|
||||
|
||||
AddAction(ActionYASearch, InputActionMap);
|
||||
|
||||
Print("[YAInteraction] Registered player action (Search)");
|
||||
}
|
||||
}
|
||||
|
||||
modded class Wrench
|
||||
{
|
||||
override void SetActions()
|
||||
{
|
||||
super.SetActions(); // preserve inherited actions (doors, fireplaces, etc.)
|
||||
|
||||
array<typename> actionsToAdd = YAInteractionManager.GetActionTypesFor(GetType());
|
||||
foreach (typename actionType : actionsToAdd)
|
||||
{
|
||||
AddAction(actionType);
|
||||
Print("[YAInteraction] Registered " + actionType + " on '" + GetType() + "'");
|
||||
}
|
||||
super.SetActions();
|
||||
AddAction(ActionYARepairSink);
|
||||
}
|
||||
}
|
||||
|
||||
modded class PipeWrench
|
||||
{
|
||||
override void SetActions()
|
||||
{
|
||||
super.SetActions();
|
||||
AddAction(ActionYARepairSink);
|
||||
}
|
||||
}
|
||||
|
||||
modded class Bottle_Base
|
||||
{
|
||||
override void SetActions()
|
||||
{
|
||||
super.SetActions();
|
||||
AddAction(ActionYAGatherWater);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue