diff --git a/Prefix.cpp b/Prefix.cpp index 0169c28..a689c78 100644 --- a/Prefix.cpp +++ b/Prefix.cpp @@ -13,25 +13,26 @@ // FOLDER LAYOUT SUMMARY // ============================================================ // YALootBoxes/ -// ├── config.cpp +// ├── config.cpp (CfgPatches/CfgMods module bindings + CfgVehicles) // ├── Prefix.cpp +// ├── configfiles.readme (reference copies of the four default JSONs) +// ├── README.md +// ├── tools/ (Collect-YALootBoxesLogs.ps1 debug helper) // └── scripts/ -// └── 4_World/ -// ├── YALootBoxes_Config.c -// ├── YALootBoxes_Lootbox.c -// ├── YALootBoxes_ActionOpen.c -// └── YALootBoxes_Manager.c +// ├── 4_World/ (-> worldScriptModule) +// │ ├── YALootBoxes_Config.c (data classes, loader, logging, defaults) +// │ ├── YALootBoxes_Lootbox.c (YALootBoxes_Base entity) +// │ ├── YALootBoxes_ActionOpen.c(hold-to-open action + fail effects) +// │ └── YALootBoxes_Manager.c (spawn/despawn lifecycle + proximity msgs) +// └── 5_Mission/ (-> missionScriptModule) +// └── YALootBoxes_MissionServer.c (modded MissionServer; self-starts manager) // -// Server mission init.c needs the small hook shown at the -// bottom of YALootBoxes_Manager.c. +// The manager self-starts via the 5_Mission modded MissionServer - +// NO init.c edit is required. // -// 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. +// CLASS BINDING: the CfgVehicles crate classes inherit from a shared +// "YALootBoxes_Base" config class whose name matches the YALootBoxes_Base +// script class; the engine resolves the script type by walking that config +// inheritance chain. (The old simulationModule approach did not bind script +// classes and has been removed.) // ============================================================ \ No newline at end of file diff --git a/README.md b/README.md index 1583ce2..af86121 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,24 @@ All settings live in four JSON files auto-generated on first run at > Edit the copies under `$profile:YALootBoxes\`, **not** the ones inside the PBO. > Reference/default versions of all four are in `configfiles.readme`. +## Install & first run + +1. Pack the mod into `@YALootBoxes\addons` and add `@YALootBoxes` to the + server's `-mod=` list (after any frameworks it uses). +2. Start the server. On first run the mod **auto-generates all four JSON files + with working defaults** at `$profile:YALootBoxes\`, so boxes spawn + immediately — no manual config needed. +3. **No `init.c` edit is required.** The mod self-starts via a modded + `MissionServer` (`scripts/5_Mission/YALootBoxes_MissionServer.c`). +4. Stop the server, edit the generated JSONs to taste, restart. + +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] Manager initialized. N lootboxes spawned. +``` + ## Logging Logging verbosity is controlled by a single field in `main_config.json`: diff --git a/config.cpp b/config.cpp index 6355467..458c020 100644 --- a/config.cpp +++ b/config.cpp @@ -44,6 +44,14 @@ class CfgMods value = ""; files[] = { "YALootBoxes/scripts/4_World" }; }; + // 5_Mission bootstrap: a modded MissionServer that self-starts the + // lootbox manager. Lives in the Mission module because that's the + // only module with a mission-lifecycle entry point (OnInit). + class missionScriptModule + { + value = ""; + files[] = { "YALootBoxes/scripts/5_Mission" }; + }; }; }; }; diff --git a/scripts/4_World/YALootBoxes_Config.c b/scripts/4_World/YALootBoxes_Config.c index 044ad74..3fb4718 100644 --- a/scripts/4_World/YALootBoxes_Config.c +++ b/scripts/4_World/YALootBoxes_Config.c @@ -174,6 +174,50 @@ class YALB_SpawnLocationsFile } }; +// ------------------------------------------------------------ +// 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 // ------------------------------------------------------------ @@ -198,61 +242,147 @@ class YALB_ConfigLoader } } + // 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 mainConfigExisted = FileExist(YALB_MAIN_CONFIG_FILE); - if (mainConfigExisted) - { - JsonFileLoader.JsonLoadFile(YALB_MAIN_CONFIG_FILE, MainConfig); - } - else - { + bool createdMain = !FileExist(YALB_MAIN_CONFIG_FILE); + if (createdMain) JsonFileLoader.JsonSaveFile(YALB_MAIN_CONFIG_FILE, MainConfig); - } + else + JsonFileLoader.JsonLoadFile(YALB_MAIN_CONFIG_FILE, MainConfig); // Parse the configured level NOW so every log below honours it. ActiveLogLevel = YALB_ParseLogLevel(MainConfig.DebugLogLevel); + if (createdMain) Log_Info("main_config.json not found - created with defaults."); - if (!mainConfigExisted) - Log_Info("main_config.json not found - created with defaults."); - - BoxTypes = new YALB_BoxTypesFile(); + // --- box types --- if (FileExist(YALB_BOX_TYPES_FILE)) { + BoxTypes = new YALB_BoxTypesFile(); JsonFileLoader.JsonLoadFile(YALB_BOX_TYPES_FILE, BoxTypes); } else { - Log_Error("box_types.json not found - no lootbox types loaded! Mod will not spawn anything until this exists."); + BoxTypes = DefaultBoxTypes(); + JsonFileLoader.JsonSaveFile(YALB_BOX_TYPES_FILE, BoxTypes); + Log_Info("box_types.json not found - created with defaults."); } - LootLists = new YALB_LootListsFile(); + // --- loot lists --- if (FileExist(YALB_LOOT_LISTS_FILE)) { + LootLists = new YALB_LootListsFile(); JsonFileLoader.JsonLoadFile(YALB_LOOT_LISTS_FILE, LootLists); } else { - Log_Warn("loot_lists.json not found - lootboxes will open empty!"); + LootLists = DefaultLootLists(); + JsonFileLoader.JsonSaveFile(YALB_LOOT_LISTS_FILE, LootLists); + Log_Info("loot_lists.json not found - created with defaults."); } - SpawnLocations = new YALB_SpawnLocationsFile(); + // --- spawn locations --- if (FileExist(YALB_SPAWN_LOCATIONS_FILE)) { + SpawnLocations = new YALB_SpawnLocationsFile(); JsonFileLoader.JsonLoadFile(YALB_SPAWN_LOCATIONS_FILE, SpawnLocations); } else { - Log_Error("spawn_locations.json not found - no spawn points available!"); + 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; diff --git a/scripts/4_World/YALootBoxes_Manager.c b/scripts/4_World/YALootBoxes_Manager.c index fb30c61..bad29bc 100644 --- a/scripts/4_World/YALootBoxes_Manager.c +++ b/scripts/4_World/YALootBoxes_Manager.c @@ -223,18 +223,17 @@ class YALootBoxesManager }; // ============================================================ -// MISSION INIT HOOK -// Add this to your mission's init.c (server side only): +// BOOTSTRAP +// This manager is started automatically by the modded MissionServer +// in scripts/5_Mission/YALootBoxes_MissionServer.c (OnInit). No +// init.c edit is required. // -// YALootBoxesManager g_YALootBoxesManager; +// If you instead run a mission that does NOT go through MissionServer +// and want to start it manually, the equivalent init.c hook is: // -// void main() +// if (GetGame().IsServer()) // { -// ... -// if (GetGame().IsServer()) -// { -// g_YALootBoxesManager = new YALootBoxesManager(); -// g_YALootBoxesManager.Init(); -// } +// g_YALootBoxesManager = new YALootBoxesManager(); +// g_YALootBoxesManager.Init(); // } // ============================================================ \ No newline at end of file diff --git a/scripts/5_Mission/YALootBoxes_MissionServer.c b/scripts/5_Mission/YALootBoxes_MissionServer.c new file mode 100644 index 0000000..5758a46 --- /dev/null +++ b/scripts/5_Mission/YALootBoxes_MissionServer.c @@ -0,0 +1,23 @@ +// ============================================================ +// YALootBoxes_MissionServer.c +// Self-starts the lootbox manager on the server - no init.c edit +// required. Lives in: scripts/5_Mission/ +// +// MissionServer exists/instantiates on the dedicated server only, +// so this whole bootstrap is inherently server-side. It augments +// whatever mission the map ships (via 'modded') instead of +// replacing it, so it coexists with other mission mods. +// ============================================================ + +modded class MissionServer +{ + ref YALootBoxesManager m_YALootBoxes_Manager; + + override void OnInit() + { + super.OnInit(); + + m_YALootBoxes_Manager = new YALootBoxesManager(); + m_YALootBoxes_Manager.Init(); + } +}