// ============================================================ // 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 { ref array _comment; // admin-facing field docs (ignored by logic) int MaxActiveLootboxes; int GlobalCheckIntervalSeconds; int DefaultDespawnMinutes; int OpenChannelDurationSeconds; float ProximityMessageRangeMeters; int DebugLogLevel; // numeric verbosity, see _comment / YALB_LogLevel void YALB_MainConfig() { // Sane defaults if file is missing/corrupt MaxActiveLootboxes = 30; GlobalCheckIntervalSeconds = 60; DefaultDespawnMinutes = 15; OpenChannelDurationSeconds = 5; ProximityMessageRangeMeters = 3.0; 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." }; } }; // ------------------------------------------------------------ // BOX TYPES // ------------------------------------------------------------ class YALB_RequiredItem { string classname; string displayName; }; class YALB_FailEffect { string type; // EXPLOSIVE, HEALTH, BLEEDING, SICKNESS, BROKEN_BONE float chance; // RELATIVE WEIGHT for the single weighted pick - NOT an // independent probability. Exactly one effect is chosen // per failure; an effect's odds = its chance / sum(chances). 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 requiredItems; float openChance; float itemDamageOnOpen; int despawnMinutes; ref array failEffects; void YALB_BoxTypeDef() { requiredItems = new array(); failEffects = new array(); 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 _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)." }; } }; // ------------------------------------------------------------ // LOOT LISTS // ------------------------------------------------------------ class YALB_LootEntry { string classname; float chance; int minCount; int maxCount; }; // One loot list per box "type". Stored as a flat array of {type, entries} // objects rather than a map> - DayZ's JsonFileLoader is // unreliable at deserializing nested generic containers (a map whose values // are themselves ref arrays), whereas an array of typed objects deserializes // dependably. Lookup is a small linear scan (only a handful of types). class YALB_LootList { string type; ref array entries; void YALB_LootList() { entries = new array(); } }; 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)." }; } }; // ------------------------------------------------------------ // SPAWN LOCATIONS // ------------------------------------------------------------ class YALB_SpawnLocationDef { string name; ref array position; // [x, y, z] - 'ref' so the array member is owned/allocated 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 _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." }; } }; // ------------------------------------------------------------ // DEFAULT-DATA FACTORIES // Small constructors used to build the first-run default files so // the mod is self-provisioning. These mirror configfiles.readme. // ------------------------------------------------------------ YALB_RequiredItem YALB_MkReq(string cls, string disp) { YALB_RequiredItem r = new YALB_RequiredItem(); r.classname = cls; r.displayName = disp; return r; } YALB_FailEffect YALB_MkFail(string type, float chance, float damage = 0.0, float radius = 0.0, int bleed = 0, string agent = "") { YALB_FailEffect e = new YALB_FailEffect(); e.type = type; e.chance = chance; e.damage = damage; e.radius = radius; e.bleedSources = bleed; e.agent = agent; return e; } YALB_LootEntry YALB_MkLoot(string cls, float chance, int mn, int mx) { YALB_LootEntry l = new YALB_LootEntry(); l.classname = cls; l.chance = chance; l.minCount = mn; l.maxCount = mx; return l; } YALB_SpawnLocationDef YALB_MkLoc(string name, float x, float y, float z, string boxType) { YALB_SpawnLocationDef d = new YALB_SpawnLocationDef(); d.name = name; d.boxType = boxType; d.position = new array(); d.position.Insert(x); d.position.Insert(y); d.position.Insert(z); return d; } YALB_BoxTypeDef YALB_MkBoxType(string className, string type, float openChance, float itemDmg, int despawn) { YALB_BoxTypeDef b = new YALB_BoxTypeDef(); // requiredItems/failEffects arrays alloc'd in ctor b.className = className; b.type = type; b.openChance = openChance; b.itemDamageOnOpen = itemDmg; b.despawnMinutes = despawn; return b; } // ------------------------------------------------------------ // 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; // Parsed-once numeric mirror of MainConfig.DebugLogLevel (see YALB_LogLevel). // Defaults to INFO so logging behaves sanely before LoadAll() runs. // NB: named ActiveLogLevel, not LogLevel - a loaded framework // (CommunityFramework/DabsFramework) already defines a type 'LogLevel'. static int ActiveLogLevel = YALB_LogLevel.INFO; static void EnsureDirectoryExists() { if (!FileExist(YALB_CONFIG_DIR)) { MakeDirectory(YALB_CONFIG_DIR); } } // Loads each config file, writing a populated default if it is missing, // so a fresh install is fully self-provisioning (boxes spawn on first run). static void LoadAll() { EnsureDirectoryExists(); // --- main config (load, or save defaults) --- MainConfig = new YALB_MainConfig(); bool createdMain = !FileExist(YALB_MAIN_CONFIG_FILE); if (createdMain) JsonFileLoader.JsonSaveFile(YALB_MAIN_CONFIG_FILE, MainConfig); else JsonFileLoader.JsonLoadFile(YALB_MAIN_CONFIG_FILE, MainConfig); // 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 --- if (FileExist(YALB_BOX_TYPES_FILE)) { BoxTypes = new YALB_BoxTypesFile(); JsonFileLoader.JsonLoadFile(YALB_BOX_TYPES_FILE, BoxTypes); } else { BoxTypes = DefaultBoxTypes(); JsonFileLoader.JsonSaveFile(YALB_BOX_TYPES_FILE, BoxTypes); Log_Info("box_types.json not found - created with defaults."); } // --- loot lists --- if (FileExist(YALB_LOOT_LISTS_FILE)) { LootLists = new YALB_LootListsFile(); JsonFileLoader.JsonLoadFile(YALB_LOOT_LISTS_FILE, LootLists); } else { LootLists = DefaultLootLists(); JsonFileLoader.JsonSaveFile(YALB_LOOT_LISTS_FILE, LootLists); Log_Info("loot_lists.json not found - created with defaults."); } // --- spawn locations --- if (FileExist(YALB_SPAWN_LOCATIONS_FILE)) { SpawnLocations = new YALB_SpawnLocationsFile(); JsonFileLoader.JsonLoadFile(YALB_SPAWN_LOCATIONS_FILE, SpawnLocations); } else { SpawnLocations = DefaultSpawnLocations(); JsonFileLoader.JsonSaveFile(YALB_SPAWN_LOCATIONS_FILE, SpawnLocations); Log_Info("spawn_locations.json not found - created with defaults."); } 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)); } // ------------------------------------------------------------ // First-run default builders (mirror configfiles.readme) // ------------------------------------------------------------ static YALB_BoxTypesFile DefaultBoxTypes() { YALB_BoxTypesFile f = new YALB_BoxTypesFile(); YALB_BoxTypeDef mil = YALB_MkBoxType("YALootBoxes_Crate_Military", "military", 0.65, 8.0, 15); mil.requiredItems.Insert(YALB_MkReq("BoltCutters", "Bolt Cutters")); mil.requiredItems.Insert(YALB_MkReq("Crowbar", "Crowbar")); mil.failEffects.Insert(YALB_MkFail("EXPLOSIVE", 0.15, 45.0, 2.5)); mil.failEffects.Insert(YALB_MkFail("HEALTH", 0.25, 15.0)); mil.failEffects.Insert(YALB_MkFail("BLEEDING", 0.30, 0.0, 0.0, 2)); mil.failEffects.Insert(YALB_MkFail("SICKNESS", 0.15, 0.0, 0.0, 0, "Salmonella")); mil.failEffects.Insert(YALB_MkFail("BROKEN_BONE", 0.15)); f.BoxTypes.Insert(mil); YALB_BoxTypeDef med = YALB_MkBoxType("YALootBoxes_Crate_Medical", "medical", 0.80, 4.0, 15); med.requiredItems.Insert(YALB_MkReq("Hacksaw", "Hacksaw")); med.failEffects.Insert(YALB_MkFail("SICKNESS", 0.50, 0.0, 0.0, 0, "Influenza")); med.failEffects.Insert(YALB_MkFail("HEALTH", 0.30, 8.0)); med.failEffects.Insert(YALB_MkFail("BLEEDING", 0.20, 0.0, 0.0, 1)); f.BoxTypes.Insert(med); YALB_BoxTypeDef ind = YALB_MkBoxType("YALootBoxes_Crate_Industrial", "industrial", 0.50, 12.0, 20); ind.requiredItems.Insert(YALB_MkReq("Crowbar", "Crowbar")); ind.requiredItems.Insert(YALB_MkReq("WeldingMask", "Angle Grinder")); ind.failEffects.Insert(YALB_MkFail("EXPLOSIVE", 0.20, 60.0, 3.0)); ind.failEffects.Insert(YALB_MkFail("BROKEN_BONE", 0.25)); ind.failEffects.Insert(YALB_MkFail("BLEEDING", 0.30, 0.0, 0.0, 2)); ind.failEffects.Insert(YALB_MkFail("HEALTH", 0.25, 20.0)); f.BoxTypes.Insert(ind); return f; } static YALB_LootListsFile DefaultLootLists() { YALB_LootListsFile f = new YALB_LootListsFile(); YALB_LootList mil = new YALB_LootList(); mil.type = "military"; mil.entries.Insert(YALB_MkLoot("AKM", 0.10, 1, 1)); mil.entries.Insert(YALB_MkLoot("Mag_AKM_30Rnd", 0.40, 1, 3)); mil.entries.Insert(YALB_MkLoot("PlateCarrierVest", 0.20, 1, 1)); mil.entries.Insert(YALB_MkLoot("TacticalHelmet", 0.15, 1, 1)); mil.entries.Insert(YALB_MkLoot("Ammo_762x39", 0.50, 1, 2)); f.LootLists.Insert(mil); YALB_LootList med = new YALB_LootList(); med.type = "medical"; med.entries.Insert(YALB_MkLoot("Morphine", 0.30, 1, 1)); med.entries.Insert(YALB_MkLoot("SalineBagIV", 0.25, 1, 2)); med.entries.Insert(YALB_MkLoot("VitaminBottle", 0.50, 1, 3)); med.entries.Insert(YALB_MkLoot("Bandage", 0.60, 2, 5)); med.entries.Insert(YALB_MkLoot("TetracyclineAntibiotics", 0.20, 1, 2)); f.LootLists.Insert(med); YALB_LootList ind = new YALB_LootList(); ind.type = "industrial"; ind.entries.Insert(YALB_MkLoot("WeldingMask", 0.15, 1, 1)); ind.entries.Insert(YALB_MkLoot("Battery_Car", 0.30, 1, 1)); ind.entries.Insert(YALB_MkLoot("SparkPlug", 0.40, 1, 2)); ind.entries.Insert(YALB_MkLoot("MetalPipe", 0.50, 1, 4)); ind.entries.Insert(YALB_MkLoot("DuctTape", 0.45, 1, 2)); f.LootLists.Insert(ind); return f; } static YALB_SpawnLocationsFile DefaultSpawnLocations() { YALB_SpawnLocationsFile f = new YALB_SpawnLocationsFile(); f.SpawnLocations.Insert(YALB_MkLoc("NWAF_Hangar1", 4985.3, 320.5, 2456.8, "military")); f.SpawnLocations.Insert(YALB_MkLoc("NWAF_Hangar2", 5102.7, 318.9, 2401.2, "military")); f.SpawnLocations.Insert(YALB_MkLoc("Tisy_Bunker_A", 1606.4, 280.1, 14383.9, "military")); f.SpawnLocations.Insert(YALB_MkLoc("Berezino_Hospital", 7245.1, 12.4, 13889.6, "medical")); f.SpawnLocations.Insert(YALB_MkLoc("Zelenogorsk_Clinic",2543.9, 8.2, 5234.7, "medical")); f.SpawnLocations.Insert(YALB_MkLoc("Tractor_Plant_1", 9821.2, 145.6, 8421.3, "industrial")); f.SpawnLocations.Insert(YALB_MkLoc("Tractor_Plant_2", 9890.5, 142.1, 8390.7, "industrial")); return f; } 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 GetLootListForType(string type) { if (!LootLists) return null; foreach (YALB_LootList list : LootLists.LootLists) { if (list.type == type) return list.entries; } return null; } }; // ------------------------------------------------------------ // LOGGING SYSTEM // ------------------------------------------------------------ // Ordered severity. A message at level X is emitted only when the // configured level >= X. NONE silences everything. enum YALB_LogLevel { NONE = 0, ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4, VERBOSE = 5 }; // Shared gate + format for the non-error levels. void YALB_Emit(int level, string tag, string msg) { if (YALB_ConfigLoader.ActiveLogLevel < level) return; 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.ActiveLogLevel < 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); }