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:
parent
ac90535b5e
commit
c1cd01f224
12 changed files with 383 additions and 219 deletions
41
README.md
41
README.md
|
|
@ -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));
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
167
scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c
Normal file
167
scripts/4_World/YAInteraction/Config/YAInteractionConfigLoader.c
Normal 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;
|
||||
}
|
||||
}
|
||||
43
scripts/4_World/YAInteraction/Config/YAInteractionData.c
Normal file
43
scripts/4_World/YAInteraction/Config/YAInteractionData.c
Normal 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>();
|
||||
}
|
||||
20
scripts/4_World/YAInteraction/Config/YAInteractionSettings.c
Normal file
20
scripts/4_World/YAInteraction/Config/YAInteractionSettings.c
Normal 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)
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
20
scripts/5_Mission/YAInteraction/YAInteractionMissionInit.c
Normal file
20
scripts/5_Mission/YAInteraction/YAInteractionMissionInit.c
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue