Phase 3: leveled logging + green cleanups

Logging:
- Replace EnableDebugLogging (bool) with DebugLogLevel (string) in
  YALB_MainConfig; default "INFO". Stored as a string for admin-friendliness,
  parsed once at load into YALB_ConfigLoader.LogLevel (int) for cheap compares.
- Add YALB_LogLevel enum NONE(0)<ERROR(1)<WARN(2)<INFO(3)<DEBUG(4)<VERBOSE(5),
  YALB_ParseLogLevel (case-insensitive, falls back to INFO), and leveled
  helpers Log_Error/Warn/Info/Debug/Verbose. ERROR also routes via ErrorEx().
- Reclassify every existing log call by content (missing critical config ->
  ERROR, recoverable -> WARN, milestones -> INFO, per-tick summary -> DEBUG,
  per-player/per-roll detail -> VERBOSE). Add a VERBOSE proximity-send log.
- Update main_config.json example and the default constructor to "INFO".
- Add README.md documenting levels, the config field, and the VERBOSE warning.

Green cleanups (from Phase 2):
- Remove dead OnActionInfoUpdate override (behaviour is inherited).
- Remove unused m_LocationInUse map (the live pool is m_FreeLocationNames).
- Null-guard player identity in the fail-effect log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-19 21:57:31 +03:00
parent a84b6b1a2b
commit f64425a840
6 changed files with 173 additions and 40 deletions

78
README.md Normal file
View file

@ -0,0 +1,78 @@
# YALootBoxes
A DayZ server mod that spawns lockable lootboxes at config-defined locations.
Players need a specific held item to attempt opening a box via a hold-to-open
action: success unlocks and fills the box; failure rolls one random negative
effect. Locked boxes despawn/respawn on a timer so the loot stays fresh; opened
boxes despawn a configurable number of minutes after being unlocked.
All settings live in four JSON files auto-generated on first run at
`$profile:YALootBoxes\`:
| File | Purpose |
|------|---------|
| `main_config.json` | Global tunables (limits, timers, logging) |
| `box_types.json` | Box definitions: required items, open chance, fail effects |
| `loot_lists.json` | Per-type loot tables |
| `spawn_locations.json` | Named world positions and which box type spawns there |
> Edit the copies under `$profile:YALootBoxes\`, **not** the ones inside the PBO.
> Reference/default versions of all four are in `configfiles.readme`.
## Logging
Logging verbosity is controlled by a single field in `main_config.json`:
```json
{
"DebugLogLevel": "INFO"
}
```
### 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.
### 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)`
| Level | When to use | Examples in this mod |
|-------|-------------|----------------------|
| `NONE` | Silence all mod logging. | — |
| `ERROR` | The mod cannot do its job; needs admin attention. Routed through `ErrorEx()` so it stands out in the `.RPT`. | `box_types.json` / `spawn_locations.json` missing; an entity class failed to spawn. |
| `WARN` | Recoverable problem; the mod continues with degraded behaviour. | Unknown `boxType` on a spawn location; `loot_lists.json` missing (boxes open empty); unknown fail-effect type. |
| `INFO` | Normal high-level operational milestones. **Recommended for production.** | Manager initialized; config loaded summary; box spawned; box unlocked and filled. |
| `DEBUG` | Periodic internal state useful when diagnosing behaviour. | Per-tick lifecycle summary (`removed N, active now M`). |
| `VERBOSE` | Very high-frequency, per-player / per-roll detail. | Each proximity message sent; each fail-effect roll resolved. |
Setting the level to `X` enables `X` **and everything more severe**. For example
`DEBUG` shows ERROR + WARN + INFO + DEBUG, but not VERBOSE.
### A note on VERBOSE
`VERBOSE` fires on hot paths (proximity messages run every ~1.5s per nearby
player; fail-effect rolls run on every failed open). It will flood your `.RPT`
on a populated server and is intended **only for active debugging sessions**.
Leave production servers at `INFO` (or `WARN`/`ERROR` if you want it quieter).
## Log code API (for modders)
```c
Log_Error("..."); // ERROR — also ErrorEx() to the .RPT
Log_Warn("..."); // WARN
Log_Info("..."); // INFO
Log_Debug("..."); // DEBUG
Log_Verbose("..."); // VERBOSE
```
Each prefixes its output as `[YALootBoxes][LEVEL] ...` and self-gates on the
configured level — no need to check the level at the call site.

