Replace EnScript config with JSON-based system

Move the registry, loot tables and scalar tunables out of hardcoded EnScript
into JSON loaded via DayZ's JsonFileLoader. Defaults are written to
<ProfileFolder>/YAInteraction/{settings.json,interactables.json} on first start
and read back (overriding code defaults) on subsequent starts.

- Add YAInteractionSettings (settings.json) and YAInteractionData
  (interactables.json) serializable classes with inline defaults
- Add YAInteractionConfigLoader singleton: MakeDirectory + FileExist driven
  load-or-save, plus client-safe in-code default fallback (EnsureDefaults)
- Load server-side via modded MissionServer.OnInit before players connect
- Gut YAInteractionConfig down to compile-time constants
- Rebuild manager lookups from loaded Data; update field names and float weights
- Remove redundant 5_Mission placeholder; update README

CF persistence and config hot-reload remain marked with // TODO (v2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-18 21:45:56 +03:00
parent ac90535b5e
commit c1cd01f224
12 changed files with 383 additions and 219 deletions

View file

@ -9,9 +9,10 @@ two-stage repairable sink (repair, then gather water).
random items from weighted, config-defined loot tables. Per-object cooldown.
- **Repairable sink** — repair with a `Wrench`/`PipeWrench`, then gather water
into a held/inventory container. Per-player cooldown, configurable amount.
- **Data-driven** — every interactable is one line in
[`YAInteractionConfig`](scripts/4_World/YAInteraction/Config/YAInteractionConfig.c).
No target/loot classnames exist anywhere else in the codebase.
- **JSON-configured** — interactables, loot tables and scalar tunables live in
JSON under `<ProfileFolder>/YAInteraction/`. Defaults are written on first
start and read (overriding defaults) on every start after. No target/loot
classnames exist anywhere else in the codebase.
## Target objects (v1)
@ -22,30 +23,48 @@ two-stage repairable sink (repair, then gather water).
| `StaticObj_Furniture_fridge` | Search | FoodTable |
| `StaticObj_Furniture_vending_machine` | Search | VendingTable |
## Configuration files
Generated on first start under `<ProfileFolder>/YAInteraction/`:
- **`settings.json`** — scalar tunables: `WaterAmountMin`, `WaterAmountMax`,
`SinkPlayerCooldownSec`, `ObjectCooldownSec`.
- **`interactables.json`** — the `Interactables` registry and all `LootTables`.
Delete a file and restart to regenerate it from code defaults.
## Structure
```
config.cpp / mod.cpp mod + script-module registration, metadata
scripts/3_Game shared game-layer placeholder (v2 hooks)
scripts/4_World/YAInteraction
Config/YAInteractionConfig.c single source of truth (registry + loot tables)
YALootTableManager.c weighted pick + item spawning
Entities/YAInteractionManager.c registry lookup + repair state
Actions/ Search / RepairSink / GatherWater actions
YAInteractionInit.c modded BuildingBase action injection
scripts/5_Mission mission-layer placeholder (v2 hooks)
Config/YAInteractionSettings.c serializable scalar settings (settings.json)
Config/YAInteractionData.c serializable registry + loot tables (interactables.json)
Config/YAInteractionConfigLoader.c JSON load/save + code defaults (singleton)
Config/YAInteractionConfig.c compile-time constants only
YALootTableManager.c weighted pick + item spawning
Entities/YAInteractionManager.c registry lookup + repair state
Actions/ Search / RepairSink / GatherWater actions
YAInteractionInit.c modded BuildingBase action injection
scripts/5_Mission/YAInteraction
YAInteractionMissionInit.c modded MissionServer.OnInit -> load config
```
## Notes
- Config is loaded server-side at `MissionServer.OnInit` (before any player
connects). Clients fall back to in-code defaults for action injection.
- Cooldowns and sink repair state are tracked in memory and **reset on server
restart**. CF persistence hooks are marked with `// TODO (v2)`.
- All item and tool classnames are verified against the base game data.
## Adding a new interactable
Add one line to `GetInteractables()` in `YAInteractionConfig.c`:
Edit `interactables.json` (add to `Interactables`, and a `LootTables` entry if
it searches), then restart. To change the built-in defaults, add one line to
`BuildDefaultData()` in `YAInteractionConfigLoader.c`:
```c
m_Interactables.Insert(new YAInteractableEntry("Your_Classname", ACTION_SEARCH, TABLE_FOOD, 7200));
d.Interactables.Insert(MakeEntry("Your_Classname", YAInteractionConfig.ACTION_SEARCH, YAInteractionConfig.TABLE_FOOD, Settings.ObjectCooldownSec));
```

View file

@ -58,7 +58,7 @@ class ActionYAGatherWater : ActionContinuousBase
return false;
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
if (!entry || entry.m_ActionType != YAInteractionConfig.ACTION_REPAIR_WATER)
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_REPAIR_WATER)
return false;
if (!YAInteractionManager.IsRepaired(targetObject.GetNetworkIDString()))
@ -108,10 +108,10 @@ class ActionYAGatherWater : ActionContinuousBase
return;
}
float amount = Math.RandomFloatInclusive(YAInteractionConfig.WATER_MIN_AMOUNT, YAInteractionConfig.WATER_MAX_AMOUNT);
float amount = Math.RandomFloatInclusive(YAInteractionConfigLoader.Settings.WaterAmountMin, YAInteractionConfigLoader.Settings.WaterAmountMax);
Liquid.FillContainer(container, YAInteractionConfig.WATER_LIQUID_TYPE, amount);
StampCooldown(uid, entry.m_CooldownSec);
StampCooldown(uid, entry.CooldownSec);
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)));

View file

@ -53,7 +53,7 @@ class ActionYARepairSink : ActionContinuousBase
return false;
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
if (!entry || entry.m_ActionType != YAInteractionConfig.ACTION_REPAIR_WATER)
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_REPAIR_WATER)
return false;
if (!IsRepairTool(item))

View file

@ -60,7 +60,7 @@ class ActionYASearch : ActionContinuousBase
return false;
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
return entry != null && entry.m_ActionType == YAInteractionConfig.ACTION_SEARCH;
return entry != null && entry.ActionType == YAInteractionConfig.ACTION_SEARCH;
}
override bool IsLockTargetOnUse()
@ -76,7 +76,7 @@ class ActionYASearch : ActionContinuousBase
return;
YAInteractableEntry entry = YAInteractionManager.GetEntry(targetObject.GetType());
if (!entry || entry.m_ActionType != YAInteractionConfig.ACTION_SEARCH)
if (!entry || entry.ActionType != YAInteractionConfig.ACTION_SEARCH)
return;
string key = targetObject.GetNetworkIDString();
@ -96,7 +96,7 @@ class ActionYASearch : ActionContinuousBase
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.m_CooldownSec);
StampCooldown(key, entry.CooldownSec);
Print(string.Format("[YAInteraction] %1 searched '%2' -> %3 item(s)", player.GetIdentity().GetId(), targetObject.GetType(), spawned.Count()));

View file

@ -1,159 +1,31 @@
// ============================================================================
// YAInteractionConfig - SINGLE SOURCE OF TRUTH
// YAInteractionConfig - compile-time constants only
// ----------------------------------------------------------------------------
// Everything tunable lives here. To add a new interactable object you add ONE
// line to GetInteractables() (and, if it searches, define/extend a loot table).
// No other file in the mod contains a target classname or loot classname.
//
// YAInteractableEntry - one registered object (classname -> behaviour)
// YALootEntry - one row of a named loot table
//
// Cooldowns / repair state are tracked in memory and reset on server restart.
// TODO (v2): CF persistence hooks are marked in YAInteractionManager and the
// action classes - this config stays the built-in default set.
// 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.
// ============================================================================
// One row of a loot table: what to spawn, how likely, and quantity range.
class YALootEntry
{
string m_ClassName; // item classname (verified to exist in P:\DZ)
int m_Weight; // relative weight within its table (higher = more likely)
int m_MinQty; // min stack quantity (<=0 => leave item default)
int m_MaxQty; // max stack quantity (<=0 => leave item default)
void YALootEntry(string className, int weight, int minQty = 0, int maxQty = 0)
{
m_ClassName = className;
m_Weight = weight;
m_MinQty = minQty;
m_MaxQty = maxQty;
}
}
// One registered interactable object.
class YAInteractableEntry
{
string m_ClassName; // target object classname
string m_ActionType; // YAInteractionConfig.ACTION_SEARCH or ACTION_REPAIR_WATER
string m_LootTableName; // named loot table (empty for repair_water)
float m_CooldownSec; // search: per-object cooldown / water: per-player cooldown
void YAInteractableEntry(string className, string actionType, string lootTableName, float cooldownSec)
{
m_ClassName = className;
m_ActionType = actionType;
m_LootTableName = lootTableName;
m_CooldownSec = cooldownSec;
}
}
class YAInteractionConfig
{
// ---- Action type ids (referenced instead of magic strings) -----------
// Action type ids - must match YAInteractableEntry.ActionType in the JSON.
static const string ACTION_SEARCH = "search";
static const string ACTION_REPAIR_WATER = "repair_water";
// ---- Loot table names ------------------------------------------------
// Loot table name ids - must match YALootTable.Name in the JSON.
static const string TABLE_MED = "MedTable";
static const string TABLE_FOOD = "FoodTable";
static const string TABLE_VENDING = "VendingTable";
// ---- Global tunables -------------------------------------------------
static const float WATER_MIN_AMOUNT = 50.0; // ml added per gather (min)
static const float WATER_MAX_AMOUNT = 200.0; // ml added per gather (max)
// 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 SEARCH_ROLLS_MIN = 1; // weighted picks per search (min)
static const int SEARCH_ROLLS_MAX = 2; // weighted picks per search (max)
// Progress-bar durations (seconds) for each action.
// Progress-bar durations (seconds).
static const float SEARCH_TIME = 3.0;
static const float GATHER_WATER_TIME = 4.0;
static const float REPAIR_TIME = 5.0;
// ====================================================================
// THE REGISTRY - add one YAInteractableEntry line per new object.
// (classname, actionType, lootTableName, cooldownSec)
// ====================================================================
private static ref array<ref YAInteractableEntry> m_Interactables;
static array<ref YAInteractableEntry> GetInteractables()
{
if (!m_Interactables)
{
m_Interactables = new array<ref YAInteractableEntry>();
// ---------- ADD NEW INTERACTABLES BELOW (one line each) ----------
m_Interactables.Insert(new YAInteractableEntry("StaticObj_Furniture_case_sink_a", ACTION_REPAIR_WATER, "", 300)); // 5 min per-player water cooldown
m_Interactables.Insert(new YAInteractableEntry("StaticObj_Furniture_lekarnicka", ACTION_SEARCH, TABLE_MED, 7200)); // 2 h per-object
m_Interactables.Insert(new YAInteractableEntry("StaticObj_Furniture_fridge", ACTION_SEARCH, TABLE_FOOD, 7200));
m_Interactables.Insert(new YAInteractableEntry("StaticObj_Furniture_vending_machine", ACTION_SEARCH, TABLE_VENDING, 7200));
// -----------------------------------------------------------------
}
return m_Interactables;
}
// ---- Named loot tables ----------------------------------------------
private static ref map<string, ref array<ref YALootEntry>> m_LootTables;
static array<ref YALootEntry> GetLootTable(string name)
{
if (!m_LootTables)
BuildLootTables();
array<ref YALootEntry> table;
if (m_LootTables.Find(name, table))
return table;
Print(string.Format("[YAInteraction] WARNING: loot table '%1' not found", name));
return null;
}
private static void BuildLootTables()
{
m_LootTables = new map<string, ref array<ref YALootEntry>>();
m_LootTables.Set(TABLE_MED, BuildMedTable());
m_LootTables.Set(TABLE_FOOD, BuildFoodTable());
m_LootTables.Set(TABLE_VENDING, BuildVendingTable());
}
// --- Table definitions (all classnames verified in P:\DZ) -------------
private static array<ref YALootEntry> BuildMedTable()
{
array<ref YALootEntry> t = new array<ref YALootEntry>();
t.Insert(new YALootEntry("BandageDressing", 40, 1, 2));
t.Insert(new YALootEntry("CharcoalTablets", 25, 1, 1));
t.Insert(new YALootEntry("TetracyclineAntibiotics", 15, 1, 1));
t.Insert(new YALootEntry("SalineBag", 10, 1, 1));
t.Insert(new YALootEntry("Morphine", 6, 1, 1)); // rarer
t.Insert(new YALootEntry("Epinephrine", 4, 1, 1)); // rarest
return t;
}
private static array<ref YALootEntry> BuildFoodTable()
{
array<ref YALootEntry> t = new array<ref YALootEntry>();
t.Insert(new YALootEntry("Apple", 30, 1, 2));
t.Insert(new YALootEntry("BakedBeansCan", 20, 1, 1)); // "can of beans"
t.Insert(new YALootEntry("SardinesCan", 15, 1, 1)); // "can of sardines"
t.Insert(new YALootEntry("Rice", 15, 1, 1));
// NOTE: vanilla has no "dried banana" - using Banana (closest match in P:\DZ).
t.Insert(new YALootEntry("Banana", 12, 1, 2));
t.Insert(new YALootEntry("PowderedMilk", 8, 1, 1));
return t;
}
private static array<ref YALootEntry> BuildVendingTable()
{
array<ref YALootEntry> t = new array<ref YALootEntry>();
// NOTE: "SodaCan_Lemonade" does not exist in P:\DZ; using verified vanilla
// flavours. "PET_BottledWater" does not exist; the water bottle is WaterBottle.
t.Insert(new YALootEntry("SodaCan_Cola", 20));
t.Insert(new YALootEntry("SodaCan_Spite", 20));
t.Insert(new YALootEntry("SodaCan_Fronta", 20));
t.Insert(new YALootEntry("SodaCan_Pipsi", 15));
t.Insert(new YALootEntry("WaterBottle", 25));
return t;
}
}

