Fix open action (register custom action), stop restart-stacking, numeric log level, config docs
Action (the real "no action" cause): - DayZ does not auto-discover custom actions; ActionConstructor holds a hardcoded list and AddAction() silently drops anything missing from it. Added a modded ActionConstructor that registers YALB_ActionOpenLootbox, so the open action finally attaches to the box. - Switched the action target condition CCTObject -> CCTCursor (the look-at pattern vanilla uses for world-object interactions). Restart stacking: - Spawn boxes with CreateObjectEx + ECE_NOPERSISTENCY_WORLD so they are never saved to the world DB and don't survive restarts. - Added CleanupOrphanedBoxes(): on init, scan each spawn location and delete any leftover YALootBoxes_Base before spawning (clears boxes persisted before this change). Logging: - DebugLogLevel changed from a string to a numeric field (0=off 1=ERROR 2=WARN 3=INFO 4=DEBUG 5=VERBOSE), clamped at load. Removed the string parser. NOTE: existing main_config.json has a string value and must be regenerated. Config docs: - Each generated JSON now includes a "_comment" array describing every field and its range (ignored by the loader). README + configfiles.readme updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c6f6cea9ad
commit
ff51bde976
5 changed files with 123 additions and 59 deletions
21
README.md
21
README.md
|
|
@ -33,35 +33,32 @@ All settings live in four JSON files auto-generated on first run at
|
|||
Confirm it loaded by looking for these lines in the server `.RPT`:
|
||||
|
||||
```
|
||||
[YALootBoxes][INFO] Config loaded: 3 box types, 7 spawn locations, 3 loot lists. Log level = INFO.
|
||||
[YALootBoxes][INFO] Config loaded: 3 box types, 7 spawn locations, 3 loot lists. Log level = 3.
|
||||
[YALootBoxes][INFO] Manager initialized. N lootboxes spawned.
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
Logging verbosity is controlled by a single field in `main_config.json`:
|
||||
Logging verbosity is controlled by a single **numeric** field in `main_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"DebugLogLevel": "INFO"
|
||||
"DebugLogLevel": 3
|
||||
}
|
||||
```
|
||||
|
||||
### Why a string?
|
||||
|
||||
The level is stored as a **string** in JSON because it is far friendlier for
|
||||
admins hand-editing the file (`"DEBUG"` reads better than `4`). It is parsed
|
||||
**once at load time** into an integer (`YALB_ConfigLoader.LogLevel`) so every
|
||||
runtime log call is a cheap integer comparison, not a string compare. Parsing
|
||||
is case-insensitive, and an unrecognised value falls back to `INFO` so a typo
|
||||
can never silently disable logging.
|
||||
The value is read once at load into `YALB_ConfigLoader.ActiveLogLevel` and
|
||||
clamped to 0–5, so every runtime log call is a cheap integer comparison.
|
||||
|
||||
### Levels
|
||||
|
||||
Levels are ordered. A message is printed only when the configured level is
|
||||
**at or above** the message's level:
|
||||
|
||||
`NONE(0) < ERROR(1) < WARN(2) < INFO(3) < DEBUG(4) < VERBOSE(5)`
|
||||
`0 = off < 1 = ERROR < 2 = WARN < 3 = INFO < 4 = DEBUG < 5 = VERBOSE`
|
||||
|
||||
Set `DebugLogLevel` to the highest level you want to see; everything more severe
|
||||
is included. Default is `3` (INFO), recommended for production.
|
||||
|
||||
| Level | When to use | Examples in this mod |
|
||||
|-------|-------------|----------------------|
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@
|
|||
// ------------------------------------------------------------
|
||||
// FILE: main_config.json
|
||||
// ------------------------------------------------------------
|
||||
// On first run the mod also writes a "_comment" array into each generated
|
||||
// file describing every field and its range. (Delete the json to regenerate.)
|
||||
{
|
||||
"MaxActiveLootboxes": 30,
|
||||
"GlobalCheckIntervalSeconds": 60,
|
||||
"DefaultDespawnMinutes": 15,
|
||||
"OpenChannelDurationSeconds": 5,
|
||||
"ProximityMessageRangeMeters": 3.0,
|
||||
"DebugLogLevel": "INFO"
|
||||
"MaxActiveLootboxes": 30, // max boxes alive at once (1-100; capped by spawn_locations count)
|
||||
"GlobalCheckIntervalSeconds": 60, // seconds between despawn/respawn checks (10-600)
|
||||
"DefaultDespawnMinutes": 15, // fallback lifetime if a box type omits despawnMinutes (1-1440)
|
||||
"OpenChannelDurationSeconds": 5, // hold-to-open time in seconds (1-30)
|
||||
"ProximityMessageRangeMeters": 3.0,// range of the "Requires: X" hint, meters (1-20)
|
||||
"DebugLogLevel": 3 // 0=off 1=ERROR 2=WARN 3=INFO 4=DEBUG 5=VERBOSE
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ class YALB_ActionOpenLootbox : ActionContinuousBase
|
|||
|
||||
override void CreateConditionComponents()
|
||||
{
|
||||
m_ConditionItem = new CCINone();
|
||||
m_ConditionTarget = new CCTObject(UAMaxDistances.DEFAULT);
|
||||
m_ConditionItem = new CCINone(); // no item-in-hands restriction here
|
||||
m_ConditionTarget = new CCTCursor(UAMaxDistances.DEFAULT); // look-at-object targeting
|
||||
}
|
||||
|
||||
override string GetText()
|
||||
|
|
@ -253,16 +253,27 @@ void YALB_NotifyPlayer(PlayerBase player, string message)
|
|||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// NOTE: Hooking this action onto the lootbox class
|
||||
// ACTION REGISTRATION (REQUIRED)
|
||||
// ------------------------------------------------------------
|
||||
// The cleanest approach for a fully custom action without
|
||||
// touching vanilla configs is to override SetActions() directly
|
||||
// on YALootBoxes_Base:
|
||||
//
|
||||
// override void SetActions()
|
||||
// {
|
||||
// super.SetActions();
|
||||
// AddAction(YALB_ActionOpenLootbox);
|
||||
// }
|
||||
//
|
||||
// This override lives in YALootBoxes_Lootbox.c.
|
||||
// DayZ does NOT auto-discover custom actions. ActionConstructor
|
||||
// holds a hardcoded list of every action; AddAction() looks an
|
||||
// action up there and silently drops it if missing. So a custom
|
||||
// action MUST be added via a modded ActionConstructor, or it will
|
||||
// never attach to anything (this is why the open prompt was
|
||||
// missing even though the box and SetActions() were correct).
|
||||
// ------------------------------------------------------------
|
||||
modded class ActionConstructor
|
||||
{
|
||||
override void RegisterActions(TTypenameArray actions)
|
||||
{
|
||||
super.RegisterActions(actions);
|
||||
actions.Insert(YALB_ActionOpenLootbox);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// NOTE: the action is attached to the box via SetActions()/
|
||||
// AddAction() in YALootBoxes_Lootbox.c. ContinuousInteractActionInput
|
||||
// detects actions from the TARGET object you look at, so registering
|
||||
// it on the box (not the held tool) is correct.
|
||||
// ------------------------------------------------------------
|
||||
|
|
@ -15,15 +15,13 @@ const string YALB_SPAWN_LOCATIONS_FILE = YALB_CONFIG_DIR + "spawn_locations.js
|
|||
// ------------------------------------------------------------
|
||||
class YALB_MainConfig
|
||||
{
|
||||
ref array<string> _comment; // admin-facing field docs (ignored by logic)
|
||||
int MaxActiveLootboxes;
|
||||
int GlobalCheckIntervalSeconds;
|
||||
int DefaultDespawnMinutes;
|
||||
int OpenChannelDurationSeconds;
|
||||
float ProximityMessageRangeMeters;
|
||||
// Logging verbosity. Stored as a STRING in JSON (admin-friendly to hand-edit)
|
||||
// and parsed once at load into YALB_ConfigLoader.LogLevel (an int) for cheap
|
||||
// runtime comparisons. Valid: NONE, ERROR, WARN, INFO, DEBUG, VERBOSE.
|
||||
string DebugLogLevel;
|
||||
int DebugLogLevel; // numeric verbosity, see _comment / YALB_LogLevel
|
||||
|
||||
void YALB_MainConfig()
|
||||
{
|
||||
|
|
@ -33,7 +31,15 @@ class YALB_MainConfig
|
|||
DefaultDespawnMinutes = 15;
|
||||
OpenChannelDurationSeconds = 5;
|
||||
ProximityMessageRangeMeters = 3.0;
|
||||
DebugLogLevel = "INFO";
|
||||
DebugLogLevel = 3; // INFO
|
||||
_comment = {
|
||||
"MaxActiveLootboxes: max boxes alive at once (1-100; also capped by number of spawn_locations).",
|
||||
"GlobalCheckIntervalSeconds: seconds between despawn/respawn checks (10-600).",
|
||||
"DefaultDespawnMinutes: fallback box lifetime if a box type omits despawnMinutes (1-1440).",
|
||||
"OpenChannelDurationSeconds: hold-to-open time in seconds (1-30).",
|
||||
"ProximityMessageRangeMeters: range of the 'Requires: X' hint, in meters (1-20).",
|
||||
"DebugLogLevel: 0=off, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE."
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -102,11 +108,24 @@ class YALB_BoxTypeDef
|
|||
|
||||
class YALB_BoxTypesFile
|
||||
{
|
||||
ref array<string> _comment;
|
||||
ref array<ref YALB_BoxTypeDef> BoxTypes;
|
||||
|
||||
void YALB_BoxTypesFile()
|
||||
{
|
||||
BoxTypes = new array<ref YALB_BoxTypeDef>();
|
||||
_comment = {
|
||||
"className: must match a CfgVehicles class in the mod's config.cpp.",
|
||||
"type: id linking this box to a loot_lists entry and to spawn_locations.",
|
||||
"requiredItems: holding ANY one of these enables the open action (classname=item, displayName=shown text).",
|
||||
"openChance: success probability, 0.0-1.0.",
|
||||
"itemDamageOnOpen: damage dealt to the used tool on success (health points, >=0).",
|
||||
"despawnMinutes: box lifetime in minutes, locked or after opening (1-1440).",
|
||||
"failEffects: on failure exactly ONE is picked, weighted by 'chance' (weights need not sum to 1).",
|
||||
" type: EXPLOSIVE | HEALTH | BLEEDING | SICKNESS | BROKEN_BONE.",
|
||||
" damage: HP (EXPLOSIVE/HEALTH). radius: meters (EXPLOSIVE). bleedSources: count (BLEEDING).",
|
||||
" agent: Salmonella|Influenza|Cholera|Brain|Wound_Bacterial (SICKNESS)."
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -139,11 +158,17 @@ class YALB_LootList
|
|||
|
||||
class YALB_LootListsFile
|
||||
{
|
||||
ref array<string> _comment;
|
||||
ref array<ref YALB_LootList> LootLists;
|
||||
|
||||
void YALB_LootListsFile()
|
||||
{
|
||||
LootLists = new array<ref YALB_LootList>();
|
||||
_comment = {
|
||||
"type: must match a box_types 'type'. entries[] are rolled INDEPENDENTLY when a box opens.",
|
||||
"classname: item to spawn. chance: 0.0-1.0 independent probability.",
|
||||
"minCount/maxCount: quantity range when an item is rolled (integers, >=1, max>=min)."
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -166,11 +191,17 @@ class YALB_SpawnLocationDef
|
|||
|
||||
class YALB_SpawnLocationsFile
|
||||
{
|
||||
ref array<string> _comment;
|
||||
ref array<ref YALB_SpawnLocationDef> SpawnLocations;
|
||||
|
||||
void YALB_SpawnLocationsFile()
|
||||
{
|
||||
SpawnLocations = new array<ref YALB_SpawnLocationDef>();
|
||||
_comment = {
|
||||
"name: unique label for this spot. boxType: must match a box_types 'type'.",
|
||||
"position: [x, y, z] world coords. Only one box spawns per location at a time.",
|
||||
"Add as many locations as you like; boxes are placed at a random free one each cycle."
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -256,8 +287,11 @@ class YALB_ConfigLoader
|
|||
else
|
||||
JsonFileLoader<YALB_MainConfig>.JsonLoadFile(YALB_MAIN_CONFIG_FILE, MainConfig);
|
||||
|
||||
// Parse the configured level NOW so every log below honours it.
|
||||
ActiveLogLevel = YALB_ParseLogLevel(MainConfig.DebugLogLevel);
|
||||
// Apply the configured level NOW (clamped to a valid range) so every
|
||||
// log below honours it.
|
||||
ActiveLogLevel = MainConfig.DebugLogLevel;
|
||||
if (ActiveLogLevel < YALB_LogLevel.NONE) ActiveLogLevel = YALB_LogLevel.NONE;
|
||||
if (ActiveLogLevel > YALB_LogLevel.VERBOSE) ActiveLogLevel = YALB_LogLevel.VERBOSE;
|
||||
if (createdMain) Log_Info("main_config.json not found - created with defaults.");
|
||||
|
||||
// --- box types ---
|
||||
|
|
@ -432,25 +466,6 @@ enum YALB_LogLevel
|
|||
VERBOSE = 5
|
||||
};
|
||||
|
||||
// Parse the admin-facing string (case-insensitive) to a YALB_LogLevel int.
|
||||
// Unrecognised values fall back to INFO so a typo never silences the log.
|
||||
int YALB_ParseLogLevel(string raw)
|
||||
{
|
||||
string s = raw;
|
||||
s.ToUpper();
|
||||
switch (s)
|
||||
{
|
||||
case "NONE": return YALB_LogLevel.NONE;
|
||||
case "ERROR": return YALB_LogLevel.ERROR;
|
||||
case "WARN": return YALB_LogLevel.WARN;
|
||||
case "WARNING": return YALB_LogLevel.WARN; // tolerate common variant
|
||||
case "INFO": return YALB_LogLevel.INFO;
|
||||
case "DEBUG": return YALB_LogLevel.DEBUG;
|
||||
case "VERBOSE": return YALB_LogLevel.VERBOSE;
|
||||
}
|
||||
return YALB_LogLevel.INFO;
|
||||
}
|
||||
|
||||
// Shared gate + format for the non-error levels.
|
||||
void YALB_Emit(int level, string tag, string msg)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ class YALootBoxesManager
|
|||
m_FreeLocationNames.Insert(loc.name);
|
||||
}
|
||||
|
||||
// Remove any lootboxes left over from a previous session (e.g. ones
|
||||
// that persisted) BEFORE spawning, so they don't stack on restart.
|
||||
CleanupOrphanedBoxes();
|
||||
|
||||
SpawnInitialBatch();
|
||||
|
||||
// Periodic lifecycle tick (despawn/respawn checks)
|
||||
|
|
@ -55,6 +59,37 @@ class YALootBoxesManager
|
|||
Log_Info("Manager initialized. " + m_ActiveBoxes.Count() + " lootboxes spawned.");
|
||||
}
|
||||
|
||||
// Scans every configured spawn location and deletes any YALootBoxes_Base
|
||||
// found there. Catches boxes that persisted from a previous session so a
|
||||
// restart doesn't stack a fresh set on top of the old one. (New boxes are
|
||||
// also spawned non-persistent below, so this is mostly a one-time cleanup
|
||||
// for boxes created before that change.)
|
||||
protected void CleanupOrphanedBoxes()
|
||||
{
|
||||
int removed = 0;
|
||||
array<Object> nearby = new array<Object>();
|
||||
array<CargoBase> cargos = new array<CargoBase>();
|
||||
|
||||
foreach (YALB_SpawnLocationDef loc : YALB_ConfigLoader.SpawnLocations.SpawnLocations)
|
||||
{
|
||||
nearby.Clear();
|
||||
cargos.Clear();
|
||||
GetGame().GetObjectsAtPosition3D(loc.GetPosition(), 5.0, nearby, cargos);
|
||||
foreach (Object o : nearby)
|
||||
{
|
||||
YALootBoxes_Base box = YALootBoxes_Base.Cast(o);
|
||||
if (box)
|
||||
{
|
||||
GetGame().ObjectDelete(box);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
Log_Info("Removed " + removed + " leftover lootbox(es) from a previous session before spawning.");
|
||||
}
|
||||
|
||||
protected void SpawnInitialBatch()
|
||||
{
|
||||
int target = YALB_ConfigLoader.MainConfig.MaxActiveLootboxes;
|
||||
|
|
@ -89,7 +124,11 @@ class YALootBoxesManager
|
|||
return SpawnOneAtRandomFreeLocation();
|
||||
}
|
||||
|
||||
Object obj = GetGame().CreateObject(typeDef.className, locDef.GetPosition(), false, true, true);
|
||||
// ECE_NOPERSISTENCY_WORLD: never saved to the world DB, so boxes don't
|
||||
// survive a restart and stack. ECE_SETUP gives full entity init (cargo
|
||||
// etc.); ECE_CREATEPHYSICS gives collision/selection geometry.
|
||||
Object obj = GetGame().CreateObjectEx(typeDef.className, locDef.GetPosition(),
|
||||
ECE_SETUP | ECE_CREATEPHYSICS | ECE_NOPERSISTENCY_WORLD);
|
||||
YALootBoxes_Base box = YALootBoxes_Base.Cast(obj);
|
||||
if (!box)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue