diff --git a/README.md b/README.md index af86121..7029093 100644 --- a/README.md +++ b/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 | |-------|-------------|----------------------| diff --git a/configfiles.readme b/configfiles.readme index 238ad6a..b7b086b 100644 --- a/configfiles.readme +++ b/configfiles.readme @@ -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 } diff --git a/scripts/4_World/YALootBoxes_ActionOpen.c b/scripts/4_World/YALootBoxes_ActionOpen.c index c7ca931..60816c1 100644 --- a/scripts/4_World/YALootBoxes_ActionOpen.c +++ b/scripts/4_World/YALootBoxes_ActionOpen.c @@ -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. \ No newline at end of file +// 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. +// ------------------------------------------------------------ \ No newline at end of file diff --git a/scripts/4_World/YALootBoxes_Config.c b/scripts/4_World/YALootBoxes_Config.c index 3fb4718..fec750f 100644 --- a/scripts/4_World/YALootBoxes_Config.c +++ b/scripts/4_World/YALootBoxes_Config.c @@ -15,15 +15,13 @@ const string YALB_SPAWN_LOCATIONS_FILE = YALB_CONFIG_DIR + "spawn_locations.js // ------------------------------------------------------------ class YALB_MainConfig { + ref array _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 _comment; ref array BoxTypes; void YALB_BoxTypesFile() { BoxTypes = new array(); + _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 _comment; ref array LootLists; void YALB_LootListsFile() { LootLists = new array(); + _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 _comment; ref array SpawnLocations; void YALB_SpawnLocationsFile() { SpawnLocations = new array(); + _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.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) { diff --git a/scripts/4_World/YALootBoxes_Manager.c b/scripts/4_World/YALootBoxes_Manager.c index 590c16e..8c08890 100644 --- a/scripts/4_World/YALootBoxes_Manager.c +++ b/scripts/4_World/YALootBoxes_Manager.c @@ -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 nearby = new array(); + array cargos = new array(); + + 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) {