View file

@ -0,0 +1,167 @@
// ============================================================================
// YAInteractionConfigLoader
// ----------------------------------------------------------------------------
// Owns the loaded configuration (Settings + Data) and the JSON load/save flow.
//
// Behaviour:
// * First server start (no JSON present): write code defaults to
// <ProfileFolder>/YAInteraction/{settings.json,interactables.json}.
// * Subsequent starts: read those files; values present in JSON override the
// inline/code defaults.
//
// Init() is called server-side from modded MissionServer.OnInit(), before any
// player connects (see YAInteractionMissionInit.c).
//
// Clients never run MissionServer.OnInit and have no JSON in their profile, so
// EnsureDefaults() provides the in-code default registry without touching files
// - enough for client-side action injection (loot/cooldowns are server-only).
// TODO (v2): sync the registry to clients so admin JSON edits to Interactables
// are reflected client-side without shipping a new build.
// ============================================================================
class YAInteractionConfigLoader
{
static ref YAInteractionSettings Settings;
static ref YAInteractionData Data;
private static const string DIR_NAME = "YAInteraction";
private static const string SETTINGS_FILE = "settings.json";
private static const string DATA_FILE = "interactables.json";
// ------------------------------------------------------------------
// Full server-side init: build paths, ensure dir, load-or-save each
// file, then rebuild the manager lookups.
// ------------------------------------------------------------------
static void Init()
{
string folder = GetGame().GetProfileFolder() + DIR_NAME + "/"; // GetProfileFolder() has trailing slash
MakeDirectory(folder); // no-op / harmless if it already exists
string settingsPath = folder + SETTINGS_FILE;
string dataPath = folder + DATA_FILE;
// --- settings.json (per-key override of inline defaults) ---
Settings = new YAInteractionSettings(); // inline defaults
if (FileExist(settingsPath))
{
JsonFileLoader<YAInteractionSettings>.JsonLoadFile(settingsPath, Settings);
Print("[YAInteraction] Loaded settings from " + settingsPath);
}
else
{
JsonFileLoader<YAInteractionSettings>.JsonSaveFile(settingsPath, Settings);
Print("[YAInteraction] No settings.json - wrote defaults to " + settingsPath);
}
// --- interactables.json (full registry/loot definition) ---
if (FileExist(dataPath))
{
Data = new YAInteractionData(); // fresh empty arrays, fully defined by JSON
JsonFileLoader<YAInteractionData>.JsonLoadFile(dataPath, Data);
Print(string.Format("[YAInteraction] Loaded interactables from %1 (%2 entries, %3 tables)", dataPath, Data.Interactables.Count(), Data.LootTables.Count()));
}
else
{
Data = BuildDefaultData();
JsonFileLoader<YAInteractionData>.JsonSaveFile(dataPath, Data);
Print(string.Format("[YAInteraction] No interactables.json - wrote defaults to %1 (%2 entries, %3 tables)", dataPath, Data.Interactables.Count(), Data.LootTables.Count()));
}
YAInteractionManager.RebuildLookups();
// TODO (v2): hot-reload - re-run Init() + RebuildLookups() on an admin
// command or file watch so JSON edits apply without a server restart.
}
// ------------------------------------------------------------------
// Client-safe fallback: ensure Settings/Data exist from code defaults
// without any file I/O. Used when Init() hasn't run (i.e. on clients).
// ------------------------------------------------------------------
static void EnsureDefaults()
{
if (!Settings)
Settings = new YAInteractionSettings();
if (!Data)
{
Data = BuildDefaultData();
Print("[YAInteraction] EnsureDefaults: built in-code default registry (no JSON / client side)");
}
}
// ==================================================================
// Code defaults - the same items as the original config. This is the
// single place defaults are defined; edit here, delete the JSON, and
// restart to regenerate.
// ==================================================================
static YAInteractionData BuildDefaultData()
{
if (!Settings)
Settings = new YAInteractionSettings(); // default cooldowns feed the default entries below
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));
// --- Loot tables (classnames verified in P:\DZ) ---
YALootTable med = MakeTable(YAInteractionConfig.TABLE_MED);
med.Entries.Insert(MakeLoot("BandageDressing", 40, 1, 2));
med.Entries.Insert(MakeLoot("CharcoalTablets", 25, 1, 1));
med.Entries.Insert(MakeLoot("TetracyclineAntibiotics", 15, 1, 1));
med.Entries.Insert(MakeLoot("SalineBag", 10, 1, 1));
med.Entries.Insert(MakeLoot("Morphine", 6, 1, 1));
med.Entries.Insert(MakeLoot("Epinephrine", 4, 1, 1));
d.LootTables.Insert(med);
YALootTable food = MakeTable(YAInteractionConfig.TABLE_FOOD);
food.Entries.Insert(MakeLoot("Apple", 30, 1, 2));
food.Entries.Insert(MakeLoot("BakedBeansCan", 20, 1, 1));
food.Entries.Insert(MakeLoot("SardinesCan", 15, 1, 1));
food.Entries.Insert(MakeLoot("Rice", 15, 1, 1));
food.Entries.Insert(MakeLoot("Banana", 12, 1, 2)); // vanilla has no "dried banana"
food.Entries.Insert(MakeLoot("PowderedMilk", 8, 1, 1));
d.LootTables.Insert(food);
YALootTable vending = MakeTable(YAInteractionConfig.TABLE_VENDING);
vending.Entries.Insert(MakeLoot("SodaCan_Cola", 20, 0, 0));
vending.Entries.Insert(MakeLoot("SodaCan_Spite", 20, 0, 0));
vending.Entries.Insert(MakeLoot("SodaCan_Fronta", 20, 0, 0));
vending.Entries.Insert(MakeLoot("SodaCan_Pipsi", 15, 0, 0));
vending.Entries.Insert(MakeLoot("WaterBottle", 25, 0, 0)); // "PET_BottledWater" does not exist
d.LootTables.Insert(vending);
return d;
}
// --- 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)
{
YAInteractableEntry e = new YAInteractableEntry();
e.ClassName = className;
e.ActionType = actionType;
e.LootTableName = lootTableName;
e.CooldownSec = cooldownSec;
return e;
}
private static YALootTable MakeTable(string name)
{
YALootTable t = new YALootTable();
t.Name = name;
return t;
}
private static YALootEntry MakeLoot(string className, float weight, int minQty, int maxQty)
{
YALootEntry e = new YALootEntry();
e.ClassName = className;
e.Weight = weight;
e.MinQty = minQty;
e.MaxQty = maxQty;
return e;
}
}

