Phase 1: rename audit complete (LootboxSpawner -> YALootBoxes)

- Renamed code prefix LB_ -> YALB_ across all four script files
- Renamed source files LootboxSpawner_*.c -> YALootBoxes_*.c
- Deleted three empty 0-byte stub files (_BoxTypes/_LootLists/_SpawnLocations)
- Fixed folder case scripts/4_world -> scripts/4_World to match config.cpp
- Verified box_types.json className values match CfgVehicles class names

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
smthspec 2026-06-19 21:27:38 +03:00
commit 354dfe34d5
9 changed files with 1296 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.pbo
*.log
*.rpt

144
CLAUDE_TASK.md Normal file
View file

@ -0,0 +1,144 @@
# YALootBoxes — Polish, Verify, and Extend Logging
**Project root:** `P:\YALootBoxes`
**Context:** This is a DayZ server mod (Enforce Script). It was originally scaffolded under the name `LootboxSpawner` and has since been renamed to `YALootBoxes`. The mod spawns lockable lootboxes at config-defined locations; players need a specific held item to attempt opening one via a hold-to-open action; success unlocks+fills the box, failure rolls one random negative effect. All settings live in four JSON files under `$profile:YALootBoxes\`.
This is a **source-level review and polish pass**. There is no DayZ Server/Tools setup available yet, so do not attempt to pack a PBO or boot a server — focus on static correctness, consistency, and completeness of the Enforce Script source and JSON schemas.
Work through the phases below **in order**. Do not skip ahead. At the end of each phase, summarize what you found/changed before moving to the next.
---
## Phase 0 — Orientation
1. Recursively list all files under `P:\YALootBoxes`.
2. Read every `.c`, `.cpp`, and `.json` file you find.
3. Build a mental (or written, in scratch notes) map of: mod folder structure vs. the expected layout below, and flag any structural differences immediately.
**Expected layout:**
```
YALootBoxes/
├── config.cpp
├── Prefix.cpp
└── scripts/
└── 4_World/
├── YALootBoxes_Config.c
├── YALootBoxes_Lootbox.c
├── YALootBoxes_ActionOpen.c
└── YALootBoxes_Manager.c
```
(File names above assume the rename also touched filenames — verify and report actual names found, don't assume.)
---
## Phase 1 — Rename Audit (the user says rename "should" be complete — verify, don't trust)
Search the **entire** project for every leftover instance of the old naming scheme. Check ALL of:
- [ ] String `LootboxSpawner` (any casing) — in `.c`, `.cpp`, `.json`, comments, file names, folder names
- [ ] String `LB_` prefix on classes/functions/constants (e.g. `LB_Log`, `LB_MainConfig`, `LB_ConfigLoader`, `LB_BoxTypeDef`, `LB_FailEffect`, `LB_ActionOpenLootbox`, `LB_ApplyRandomFailEffect`, etc.) — decide on and apply a **consistent new prefix** (suggest `YLB_` or `YALB_`) if these were meant to be renamed too
- [ ] `CfgPatches` / `CfgMods` class name (`LootboxSpawner`) in `config.cpp`
- [ ] `CfgVehicles` class names (`LootboxSpawner_Crate_Military`, `_Medical`, `_Industrial`) — these **must** exactly match the `className` fields inside `box_types.json`, so check both sides together
- [ ] `$profile:LootboxSpawner\` path constant — should become `$profile:YALootBoxes\`
- [ ] Any `displayName` strings, log prefixes (`Print("[LootboxSpawner] ...")`), or code comments referencing the old name
- [ ] `Prefix.cpp` content itself
**Action:** Fix every occurrence found. Keep a running checklist in your response of exactly what was renamed, file by file, so the user can review the diff. Do not silently rename things — call out each change.
**Cross-check after renaming:** confirm `box_types.json`'s `className` values still exactly match the `CfgVehicles` class names in `config.cpp` post-rename (this is the #1 way these mods silently fail to spawn anything — a one-character mismatch and the server just won't spawn the object, often without an obvious error).
---
## Phase 2 — Structural / Completeness Review
Go file by file and check for the following. This codebase was written without ever being compiled against the real DayZ SDK headers, so treat it as a first draft that needs adversarial review, not a finished product.
### General Enforce Script correctness
- [ ] Confirm every class that's referenced is actually defined somewhere (no dangling references to types that don't exist, e.g. double check `ContinuousInteractActionInput`, `ActionContinuousBase`, `CCINone`, `CCTObject`, `UAMaxDistances` are real DayZ API symbols available in the modding SDK — if you have access to DayZ SDK docs/headers via web search, verify; otherwise flag as "needs verification against SDK" rather than assuming correctness)
- [ ] Check `RegisterNetSyncVariableBool` usage — confirm sync var registration pattern matches how `ContainerObject`/`ItemBase` subclasses normally do it in DayZ mods
- [ ] Check for null-safety: every place a JSON-loaded reference (`m_TypeDef`, `LB_ConfigLoader.MainConfig`, etc.) is used, confirm there's a null check or a guaranteed-non-null path
- [ ] Check `GetActions()` override signature/pattern is correct for hooking custom actions onto a `ContainerObject`-derived class — this was a best-effort implementation and deserves scrutiny
- [ ] Verify `eAgents` enum members used (`SALMONELLA`, `INFLUENZA`, `CHOLERA`, `BRAIN`, `WOUND_AGENT`) are the real DayZ agent enum names
- [ ] Verify `GetBleedingManagerServer()`, `GetHealthDamageManager()`, `SetFracture()`, `InsertAgent()` are real PlayerBase/HealthDamageManager API calls and used with correct signatures
### Config loading
- [ ] Confirm `JsonFileLoader<T>.JsonLoadFile` / `JsonSaveFile` usage matches real DayZ API (class name and static usage pattern)
- [ ] Confirm the `map<string, ref array<ref LB_LootEntry>>` (or renamed equivalent) is a supported Enforce Script generic — DayZ's Enforce Script has known limitations with nested generic containers in JSON deserialization. **This is a likely problem area** — flag clearly if you believe `JsonFileLoader` cannot deserialize a `map<string, array<...>>` directly. If so, propose a fix: e.g. restructure `loot_lists.json` as an array of `{type, entries}` objects instead of a map, since flat arrays deserialize far more reliably in Enforce Script's JSON layer.
- [ ] Check that `EnsureDirectoryExists` / `MakeDirectory` calls match the real API (case, return type)
- [ ] Confirm default-object construction (e.g. `LB_MainConfig()` constructor) actually runs before `JsonLoadFile` overwrites fields, so a partially-specified JSON file doesn't leave other fields zero/null
### Lifecycle & manager logic
- [ ] Re-derive the despawn timer logic by hand against the user's original spec: locked boxes despawn/respawn after N minutes (config-driven per box type) **even if never opened**; opened boxes despawn N minutes after becoming unlocked. Confirm `HasExpiredUnopened` / `HasExpiredAfterOpening` actually implement this without overlap or gaps.
- [ ] Check for a race/edge case: what happens if a box is mid-open-action (channeling) at the exact moment the lifecycle tick deletes it for expiring? Flag this as a known edge case if not already handled, and propose a minimal guard (e.g. skip despawn-delete if an action is actively targeting it, or just accept and document the rare edge case).
- [ ] Check `CallLater(..., true)` repeating-timer usage is correct DayZ API usage (3rd param = repeat flag, confirm order of args).
- [ ] Verify location pool logic (`m_FreeLocationNames`, `m_LocationInUse`) doesn't have an off-by-one or duplicate-assignment bug when `SpawnOneAtRandomFreeLocation` recurses on failure.
### Action / interaction logic
- [ ] Confirm the "fixed channel time, instant roll, but failure can interrupt early" requirement is actually satisfiable with `ActionContinuousBase` + `OnFinishProgressServer` — i.e. confirm that bailing out early (releasing the button) does NOT call `OnFinishProgressServer` and does NOT roll anything. Flag if the current code doesn't clearly guarantee this.
- [ ] Confirm the proximity message system (manager's `TickProximityMessages`) and the action's "show prompt when holding right item" are not redundant/conflicting — i.e. one always shows "Requires: X", the other only enables the action — and that nothing in the action class duplicates the proximity broadcast.
- [ ] Verify `RPCSingleParam` / `ERPCs.RPC_USER_ACTION_MESSAGE` is a real, appropriate way to send a HUD text message to a single client in DayZ's API. If this is not correct, propose the standard pattern used by DayZ mods for "send toast/notification to one player" (often a custom RPC type registered in `Init.c`/`Missions`, since `RPC_USER_ACTION_MESSAGE` may not exist or may not behave as a generic toast).
### Config/JSON schema consistency
- [ ] Confirm every field referenced in `.c` files has a matching field (same name, same casing) in the example JSON files, and vice versa.
- [ ] Confirm `failEffects[].chance` semantics are documented clearly as "weight" not "independent probability" (since the code does a weighted pick among effects, not N independent rolls) — fix any comment/naming that implies otherwise.
Produce a written list of every issue found in this phase, categorized as:
- 🔴 **Will likely fail to compile or run** (must fix)
- 🟡 **Will likely behave incorrectly at runtime** (should fix)
- 🟢 **Style/maintainability nit** (optional)
Fix all 🔴 and 🟡 items. For 🟢 items, list them but ask before changing anything purely stylistic.
---
## Phase 3 — Add Debug Logging Levels
Extend the logging system to support **NONE / ERROR / WARN / INFO / DEBUG / VERBOSE** levels, replacing the current single `EnableDebugLogging: bool`.
### Requirements:
1. In the main config class, replace `EnableDebugLogging` (bool) with `DebugLogLevel` (string or int — your call, but document which and why; string is more admin-friendly for hand-editing JSON, int is cheaper to compare at runtime — consider storing as string in JSON but parsing to an int enum at load time for fast comparisons).
2. Define the level ordering: `NONE(0) < ERROR(1) < WARN(2) < INFO(3) < DEBUG(4) < VERBOSE(5)`. A message at level X is printed only if `DebugLogLevel >= X`.
3. Replace the single `LB_Log(string msg)` (or renamed equivalent) helper with level-specific variants, e.g.:
- `Log_Error(string msg)`
- `Log_Warn(string msg)`
- `Log_Info(string msg)`
- `Log_Debug(string msg)`
- `Log_Verbose(string msg)`
Each should prefix output appropriately, e.g. `[YALootBoxes][ERROR] ...`, and route to both `Print()` (.log/.rpt via engine default) and, if straightforward, `ErrorEx()` for ERROR-level messages specifically (DayZ's standard pattern for messages that should be prominent in the `.RPT` file) — verify `ErrorEx` is real API before using it; if uncertain, just use `Print()` consistently and note the limitation.
4. Go through **every existing log call in the codebase** and reclassify it to the correct level based on its actual content:
- Config file missing / failed to parse / unknown class referenced → `ERROR` or `WARN` depending on whether it's recoverable
- "Spawned box at location X", "Manager initialized" → `INFO`
- Per-tick lifecycle summaries ("removed N, active now M") → `DEBUG`
- Per-player proximity message sends, per-effect-roll details → `VERBOSE`
5. Update the example `main_config.json` to use the new field with a sensible default (`INFO` recommended for normal server operation).
6. Update the default-constructor fallback value to match.
Add a short section to the project's README (create one at `P:\YALootBoxes\README.md` if it doesn't exist) documenting:
- What each log level is meant to be used for
- The config field name and JSON example
- A reminder that VERBOSE will be noisy and is meant for active debugging sessions only, not production
---
## Phase 4 — Final Consistency Pass
1. Re-read every file once more, top to bottom, now that renaming and logging changes have landed, specifically checking that:
- No file still calls the old single-arg `LB_Log(...)` (or its renamed equivalent) — every call site should now use a leveled function
- No JSON file and `.c` file have drifted out of sync on field names again as a side effect of your edits
- All four JSON example files are valid JSON (mentally parse them, or note if you want to validate via a tool)
2. Produce a final summary report with these sections:
- **Renames applied** (file-by-file list)
- **Bugs fixed** (🔴 and 🟡 items from Phase 2, with before/after description)
- **Known limitations / things that need real DayZ SDK to verify** (be explicit — this matters since nothing has been compiled yet)
- **Logging system** (what changed, default level, example config)
- **Suggested next steps** for the user once they have DayZ Tools available (e.g. "pack PBO, boot test server, spawn near a military crate empty-handed, confirm proximity message text, pick up a crowbar, confirm action prompt appears, hold to open, confirm one of the two outcomes fires correctly")
---
## Ground rules throughout
- **Do not invent DayZ API behavior you're not confident about.** If you can't verify something against real SDK docs/headers, say so explicitly in the summary rather than presenting a guess as fact. This is real game-server code the user intends to run.
- **Do not silently delete functionality.** If you think something should be removed (e.g. a redundant message path), propose it and explain why before removing it, in your phase summary.
- **Keep commit-sized changes traceable** — if this is a git repo, make small logical commits per phase rather than one giant diff; if it's not a git repo, mention that initializing one would make this kind of iterative work much easier to review going forward.
- This is **review + polish + logging**, not a redesign. Don't change the core mechanics (proximity message always shown, fixed-channel hold-to-open, weighted single fail-effect roll, post-open despawn timer) unless you find them to be actually broken as currently spec'd — flag concerns rather than unilaterally redesigning.

37
Prefix.cpp Normal file
View file

@ -0,0 +1,37 @@
// ============================================================
// FILE: Prefix.cpp
// Place at mod root: YALootBoxes/Prefix.cpp
// (Tells the engine the include-prefix for this mod's paths)
// ============================================================
// Prefix.cpp content:
//
// YALootBoxes
// ============================================================
// FOLDER LAYOUT SUMMARY
// ============================================================
// YALootBoxes/
// ├── config.cpp
// ├── Prefix.cpp
// └── scripts/
// └── 4_World/
// ├── YALootBoxes_Config.c
// ├── YALootBoxes_Lootbox.c
// ├── YALootBoxes_ActionOpen.c
// └── YALootBoxes_Manager.c
//
// Server mission init.c needs the small hook shown at the
// bottom of YALootBoxes_Manager.c.
//
// IMPORTANT NOTE ON simulationModule / class binding:
// DayZ's Enforce Script binds a .c class to a cfgVehicles entry
// either by the entry's class name matching the script class
// name directly, OR via simulationModule for non-standard cases.
// For most setups, simply naming the cfgVehicles class identically
// to your Enforce Script class (as done above: e.g.
// "YALootBoxes_Crate_Military") is what actually binds them -
// if your DayZ Server/SDK version doesn't honor simulationModule,
// delete that line; the name match alone is sufficient.
// ============================================================

90
config.cpp Normal file
View file

@ -0,0 +1,90 @@
// ============================================================
// FILE: config.cpp
// Place at mod root: YALootBoxes/config.cpp
// ============================================================
class CfgPatches
{
class YALootBoxes
{
units[] = {};
weapons[] = {};
requiredVersion = 0.1;
requiredAddons[] = { "DZ_Data", "DZ_Scripts" };
};
};
class CfgMods
{
class YALootBoxes
{
dir = "YALootBoxes";
picture = "";
action = "";
hideName = 1;
hidePicture = 1;
name = "YALootBoxes";
credits = "";
author = "";
authorID = "0";
version = "1.0.0";
extra = 0;
type = "mod";
dependencies[] = { "Game", "World", "Mission" };
class defs
{
class gameScriptModule
{
value = "";
files[] = { "YALootBoxes/scripts/4_World" };
};
class worldScriptModule
{
value = "";
files[] = { "YALootBoxes/scripts/4_World" };
};
};
};
};
// ------------------------------------------------------------
// Lootbox entity classes - one per box type defined in
// box_types.json. The "className" field in that JSON file
// MUST exactly match one of these class names.
//
// Swap "ContainerObject" inheritance/model/physics as needed -
// these reuse a vanilla static container model purely as a
// placeholder. Replace 'model=' with your own p3d if you have one.
// ------------------------------------------------------------
class CfgVehicles
{
class ContainerObject;
class YALootBoxes_Crate_Military : ContainerObject
{
scope = 2;
displayName = "Military Crate (Locked)";
model = "\dz\structures\wood\wooden_crate.p3d";
vehicleClass = "Containers";
simulationModule = "YALootBoxes_Base";
};
class YALootBoxes_Crate_Medical : ContainerObject
{
scope = 2;
displayName = "Medical Crate (Locked)";
model = "\dz\structures\wood\wooden_crate.p3d";
vehicleClass = "Containers";
simulationModule = "YALootBoxes_Base";
};
class YALootBoxes_Crate_Industrial : ContainerObject
{
scope = 2;
displayName = "Industrial Crate (Locked)";
model = "\dz\structures\wood\wooden_crate.p3d";
vehicleClass = "Containers";
simulationModule = "YALootBoxes_Base";
};
};

123
configfiles.readme Normal file
View file

@ -0,0 +1,123 @@
// ============================================================
// These are REFERENCE/DEFAULT files. The mod auto-generates
// these on first run at: $profile:YALootBoxes\*.json
// Edit them there - do NOT edit inside the .pbo.
// ============================================================
// ------------------------------------------------------------
// FILE: main_config.json
// ------------------------------------------------------------
{
"MaxActiveLootboxes": 30,
"GlobalCheckIntervalSeconds": 60,
"DefaultDespawnMinutes": 15,
"OpenChannelDurationSeconds": 5,
"ProximityMessageRangeMeters": 3.0,
"EnableDebugLogging": true
}
// ------------------------------------------------------------
// FILE: box_types.json
// ------------------------------------------------------------
{
"BoxTypes": [
{
"className": "YALootBoxes_Crate_Military",
"type": "military",
"requiredItems": [
{ "classname": "BoltCutters", "displayName": "Bolt Cutters" },
{ "classname": "Crowbar", "displayName": "Crowbar" }
],
"openChance": 0.65,
"itemDamageOnOpen": 8.0,
"despawnMinutes": 15,
"failEffects": [
{ "type": "EXPLOSIVE", "chance": 0.15, "damage": 45.0, "radius": 2.5 },
{ "type": "HEALTH", "chance": 0.25, "damage": 15.0 },
{ "type": "BLEEDING", "chance": 0.30, "bleedSources": 2 },
{ "type": "SICKNESS", "chance": 0.15, "agent": "Salmonella" },
{ "type": "BROKEN_BONE", "chance": 0.15 }
]
},
{
"className": "YALootBoxes_Crate_Medical",
"type": "medical",
"requiredItems": [
{ "classname": "Hacksaw", "displayName": "Hacksaw" }
],
"openChance": 0.80,
"itemDamageOnOpen": 4.0,
"despawnMinutes": 15,
"failEffects": [
{ "type": "SICKNESS", "chance": 0.50, "agent": "Influenza" },
{ "type": "HEALTH", "chance": 0.30, "damage": 8.0 },
{ "type": "BLEEDING", "chance": 0.20, "bleedSources": 1 }
]
},
{
"className": "YALootBoxes_Crate_Industrial",
"type": "industrial",
"requiredItems": [
{ "classname": "Crowbar", "displayName": "Crowbar" },
{ "classname": "WeldingMask", "displayName": "Angle Grinder" }
],
"openChance": 0.50,
"itemDamageOnOpen": 12.0,
"despawnMinutes": 20,
"failEffects": [
{ "type": "EXPLOSIVE", "chance": 0.20, "damage": 60.0, "radius": 3.0 },
{ "type": "BROKEN_BONE", "chance": 0.25 },
{ "type": "BLEEDING", "chance": 0.30, "bleedSources": 2 },
{ "type": "HEALTH", "chance": 0.25, "damage": 20.0 }
]
}
]
}
// ------------------------------------------------------------
// FILE: loot_lists.json
// ------------------------------------------------------------
{
"LootLists": {
"military": [
{ "classname": "AKM", "chance": 0.10, "minCount": 1, "maxCount": 1 },
{ "classname": "Mag_AKM_30Rnd", "chance": 0.40, "minCount": 1, "maxCount": 3 },
{ "classname": "PlateCarrierVest","chance": 0.20, "minCount": 1, "maxCount": 1 },
{ "classname": "TacticalHelmet", "chance": 0.15, "minCount": 1, "maxCount": 1 },
{ "classname": "Ammo_762x39", "chance": 0.50, "minCount": 1, "maxCount": 2 }
],
"medical": [
{ "classname": "Morphine", "chance": 0.30, "minCount": 1, "maxCount": 1 },
{ "classname": "SalineBagIV", "chance": 0.25, "minCount": 1, "maxCount": 2 },
{ "classname": "VitaminBottle", "chance": 0.50, "minCount": 1, "maxCount": 3 },
{ "classname": "Bandage", "chance": 0.60, "minCount": 2, "maxCount": 5 },
{ "classname": "TetracyclineAntibiotics", "chance": 0.20, "minCount": 1, "maxCount": 2 }
],
"industrial": [
{ "classname": "WeldingMask", "chance": 0.15, "minCount": 1, "maxCount": 1 },
{ "classname": "Battery_Car", "chance": 0.30, "minCount": 1, "maxCount": 1 },
{ "classname": "SparkPlug", "chance": 0.40, "minCount": 1, "maxCount": 2 },
{ "classname": "MetalPipe", "chance": 0.50, "minCount": 1, "maxCount": 4 },
{ "classname": "DuctTape", "chance": 0.45, "minCount": 1, "maxCount": 2 }
]
}
}
// ------------------------------------------------------------
// FILE: spawn_locations.json
// ------------------------------------------------------------
{
"SpawnLocations": [
{ "name": "NWAF_Hangar1", "position": [4985.3, 320.5, 2456.8], "boxType": "military" },
{ "name": "NWAF_Hangar2", "position": [5102.7, 318.9, 2401.2], "boxType": "military" },
{ "name": "Tisy_Bunker_A", "position": [1606.4, 280.1, 14383.9],"boxType": "military" },
{ "name": "Berezino_Hospital", "position": [7245.1, 12.4, 13889.6], "boxType": "medical" },
{ "name": "Zelenogorsk_Clinic", "position": [2543.9, 8.2, 5234.7], "boxType": "medical" },
{ "name": "Tractor_Plant_1", "position": [9821.2, 145.6, 8421.3], "boxType": "industrial" },
{ "name": "Tractor_Plant_2", "position": [9890.5, 142.1, 8390.7], "boxType": "industrial" }
]
}

View file

@ -0,0 +1,235 @@
// ============================================================
// YALootBoxes_ActionOpen.c
// Custom UserAction: hold-to-open a locked lootbox.
// Lives in: scripts/4_World/YALootBoxes_ActionOpen.c
//
// Registration of this action onto YALootBoxes_Base happens
// via GetActionComponentName/cfgVehicles "actions[]" in config.cpp,
// OR (simpler, used here) by overriding GetActions() — see note
// at bottom of file.
// ============================================================
class YALB_OpenLootboxActionData : ActionData
{
// currently empty - base ActionData carries everything we need
};
class YALB_ActionOpenLootbox : ActionContinuousBase
{
void YALB_ActionOpenLootbox()
{
m_CommandUID = DayZPlayerConstants.CMD_ACTIONFB_ONCE; // generic "use" anim hook
m_FullBody = false;
m_ReloadFinishAnim = false;
}
override void CreateConditionComponents()
{
m_ConditionItem = new CCINone();
m_ConditionTarget = new CCTObject(UAMaxDistances.DEFAULT);
}
override string GetText()
{
return "Open lootbox";
}
override typename GetInputType()
{
return ContinuousInteractActionInput;
}
// Only offer the action if: target is our lootbox, it's locked,
// and the player is holding a valid key item for it.
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
{
YALootBoxes_Base box = YALootBoxes_Base.Cast(target.GetObject());
if (!box) return false;
if (!box.IsLocked()) return false;
ItemBase handItem = player.GetItemInHands();
if (!handItem) return false;
YALB_BoxTypeDef def = box.GetTypeDef();
if (!def) return false;
return def.IsValidKeyItem(handItem.GetType());
}
override int GetDuration(ActionData action_data)
{
if (YALB_ConfigLoader.MainConfig)
return YALB_ConfigLoader.MainConfig.OpenChannelDurationSeconds * 1000; // ms
return 5000;
}
// Fired server-side when the channel completes WITHOUT being cancelled.
override void OnFinishProgressServer(ActionData action_data)
{
super.OnFinishProgressServer(action_data);
YALootBoxes_Base box = YALootBoxes_Base.Cast(action_data.m_Target.GetObject());
PlayerBase player = action_data.m_Player;
if (!box || !player) return;
YALB_BoxTypeDef def = box.GetTypeDef();
if (!def) return;
ItemBase handItem = player.GetItemInHands();
float roll = Math.RandomFloat01();
if (roll <= def.openChance)
{
// SUCCESS
box.UnlockAndFill();
if (handItem)
handItem.DecreaseHealth(def.itemDamageOnOpen, false);
YALB_NotifyPlayer(player, "The lock gives way.");
}
else
{
// FAILURE - apply exactly one weighted-random effect
YALB_ApplyRandomFailEffect(player, def, handItem);
YALB_NotifyPlayer(player, "The attempt failed.");
}
}
// 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);
}
};
// ------------------------------------------------------------
// Failure-effect resolution
// ------------------------------------------------------------
void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase handItem)
{
if (!def.failEffects || def.failEffects.Count() == 0) return;
// Weighted pick: sum chances, roll within sum, walk the list.
float totalWeight = 0;
foreach (YALB_FailEffect e : def.failEffects)
totalWeight += e.chance;
if (totalWeight <= 0) return;
float roll = Math.RandomFloat(0, totalWeight);
float cumulative = 0;
YALB_FailEffect chosen = null;
foreach (YALB_FailEffect effect : def.failEffects)
{
cumulative += effect.chance;
if (roll <= cumulative)
{
chosen = effect;
break;
}
}
if (!chosen) return;
YALB_Log(string.Format("Player %1 triggered fail effect '%2' on box type '%3'.",
player.GetIdentity().GetName(), chosen.type, def.type));
switch (chosen.type)
{
case "EXPLOSIVE":
YALB_ApplyExplosiveDamage(player, chosen.damage, chosen.radius);
break;
case "HEALTH":
player.DecreaseHealth("", "", chosen.damage);
break;
case "BLEEDING":
for (int i = 0; i < chosen.bleedSources; i++)
{
player.GetBleedingManagerServer().AttemptAddBleedingSourceByMaxCount();
}
break;
case "SICKNESS":
YALB_ApplySickness(player, chosen.agent);
break;
case "BROKEN_BONE":
player.GetHealthDamageManager().SetHealthLimitDamage(true);
player.SetHealth("", "Health", player.GetHealth("", "Health") - 1); // nudge to trigger fracture check
player.GetHealthDamageManager().SetFracture(true);
break;
default:
YALB_Log("Unknown fail effect type: " + chosen.type);
break;
}
}
void YALB_ApplyExplosiveDamage(PlayerBase player, float damage, float radius)
{
// Direct damage to the player rather than a real grenade explosion entity,
// since we don't want a separate live explosive object to manage/clean up.
vector pos = player.GetPosition();
player.DecreaseHealth("", "", damage);
// Optional: damage nearby players too, to sell the "explosion" effect
array<Man> nearbyPlayers = new array<Man>();
GetGame().GetWorld().GetPlayerList(nearbyPlayers);
foreach (Man m : nearbyPlayers)
{
PlayerBase other = PlayerBase.Cast(m);
if (!other || other == player) continue;
float dist = vector.Distance(other.GetPosition(), pos);
if (dist <= radius)
{
float falloff = 1.0 - (dist / radius);
other.DecreaseHealth("", "", damage * falloff);
}
}
}
void YALB_ApplySickness(PlayerBase player, string agentName)
{
// Maps to vanilla DayZ agent system (e.g. eAgents.SALMONELLA, eAgents.INFLUENZA)
int agentId = YALB_AgentNameToId(agentName);
if (agentId >= 0)
{
player.InsertAgent(agentId, 1000); // 1000 = full dose, matches vanilla scale
}
}
int YALB_AgentNameToId(string agentName)
{
if (agentName == "Salmonella") return eAgents.SALMONELLA;
if (agentName == "Influenza") return eAgents.INFLUENZA;
if (agentName == "Cholera") return eAgents.CHOLERA;
if (agentName == "Brain") return eAgents.BRAIN;
if (agentName == "Wound_Bacterial") return eAgents.WOUND_AGENT;
return -1;
}
void YALB_NotifyPlayer(PlayerBase player, string message)
{
if (!player || !player.GetIdentity()) return;
GetGame().RPCSingleParam(player, ERPCs.RPC_USER_ACTION_MESSAGE, new Param1<string>(message), true, player.GetIdentity());
}
// ------------------------------------------------------------
// NOTE: Hooking this action onto the lootbox class
// ------------------------------------------------------------
// The cleanest approach for a fully custom action without
// touching vanilla configs is to override GetActions() directly
// on YALootBoxes_Base:
//
// override void GetActions(out array<typename> actions)
// {
// actions.Insert(YALB_ActionOpenLootbox);
// }
//
// This override is included in YALootBoxes_Lootbox.c via a
// partial addition — see the updated lootbox entity file.

View file

@ -0,0 +1,264 @@
// ============================================================
// YALootBoxes_Config.c
// Typed data classes for all JSON files + load/save/defaults.
// Lives in: scripts/4_World/YALootBoxes_Config.c
// ============================================================
const string YALB_CONFIG_DIR = "$profile:YALootBoxes\\";
const string YALB_MAIN_CONFIG_FILE = YALB_CONFIG_DIR + "main_config.json";
const string YALB_BOX_TYPES_FILE = YALB_CONFIG_DIR + "box_types.json";
const string YALB_LOOT_LISTS_FILE = YALB_CONFIG_DIR + "loot_lists.json";
const string YALB_SPAWN_LOCATIONS_FILE = YALB_CONFIG_DIR + "spawn_locations.json";
// ------------------------------------------------------------
// MAIN CONFIG
// ------------------------------------------------------------
class YALB_MainConfig
{
int MaxActiveLootboxes;
int GlobalCheckIntervalSeconds;
int DefaultDespawnMinutes;
int OpenChannelDurationSeconds;
float ProximityMessageRangeMeters;
bool EnableDebugLogging;
void YALB_MainConfig()
{
// Sane defaults if file is missing/corrupt
MaxActiveLootboxes = 30;
GlobalCheckIntervalSeconds = 60;
DefaultDespawnMinutes = 15;
OpenChannelDurationSeconds = 5;
ProximityMessageRangeMeters = 3.0;
EnableDebugLogging = true;
}
};
// ------------------------------------------------------------
// BOX TYPES
// ------------------------------------------------------------
class YALB_RequiredItem
{
string classname;
string displayName;
};
class YALB_FailEffect
{
string type; // EXPLOSIVE, HEALTH, BLEEDING, SICKNESS, BROKEN_BONE
float chance; // 0.0 - 1.0
float damage; // used by EXPLOSIVE / HEALTH
float radius; // used by EXPLOSIVE
int bleedSources; // used by BLEEDING
string agent; // used by SICKNESS
};
class YALB_BoxTypeDef
{
string className;
string type;
ref array<ref YALB_RequiredItem> requiredItems;
float openChance;
float itemDamageOnOpen;
int despawnMinutes;
ref array<ref YALB_FailEffect> failEffects;
void YALB_BoxTypeDef()
{
requiredItems = new array<ref YALB_RequiredItem>();
failEffects = new array<ref YALB_FailEffect>();
openChance = 0.5;
itemDamageOnOpen = 5.0;
despawnMinutes = 15;
}
// Builds the player-facing "requires: X, Y" string
string GetRequiredItemsDisplayString()
{
string result = "";
for (int i = 0; i < requiredItems.Count(); i++)
{
if (i > 0) result += ", ";
result += requiredItems[i].displayName;
}
return result;
}
bool IsValidKeyItem(string itemClassName)
{
for (int i = 0; i < requiredItems.Count(); i++)
{
if (requiredItems[i].classname == itemClassName)
return true;
}
return false;
}
};
class YALB_BoxTypesFile
{
ref array<ref YALB_BoxTypeDef> BoxTypes;
void YALB_BoxTypesFile()
{
BoxTypes = new array<ref YALB_BoxTypeDef>();
}
};
// ------------------------------------------------------------
// LOOT LISTS
// ------------------------------------------------------------
class YALB_LootEntry
{
string classname;
float chance;
int minCount;
int maxCount;
};
class YALB_LootListsFile
{
// Map: box "type" string -> array of loot entries
ref map<string, ref array<ref YALB_LootEntry>> LootLists;
void YALB_LootListsFile()
{
LootLists = new map<string, ref array<ref YALB_LootEntry>>();
}
};
// ------------------------------------------------------------
// SPAWN LOCATIONS
// ------------------------------------------------------------
class YALB_SpawnLocationDef
{
string name;
array<float> position; // [x, y, z]
string boxType;
vector GetPosition()
{
if (position && position.Count() == 3)
return Vector(position[0], position[1], position[2]);
return vector.Zero;
}
};
class YALB_SpawnLocationsFile
{
ref array<ref YALB_SpawnLocationDef> SpawnLocations;
void YALB_SpawnLocationsFile()
{
SpawnLocations = new array<ref YALB_SpawnLocationDef>();
}
};
// ------------------------------------------------------------
// LOADER / SAVER
// ------------------------------------------------------------
class YALB_ConfigLoader
{
static ref YALB_MainConfig MainConfig;
static ref YALB_BoxTypesFile BoxTypes;
static ref YALB_LootListsFile LootLists;
static ref YALB_SpawnLocationsFile SpawnLocations;
static void EnsureDirectoryExists()
{
if (!FileExist(YALB_CONFIG_DIR))
{
MakeDirectory(YALB_CONFIG_DIR);
}
}
static void LoadAll()
{
EnsureDirectoryExists();
MainConfig = new YALB_MainConfig();
if (FileExist(YALB_MAIN_CONFIG_FILE))
{
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.");
}
BoxTypes = new YALB_BoxTypesFile();
if (FileExist(YALB_BOX_TYPES_FILE))
{
JsonFileLoader<YALB_BoxTypesFile>.JsonLoadFile(YALB_BOX_TYPES_FILE, BoxTypes);
}
else
{
YALB_Log("box_types.json not found - no lootbox types loaded! Mod will not spawn anything until this exists.");
}
LootLists = new YALB_LootListsFile();
if (FileExist(YALB_LOOT_LISTS_FILE))
{
JsonFileLoader<YALB_LootListsFile>.JsonLoadFile(YALB_LOOT_LISTS_FILE, LootLists);
}
else
{
YALB_Log("loot_lists.json not found - lootboxes will open empty!");
}
SpawnLocations = new YALB_SpawnLocationsFile();
if (FileExist(YALB_SPAWN_LOCATIONS_FILE))
{
JsonFileLoader<YALB_SpawnLocationsFile>.JsonLoadFile(YALB_SPAWN_LOCATIONS_FILE, SpawnLocations);
}
else
{
YALB_Log("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()));
}
static YALB_BoxTypeDef GetBoxTypeByClassName(string className)
{
if (!BoxTypes) return null;
foreach (YALB_BoxTypeDef def : BoxTypes.BoxTypes)
{
if (def.className == className)
return def;
}
return null;
}
static YALB_BoxTypeDef GetBoxTypeByType(string type)
{
if (!BoxTypes) return null;
foreach (YALB_BoxTypeDef def : BoxTypes.BoxTypes)
{
if (def.type == type)
return def;
}
return null;
}
static array<ref YALB_LootEntry> GetLootListForType(string type)
{
if (!LootLists) return null;
array<ref YALB_LootEntry> list;
if (LootLists.LootLists.Find(type, list))
return list;
return null;
}
};
// ------------------------------------------------------------
// LOGGING HELPER
// ------------------------------------------------------------
void YALB_Log(string msg)
{
if (YALB_ConfigLoader.MainConfig && !YALB_ConfigLoader.MainConfig.EnableDebugLogging)
return;
Print("[YALootBoxes] " + msg);
}

View file

@ -0,0 +1,155 @@
// ============================================================
// YALootBoxes_Lootbox.c
// The actual in-world lootbox entity.
// Lives in: scripts/4_World/YALootBoxes_Lootbox.c
//
// NOTE ON className: each entry in box_types.json should have a
// matching config.cpp class inheriting from YALootBoxes_Base
// (see config.cpp artifact). This script class provides the
// shared behavior for ALL of them.
// ============================================================
class YALootBoxes_Base extends ContainerObject
{
protected bool m_IsLocked;
protected string m_BoxTypeId; // matches "type" in box_types.json
protected float m_SpawnTime; // world time (seconds) when spawned
protected float m_UnlockedTime; // world time when it became unlocked (-1 if still locked)
protected ref YALB_BoxTypeDef m_TypeDef; // cached definition
void YALootBoxes_Base()
{
m_IsLocked = true;
m_UnlockedTime = -1;
RegisterNetSyncVariableBool("m_IsLocked");
}
override void EEInit()
{
super.EEInit();
SetSynchDirty();
}
// Called by the manager right after spawning, before placing in world
void Init(string boxTypeId)
{
m_BoxTypeId = boxTypeId;
m_TypeDef = YALB_ConfigLoader.GetBoxTypeByType(boxTypeId);
m_IsLocked = true;
m_UnlockedTime = -1;
m_SpawnTime = GetGame().GetTime() / 1000.0;
if (!m_TypeDef)
{
YALB_Log("WARNING: Lootbox spawned with unknown type '" + boxTypeId + "'. Using fallback values.");
}
SetSynchDirty();
}
bool IsLocked()
{
return m_IsLocked;
}
string GetBoxTypeId()
{
return m_BoxTypeId;
}
YALB_BoxTypeDef GetTypeDef()
{
if (!m_TypeDef)
m_TypeDef = YALB_ConfigLoader.GetBoxTypeByType(m_BoxTypeId);
return m_TypeDef;
}
float GetSpawnTime()
{
return m_SpawnTime;
}
float GetUnlockedTime()
{
return m_UnlockedTime;
}
// How many minutes this box should live for, based on its current state
int GetDespawnMinutes()
{
if (m_TypeDef)
return m_TypeDef.despawnMinutes;
return YALB_ConfigLoader.MainConfig.DefaultDespawnMinutes;
}
// Returns true if (locked, never-opened) box has expired and should
// be removed/respawned elsewhere. Locked boxes use the SAME despawn
// timer as unlocked ones by design (keeps the loot table fresh even
// if nobody opens it) - manager checks this each cycle.
bool HasExpiredUnopened(float currentWorldTime)
{
if (!m_IsLocked) return false; // handled separately once opened
float ageMinutes = (currentWorldTime - m_SpawnTime) / 60.0;
return ageMinutes >= GetDespawnMinutes();
}
bool HasExpiredAfterOpening(float currentWorldTime)
{
if (m_IsLocked || m_UnlockedTime < 0) return false;
float ageMinutes = (currentWorldTime - m_UnlockedTime) / 60.0;
return ageMinutes >= GetDespawnMinutes();
}
// Called by YALB_OpenLootboxAction on a successful unlock roll.
// Converts this from a locked decoration into a real container
// populated with loot, then starts its post-open despawn clock.
void UnlockAndFill()
{
if (!m_IsLocked) return; // already unlocked, no-op
m_IsLocked = false;
m_UnlockedTime = GetGame().GetTime() / 1000.0;
SetSynchDirty();
PopulateLoot();
YALB_Log(string.Format("Lootbox '%1' (%2) unlocked and filled.", GetBoxTypeId(), GetPosition().ToString()));
}
protected void PopulateLoot()
{
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.");
return;
}
foreach (YALB_LootEntry entry : lootList)
{
float roll = Math.RandomFloat01();
if (roll > entry.chance) continue;
int count = Math.RandomIntInclusive(entry.minCount, entry.maxCount);
for (int i = 0; i < count; i++)
{
GetInventory().CreateInInventory(entry.classname);
}
}
}
// Player-facing string used by the proximity message and the action menu
string GetRequiredItemsMessage()
{
if (!m_TypeDef) return "Locked (unknown requirements)";
return "Requires: " + m_TypeDef.GetRequiredItemsDisplayString();
}
// Hooks our custom hold-to-open action onto this object without
// needing to touch any vanilla cfgVehicles action arrays.
override void GetActions(out array<typename> actions)
{
super.GetActions(actions);
actions.Insert(YALB_ActionOpenLootbox);
}
};

View file

@ -0,0 +1,244 @@
// ============================================================
// YALootBoxes_Manager.c
// Owns the lifecycle of all lootboxes: initial spawn, periodic
// despawn/respawn checks, and proximity "requires X" messages.
// Lives in: scripts/4_World/YALootBoxes_Manager.c
//
// Instantiated once from mission init (see init.c snippet at
// bottom of this file).
// ============================================================
class YALB_ActiveBoxEntry
{
YALootBoxes_Base box;
string locationName; // which spawn_locations.json entry it's using
};
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 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>();
}
void Init()
{
YALB_ConfigLoader.LoadAll();
if (!YALB_ConfigLoader.SpawnLocations || YALB_ConfigLoader.SpawnLocations.SpawnLocations.Count() == 0)
{
YALB_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();
// Periodic lifecycle tick (despawn/respawn checks)
float intervalMs = YALB_ConfigLoader.MainConfig.GlobalCheckIntervalSeconds * 1000;
GetGame().GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(TickLifecycle, intervalMs, true);
// Proximity message tick - runs more frequently than the
// 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.");
}
protected void SpawnInitialBatch()
{
int target = YALB_ConfigLoader.MainConfig.MaxActiveLootboxes;
for (int i = 0; i < target; i++)
{
if (!SpawnOneAtRandomFreeLocation())
break; // ran out of free locations
}
}
// Picks a free location, picks the box type config matching that
// location's boxType, spawns the entity, marks location occupied.
protected bool SpawnOneAtRandomFreeLocation()
{
if (m_FreeLocationNames.Count() == 0) return false;
int idx = Math.RandomInt(0, m_FreeLocationNames.Count());
string locName = m_FreeLocationNames[idx];
YALB_SpawnLocationDef locDef = FindLocationDef(locName);
if (!locDef)
{
m_FreeLocationNames.Remove(idx);
return SpawnOneAtRandomFreeLocation(); // try again with a different one
}
YALB_BoxTypeDef typeDef = YALB_ConfigLoader.GetBoxTypeByType(locDef.boxType);
if (!typeDef)
{
YALB_Log("WARNING: spawn location '" + locName + "' references unknown boxType '" + locDef.boxType + "'. Skipping.");
m_FreeLocationNames.Remove(idx);
return SpawnOneAtRandomFreeLocation();
}
Object obj = GetGame().CreateObject(typeDef.className, locDef.GetPosition(), false, true, true);
YALootBoxes_Base box = YALootBoxes_Base.Cast(obj);
if (!box)
{
YALB_Log("ERROR: Failed to spawn class '" + typeDef.className + "' at '" + locName + "'.");
m_FreeLocationNames.Remove(idx);
return SpawnOneAtRandomFreeLocation();
}
box.SetPosition(locDef.GetPosition());
box.Init(locDef.boxType);
YALB_ActiveBoxEntry entry = new YALB_ActiveBoxEntry();
entry.box = box;
entry.locationName = locName;
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()));
return true;
}
protected YALB_SpawnLocationDef FindLocationDef(string name)
{
foreach (YALB_SpawnLocationDef d : YALB_ConfigLoader.SpawnLocations.SpawnLocations)
{
if (d.name == name) return d;
}
return null;
}
// Called every GlobalCheckIntervalSeconds. Removes expired boxes
// (whether still locked or already opened) and frees their location,
// then immediately tries to fill the gap at a NEW random location.
void TickLifecycle()
{
float now = GetGame().GetTime() / 1000.0;
array<ref YALB_ActiveBoxEntry> toRemove = new array<ref YALB_ActiveBoxEntry>();
foreach (YALB_ActiveBoxEntry entry : m_ActiveBoxes)
{
if (!entry.box)
{
toRemove.Insert(entry);
continue;
}
bool expired = entry.box.HasExpiredUnopened(now) || entry.box.HasExpiredAfterOpening(now);
if (expired)
{
toRemove.Insert(entry);
}
}
foreach (YALB_ActiveBoxEntry deadEntry : toRemove)
{
RemoveAndFreeLocation(deadEntry);
}
// Backfill up to MaxActiveLootboxes using newly-freed (and any
// previously unused) locations.
int deficit = YALB_ConfigLoader.MainConfig.MaxActiveLootboxes - m_ActiveBoxes.Count();
for (int i = 0; i < deficit; i++)
{
if (!SpawnOneAtRandomFreeLocation()) break;
}
YALB_Log(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)
{
GetGame().ObjectDelete(entry.box);
}
}
// Broadcasts "Requires: X, Y" to any player standing within range
// of a LOCKED box, regardless of what they're holding (per design:
// always show required items, even empty-handed).
void TickProximityMessages()
{
if (!YALB_ConfigLoader.MainConfig) return;
float range = YALB_ConfigLoader.MainConfig.ProximityMessageRangeMeters;
float now = GetGame().GetTime() / 1000.0;
array<Man> players = new array<Man>();
GetGame().GetWorld().GetPlayerList(players);
foreach (Man m : players)
{
PlayerBase player = PlayerBase.Cast(m);
if (!player || !player.GetIdentity()) continue;
YALootBoxes_Base nearest = FindNearestLockedBox(player.GetPosition(), range);
if (!nearest) continue;
// Light cooldown per player so we're not spamming every 1.5s
int playerId = player.GetIdentity().GetPlainId().HashCode();
float lastShown = 0;
m_PlayerMessageCooldown.Find(playerId, lastShown);
if (now - lastShown < 4.0) continue;
YALB_NotifyPlayer(player, nearest.GetRequiredItemsMessage());
m_PlayerMessageCooldown.Set(playerId, now);
}
}
protected YALootBoxes_Base FindNearestLockedBox(vector pos, float maxRange)
{
YALootBoxes_Base closest = null;
float closestDist = maxRange;
foreach (YALB_ActiveBoxEntry entry : m_ActiveBoxes)
{
if (!entry.box || !entry.box.IsLocked()) continue;
float dist = vector.Distance(entry.box.GetPosition(), pos);
if (dist <= closestDist)
{
closest = entry.box;
closestDist = dist;
}
}
return closest;
}
};
// ============================================================
// MISSION INIT HOOK
// Add this to your mission's init.c (server side only):
//
// YALootBoxesManager g_YALootBoxesManager;
//
// void main()
// {
// ...
// if (GetGame().IsServer())
// {
// g_YALootBoxesManager = new YALootBoxesManager();
// g_YALootBoxesManager.Init();
// }
// }
// ============================================================