View file

@ -14,7 +14,7 @@
"DefaultDespawnMinutes": 15,
"OpenChannelDurationSeconds": 5,
"ProximityMessageRangeMeters": 3.0,
"EnableDebugLogging": true
"DebugLogLevel": "INFO"
}

View file

@ -94,13 +94,11 @@ class YALB_ActionOpenLootbox : ActionContinuousBase
}
}
// Player can bail out early (per requirements: fixed channel,
// instant roll at the end, but interruption is allowed and
// simply cancels - no roll, no penalty, no progress saved).
override void OnActionInfoUpdate(PlayerBase player, ActionData action_data)
{
super.OnActionInfoUpdate(player, action_data);
}
// NOTE: early bail-out (per requirements - fixed channel, instant roll
// at the end, interruption allowed) is handled by ActionContinuousBase
// itself: releasing the button cancels the action WITHOUT calling
// OnFinishProgressServer, so no roll/penalty/progress is saved. No
// override is needed here.
};
// ------------------------------------------------------------
@ -133,8 +131,10 @@ void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase
if (!chosen) return;
YALB_Log(string.Format("Player %1 triggered fail effect '%2' on box type '%3'.",
player.GetIdentity().GetName(), chosen.type, def.type));
string playerName = "<unknown>";
if (player.GetIdentity()) playerName = player.GetIdentity().GetName();
Log_Verbose(string.Format("Player %1 triggered fail effect '%2' on box type '%3'.",
playerName, chosen.type, def.type));
switch (chosen.type)
{
@ -164,7 +164,7 @@ void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase
break;
default:
YALB_Log("Unknown fail effect type: " + chosen.type);
Log_Warn("Unknown fail effect type: " + chosen.type);
break;
}
}

View file

@ -20,7 +20,10 @@ class YALB_MainConfig
int DefaultDespawnMinutes;
int OpenChannelDurationSeconds;
float ProximityMessageRangeMeters;
bool EnableDebugLogging;
// 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;
void YALB_MainConfig()
{
@ -30,7 +33,7 @@ class YALB_MainConfig
DefaultDespawnMinutes = 15;
OpenChannelDurationSeconds = 5;
ProximityMessageRangeMeters = 3.0;
EnableDebugLogging = true;
DebugLogLevel = "INFO";
}
};
@ -181,6 +184,10 @@ class YALB_ConfigLoader
static ref YALB_LootListsFile LootLists;
static ref YALB_SpawnLocationsFile SpawnLocations;
// Parsed-once numeric mirror of MainConfig.DebugLogLevel (see YALB_LogLevel).
// Defaults to INFO so logging behaves sanely before LoadAll() runs.
static int LogLevel = YALB_LogLevel.INFO;
static void EnsureDirectoryExists()
{
if (!FileExist(YALB_CONFIG_DIR))
@ -194,16 +201,22 @@ class YALB_ConfigLoader
EnsureDirectoryExists();
MainConfig = new YALB_MainConfig();
if (FileExist(YALB_MAIN_CONFIG_FILE))
bool mainConfigExisted = FileExist(YALB_MAIN_CONFIG_FILE);
if (mainConfigExisted)
{
JsonFileLoader<YALB_MainConfig>.JsonLoadFile(YALB_MAIN_CONFIG_FILE, MainConfig);
}
else
{
JsonFileLoader<YALB_MainConfig>.JsonSaveFile(YALB_MAIN_CONFIG_FILE, MainConfig);
YALB_Log("main_config.json not found - created with defaults.");
}
// Parse the configured level NOW so every log below honours it.
LogLevel = YALB_ParseLogLevel(MainConfig.DebugLogLevel);
if (!mainConfigExisted)
Log_Info("main_config.json not found - created with defaults.");
BoxTypes = new YALB_BoxTypesFile();
if (FileExist(YALB_BOX_TYPES_FILE))
{
@ -211,7 +224,7 @@ class YALB_ConfigLoader
}
else
{
YALB_Log("box_types.json not found - no lootbox types loaded! Mod will not spawn anything until this exists.");
Log_Error("box_types.json not found - no lootbox types loaded! Mod will not spawn anything until this exists.");
}
LootLists = new YALB_LootListsFile();
@ -221,7 +234,7 @@ class YALB_ConfigLoader
}
else
{
YALB_Log("loot_lists.json not found - lootboxes will open empty!");
Log_Warn("loot_lists.json not found - lootboxes will open empty!");
}
SpawnLocations = new YALB_SpawnLocationsFile();
@ -231,11 +244,11 @@ class YALB_ConfigLoader
}
else
{
YALB_Log("spawn_locations.json not found - no spawn points available!");
Log_Error("spawn_locations.json not found - no spawn points available!");
}
YALB_Log(string.Format("Config loaded: %1 box types, %2 spawn locations, %3 loot lists.",
BoxTypes.BoxTypes.Count(), SpawnLocations.SpawnLocations.Count(), LootLists.LootLists.Count()));
Log_Info(string.Format("Config loaded: %1 box types, %2 spawn locations, %3 loot lists. Log level = %4.",
BoxTypes.BoxTypes.Count(), SpawnLocations.SpawnLocations.Count(), LootLists.LootLists.Count(), MainConfig.DebugLogLevel));
}
static YALB_BoxTypeDef GetBoxTypeByClassName(string className)
@ -273,11 +286,57 @@ class YALB_ConfigLoader
};
// ------------------------------------------------------------
// LOGGING HELPER
// LOGGING SYSTEM
// ------------------------------------------------------------
void YALB_Log(string msg)
// Ordered severity. A message at level X is emitted only when the
// configured level >= X. NONE silences everything.
enum YALB_LogLevel
{
if (YALB_ConfigLoader.MainConfig && !YALB_ConfigLoader.MainConfig.EnableDebugLogging)
NONE = 0,
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4,
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)
{
if (YALB_ConfigLoader.LogLevel < level)
return;
Print("[YALootBoxes] " + msg);
}
Print("[YALootBoxes][" + tag + "] " + msg);
}
// ERROR is routed through ErrorEx() so it stands out in the .RPT, in
// addition to honouring the level gate. (ErrorEx is standard DayZ API.)
void Log_Error(string msg)
{
if (YALB_ConfigLoader.LogLevel < YALB_LogLevel.ERROR)
return;
ErrorEx("[YALootBoxes][ERROR] " + msg);
}
void Log_Warn(string msg) { YALB_Emit(YALB_LogLevel.WARN, "WARN", msg); }
void Log_Info(string msg) { YALB_Emit(YALB_LogLevel.INFO, "INFO", msg); }
void Log_Debug(string msg) { YALB_Emit(YALB_LogLevel.DEBUG, "DEBUG", msg); }
void Log_Verbose(string msg) { YALB_Emit(YALB_LogLevel.VERBOSE, "VERBOSE", msg); }

View file

@ -41,7 +41,7 @@ class YALootBoxes_Base extends Container_Base
if (!m_TypeDef)
{
YALB_Log("WARNING: Lootbox spawned with unknown type '" + boxTypeId + "'. Using fallback values.");
Log_Warn("Lootbox spawned with unknown type '" + boxTypeId + "'. Using fallback values.");
}
SetSynchDirty();
@ -113,7 +113,7 @@ class YALootBoxes_Base extends Container_Base
PopulateLoot();
YALB_Log(string.Format("Lootbox '%1' (%2) unlocked and filled.", GetBoxTypeId(), GetPosition().ToString()));
Log_Info(string.Format("Lootbox '%1' (%2) unlocked and filled.", GetBoxTypeId(), GetPosition().ToString()));
}
protected void PopulateLoot()
@ -121,7 +121,7 @@ class YALootBoxes_Base extends Container_Base
array<ref YALB_LootEntry> lootList = YALB_ConfigLoader.GetLootListForType(m_BoxTypeId);
if (!lootList)
{
YALB_Log("No loot list found for type '" + m_BoxTypeId + "' - box will be empty.");
Log_Warn("No loot list found for type '" + m_BoxTypeId + "' - box will be empty.");
return;
}

View file