View file

@ -0,0 +1,43 @@
// ============================================================================
// YAInteractionData (+ YALootEntry, YALootTable, YAInteractableEntry)
// ----------------------------------------------------------------------------
// Serializable model for <ProfileFolder>/YAInteraction/interactables.json:
// the interactable registry and all loot tables.
//
// 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.
// ============================================================================
// 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)
}
// 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 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>();
ref array<ref YALootTable> LootTables = new array<ref YALootTable>();
}

View file

@ -0,0 +1,20 @@
// ============================================================================
// YAInteractionSettings
// ----------------------------------------------------------------------------
// Serializable scalar tunables, mapped 1:1 to <ProfileFolder>/YAInteraction/
// settings.json. Field names MUST match the JSON keys exactly (the JSON
// serializer matches by field name).
//
// Defaults are set inline so a fresh instance is already valid; on load, keys
// present in the JSON override these, keys absent keep the default.
//
// Used with JsonFileLoader<YAInteractionSettings> in YAInteractionConfigLoader.
// No explicit constructor: the serializer instantiates via the default ctor.
// ============================================================================
class YAInteractionSettings
{
float WaterAmountMin = 100.0; // min ml added per water gather
float WaterAmountMax = 500.0; // max ml added per water gather
float SinkPlayerCooldownSec = 300.0; // default per-player sink cooldown (5 min)
float ObjectCooldownSec = 7200.0; // default per-object search cooldown (2 h)
}

View file

@ -1,45 +1,71 @@
// ============================================================================
// YAInteractionManager
// ----------------------------------------------------------------------------
// Reads the YAInteractionConfig.INTERACTABLES registry once and builds the
// runtime lookup maps. This is the ONLY place that consumes the registry, and
// nothing here contains a hardcoded target classname - everything is derived
// from config. Also owns per-object repair state for sinks.
// 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.
//
// 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.
// ============================================================================
class YAInteractionManager
{
// lowercased classname -> registry entry
private static ref map<string, ref YAInteractableEntry> m_EntryByClass;
private static bool m_Initialized = false;
private static ref map<string, ref YAInteractableEntry> m_EntryByClass = new map<string, ref YAInteractableEntry>();
// loot table name -> table
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, bool> m_RepairState = new map<string, bool>();
private static bool m_Ready = false;
// ------------------------------------------------------------------
// Lazy init: fold the registry into a case-insensitive lookup map.
// Fold the loaded Data into case-insensitive lookup maps.
// Called by YAInteractionConfigLoader after (re)loading Data.
// ------------------------------------------------------------------
static void Init()
static void RebuildLookups()
{
if (m_Initialized)
return;
m_EntryByClass.Clear();
m_TableByName.Clear();
m_EntryByClass = new map<string, ref YAInteractableEntry>();
array<ref YAInteractableEntry> entries = YAInteractionConfig.GetInteractables();
foreach (YAInteractableEntry e : entries)
if (!YAInteractionConfigLoader.Data)
{
if (!e || e.m_ClassName == "")
continue;
string key = e.m_ClassName;
key.ToLower();
m_EntryByClass.Set(key, e);
Print(string.Format("[YAInteraction] Registered interactable '%1' (%2)", e.m_ClassName, e.m_ActionType));
Print("[YAInteraction] RebuildLookups: Data not loaded");
m_Ready = false;
return;
}
m_Initialized = true;
Print(string.Format("[YAInteraction] Manager init complete: %1 interactable(s)", m_EntryByClass.Count()));
foreach (YAInteractableEntry e : YAInteractionConfigLoader.Data.Interactables)
{
if (!e || e.ClassName == "")
continue;
string key = e.ClassName;
key.ToLower();
m_EntryByClass.Set(key, e);
}
foreach (YALootTable t : YAInteractionConfigLoader.Data.LootTables)
{
if (!t || t.Name == "")
continue;
m_TableByName.Set(t.Name, t);
}
m_Ready = true;
Print(string.Format("[YAInteraction] Lookups built: %1 interactable(s), %2 loot table(s)", m_EntryByClass.Count(), m_TableByName.Count()));
}
// 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();
}
// ------------------------------------------------------------------
@ -47,7 +73,7 @@ class YAInteractionManager
// ------------------------------------------------------------------
static YAInteractableEntry GetEntry(string className)
{
Init();
EnsureReady();
if (className == "")
return null;
@ -68,15 +94,22 @@ class YAInteractionManager
static array<ref YALootEntry> GetLootTableForEntry(YAInteractableEntry entry)
{
if (!entry)
EnsureReady();
if (!entry || entry.LootTableName == "")
return null;
return YAInteractionConfig.GetLootTable(entry.m_LootTableName);
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));
return null;
}
// ------------------------------------------------------------------
// Action injection mapping. Returns the action typenames to register
// on a given classname. Keeps YAInteractionInit free of any classname
// strings or action-type branching.
// on a classname. Keeps YAInteractionInit free of classname strings.
// ------------------------------------------------------------------
static array<typename> GetActionTypesFor(string className)
{
@ -86,11 +119,11 @@ class YAInteractionManager
if (!entry)
return result;
if (entry.m_ActionType == YAInteractionConfig.ACTION_SEARCH)
if (entry.ActionType == YAInteractionConfig.ACTION_SEARCH)
{
result.Insert(ActionYASearch);
}
else if (entry.m_ActionType == YAInteractionConfig.ACTION_REPAIR_WATER)
else if (entry.ActionType == YAInteractionConfig.ACTION_REPAIR_WATER)
{
// Both always registered; their ActionCondition gates which is usable.
result.Insert(ActionYARepairSink);

View file

@ -2,7 +2,7 @@
// YALootTableManager
// ----------------------------------------------------------------------------
// Singleton that turns a loot table (array<YALootEntry>) into spawned items.
// Selection is weighted by YALootEntry.m_Weight. Server-authoritative: all
// Selection is weighted by YALootEntry.Weight. Server-authoritative: all
// spawning is expected to run from server-side action code. Items spawn into a
// target container when one is supplied, falling back to the ground.
// ============================================================================
@ -28,24 +28,24 @@ class YALootTableManager
if (!table || table.Count() == 0)
return null;
int totalWeight = 0;
float totalWeight = 0;
foreach (YALootEntry e : table)
{
if (e && e.m_Weight > 0)
totalWeight += e.m_Weight;
if (e && e.Weight > 0)
totalWeight += e.Weight;
}
if (totalWeight <= 0)
return null;
int roll = Math.RandomInt(0, totalWeight); // [0, totalWeight)
int cursor = 0;
float roll = Math.RandomFloat(0, totalWeight); // [0, totalWeight)
float cursor = 0;
foreach (YALootEntry entry : table)
{
if (!entry || entry.m_Weight <= 0)
if (!entry || entry.Weight <= 0)
continue;
cursor += entry.m_Weight;
cursor += entry.Weight;
if (roll < cursor)
return entry;
}
@ -59,30 +59,30 @@ class YALootTableManager
// ------------------------------------------------------------------
EntityAI SpawnEntry(YALootEntry entry, EntityAI container, vector groundPos)
{
if (!entry || entry.m_ClassName == "")
if (!entry || entry.ClassName == "")
return null;
EntityAI spawned;
if (container)
{
spawned = container.GetInventory().CreateInInventory(entry.m_ClassName);
spawned = container.GetInventory().CreateInInventory(entry.ClassName);
if (spawned)
Print(string.Format("[YAInteraction] Spawned '%1' into %2", entry.m_ClassName, container.GetType()));
Print(string.Format("[YAInteraction] Spawned '%1' into %2", entry.ClassName, container.GetType()));
}
// Container full or not provided -> drop on the ground.
if (!spawned)
{
Object obj = GetGame().CreateObjectEx(entry.m_ClassName, groundPos, ECE_PLACE_ON_SURFACE);
Object obj = GetGame().CreateObjectEx(entry.ClassName, groundPos, ECE_PLACE_ON_SURFACE);
spawned = EntityAI.Cast(obj);
if (spawned)
Print(string.Format("[YAInteraction] Spawned '%1' on ground at %2", entry.m_ClassName, groundPos.ToString()));
Print(string.Format("[YAInteraction] Spawned '%1' on ground at %2", entry.ClassName, groundPos.ToString()));
}
if (!spawned)
{
Print(string.Format("[YAInteraction] FAILED to spawn '%1' (bad classname?)", entry.m_ClassName));
Print(string.Format("[YAInteraction] FAILED to spawn '%1' (bad classname?)", entry.ClassName));
return null;
}
@ -121,18 +121,18 @@ class YALootTableManager
// ------------------------------------------------------------------
private void ApplyQuantity(EntityAI ent, YALootEntry entry)
{
if (entry.m_MaxQty <= 0)
if (entry.MaxQty <= 0)
return; // leave the item's default quantity
ItemBase item = ItemBase.Cast(ent);
if (!item || !item.HasQuantity())
return;
int minQ = entry.m_MinQty;
int minQ = entry.MinQty;
if (minQ < 1)
minQ = 1;
int qty = Math.RandomIntInclusive(minQ, entry.m_MaxQty);
int qty = Math.RandomIntInclusive(minQ, entry.MaxQty);
// Clamp to the item's own stack max so a generous table can't overflow.
float maxQ = item.GetQuantityMax();
@ -140,6 +140,6 @@ class YALootTableManager
qty = maxQ;
item.SetQuantity(qty);
Print(string.Format("[YAInteraction] Set quantity %1 on '%2'", qty, entry.m_ClassName));
Print(string.Format("[YAInteraction] Set quantity %1 on '%2'", qty, entry.ClassName));
}
}

View file

@ -1,10 +0,0 @@
// 5_Mission layer placeholder for YAInteraction.
//
// Nothing mission-specific is required in v1; the action injection happens via modded
// entity classes in 4_World. This keeps the declared missionScriptModule non-empty.
//
// TODO (v2): Mission-side hooks for an admin/debug menu (e.g. clearing cooldowns live,
// reloading loot tables) would be wired up here.
class YAInteractionMission
{
}

View file

@ -0,0 +1,20 @@
// ============================================================================
// YAInteractionMissionInit
// ----------------------------------------------------------------------------
// Loads the JSON configuration server-side at mission init, before any player
// connects. MissionServer only exists on the server, which is exactly where we
// want the authoritative config (loot, cooldowns, water amounts) to live.
//
// Clients do not run this; they fall back to in-code defaults for action
// injection via YAInteractionConfigLoader.EnsureDefaults() (see the loader).
// ============================================================================
modded class MissionServer
{
override void OnInit()
{
super.OnInit();
Print("[YAInteraction] MissionServer.OnInit - loading configuration");
YAInteractionConfigLoader.Init();
}
}