@ -17,15 +17,13 @@ class YALB_ActiveBoxEntry
class YALootBoxesManager
{
protected ref array<ref YALB_ActiveBoxEntry> m_ActiveBoxes;
protected ref array<string> m_FreeLocationNames; // locations currently unused
protected ref map<string, bool> m_LocationInUse; // name -> occupied
protected ref array<string> m_FreeLocationNames; // locations currently unused (the live pool)
protected ref map<int, float> m_PlayerMessageCooldown; // playerId -> last message world time
void YALootBoxesManager()
{
m_ActiveBoxes = new array<ref YALB_ActiveBoxEntry>();
m_FreeLocationNames = new array<string>();
m_LocationInUse = new map<string, bool>();
m_PlayerMessageCooldown = new map<int, float>();
}
@ -35,14 +33,13 @@ class YALootBoxesManager
if (!YALB_ConfigLoader.SpawnLocations || YALB_ConfigLoader.SpawnLocations.SpawnLocations.Count() == 0)
{
YALB_Log("ERROR: No spawn locations loaded. Manager will not spawn any lootboxes.");
Log_Error("No spawn locations loaded. Manager will not spawn any lootboxes.");
return;
}
foreach (YALB_SpawnLocationDef loc : YALB_ConfigLoader.SpawnLocations.SpawnLocations)
{
m_FreeLocationNames.Insert(loc.name);
m_LocationInUse.Insert(loc.name, false);
}
SpawnInitialBatch();
@ -55,7 +52,7 @@ class YALootBoxesManager
// lifecycle check since it needs to feel responsive to players
GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(TickProximityMessages, 1500, true);
YALB_Log("Manager initialized. " + m_ActiveBoxes.Count() + " lootboxes spawned.");
Log_Info("Manager initialized. " + m_ActiveBoxes.Count() + " lootboxes spawned.");
}
protected void SpawnInitialBatch()
@ -87,7 +84,7 @@ class YALootBoxesManager
YALB_BoxTypeDef typeDef = YALB_ConfigLoader.GetBoxTypeByType(locDef.boxType);
if (!typeDef)
{
YALB_Log("WARNING: spawn location '" + locName + "' references unknown boxType '" + locDef.boxType + "'. Skipping.");
Log_Warn("Spawn location '" + locName + "' references unknown boxType '" + locDef.boxType + "'. Skipping.");
m_FreeLocationNames.Remove(idx);
return SpawnOneAtRandomFreeLocation();
}
@ -96,7 +93,7 @@ class YALootBoxesManager
YALootBoxes_Base box = YALootBoxes_Base.Cast(obj);
if (!box)
{
YALB_Log("ERROR: Failed to spawn class '" + typeDef.className + "' at '" + locName + "'.");
Log_Error("Failed to spawn class '" + typeDef.className + "' at '" + locName + "'.");
m_FreeLocationNames.Remove(idx);
return SpawnOneAtRandomFreeLocation();
}
@ -110,9 +107,8 @@ class YALootBoxesManager
m_ActiveBoxes.Insert(entry);
m_FreeLocationNames.Remove(idx);
m_LocationInUse.Set(locName, true);
YALB_Log(string.Format("Spawned '%1' at location '%2' (%3).", typeDef.className, locName, locDef.GetPosition().ToString()));
Log_Info(string.Format("Spawned '%1' at location '%2' (%3).", typeDef.className, locName, locDef.GetPosition().ToString()));
return true;
}
@ -161,13 +157,12 @@ class YALootBoxesManager
if (!SpawnOneAtRandomFreeLocation()) break;
}
YALB_Log(string.Format("Lifecycle tick: removed %1, active now %2.", toRemove.Count(), m_ActiveBoxes.Count()));
Log_Debug(string.Format("Lifecycle tick: removed %1, active now %2.", toRemove.Count(), m_ActiveBoxes.Count()));
}
protected void RemoveAndFreeLocation(YALB_ActiveBoxEntry entry)
{
m_ActiveBoxes.RemoveItem(entry);
m_LocationInUse.Set(entry.locationName, false);
m_FreeLocationNames.Insert(entry.locationName);
if (entry.box)
@ -204,6 +199,7 @@ class YALootBoxesManager
YALB_NotifyPlayer(player, nearest.GetRequiredItemsMessage());
m_PlayerMessageCooldown.Set(playerId, now);
Log_Verbose("Proximity message sent to '" + player.GetIdentity().GetName() + "' near box type '" + nearest.GetBoxTypeId() + "'.");
}
}