diff --git a/.backlog/README.md b/.backlog/README.md index df662cb..c913681 100644 --- a/.backlog/README.md +++ b/.backlog/README.md @@ -14,7 +14,7 @@ authored **per lot, when the lot is started** (not in batch). | Lot | Status | Description | Branch | |-----|--------|-------------|--------| -| `FEAT-USERCONFIG-API` | ⬜ ready | Replace broken Section 2 of `CTLD_userConfig.lua` with a safe MM API (`ctld.userSetup` callbacks + helpers), relocate `injectAACrates` to bootstrap, fix all parity bugs in userConfig template. | — | +| `FEAT-USERCONFIG-API` | ✅ merged (PR #45) | Replace broken Section 2 of `CTLD_userConfig.lua` with a safe MM API (`ctld.userSetup` callbacks + helpers), relocate `injectAACrates` to bootstrap, fix all parity bugs in userConfig template. | `feature/userconfig-api` | | `CTLD-TOOLS-CONFIG` | ⬜ ready | Lot 2 of `ctld-tools` ([ADR 0009](../dev/adr/0009-external-yaml-authoring-ctld-tools.md)): move engine defaults out of `CTLD_config.lua` into `ctld-config.yaml` (source of truth, sectioned MM-facing/advanced); Python `ctld-tools` package + `gen-config` (YAML→Lua) as a build step; Lua-5.1 round-trip parity test guards the switch-over. MM volet / miz injection / TUI out of scope. | — | | `CTLD-TOOLS-USERCONFIG` | ⬜ ready | Lot 3 of `ctld-tools` ([ADR 0009](../dev/adr/0009-external-yaml-authoring-ctld-tools.md)): MM volet — `validate` (user-config.yaml against the reference + datamine, clear report) + `gen-user` (compile add/delete/edit ops into `CTLD_userConfig.lua` calling the `ctld.userSetup` helpers) + `gen-user --scaffold` (commented starter); `ctld-tools.exe` attached to GitHub Releases. Depends on `FEAT-USERCONFIG-API` + `CTLD-TOOLS-CONFIG`. miz injection / TUI out of scope. | — | | `DOC-README-CLEANUP` | ✅ merged (PR #21) | Fix README title, restructure Crate Operations sub-sections, relocate AA System Construction, replace Developer Guide with Documentation links. | — | diff --git a/CHANGELOG.md b/CHANGELOG.md index 37bb52b..e2afe3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ Versioning follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Feature — safe Mission Maker config API `ctld.userSetup` (FEAT-USERCONFIG-API) + +- **New `ctld.userSetup` API**: Mission Makers customise the complex config tables from setup + callbacks instead of the silently-broken Section 2 of `CTLD_userConfig.lua` (which called + `CTLDConfig.get()` before CTLD had defined it). Helpers on `ctld`: `addCrate`, `removeCrate`, + `patchCrate` (deep-merge one level), `addTroopGroup`, `removeTroopGroup`, `addTo`, `logDefaults`. + Each callback runs guarded, so a failing one warns without aborting the others or the mission. +- **`injectAACrates` relocated** from `CTLDCrateManager:_processSpawnableCrates()` to + `ctld.initialize()` (before the userSetup callbacks), so the AA-system crate sections are visible + and patchable from a callback. `ctld.initialize()` is now the single place that materialises the + full config: defaults → AA injection → userSetup callbacks → managers. +- **`CTLD_userConfig.lua` template rewritten**: the broken Section 2 (direct `CTLDConfig.get()` + edits) is replaced by documented `ctld.userSetup` examples + per-table field schemas; the + test-only debug block (with its hardcoded `aiZones`) is removed; Section 1 scalar defaults + corrected (`parachuteMinAltitude*` = 152, `JTAC_droneAltitude` = 4000). +- **Docs**: `mission-maker/configuration.md` (EN + FR) updated to the callback-based flow. + ### Tooling — release pre-release channel + `published-latest` (RELEASE-RC-CHANNEL) - **`release.yml`**: a `-rc`-suffixed version (e.g. `published-v2.0.0-rc1`) now publishes the GitHub diff --git a/CTLD.lua b/CTLD.lua index 4f3ae3c..e9b4b06 100644 --- a/CTLD.lua +++ b/CTLD.lua @@ -4,7 +4,7 @@ --[[ CTLD.lua - Combined Transport and Logistics Dispatcher for DCS World Version : 2.0.0-rc1 - Built : 2026-07-19 + Built : 2026-07-20 Source : https://github.com/VEAF/CTLD Licence : MIT DO NOT EDIT - generated by tools/build/merge_CTLD.ps1 @@ -1204,6 +1204,194 @@ CTLDConfig.reset() -- class method (dot notation) -- End : CTLD_config.lua -- ==================================================================================================== +-- Start : CTLD_userSetup.lua +-- ============================================================ +-- CTLD_userSetup.lua +-- Mission Maker configuration helper API (FEAT-USERCONFIG-API). +-- +-- Surgical add/remove/patch operations on the live config, meant to be called +-- from `ctld.userSetup` callbacks. The bootstrap runs those callbacks after +-- CTLDConfig:load() and injectAACrates(), so AA entries are already present and +-- the whole default catalogue is patchable. +-- +-- ctld.userSetup = ctld.userSetup or {} +-- table.insert(ctld.userSetup, function(cfg) +-- ctld.addCrate("Support", { weight = 2000.01, desc = "My Unit", unit = "Ural-375", side = 1 }) +-- ctld.patchCrate(1001.01, { cratesRequired = 3 }) +-- end) +-- +-- Merged after CTLD_config.lua. Defines functions only — the ctld.utils.* calls +-- resolve at callback time (runtime), so load order beyond `ctld` is irrelevant. +-- ============================================================ + +ctld = ctld or {} + +-- Live settings table of the config singleton. +local function _settings() + return CTLDConfig.get().settings +end + +-- Locate a crate entry by weight across all spawnableCrates sections. +-- Returns entry, sectionTable, index (or nil). +local function _findCrate(weight) + local crates = _settings()["spawnableCrates"] + if type(crates) ~= "table" then return nil end + for _, entries in pairs(crates) do + for i, e in ipairs(entries) do + if e.weight == weight then return e, entries, i end + end + end + return nil +end + +--- Add a crate entry to a spawnableCrates section (section created if absent). +-- A duplicate weight is fatal (irrecoverable F10-menu corruption): the entry is +-- rejected with a visible warning, but the mission is NOT aborted. +---@param section string +---@param entry table +---@return boolean added +function ctld.addCrate(section, entry) + local crates = _settings()["spawnableCrates"] + if type(crates) ~= "table" then + ctld.logWarning("ctld.addCrate: spawnableCrates unavailable — entry rejected") + return false + end + if entry and entry.weight and _findCrate(entry.weight) then + local msg = string.format( + "CTLD userConfig: duplicate crate weight %s — entry rejected", tostring(entry.weight)) + ctld.logWarning("ctld.addCrate: %s", msg) + if trigger and trigger.action and trigger.action.outText then + trigger.action.outText(msg, 30) + end + return false + end + crates[section] = crates[section] or {} + table.insert(crates[section], entry) + return true +end + +--- Remove a crate entry by weight (searched across all sections). +---@param weight number +---@return boolean removed +function ctld.removeCrate(weight) + local _, entries, idx = _findCrate(weight) + if not entries then + ctld.logWarning("ctld.removeCrate: no crate with weight %s — nothing removed", tostring(weight)) + return false + end + table.remove(entries, idx) + return true +end + +--- Patch a crate entry found by weight. Shallow merge at the top level, plus a +-- one-level deep merge for table-valued fields (e.g. specificParams), so a single +-- nested field can be changed without rewriting the whole sub-table. +---@param weight number +---@param patch table +---@return boolean patched +function ctld.patchCrate(weight, patch) + local entry = _findCrate(weight) + if not entry then + ctld.logWarning("ctld.patchCrate: no crate with weight %s — nothing patched", tostring(weight)) + return false + end + for k, v in pairs(patch or {}) do + if type(v) == "table" and type(entry[k]) == "table" then + for kk, vv in pairs(v) do + entry[k][kk] = vv + end + else + entry[k] = v + end + end + return true +end + +--- Add an infantry group template to loadableGroups. +---@param entry table +---@return boolean added +function ctld.addTroopGroup(entry) + local groups = _settings()["loadableGroups"] + if type(groups) ~= "table" then + ctld.logWarning("ctld.addTroopGroup: loadableGroups unavailable — entry rejected") + return false + end + table.insert(groups, entry) + return true +end + +--- Remove an infantry group template by name. +---@param name string +---@return boolean removed +function ctld.removeTroopGroup(name) + local groups = _settings()["loadableGroups"] + if type(groups) == "table" then + for i, g in ipairs(groups) do + if g.name == name then + table.remove(groups, i) + return true + end + end + end + ctld.logWarning("ctld.removeTroopGroup: no group named '%s' — nothing removed", tostring(name)) + return false +end + +--- Append an entry to an array-type setting (transportPilotNames, troopZones, +-- wpZones, extractableGroups, logisticUnits, ...). +---@param settingName string +---@param entry any +---@return boolean added +function ctld.addTo(settingName, entry) + local value = _settings()[settingName] + if type(value) ~= "table" then + ctld.logWarning("ctld.addTo: setting '%s' is not an array — nothing added", tostring(settingName)) + return false + end + table.insert(value, entry) + return true +end + +--- Serialise a config setting to CTLD.log in copy-pasteable Lua form, so the MM +-- can inspect the real defaults at runtime. Intended to be called from a ME +-- DO SCRIPT trigger a few seconds after mission start. +---@param settingName string +function ctld.logDefaults(settingName) + local value = ctld.gs(settingName) + if value == nil then + ctld.logWarning("ctld.logDefaults: unknown setting '%s'", tostring(settingName)) + return + end + local out + if type(value) == "table" then + out = ctld.utils.tableShowScript("ctld.logDefaults", value, settingName) + else + out = settingName .. " = " .. ctld.utils.basicSerialize("ctld.logDefaults", value) + end + ctld.utils.log("INFO", "ctld.logDefaults(%s):\n%s", tostring(settingName), out) +end + +--- Run every registered ctld.userSetup callback, in registration order, against the +-- live config singleton. Each callback is guarded so a failure in one is reported +-- and neither aborts the other callbacks nor the mission (FEAT-USERCONFIG-API US 11). +-- Called by ctld.initialize() after injectAACrates and before the managers boot. +function ctld.runUserSetup() + local callbacks = ctld.userSetup or {} + local cfg = CTLDConfig.get() + for i, cb in ipairs(callbacks) do + if type(cb) == "function" then + local ok, err = pcall(cb, cfg) + if not ok then + ctld.logWarning("ctld.userSetup callback #%d failed: %s", i, tostring(err)) + end + else + ctld.logWarning("ctld.userSetup entry #%d is not a function — skipped", i) + end + end +end + +-- End : CTLD_userSetup.lua +-- ==================================================================================================== -- Start : CTLD_i18n.lua --[[ CTLD — Internationalization class (CTLDi18n) @@ -11467,9 +11655,8 @@ end -- Results stored in self._processedCrates[category] and self._weightIndex[weight]. function CTLDCrateManager:_processSpawnableCrates() local spawnableCrates = ctld.gs("spawnableCrates") or {} - -- Inject AA system crate entries (parts + repair) from CTLDCrateAssemblyManager.TEMPLATES. - -- Must run before the main loop so injected entries enter _processedCrates and _weightIndex. - CTLDCrateAssemblyManager.injectAACrates(spawnableCrates) + -- AA system crate entries are injected once by ctld.initialize() (before the userSetup + -- callbacks), so spawnableCrates already contains them here — see ADR 0008/0009. local showCrateSets = ctld.gs("enableAllCrates") ~= false local allSuffix = " - " .. ctld.tr("All crates") @@ -24290,6 +24477,14 @@ function ctld.initialize() CTLDConfig.get():load() ctld.utils.initLog() + -- Materialise the full configuration before any manager reads it: + -- 1) inject the AA system crate entries into spawnableCrates, + -- 2) run the Mission Maker userSetup callbacks (add/remove/patch). + -- This is the single place that controls config materialisation order, so managers + -- always see the final, complete config table (ADR 0008/0009). + CTLDCrateAssemblyManager.injectAACrates(ctld.gs("spawnableCrates")) + ctld.runUserSetup() + -- Boot all domain managers first so they can register their menu sections. -- Order matters: PlayerManager must be up before any other manager calls -- registerMenuSection(), and _scanExistingPlayers() must run last so all diff --git a/docs/mission-maker/configuration.fr.md b/docs/mission-maker/configuration.fr.md index 05953de..26dcc2c 100644 --- a/docs/mission-maker/configuration.fr.md +++ b/docs/mission-maker/configuration.fr.md @@ -55,15 +55,29 @@ ctld.slingLoad: true ]] ``` -Les **tables** (capacités d'appareils, listes de zones, catalogues de crate…) sont affectées -directement sur l'instance de configuration. Chaque table que vous affectez **remplace** -entièrement sa valeur par défaut : +Les **tables** (catalogues de crate, groupes de troupes, listes de zones, capacités d'appareils…) +se personnalisent depuis un ou plusieurs **callbacks de configuration** enregistrés dans +`ctld.userSetup`. CTLD exécute chaque callback une fois à l'initialisation, après la mise en place +des valeurs par défaut (et des crates AA auto-injectées) — vous modifiez donc le vrai catalogue de +façon chirurgicale au lieu de le recopier et de le maintenir en entier : ```lua -local _cfg = CTLDConfig.get() -_cfg.settings["capabilitiesByType"] = { --[[ ... ]] } +ctld.userSetup = ctld.userSetup or {} +table.insert(ctld.userSetup, function(cfg) + ctld.addCrate("Support", { weight = 2000.01, desc = "Ural Ammo", unit = "Ural-375", side = 1 }) + ctld.removeCrate(1000.05) -- retire une crate par défaut par son poids + ctld.patchCrate(1000.02, { cratesRequired = 3 }) -- change un champ, garde le reste + ctld.addTroopGroup({ name = "Recon Team", inf = 3, jtac = 1 }) + ctld.addTo("transportPilotNames", "helicargo_custom_1") +end) ``` +Les helpers — `ctld.addCrate`, `ctld.removeCrate`, `ctld.patchCrate`, `ctld.addTroopGroup`, +`ctld.removeTroopGroup`, `ctld.addTo` — opèrent sur la configuration vivante. Les dictionnaires +sans helper (`capabilitiesByType`, `aiZones`, …) s'éditent directement sur l'argument `cfg`. Appelez +`ctld.logDefaults("spawnableCrates")` depuis un trigger de mission pour écrire les valeurs par +défaut courantes dans `CTLD.log`, et consultez `CTLD_userConfig.lua` pour le modèle complet documenté. + ## Réglages globaux ### Système @@ -232,8 +246,8 @@ appareils listés ici reçoivent les menus F10 de CTLD.** Chaque clé est le **n exact** de l'appareil (mods inclus, ex. `"Hercules"`, `"76MD"`, `"UH-60L"`). ```lua -local _cfg = CTLDConfig.get() -_cfg.settings["capabilitiesByType"] = { +-- dans un callback ctld.userSetup : table.insert(ctld.userSetup, function(cfg) ... end) +cfg.settings["capabilitiesByType"] = { ["UH-1H"] = { cratesEnabled = true, -- can load/unpack crates troopsEnabled = true, -- can load/deploy infantry @@ -286,9 +300,10 @@ _cfg.settings["capabilitiesByType"] = { charge ensuite depuis le cockpit. Voir le [guide Pilote](../pilot/index.md) pour le déroulement en cockpit. -Les appareils **non** listés dans `capabilitiesByType` ne reçoivent aucun menu CTLD. Affecter -votre propre table `capabilitiesByType` remplace entièrement celle intégrée, donc incluez chaque -appareil que vous voulez rendre CTLD-capable. +Les appareils **non** listés dans `capabilitiesByType` ne reçoivent aucun menu CTLD. Affecter la +table `capabilitiesByType` entière remplace celle intégrée, donc incluez chaque appareil que vous +voulez rendre CTLD-capable — ou modifiez un seul champ pour conserver les défauts, p. ex. +`cfg.settings["capabilitiesByType"]["Mi-8MT"].maxTroopsOnboard = 20`. ## Contrôle d'accès @@ -309,9 +324,9 @@ ensemble fixe de slots nommés (ex. une escadrille de transport dédiée). Les a CTLD-capables **non** listés rejoignent la mission normalement mais n'ont aucun accès CTLD. ```lua -local _cfg = CTLDConfig.get() -_cfg.settings["addPlayerAircraftByType"] = false -_cfg.settings["transportPilotNames"] = { +-- dans un callback ctld.userSetup : table.insert(ctld.userSetup, function(cfg) ... end) +cfg.settings["addPlayerAircraftByType"] = false +cfg.settings["transportPilotNames"] = { "transport_slot_1", "transport_slot_2", "transport_slot_3", diff --git a/docs/mission-maker/configuration.md b/docs/mission-maker/configuration.md index d5e5c62..c1c47a1 100644 --- a/docs/mission-maker/configuration.md +++ b/docs/mission-maker/configuration.md @@ -53,14 +53,28 @@ ctld.slingLoad: true ]] ``` -**Tables** (aircraft capabilities, zone lists, crate catalogues…) are assigned directly on -the config instance. Each table you assign **replaces** its default entirely: +**Tables** (crate catalogues, troop groups, zone lists, aircraft capabilities…) are customised +from one or more **setup callbacks** registered in `ctld.userSetup`. CTLD runs each callback once +during initialization, after the factory defaults (and the auto-injected AA-system crates) are in +place — so you patch the real catalogue surgically instead of copy-pasting and owning it wholesale: ```lua -local _cfg = CTLDConfig.get() -_cfg.settings["capabilitiesByType"] = { --[[ ... ]] } +ctld.userSetup = ctld.userSetup or {} +table.insert(ctld.userSetup, function(cfg) + ctld.addCrate("Support", { weight = 2000.01, desc = "Ural Ammo", unit = "Ural-375", side = 1 }) + ctld.removeCrate(1000.05) -- drop a default crate by weight + ctld.patchCrate(1000.02, { cratesRequired = 3 }) -- change one field, keep the rest + ctld.addTroopGroup({ name = "Recon Team", inf = 3, jtac = 1 }) + ctld.addTo("transportPilotNames", "helicargo_custom_1") +end) ``` +The helpers — `ctld.addCrate`, `ctld.removeCrate`, `ctld.patchCrate`, `ctld.addTroopGroup`, +`ctld.removeTroopGroup`, `ctld.addTo` — operate on the live config. Dictionaries without a helper +(`capabilitiesByType`, `aiZones`, …) are edited directly on the `cfg` argument. Call +`ctld.logDefaults("spawnableCrates")` from a mission trigger to dump the current defaults to +`CTLD.log`, and see `CTLD_userConfig.lua` for the fully documented template. + ## Global settings ### System @@ -228,8 +242,8 @@ aircraft listed here receive CTLD F10 menus.** Each key is the **exact DCS type aircraft (mods included, e.g. `"Hercules"`, `"76MD"`, `"UH-60L"`). ```lua -local _cfg = CTLDConfig.get() -_cfg.settings["capabilitiesByType"] = { +-- inside a ctld.userSetup callback: table.insert(ctld.userSetup, function(cfg) ... end) +cfg.settings["capabilitiesByType"] = { ["UH-1H"] = { cratesEnabled = true, -- can load/unpack crates troopsEnabled = true, -- can load/deploy infantry @@ -281,9 +295,10 @@ _cfg.settings["capabilitiesByType"] = { vehicle as a waiting unit near the transport (instead of a crate); the pilot then loads it from the cockpit. See the [Pilot guide](../pilot/index.md) for the in-cockpit flow. -Aircraft **not** listed in `capabilitiesByType` receive no CTLD menus. Assigning your own +Aircraft **not** listed in `capabilitiesByType` receive no CTLD menus. Assigning the whole `capabilitiesByType` table replaces the built-in one entirely, so include every aircraft you -want CTLD-capable. +want CTLD-capable — or patch a single field to keep the defaults, e.g. +`cfg.settings["capabilitiesByType"]["Mi-8MT"].maxTroopsOnboard = 20`. ## Access control @@ -304,9 +319,9 @@ multiplayer servers. mission normally but have no CTLD access. ```lua -local _cfg = CTLDConfig.get() -_cfg.settings["addPlayerAircraftByType"] = false -_cfg.settings["transportPilotNames"] = { +-- inside a ctld.userSetup callback: table.insert(ctld.userSetup, function(cfg) ... end) +cfg.settings["addPlayerAircraftByType"] = false +cfg.settings["transportPilotNames"] = { "transport_slot_1", "transport_slot_2", "transport_slot_3", diff --git a/src/CTLD_bootstrap.lua b/src/CTLD_bootstrap.lua index bc486f4..3f95390 100644 --- a/src/CTLD_bootstrap.lua +++ b/src/CTLD_bootstrap.lua @@ -14,6 +14,14 @@ function ctld.initialize() CTLDConfig.get():load() ctld.utils.initLog() + -- Materialise the full configuration before any manager reads it: + -- 1) inject the AA system crate entries into spawnableCrates, + -- 2) run the Mission Maker userSetup callbacks (add/remove/patch). + -- This is the single place that controls config materialisation order, so managers + -- always see the final, complete config table (ADR 0008/0009). + CTLDCrateAssemblyManager.injectAACrates(ctld.gs("spawnableCrates")) + ctld.runUserSetup() + -- Boot all domain managers first so they can register their menu sections. -- Order matters: PlayerManager must be up before any other manager calls -- registerMenuSection(), and _scanExistingPlayers() must run last so all diff --git a/src/CTLD_crate.lua b/src/CTLD_crate.lua index 1e0a480..cdc83b7 100644 --- a/src/CTLD_crate.lua +++ b/src/CTLD_crate.lua @@ -426,9 +426,8 @@ end -- Results stored in self._processedCrates[category] and self._weightIndex[weight]. function CTLDCrateManager:_processSpawnableCrates() local spawnableCrates = ctld.gs("spawnableCrates") or {} - -- Inject AA system crate entries (parts + repair) from CTLDCrateAssemblyManager.TEMPLATES. - -- Must run before the main loop so injected entries enter _processedCrates and _weightIndex. - CTLDCrateAssemblyManager.injectAACrates(spawnableCrates) + -- AA system crate entries are injected once by ctld.initialize() (before the userSetup + -- callbacks), so spawnableCrates already contains them here — see ADR 0008/0009. local showCrateSets = ctld.gs("enableAllCrates") ~= false local allSuffix = " - " .. ctld.tr("All crates") diff --git a/src/CTLD_userConfig.lua b/src/CTLD_userConfig.lua index c5bff08..dcc8bb9 100644 --- a/src/CTLD_userConfig.lua +++ b/src/CTLD_userConfig.lua @@ -208,13 +208,13 @@ ctld.yamlConfigDatas = [[ # ============================================================ # Minimum AGL altitude (m) required to initiate a parachute drop for crates. -# ctld.parachuteMinAltitudeCrates: 30 +# ctld.parachuteMinAltitudeCrates: 152 # Minimum AGL altitude (m) required to initiate a parachute drop for troops. -# ctld.parachuteMinAltitudeTroops: 50 +# ctld.parachuteMinAltitudeTroops: 152 # Minimum AGL altitude (m) required to initiate a parachute drop for vehicles. -# ctld.parachuteMinAltitudeVehicles: 30 +# ctld.parachuteMinAltitudeVehicles: 152 # Vertical descent speed (m/s) for parachuted crates (slower = more accurate landing). # ctld.parachuteDescentRateCrates: 5 @@ -348,7 +348,7 @@ ctld.yamlConfigDatas = [[ # ctld.JTAC_droneRadius: 1000 # Orbit altitude (m) for drone JTAC units. -# ctld.JTAC_droneAltitude: 7000 +# ctld.JTAC_droneAltitude: 4000 # ============================================================ @@ -401,569 +401,159 @@ ctld.yamlConfigDatas = [[ ]] -- ============================================================ --- SECTION 2 — COMPLEX TABLES --- These cannot be expressed as YAML key:value pairs. --- They are applied directly on the CTLDConfig instance. --- Each table REPLACES the default entirely when uncommented. --- ============================================================ - ----@diagnostic disable-next-line: unused-local -local _cfg = CTLDConfig.get() - --- Debug mode — active for recette (branch feature_modularisation_and_Config). --- Set to false (or remove this line) for production missions. -_cfg.settings["debug"] = true - --- ============================================================ --- Per-aircraft capabilities — the unified type registry (replaces --- aircraftTypeTable, unitActions, and all legacy parallel type-indexed tables). --- --- Only aircraft listed here get CTLD F10 menus. --- Each entry REPLACES the matching default when the table is uncommented. --- --- Fields: --- cratesEnabled : can spawn, load and unpack crates --- troopsEnabled : can load, deploy and extract infantry groups --- canParachuteDrop : enables "Parachute" F10 entries (Feature A) --- canSlingload : enables hover-pickup and "Slingload" menus --- canTransportWholeVehicle : can load/unload whole vehicles (Feature Q) --- useNativeDcsCargoSystem : uses the native DCS cargo system for crate spawning --- maxTroopsOnboard : max soldiers this aircraft can carry (overrides ctld.numberOfTroops) --- maxCratesOnboard : max crates this aircraft can carry at once --- maxWholeVehiclesOnboard : max whole vehicles carried simultaneously --- loadableVehiclesRED : DCS type names of RED vehicles loadable onto this aircraft --- loadableVehiclesBLUE : DCS type names of BLUE vehicles loadable onto this aircraft --- ============================================================ --- _cfg.settings["capabilitiesByType"] = { --- -- ── Helicopters ──────────────────────────────────────────────────────────── --- ["Mi-8MT"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=true, --- canTransportWholeVehicle=true, useNativeDcsCargoSystem=true, --- maxTroopsOnboard=16, maxCratesOnboard=2, maxWholeVehiclesOnboard=1, --- loadableVehiclesRED = { "BRDM-2", "BTR_D" }, --- loadableVehiclesBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } }, --- ["Mi-24P"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=false, --- canTransportWholeVehicle=false, useNativeDcsCargoSystem=true, --- maxTroopsOnboard=10, maxCratesOnboard=1, maxWholeVehiclesOnboard=0 }, --- ["UH-1H"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=true, canSlingload=true, --- canTransportWholeVehicle=true, useNativeDcsCargoSystem=true, --- maxTroopsOnboard=8, maxCratesOnboard=1, maxWholeVehiclesOnboard=1, --- loadableVehiclesRED = { "BRDM-2", "BTR_D" }, --- loadableVehiclesBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } }, --- ["CH-47Fbl1"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=true, --- canTransportWholeVehicle=false, useNativeDcsCargoSystem=true, --- maxTroopsOnboard=33, maxCratesOnboard=8, maxWholeVehiclesOnboard=1, --- loadableVehiclesRED = { "BRDM-2", "BTR_D" }, --- loadableVehiclesBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } }, --- -- ["Ka-50"] = { cratesEnabled=true, troopsEnabled=false, canParachuteDrop=false, canSlingload=true, --- -- canTransportWholeVehicle=false, useNativeDcsCargoSystem=false, --- -- maxTroopsOnboard=0, maxCratesOnboard=1, maxWholeVehiclesOnboard=0 }, --- -- ["SA342L"] = { cratesEnabled=false, troopsEnabled=true, canParachuteDrop=false, canSlingload=false, --- -- canTransportWholeVehicle=false, useNativeDcsCargoSystem=false, --- -- maxTroopsOnboard=4, maxCratesOnboard=1, maxWholeVehiclesOnboard=0 }, --- --- -- ── Fixed-wing ───────────────────────────────────────────────────────────── --- ["C-130J-30"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=false, --- canTransportWholeVehicle=true, useNativeDcsCargoSystem=true, --- maxTroopsOnboard=80, maxCratesOnboard=20, maxWholeVehiclesOnboard=2, --- loadableVehiclesRED = { "BRDM-2", "BTR_D" }, --- loadableVehiclesBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } }, --- --- -- ── Mods ─────────────────────────────────────────────────────────────────── --- -- ["Hercules"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=false, --- -- canTransportWholeVehicle=true, useNativeDcsCargoSystem=false, --- -- maxTroopsOnboard=30, maxCratesOnboard=1, maxWholeVehiclesOnboard=2, --- -- loadableVehiclesRED = { "BRDM-2", "BTR_D" }, --- -- loadableVehiclesBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } }, --- -- ["UH-60L"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=true, --- -- canTransportWholeVehicle=false, useNativeDcsCargoSystem=false, --- -- maxTroopsOnboard=12, maxCratesOnboard=2, maxWholeVehiclesOnboard=0 }, --- -- ["76MD"] = { cratesEnabled=true, troopsEnabled=true, canParachuteDrop=false, canSlingload=false, --- -- canTransportWholeVehicle=true, useNativeDcsCargoSystem=false, --- -- maxTroopsOnboard=80, maxCratesOnboard=20, maxWholeVehiclesOnboard=2, --- -- loadableVehiclesRED = { "BRDM-2", "BTR_D" }, --- -- loadableVehiclesBLUE = { "M1045 HMMWV TOW", "M1043 HMMWV Armament" } }, --- -- ["SK-60"] = { cratesEnabled=true, troopsEnabled=false, canParachuteDrop=false, canSlingload=false, --- -- canTransportWholeVehicle=false, useNativeDcsCargoSystem=false, --- -- maxTroopsOnboard=4, maxCratesOnboard=1, maxWholeVehiclesOnboard=0 }, --- } - --- ============================================================ --- Access control — addPlayerAircraftByType --- ============================================================ --- true (default) : any player whose aircraft type is listed in capabilitiesByType --- automatically receives CTLD F10 menus. --- false : only unit names explicitly listed in transportPilotNames below --- receive CTLD menus. Use this to restrict CTLD access to a --- fixed set of named slots (e.g. dedicated transport squadron). --- AI transports always use transportPilotNames regardless. --- ============================================================ --- _cfg.settings["addPlayerAircraftByType"] = false - --- ============================================================ --- Transport pilot / unit names authorised to carry CTLD --- Used when addPlayerAircraftByType = false, or for AI transports. --- Add any DCS unit name from the Mission Editor here. --- ============================================================ --- _cfg.settings["transportPilotNames"] = { --- -- ── Player helicopter slots ──────────────────────────── --- "helicargo1", "helicargo2", "helicargo3", "helicargo4", "helicargo5", --- "helicargo6", "helicargo7", "helicargo8", "helicargo9", "helicargo10", --- "helicargo11", "helicargo12", "helicargo13", "helicargo14", "helicargo15", --- "helicargo16", "helicargo17", "helicargo18", "helicargo19", "helicargo20", --- "helicargo21", "helicargo22", "helicargo23", "helicargo24", "helicargo25", --- --- -- ── MEDEVAC — BLUE ──────────────────────────────────── --- "MEDEVAC BLUE #1", "MEDEVAC BLUE #2", "MEDEVAC BLUE #3", --- "MEDEVAC BLUE #4", "MEDEVAC BLUE #5", "MEDEVAC BLUE #6", --- "MEDEVAC BLUE #7", "MEDEVAC BLUE #8", "MEDEVAC BLUE #9", --- "MEDEVAC BLUE #10", "MEDEVAC BLUE #11", "MEDEVAC BLUE #12", --- --- -- ── MEDEVAC — RED ───────────────────────────────────── --- "MEDEVAC RED #1", "MEDEVAC RED #2", "MEDEVAC RED #3", --- "MEDEVAC RED #4", "MEDEVAC RED #5", "MEDEVAC RED #6", --- --- -- ── MEDEVAC — both sides ────────────────────────────── --- "MEDEVAC #1", "MEDEVAC #2", "MEDEVAC #3", --- --- -- ── AI transport slots ──────────────────────────────── --- "transport1", "transport2", "transport3", "transport4", "transport5", --- "transport6", "transport7", "transport8", "transport9", "transport10", --- "transport11", "transport12", "transport13", "transport14", "transport15", --- } - --- ============================================================ --- Troop pickup zones (players only — AI transports use AIZones) --- Each entry: { "zone_or_ship_name", "smoke_color", limit, "active", side [, flag] } --- --- "zone_or_ship_name" : ME trigger zone name, or DCS unit name of a ship --- "smoke_color" : "green"|"red"|"white"|"orange"|"blue"|"none" --- limit : -1 = unlimited ; N = max groups that can be loaded --- (dropping a group back adds one to the count) --- "active" : "yes" = available at mission start --- "no" = deactivated — use ctld.activatePickupZone() to enable --- side : 0 = both coalitions ; 1 = RED only ; 2 = BLUE only --- flag (optional) : DCS flag number where remaining group count is stored --- ============================================================ --- _cfg.settings["troopZones"] = { --- { "pickzone1", "blue", -1, "yes", 0 }, --- { "pickzone2", "red", -1, "yes", 0 }, --- { "pickzone3", "none", -1, "yes", 0 }, --- { "pickzone4", "none", -1, "yes", 0 }, --- { "pickzone5", "none", -1, "yes", 0 }, --- { "pickzone6", "none", -1, "yes", 0 }, --- { "pickzone7", "none", -1, "yes", 0 }, --- { "pickzone8", "none", -1, "yes", 0 }, --- { "pickzone9", "none", 5, "yes", 1 }, -- 5 groups max, RED only --- { "pickzone10", "none", 10, "yes", 2 }, -- 10 groups max, BLUE only --- { "pickzone11", "blue", 20, "no", 2 }, -- starts inactive, BLUE only --- { "pickzone12", "red", 20, "no", 1 }, -- starts inactive, RED only --- { "pickzone13", "none", -1, "yes", 0 }, --- { "pickzone14", "none", -1, "yes", 0 }, --- { "pickzone15", "none", -1, "yes", 0 }, --- { "pickzone16", "none", -1, "yes", 0 }, --- { "pickzone17", "none", -1, "yes", 0 }, --- { "pickzone18", "none", -1, "yes", 0 }, --- { "pickzone19", "none", 5, "yes", 0 }, --- { "pickzone20", "none", 10, "yes", 0, 1000 }, -- remaining count stored in flag 1000 --- { "USA Carrier", "blue", 10, "yes", 0, 1001 }, -- ship: use DCS unit name --- } - --- ============================================================ --- AI-only zones (Feature S — config-only, no naming convention in ME). --- The DCS trigger zone name is used only for position + radius. --- Each entry supports pickup and/or dropoff on the same zone. --- --- Required fields: --- dcsZoneName : DCS trigger zone name (must exist in Mission Editor) --- coalition : "RED" | "BLUE" | "NEUTRAL" --- --- Optional pickup fields (isPickup = true): --- cargoType : "T"=troops | "V"=vehicles | "TV"=both (default "T") --- troopStock : 0=disabled, -1=unlimited, N=limited stock --- troopTemplates : list of template names (nil/{}=all compatible ; 1=guaranteed ; N=random among listed) --- vehicleTypes : list of DCS typeNames to allow (nil=all vehicles physically present in zone) --- --- Optional dropoff fields (isDropoff = true): --- aiDropMode : "G"=ground | "P"=parachute | "GP"=both (default "GP") --- --- Note: vehicles at pickup are always physically present DCS units inside the zone radius. --- CTLD does not manage a virtual vehicle stock. --- ============================================================ --- _cfg.settings["aiZones"] = { --- --- -- Pickup zone: troops only, stock=10, any compatible template --- { dcsZoneName = "Depot_bleu", coalition = "BLUE", --- isPickup = true, cargoType = "T", troopStock = 10 }, --- --- -- Pickup zone: troops, restricted to Standard Group only (guaranteed if fits heli) --- { dcsZoneName = "Depot_inf", coalition = "BLUE", --- isPickup = true, cargoType = "T", troopStock = 10, --- troopTemplates = { "Standard Group" } }, --- --- -- Pickup zone: vehicles only, Hummers only (DCS vehicles physically present in zone) --- { dcsZoneName = "Depot_hummer", coalition = "BLUE", --- isPickup = true, cargoType = "V", --- vehicleTypes = { "Hummer", "M1025 HMMWV" } }, --- --- -- Pickup zone: troops + vehicles, unlimited troop stock --- { dcsZoneName = "Depot_tv", coalition = "BLUE", --- isPickup = true, cargoType = "TV", troopStock = -1 }, --- --- -- Dropoff zone: ground only --- { dcsZoneName = "LZ_nord", coalition = "BLUE", --- isDropoff = true, aiDropMode = "G" }, --- --- -- Combined zone: pickup troops AND dropoff (same DCS zone) --- { dcsZoneName = "FARP_avance", coalition = "BLUE", --- isPickup = true, isDropoff = true, --- cargoType = "T", troopStock = 5, aiDropMode = "GP" }, --- } - --- ============================================================ --- Debug-only: AI zones for recette MT-07 to MT-10 (Feature S). --- Active only when debug=true above. Remove this block after --- all interactive AI recettes have passed. --- ============================================================ -if _cfg.settings["debug"] == true then - _cfg.settings["aiZones"] = { - -- troopStock / vehicleStock are now tables: { [templateName/typeName] = N } - -- N=-1=unlimited, N>0=limited. Key "All"=all templates, unlimited. - - -- ── MT-07 / MT-08 / MT-09 / MT-10 ────────────────────────────── - { dcsZoneName="AIZ_base_B_P_5", coalition="BLUE", isPickup=true, cargoType="T", - troopStock = { ["Standard Group"] = 5, ["Anti Tank"] = 2 } }, - { dcsZoneName="AIZ_front_B_D", coalition="BLUE", isDropoff=true, aiDropMode="GP" }, - { dcsZoneName="AIZ_depot_B_P_V_10", coalition="BLUE", isPickup=true, cargoType="V", - vehicleStock = { ["Hummer"] = 3, ["M1045 HMMWV TOW"] = -1 } }, - { dcsZoneName="AIZ_depot_B_P_TV_5_10", coalition="BLUE", isPickup=true, cargoType="TV", - troopStock = { ["All"] = -1 }, vehicleStock = { ["Hummer"] = 5 } }, - { dcsZoneName="AIZ_livraison_B_D_G", coalition="BLUE", isDropoff=true, aiDropMode="G" }, - { dcsZoneName="AIZ_mt10d_B_D_G", coalition="BLUE", isDropoff=true, aiDropMode="G" }, - { dcsZoneName="AIZ_depot_B_P_T_10", coalition="BLUE", isPickup=true, cargoType="T", - troopStock = { ["All"] = -1 } }, - - -- ── MT-11 : 2 troop templates with limited stock ────────────────── - { dcsZoneName="AIZ_mt11_B_P_T", coalition="BLUE", isPickup=true, cargoType="T", - troopStock = { ["Standard Group"] = 3, ["Anti Tank"] = 2 } }, - { dcsZoneName="AIZ_mt11_B_D", coalition="BLUE", isDropoff=true, aiDropMode="GP" }, - - -- ── MT-12 : véhicule DCS natif via vehicleStock (Hummer) ──────── - { dcsZoneName="AIZ_mt12_B_P_V", coalition="BLUE", isPickup=true, cargoType="V", - vehicleStock = { ["Hummer"] = 2 } }, - { dcsZoneName="AIZ_mt12_B_D", coalition="BLUE", isDropoff=true, aiDropMode="G" }, - - -- ── MT-13 : scène (FARP Alpha) via vehicleStock isScene=true ──── - { dcsZoneName="AIZ_mt13_B_P_V", coalition="BLUE", isPickup=true, cargoType="V", - vehicleStock = { ["FARP Alpha"] = 1 } }, - { dcsZoneName="AIZ_mt13_B_D", coalition="BLUE", isDropoff=true, aiDropMode="G" }, - - -- ── MT-14 : système AA (HAWK) via vehicleStock isAASystem=true (Feature U) ──── - { dcsZoneName="AIZ_mt14_B_P_V", coalition="BLUE", isPickup=true, cargoType="V", - vehicleStock = { ["HAWK AA System"] = 1 } }, - { dcsZoneName="AIZ_mt14_B_D", coalition="BLUE", isDropoff=true, aiDropMode="G" }, - } -end - --- ============================================================ --- Waypoint zones (AI routing — transport will fly to each active --- waypoint zone in sequence before reaching the drop-off zone) --- Each entry: { "zone_name", "smoke_color", "active", side } --- ============================================================ --- _cfg.settings["wpZones"] = { --- { "wpzone1", "green", "yes", 2 }, --- { "wpzone2", "blue", "yes", 2 }, --- { "wpzone3", "orange", "yes", 2 }, --- { "wpzone4", "none", "yes", 2 }, --- { "wpzone5", "none", "yes", 2 }, --- { "wpzone6", "none", "yes", 1 }, --- { "wpzone7", "none", "yes", 1 }, --- { "wpzone8", "none", "yes", 1 }, --- { "wpzone9", "none", "yes", 1 }, --- { "wpzone10", "none", "no", 0 }, -- inactive at start ; both sides --- } - --- ============================================================ --- Extractable groups --- DCS group names that can be extracted by a transport. --- ============================================================ --- _cfg.settings["extractableGroups"] = { --- "extract1", "extract2", "extract3", "extract4", "extract5", --- "extract6", "extract7", "extract8", "extract9", "extract10", --- "extract11", "extract12", "extract13", "extract14", "extract15", --- "extract16", "extract17", "extract18", "extract19", "extract20", --- "extract21", "extract22", "extract23", "extract24", "extract25", --- } - --- ============================================================ --- Logistic units — unit names near which crate spawning is allowed. --- When a logistic unit is destroyed, crate spawning at its location stops. --- ============================================================ --- _cfg.settings["logisticUnits"] = { --- "logistic1", "logistic2", "logistic3", "logistic4", "logistic5", --- "logistic6", "logistic7", "logistic8", "logistic9", "logistic10", --- "logistic11", "logistic12", "logistic13", "logistic14", "logistic15", --- "logistic16", "logistic17", "logistic18", "logistic19", "logistic20", --- } - --- ============================================================ --- Vehicle weights (kg) used to determine if a transport can carry a vehicle. --- Add any DCS unit type that appears in loadableVehiclesRED/BLUE. --- ============================================================ --- _cfg.settings["groundVehicleWeights"] = { --- ["BRDM-2"] = 7000, --- ["BTR_D"] = 8000, --- ["M1045 HMMWV TOW"] = 3220, --- ["M1043 HMMWV Armament"] = 2500, --- } - --- ============================================================ --- Smoke colour at troop pickup zones (TRZ_ trigger zones) --- Table indexed by coalition number: 1 = RED, 2 = BLUE. --- -1 = no smoke ; 0=Green 1=Red 2=White 3=Orange 4=Blue. --- Omit the table entirely (default) to use no smoke. --- ============================================================ --- _cfg.settings["troopZoneSmokeColor"] = { --- [1] = 1, -- RED side zones use red smoke --- [2] = 4, -- BLUE side zones use blue smoke --- } - --- ============================================================ --- Infantry spawn cap — cumulative limit on the number of troops --- that can be loaded aboard transports across the whole mission. --- { redLimit, blueLimit } — 0 = no limit. --- Example: { 200, 200 } caps each side at 200 troops total. --- ============================================================ --- _cfg.settings["nbLimitSpawnedTroops"] = { 0, 0 } - --- ============================================================ --- Loadable troop groups --- Defines the infantry group templates shown in the F10 "Troops" menu. --- Each entry replaces/adds one loadable group template. --- --- Fields: --- name : label shown in the F10 menu --- inf : number of standard riflemen --- mg : number of M249 / PKM machine-gunners --- at : number of RPG anti-tank soldiers --- aa : number of Stinger / Igla MANPAD soldiers --- mortar : number of 2B11 mortar crews --- jtac : number of JTAC soldiers (auto-lase when deployed) --- side : 1 = RED only ; 2 = BLUE only ; omit = both coalitions --- ============================================================ --- _cfg.settings["loadableGroups"] = { --- { name = "Standard Group", inf = 6, mg = 2, at = 2 }, --- { name = "Anti Air", inf = 2, aa = 3 }, --- { name = "Anti Tank", inf = 2, at = 6 }, --- { name = "Mortar Squad", mortar = 6 }, --- { name = "JTAC Group", inf = 4, jtac = 1 }, --- { name = "Single JTAC", jtac = 1 }, --- { name = "2x Standard Groups", inf = 12, mg = 4, at = 4 }, --- { name = "2x Anti Air", inf = 4, aa = 6 }, --- { name = "2x Anti Tank", inf = 4, at = 12 }, --- { name = "2x Standard + 2x Mortar", inf = 12, mg = 4, at = 4, mortar = 12 }, --- { name = "3x Standard Groups", inf = 18, mg = 6, at = 6 }, --- { name = "3x Anti Air", inf = 6, aa = 9 }, --- { name = "3x Anti Tank", inf = 6, at = 18 }, --- { name = "3x Mortar Squad", mortar = 18 }, --- { name = "5x Mortar Squad", mortar = 30 }, --- -- { name = "Red Mortar Squad", mortar = 5, side = 1 }, -- RED only --- } - --- ============================================================ --- Spawnable crates — F10 crate menu catalogue --- --- The table is keyed by sub-menu name. Each sub-menu contains --- one or more crate descriptor entries. --- --- Crate descriptor fields: --- weight (number) : unique kg value used as lookup key — MUST be unique --- desc (string) : label shown in the F10 menu --- unit (string) : DCS unit type name spawned when the crate is unpacked --- cratesRequired (number): number of identical crates that must be assembled --- within 100 m of each other to build the unit (default 1) --- side (number) : 1 = RED only ; 2 = BLUE only ; omit = both coalitions --- multiple (table) : list of weights — shortcut entry that spawns all listed --- crates at once (no weight/unit fields needed here) --- ============================================================ --- _cfg.settings["spawnableCrates"] = { --- --- ["Combat Vehicles"] = { --- --- BLUE --- { weight = 1000.01, desc = "Humvee - MG", unit = "M1043 HMMWV Armament", side = 2 }, --- { weight = 1000.02, desc = "Humvee - TOW", unit = "M1045 HMMWV TOW", side = 2, cratesRequired = 2 }, --- { multiple = { 1000.02, 1000.02 }, desc = "Humvee - TOW - All crates", side = 2 }, --- { weight = 1000.03, desc = "Light Tank - MRAP", unit = "MaxxPro_MRAP", side = 2, cratesRequired = 2 }, --- { multiple = { 1000.03, 1000.03 }, desc = "Light Tank - MRAP - All crates", side = 2 }, --- { weight = 1000.04, desc = "Med Tank - LAV-25", unit = "LAV-25", side = 2, cratesRequired = 3 }, --- { multiple = { 1000.04, 1000.04, 1000.04 }, desc = "Med Tank - LAV-25 - All crates", side = 2 }, --- { weight = 1000.05, desc = "Heavy Tank - Abrams", unit = "M-1 Abrams", side = 2, cratesRequired = 4 }, --- { multiple = { 1000.05, 1000.05, 1000.05, 1000.05 }, desc = "Heavy Tank - Abrams - All crates", side = 2 }, --- --- RED --- { weight = 1000.11, desc = "BTR-D", unit = "BTR_D", side = 1 }, --- { weight = 1000.12, desc = "BRDM-2", unit = "BRDM-2", side = 1 }, --- }, --- --- ["Support"] = { --- --- BLUE --- { weight = 1001.01, desc = "Hummer - JTAC", unit = "Hummer", side = 2, cratesRequired = 2 }, --- { multiple = { 1001.01, 1001.01 }, desc = "Hummer - JTAC - All crates", side = 2 }, --- { weight = 1001.02, desc = "M-818 Ammo Truck", unit = "M 818", side = 2, cratesRequired = 2 }, --- { multiple = { 1001.02, 1001.02 }, desc = "M-818 Ammo Truck - All crates", side = 2 }, --- { weight = 1001.03, desc = "M-978 Tanker", unit = "M978 HEMTT Tanker", side = 2, cratesRequired = 2 }, --- { multiple = { 1001.03, 1001.03 }, desc = "M-978 Tanker - All crates", side = 2 }, --- --- RED --- { weight = 1001.11, desc = "SKP-11 - JTAC", unit = "SKP-11", side = 1 }, --- { weight = 1001.12, desc = "Ural-375 Ammo Truck", unit = "Ural-375", side = 1, cratesRequired = 2 }, --- { multiple = { 1001.12, 1001.12 }, desc = "Ural-375 Ammo Truck - All crates", side = 1 }, --- { weight = 1001.13, desc = "KAMAZ Ammo Truck", unit = "KAMAZ Truck", side = 1, cratesRequired = 2 }, --- --- Both --- { weight = 1001.21, desc = "EWR Radar", unit = "FPS-117", cratesRequired = 3 }, --- { multiple = { 1001.21, 1001.21, 1001.21 }, desc = "EWR Radar - All crates" }, --- { weight = 1001.22, desc = "FOB Crate", unit = "FOB", side = nil, cratesRequired = 3 }, --- }, --- --- ["Artillery"] = { --- --- BLUE --- { weight = 1002.01, desc = "MLRS", unit = "MLRS", side = 2, cratesRequired = 3 }, --- { multiple = { 1002.01, 1002.01, 1002.01 }, desc = "MLRS - All crates", side = 2 }, --- { weight = 1002.02, desc = "SpGH DANA", unit = "SpGH_Dana", side = 2, cratesRequired = 3 }, --- { multiple = { 1002.02, 1002.02, 1002.02 }, desc = "SpGH DANA - All crates", side = 2 }, --- { weight = 1002.03, desc = "T155 Firtina", unit = "T155_Firtina", side = 2, cratesRequired = 3 }, --- { multiple = { 1002.03, 1002.03, 1002.03 }, desc = "T155 Firtina - All crates", side = 2 }, --- { weight = 1002.04, desc = "Howitzer M-109", unit = "M-109", side = 2, cratesRequired = 3 }, --- { multiple = { 1002.04, 1002.04, 1002.04 }, desc = "Howitzer M-109 - All crates", side = 2 }, --- --- RED --- { weight = 1002.11, desc = "SPH 2S19 Msta", unit = "SAU Msta", side = 1, cratesRequired = 3 }, --- { multiple = { 1002.11, 1002.11, 1002.11 }, desc = "SPH 2S19 Msta - All crates", side = 1 }, --- }, --- --- ["SAM short range"] = { --- --- BLUE --- { weight = 1003.01, desc = "M1097 Avenger", unit = "M1097 Avenger", side = 2, cratesRequired = 3 }, --- { multiple = { 1003.01, 1003.01, 1003.01 }, desc = "M1097 Avenger - All crates", side = 2 }, --- { weight = 1003.02, desc = "M48 Chaparral", unit = "M48 Chaparral", side = 2, cratesRequired = 2 }, --- { multiple = { 1003.02, 1003.02 }, desc = "M48 Chaparral - All crates", side = 2 }, --- { weight = 1003.03, desc = "Roland ADS", unit = "Roland ADS", side = 2, cratesRequired = 3 }, --- { multiple = { 1003.03, 1003.03, 1003.03 }, desc = "Roland ADS - All crates", side = 2 }, --- { weight = 1003.04, desc = "Gepard AAA", unit = "Gepard", side = 2, cratesRequired = 3 }, --- { multiple = { 1003.04, 1003.04, 1003.04 }, desc = "Gepard AAA - All crates", side = 2 }, --- { weight = 1003.05, desc = "LPWS C-RAM", unit = "HEMTT_C-RAM_Phalanx", side = 2, cratesRequired = 3 }, --- { multiple = { 1003.05, 1003.05, 1003.05 }, desc = "LPWS C-RAM - All crates", side = 2 }, --- --- RED --- { weight = 1003.11, desc = "9K33 Osa", unit = "Osa 9A33 ln", side = 1, cratesRequired = 3 }, --- { multiple = { 1003.11, 1003.11, 1003.11 }, desc = "9K33 Osa - All crates", side = 1 }, --- { weight = 1003.12, desc = "9P31 Strela-1", unit = "Strela-1 9P31", side = 1, cratesRequired = 3 }, --- { multiple = { 1003.12, 1003.12, 1003.12 }, desc = "9P31 Strela-1 - All crates", side = 1 }, --- { weight = 1003.13, desc = "9K35M Strela-10", unit = "Strela-10M3", side = 1, cratesRequired = 3 }, --- { multiple = { 1003.13, 1003.13, 1003.13 }, desc = "9K35M Strela-10 - All crates", side = 1 }, --- { weight = 1003.14, desc = "9K331 Tor", unit = "Tor 9A331", side = 1, cratesRequired = 3 }, --- { multiple = { 1003.14, 1003.14, 1003.14 }, desc = "9K331 Tor - All crates", side = 1 }, --- { weight = 1003.15, desc = "2K22 Tunguska", unit = "2S6 Tunguska", side = 1, cratesRequired = 3 }, --- { multiple = { 1003.15, 1003.15, 1003.15 }, desc = "2K22 Tunguska - All crates", side = 1 }, --- }, --- --- ["SAM mid range"] = { --- --- BLUE — HAWK system --- { weight = 1004.01, desc = "HAWK Launcher", unit = "Hawk ln", side = 2 }, --- { weight = 1004.02, desc = "HAWK Search Radar", unit = "Hawk sr", side = 2 }, --- { weight = 1004.03, desc = "HAWK Track Radar", unit = "Hawk tr", side = 2 }, --- { weight = 1004.04, desc = "HAWK PCP", unit = "Hawk pcp", side = 2 }, --- { weight = 1004.05, desc = "HAWK CWAR", unit = "Hawk cwar", side = 2 }, --- { weight = 1004.06, desc = "HAWK Repair", unit = "HAWK Repair", side = 2 }, --- { multiple = { 1004.01, 1004.02, 1004.03 }, desc = "HAWK - All crates", side = 2 }, --- --- BLUE — NASAMS system --- { weight = 1004.11, desc = "NASAMS Launcher 120C", unit = "NASAMS_LN_C", side = 2 }, --- { weight = 1004.12, desc = "NASAMS Search/Track Radar", unit = "NASAMS_Radar_MPQ64F1", side = 2 }, --- { weight = 1004.13, desc = "NASAMS Command Post", unit = "NASAMS_Command_Post", side = 2 }, --- { weight = 1004.14, desc = "NASAMS Repair", unit = "NASAMS Repair", side = 2 }, --- { multiple = { 1004.11, 1004.12, 1004.13 }, desc = "NASAMS - All crates", side = 2 }, --- --- RED — KUB system --- { weight = 1004.21, desc = "KUB Launcher", unit = "Kub 2P25 ln", side = 1 }, --- { weight = 1004.22, desc = "KUB Radar", unit = "Kub 1S91 str", side = 1 }, --- { weight = 1004.23, desc = "KUB Repair", unit = "KUB Repair", side = 1 }, --- { multiple = { 1004.21, 1004.22 }, desc = "KUB - All crates", side = 1 }, --- --- RED — BUK system --- { weight = 1004.31, desc = "BUK Launcher", unit = "SA-11 Buk LN 9A310M1", side = 1 }, --- { weight = 1004.32, desc = "BUK Search Radar", unit = "SA-11 Buk SR 9S18M1", side = 1 }, --- { weight = 1004.33, desc = "BUK CC Radar", unit = "SA-11 Buk CC 9S470M1", side = 1 }, --- { weight = 1004.34, desc = "BUK Repair", unit = "BUK Repair", side = 1 }, --- { multiple = { 1004.31, 1004.32, 1004.33 }, desc = "BUK - All crates", side = 1 }, --- }, --- --- ["SAM long range"] = { --- --- BLUE — Patriot system --- { weight = 1005.01, desc = "Patriot Launcher", unit = "Patriot ln", side = 2 }, --- { weight = 1005.02, desc = "Patriot Radar", unit = "Patriot str", side = 2 }, --- { weight = 1005.03, desc = "Patriot ECS", unit = "Patriot ECS", side = 2 }, --- { weight = 1005.06, desc = "Patriot AMG (optional)", unit = "Patriot AMG", side = 2 }, --- { weight = 1005.07, desc = "Patriot Repair", unit = "Patriot Repair", side = 2 }, --- { multiple = { 1005.01, 1005.02, 1005.03 }, desc = "Patriot - All crates", side = 2 }, --- --- RED — S-300 system --- { weight = 1005.11, desc = "S-300 TEL C", unit = "S-300PS 5P85C ln", side = 1 }, --- { weight = 1005.12, desc = "S-300 Flap Lid-A TR", unit = "S-300PS 40B6M tr", side = 1 }, --- { weight = 1005.13, desc = "S-300 Clam Shell SR", unit = "S-300PS 40B6MD sr", side = 1 }, --- { weight = 1005.14, desc = "S-300 Big Bird SR", unit = "S-300PS 64H6E sr", side = 1 }, --- { weight = 1005.15, desc = "S-300 C2", unit = "S-300PS 54K6 cp", side = 1 }, --- { weight = 1005.16, desc = "S-300 Repair", unit = "S-300 Repair", side = 1 }, --- { multiple = { 1005.11, 1005.12, 1005.13, 1005.14, 1005.15 }, desc = "S-300 - All crates", side = 1 }, --- }, --- --- ["Drone"] = { --- --- BLUE --- { weight = 1006.01, desc = "MQ-9 Reaper - JTAC", unit = "MQ-9 Reaper", side = 2 }, --- --- RED --- { weight = 1006.11, desc = "RQ-1A Predator - JTAC", unit = "RQ-1A Predator", side = 1 }, --- }, --- } - --- ============================================================ --- Crate 3D model templates --- Controls which DCS static object model is spawned for each --- crate usage type. --- --- Three slots: --- "load" : crate spawned for standard hover / menu loading --- (canCargo = false : not a real DCS slingload cargo) --- "sling" : crate spawned when ctld.slingLoad = true --- (canCargo = true : DCS native cargo physics) --- "dynamic" : crate spawned when the aircraft is in dynamicCargoUnits --- (canCargo = true : DCS native cargo physics) --- --- Available DCS cargo models and their type strings: --- model shape | type --- ───────────────────────────────────────────────────────── --- ammo_box_cargo | ammo_cargo (default load) --- bw_container_cargo | container_cargo (default sling) --- iso_container_cargo | iso_container --- iso_container_small_cargo | iso_container_small --- ab-212_cargo | uh1h_cargo --- barrels_cargo | barrels_cargo --- fueltank_cargo | fueltank_cargo --- oiltank_cargo | oiltank_cargo --- pipes_big_cargo | pipes_big_cargo --- pipes_small_cargo | pipes_small_cargo --- tetrapod_cargo | tetrapod_cargo --- trunks_long_cargo | trunks_long_cargo --- trunks_small_cargo | trunks_small_cargo --- f_bar_cargo | f_bar_cargo --- ============================================================ --- _cfg.settings["spawnableCratesModels"] = { --- ["load"] = { --- category = "Cargos", --- type = "ammo_cargo", --- canCargo = false, --- }, --- ["sling"] = { --- category = "Cargos", --- shape_name = "bw_container_cargo", --- type = "container_cargo", --- canCargo = true, --- }, --- ["dynamic"] = { --- category = "Cargos", --- type = "ammo_cargo", --- canCargo = true, --- }, --- } - - +-- SECTION 2 — COMPLEX CONFIGURATION (crates, troops, zones, capabilities) +-- +-- Section 1 above covers scalar parameters. The complex tables are customised +-- here by registering one or more *setup callbacks* in ctld.userSetup. CTLD runs +-- every callback once during initialization, AFTER the factory defaults and the +-- auto-injected AA-system crates are in place — so you patch the real, complete +-- catalogue surgically instead of copy-pasting and owning it wholesale. +-- +-- ctld.userSetup = ctld.userSetup or {} +-- table.insert(ctld.userSetup, function(cfg) +-- -- your operations go here (see the helpers below) +-- end) +-- +-- Helper functions (call them from inside a callback): +-- ctld.addCrate(section, entry) add a crate to a spawnableCrates sub-menu +-- ctld.removeCrate(weight) remove a crate by its unique weight +-- ctld.patchCrate(weight, patch) change fields of a crate (deep-merge 1 level) +-- ctld.addTroopGroup(entry) add a loadable infantry group template +-- ctld.removeTroopGroup(name) remove a loadable group by name +-- ctld.addTo(setting, entry) append to an array setting (list below) +-- ctld.logDefaults(setting) dump a setting's current value to CTLD.log +-- +-- A duplicate crate weight on addCrate is rejected with an on-screen warning +-- (weights MUST stay unique — they are the crate lookup key). Removing/patching +-- an unknown weight logs a warning and does nothing; other callbacks still run. +-- +-- Tables WITHOUT a helper are plain dictionaries — edit them directly on cfg: +-- capabilitiesByType, aiZones, spawnableCratesModels, troopZoneSmokeColor, +-- nbLimitSpawnedTroops, groundVehicleWeights, modTypes, addPlayerAircraftByType. +-- +-- DISCOVER THE REAL DEFAULTS AT RUNTIME +-- This file is optional and by default changes nothing. To inspect a setting's +-- real current value, add a separate ME "DO SCRIPT" trigger a few seconds after +-- mission start containing, e.g.: +-- ctld.logDefaults("spawnableCrates") +-- It writes a copy-pasteable Lua representation to CTLD.log (requires debug=true). +-- ============================================================ + +-- Uncomment the block below and adapt the operations you need. +-- +-- ctld.userSetup = ctld.userSetup or {} +-- table.insert(ctld.userSetup, function(cfg) +-- +-- -- ── Crates (spawnableCrates) ─────────────────────────────────────────── +-- -- Crate descriptor fields: +-- -- weight (number) : unique kg value used as the lookup key — MUST be unique +-- -- desc (string) : label shown in the F10 menu +-- -- unit (string) : DCS unit type name spawned when the crate is unpacked +-- -- cratesRequired (number) : identical crates to assemble within 100 m (default 1) +-- -- side (number) : 1 = RED only ; 2 = BLUE only ; omit = both coalitions +-- -- isJTAC (bool) : JTAC unit (hidden when JTAC_dropEnabled = false) +-- -- spawnAs (string) : e.g. "AIRPLANE" for drone JTACs +-- -- specificParams (table) : per-unit params, e.g. drone orbit +-- -- { speed, alti, orbitRadiusNoLase, orbitRadiusOnLase } +-- -- Default sub-menus: "Combat Vehicles", "Support", "Artillery", +-- -- "SAM short range", "SAM mid range", "SAM long range", "Drone". +-- +-- -- Add a custom crate to an existing sub-menu (created if absent): +-- ctld.addCrate("Support", { weight = 2000.01, desc = "Ural Ammo", unit = "Ural-375", side = 1, cratesRequired = 2 }) +-- +-- -- Remove a default crate by weight: +-- ctld.removeCrate(1000.05) -- Heavy Tank - Abrams +-- +-- -- Change one field of a default crate (all other fields preserved): +-- ctld.patchCrate(1000.02, { cratesRequired = 3 }) -- Humvee - TOW +-- +-- -- Patch a single nested field (deep-merge: siblings speed/orbit* preserved): +-- ctld.patchCrate(1006.01, { specificParams = { alti = 5000 } }) -- MQ-9 drone +-- +-- -- AA-system crates already exist here (auto-injected before this callback), +-- -- so you can add to or remove from their sections too: +-- ctld.addCrate("SAM mid range", { weight = 2004.01, desc = "Spare HAWK LN", unit = "Hawk ln", side = 2 }) +-- ctld.removeCrate(1004.06) -- HAWK Repair +-- +-- -- ── Loadable troop groups (loadableGroups) ───────────────────────────── +-- -- Fields: +-- -- name : label shown in the F10 "Troops" menu +-- -- inf : standard riflemen mg : M249/PKM machine-gunners +-- -- at : RPG anti-tank soldiers aa : Stinger/Igla MANPAD soldiers +-- -- mortar : 2B11 mortar crews jtac : JTAC soldiers (auto-lase) +-- -- side : 1 = RED only ; 2 = BLUE only ; omit = both coalitions +-- ctld.addTroopGroup({ name = "Recon Team", inf = 3, jtac = 1 }) +-- ctld.removeTroopGroup("5x Mortar Squad") +-- +-- -- ── Array settings (append with ctld.addTo) ──────────────────────────── +-- -- Valid array settings: transportPilotNames, troopZones, wpZones, +-- -- extractableGroups, logisticUnits. +-- +-- -- transportPilotNames : ME unit name authorised to carry CTLD +-- ctld.addTo("transportPilotNames", "helicargo_custom_1") +-- +-- -- troopZones : { "zone_or_ship_name", "smoke_color", limit, "active", side [, flag] } +-- -- smoke_color : "green"|"red"|"white"|"orange"|"blue"|"none" +-- -- limit : -1 unlimited ; N max groups active : "yes"|"no" +-- -- side : 0 both ; 1 RED ; 2 BLUE flag (optional) : DCS flag number +-- ctld.addTo("troopZones", { "pickzone_north", "green", -1, "yes", 0 }) +-- +-- -- wpZones : { "zone_name", "smoke_color", "active", side } (AI routing waypoints) +-- ctld.addTo("wpZones", { "wpzone_a", "blue", "yes", 2 }) +-- +-- -- extractableGroups : DCS group name that a transport can extract +-- ctld.addTo("extractableGroups", "recon_team_1") +-- +-- -- logisticUnits : unit name near which crate spawning is allowed +-- ctld.addTo("logisticUnits", "supply_depot_1") +-- +-- -- ── Aircraft capabilities (capabilitiesByType — direct dict access) ───── +-- -- Only aircraft listed here get CTLD F10 menus. Fields: +-- -- cratesEnabled, troopsEnabled, canParachuteDrop, canSlingload, +-- -- canTransportWholeVehicle, useNativeDcsCargoSystem, +-- -- maxTroopsOnboard, maxCratesOnboard, maxWholeVehiclesOnboard, +-- -- loadableVehiclesRED / loadableVehiclesBLUE (lists of DCS type names) +-- +-- -- Patch one capability without rewriting the whole entry: +-- cfg.settings["capabilitiesByType"]["Mi-8MT"].maxTroopsOnboard = 20 +-- +-- -- Add a new (e.g. mod) aircraft type: +-- cfg.settings["capabilitiesByType"]["UH-60L"] = { +-- cratesEnabled = true, troopsEnabled = true, canParachuteDrop = false, canSlingload = true, +-- canTransportWholeVehicle = false, useNativeDcsCargoSystem = false, +-- maxTroopsOnboard = 12, maxCratesOnboard = 2, maxWholeVehiclesOnboard = 0, +-- } +-- +-- -- Restrict CTLD to named slots only (AI transports always use transportPilotNames): +-- cfg.settings["addPlayerAircraftByType"] = false +-- +-- -- ── AI-only zones (Feature S — direct assignment) ────────────────────── +-- -- Each entry supports pickup and/or dropoff on the same DCS trigger zone. +-- -- Required: dcsZoneName, coalition ("RED"|"BLUE"|"NEUTRAL"). +-- -- Pickup (isPickup=true): cargoType "T"|"V"|"TV" ; troopStock/vehicleStock +-- -- tables keyed by template/type name ({ ["Standard Group"]=5 }, -1=unlimited, +-- -- "All"=every template) ; troopTemplates / vehicleTypes whitelists. +-- -- Dropoff (isDropoff=true): aiDropMode "G"|"P"|"GP". +-- cfg.settings["aiZones"] = { +-- { dcsZoneName = "Depot_bleu", coalition = "BLUE", isPickup = true, cargoType = "T", +-- troopStock = { ["Standard Group"] = 5, ["Anti Tank"] = 2 } }, +-- { dcsZoneName = "LZ_nord", coalition = "BLUE", isDropoff = true, aiDropMode = "GP" }, +-- } +-- +-- -- ── Other dictionaries (direct access) ───────────────────────────────── +-- -- Cumulative troop spawn cap { redLimit, blueLimit } (0 = no limit): +-- cfg.settings["nbLimitSpawnedTroops"] = { 200, 200 } +-- -- Troop-zone smoke by coalition (1=RED,2=BLUE ; -1 none,0 green,1 red,2 white,3 orange,4 blue): +-- cfg.settings["troopZoneSmokeColor"] = { [1] = 1, [2] = 4 } +-- -- Vehicle weights (kg) for loadable-vehicle checks: +-- cfg.settings["groundVehicleWeights"]["BRDM-2"] = 7000 +-- -- Non-stock (mod) DCS type names your config uses (silences the asset-check companion): +-- cfg.settings["modTypes"] = { "Some_Mod_Type" } +-- +-- -- ── Crate 3D model templates (spawnableCratesModels — direct access) ──── +-- -- Slots: "load" (hover/menu), "sling" (ctld.slingLoad=true), "dynamic" +-- -- (dynamicCargoUnits). Each: { category, type [, shape_name], canCargo }. +-- -- canCargo=false for "load", true for "sling"/"dynamic". +-- -- cfg.settings["spawnableCratesModels"]["load"].type = "ammo_cargo" +-- +-- end) diff --git a/src/CTLD_userSetup.lua b/src/CTLD_userSetup.lua new file mode 100644 index 0000000..ac75c7c --- /dev/null +++ b/src/CTLD_userSetup.lua @@ -0,0 +1,184 @@ +-- ============================================================ +-- CTLD_userSetup.lua +-- Mission Maker configuration helper API (FEAT-USERCONFIG-API). +-- +-- Surgical add/remove/patch operations on the live config, meant to be called +-- from `ctld.userSetup` callbacks. The bootstrap runs those callbacks after +-- CTLDConfig:load() and injectAACrates(), so AA entries are already present and +-- the whole default catalogue is patchable. +-- +-- ctld.userSetup = ctld.userSetup or {} +-- table.insert(ctld.userSetup, function(cfg) +-- ctld.addCrate("Support", { weight = 2000.01, desc = "My Unit", unit = "Ural-375", side = 1 }) +-- ctld.patchCrate(1001.01, { cratesRequired = 3 }) +-- end) +-- +-- Merged after CTLD_config.lua. Defines functions only — the ctld.utils.* calls +-- resolve at callback time (runtime), so load order beyond `ctld` is irrelevant. +-- ============================================================ + +ctld = ctld or {} + +-- Live settings table of the config singleton. +local function _settings() + return CTLDConfig.get().settings +end + +-- Locate a crate entry by weight across all spawnableCrates sections. +-- Returns entry, sectionTable, index (or nil). +local function _findCrate(weight) + local crates = _settings()["spawnableCrates"] + if type(crates) ~= "table" then return nil end + for _, entries in pairs(crates) do + for i, e in ipairs(entries) do + if e.weight == weight then return e, entries, i end + end + end + return nil +end + +--- Add a crate entry to a spawnableCrates section (section created if absent). +-- A duplicate weight is fatal (irrecoverable F10-menu corruption): the entry is +-- rejected with a visible warning, but the mission is NOT aborted. +---@param section string +---@param entry table +---@return boolean added +function ctld.addCrate(section, entry) + local crates = _settings()["spawnableCrates"] + if type(crates) ~= "table" then + ctld.logWarning("ctld.addCrate: spawnableCrates unavailable — entry rejected") + return false + end + if entry and entry.weight and _findCrate(entry.weight) then + local msg = string.format( + "CTLD userConfig: duplicate crate weight %s — entry rejected", tostring(entry.weight)) + ctld.logWarning("ctld.addCrate: %s", msg) + if trigger and trigger.action and trigger.action.outText then + trigger.action.outText(msg, 30) + end + return false + end + crates[section] = crates[section] or {} + table.insert(crates[section], entry) + return true +end + +--- Remove a crate entry by weight (searched across all sections). +---@param weight number +---@return boolean removed +function ctld.removeCrate(weight) + local _, entries, idx = _findCrate(weight) + if not entries then + ctld.logWarning("ctld.removeCrate: no crate with weight %s — nothing removed", tostring(weight)) + return false + end + table.remove(entries, idx) + return true +end + +--- Patch a crate entry found by weight. Shallow merge at the top level, plus a +-- one-level deep merge for table-valued fields (e.g. specificParams), so a single +-- nested field can be changed without rewriting the whole sub-table. +---@param weight number +---@param patch table +---@return boolean patched +function ctld.patchCrate(weight, patch) + local entry = _findCrate(weight) + if not entry then + ctld.logWarning("ctld.patchCrate: no crate with weight %s — nothing patched", tostring(weight)) + return false + end + for k, v in pairs(patch or {}) do + if type(v) == "table" and type(entry[k]) == "table" then + for kk, vv in pairs(v) do + entry[k][kk] = vv + end + else + entry[k] = v + end + end + return true +end + +--- Add an infantry group template to loadableGroups. +---@param entry table +---@return boolean added +function ctld.addTroopGroup(entry) + local groups = _settings()["loadableGroups"] + if type(groups) ~= "table" then + ctld.logWarning("ctld.addTroopGroup: loadableGroups unavailable — entry rejected") + return false + end + table.insert(groups, entry) + return true +end + +--- Remove an infantry group template by name. +---@param name string +---@return boolean removed +function ctld.removeTroopGroup(name) + local groups = _settings()["loadableGroups"] + if type(groups) == "table" then + for i, g in ipairs(groups) do + if g.name == name then + table.remove(groups, i) + return true + end + end + end + ctld.logWarning("ctld.removeTroopGroup: no group named '%s' — nothing removed", tostring(name)) + return false +end + +--- Append an entry to an array-type setting (transportPilotNames, troopZones, +-- wpZones, extractableGroups, logisticUnits, ...). +---@param settingName string +---@param entry any +---@return boolean added +function ctld.addTo(settingName, entry) + local value = _settings()[settingName] + if type(value) ~= "table" then + ctld.logWarning("ctld.addTo: setting '%s' is not an array — nothing added", tostring(settingName)) + return false + end + table.insert(value, entry) + return true +end + +--- Serialise a config setting to CTLD.log in copy-pasteable Lua form, so the MM +-- can inspect the real defaults at runtime. Intended to be called from a ME +-- DO SCRIPT trigger a few seconds after mission start. +---@param settingName string +function ctld.logDefaults(settingName) + local value = ctld.gs(settingName) + if value == nil then + ctld.logWarning("ctld.logDefaults: unknown setting '%s'", tostring(settingName)) + return + end + local out + if type(value) == "table" then + out = ctld.utils.tableShowScript("ctld.logDefaults", value, settingName) + else + out = settingName .. " = " .. ctld.utils.basicSerialize("ctld.logDefaults", value) + end + ctld.utils.log("INFO", "ctld.logDefaults(%s):\n%s", tostring(settingName), out) +end + +--- Run every registered ctld.userSetup callback, in registration order, against the +-- live config singleton. Each callback is guarded so a failure in one is reported +-- and neither aborts the other callbacks nor the mission (FEAT-USERCONFIG-API US 11). +-- Called by ctld.initialize() after injectAACrates and before the managers boot. +function ctld.runUserSetup() + local callbacks = ctld.userSetup or {} + local cfg = CTLDConfig.get() + for i, cb in ipairs(callbacks) do + if type(cb) == "function" then + local ok, err = pcall(cb, cfg) + if not ok then + ctld.logWarning("ctld.userSetup callback #%d failed: %s", i, tostring(err)) + end + else + ctld.logWarning("ctld.userSetup entry #%d is not a function — skipped", i) + end + end +end diff --git a/tests/ci/helpers/loader.lua b/tests/ci/helpers/loader.lua index 640548f..1eef07c 100644 --- a/tests/ci/helpers/loader.lua +++ b/tests/ci/helpers/loader.lua @@ -30,6 +30,7 @@ ctld.tr = ctld.tr or function(key, default) return default or key end CTLDConfig.get():load() dofile(SRC .. "CTLD_utils.lua") +dofile(SRC .. "CTLD_userSetup.lua") dofile(SRC .. "CTLD_i18n.lua") dofile(SRC .. "CTLD_i18n_en.lua") dofile(SRC .. "CTLD_menu.lua") diff --git a/tests/ci/unit/aasystem_spec.lua b/tests/ci/unit/aasystem_spec.lua index 9691fb8..077eb64 100644 --- a/tests/ci/unit/aasystem_spec.lua +++ b/tests/ci/unit/aasystem_spec.lua @@ -350,3 +350,47 @@ describe("CTLDCrateAssemblyManager _buildSpawnArrays geometry", function() end) end) + +-- ───────────────────────────────────────────────────────────── +-- injectAACrates relocated to ctld.initialize() (FEAT-USERCONFIG-API / ADR 0009): +-- the injection populates the AA sections, and a later addCrate lands after them. +describe("CTLDCrateAssemblyManager.injectAACrates + userSetup ordering", function() + + local cfg + + before_each(function() + CTLDConfig._instance = nil + ctld.yamlConfigDatas = nil + cfg = CTLDConfig.get() + cfg:load() + end) + + it("populates the AA sections in spawnableCrates", function() + local crates = cfg.settings["spawnableCrates"] + CTLDCrateAssemblyManager.injectAACrates(crates) + assert.is_not_nil(crates["SAM mid range"]) + assert.is_not_nil(crates["SAM long range"]) + assert.is_true(#crates["SAM mid range"] > 0) + end) + + it("lets a later addCrate append after the injected AA entries", function() + local crates = cfg.settings["spawnableCrates"] + CTLDCrateAssemblyManager.injectAACrates(crates) + local n = #crates["SAM mid range"] + ctld.addCrate("SAM mid range", { weight = 9998.01, desc = "Extra", unit = "Ural-375" }) + assert.equals(n + 1, #crates["SAM mid range"]) + assert.equals(9998.01, crates["SAM mid range"][#crates["SAM mid range"]].weight) + end) + + -- Non-regression for the relocation: with injection removed from + -- _processSpawnableCrates, an early injection (as ctld.initialize() does) must still + -- make the AA crates reachable in the crate manager's _weightIndex. + it("makes injected AA crates reachable via _processSpawnableCrates", function() + CTLDCrateAssemblyManager.injectAACrates(cfg.settings["spawnableCrates"]) + local w = cfg.settings["spawnableCrates"]["SAM mid range"][1].weight + CTLDCrateManager._instance = nil + local cm = CTLDCrateManager.getInstance() + assert.is_not_nil(cm._weightIndex[w]) + end) + +end) diff --git a/tests/ci/unit/config_spec.lua b/tests/ci/unit/config_spec.lua index da13a26..45cba70 100644 --- a/tests/ci/unit/config_spec.lua +++ b/tests/ci/unit/config_spec.lua @@ -206,3 +206,56 @@ describe("CTLDConfig", function() end) end) + +-- ───────────────────────────────────────────────────────────── +-- ctld.userSetup dispatch (FEAT-USERCONFIG-API): callbacks run in order, nil-safe, +-- mutations land in the live config, one failing callback does not abort the rest. +describe("ctld.userSetup dispatch", function() + + before_each(function() + CTLDConfig._instance = nil + ctld.yamlConfigDatas = nil + ctld.userSetup = nil + CTLDConfig.get():load() + end) + + after_each(function() + ctld.userSetup = nil + end) + + it("runs callbacks in registration order", function() + local order = {} + ctld.userSetup = { + function() table.insert(order, "a") end, + function() table.insert(order, "b") end, + } + ctld.runUserSetup() + assert.same({ "a", "b" }, order) + end) + + it("does not error when ctld.userSetup is nil", function() + ctld.userSetup = nil + assert.has_no_error(function() ctld.runUserSetup() end) + end) + + it("makes callback mutations visible in the live config", function() + ctld.userSetup = { function() ctld.addTo("transportPilotNames", "FromCallback") end } + ctld.runUserSetup() + local names = CTLDConfig.get().settings["transportPilotNames"] + assert.equals("FromCallback", names[#names]) + end) + + it("isolates a failing callback and still runs the others", function() + local ran = false + local warn = spy.on(ctld, "logWarning") + ctld.userSetup = { + function() error("boom") end, + function() ran = true end, + } + ctld.runUserSetup() + assert.is_true(ran) + assert.spy(warn).was_called() + ctld.logWarning:revert() + end) + +end) diff --git a/tests/ci/unit/usersetup_spec.lua b/tests/ci/unit/usersetup_spec.lua new file mode 100644 index 0000000..3e91420 --- /dev/null +++ b/tests/ci/unit/usersetup_spec.lua @@ -0,0 +1,176 @@ +---@diagnostic disable +-- tests/ci/unit/usersetup_spec.lua +-- busted specs for the CTLD_userSetup MM helper API (FEAT-USERCONFIG-API): +-- ctld.addCrate / removeCrate / patchCrate / addTroopGroup / removeTroopGroup / addTo / logDefaults +-- Tests assert observable state of the live config after each helper, not internals. +-- ============================================================ + +describe("CTLD_userSetup helpers", function() + + local cfg + + before_each(function() + CTLDConfig._instance = nil + ctld.yamlConfigDatas = nil + cfg = CTLDConfig.get() + cfg:load() + end) + + -- helper: count entries in a spawnableCrates section + local function sectionCount(section) + local s = cfg.settings["spawnableCrates"][section] + return s and #s or 0 + end + + -- helper: find a crate entry by weight across all sections + local function findCrate(weight) + for _, entries in pairs(cfg.settings["spawnableCrates"]) do + for _, e in ipairs(entries) do + if e.weight == weight then return e end + end + end + return nil + end + + -- ── addCrate ────────────────────────────────────────────── + describe("ctld.addCrate", function() + + it("appends an entry to an existing section", function() + local before = sectionCount("Support") + ctld.addCrate("Support", { weight = 9999.01, desc = "Test Unit", unit = "Ural-375", side = 2 }) + assert.equals(before + 1, sectionCount("Support")) + assert.is_not_nil(findCrate(9999.01)) + end) + + it("creates the section when absent", function() + assert.is_nil(cfg.settings["spawnableCrates"]["My Custom Section"]) + ctld.addCrate("My Custom Section", { weight = 9999.02, desc = "Custom", unit = "Ural-375" }) + assert.equals(1, sectionCount("My Custom Section")) + end) + + it("appends after pre-existing entries (post-injectAACrates order)", function() + ctld.addCrate("Support", { weight = 9999.03, desc = "Last", unit = "Ural-375" }) + local s = cfg.settings["spawnableCrates"]["Support"] + assert.equals(9999.03, s[#s].weight) + end) + + it("rejects a duplicate weight and logs a warning", function() + local warn = spy.on(ctld, "logWarning") + ctld.addCrate("Support", { weight = 9999.04, desc = "First", unit = "Ural-375" }) + local afterFirst = sectionCount("Support") + ctld.addCrate("Combat Vehicles", { weight = 9999.04, desc = "Dup", unit = "Ural-375" }) + assert.equals(afterFirst, sectionCount("Support")) + assert.spy(warn).was_called() + ctld.logWarning:revert() + end) + end) + + -- ── removeCrate ─────────────────────────────────────────── + describe("ctld.removeCrate", function() + + it("removes an entry by weight", function() + ctld.addCrate("Support", { weight = 9999.05, desc = "ToRemove", unit = "Ural-375" }) + assert.is_not_nil(findCrate(9999.05)) + ctld.removeCrate(9999.05) + assert.is_nil(findCrate(9999.05)) + end) + + it("finds the entry across sections", function() + ctld.addCrate("My Section A", { weight = 9999.06, desc = "A", unit = "Ural-375" }) + ctld.removeCrate(9999.06) + assert.is_nil(findCrate(9999.06)) + end) + + it("logs a warning on an unknown weight", function() + local warn = spy.on(ctld, "logWarning") + ctld.removeCrate(123456.99) + assert.spy(warn).was_called() + ctld.logWarning:revert() + end) + end) + + -- ── patchCrate ──────────────────────────────────────────── + describe("ctld.patchCrate", function() + + it("patches a top-level field, leaving others intact", function() + ctld.addCrate("Support", { weight = 9999.07, desc = "Patch", unit = "Ural-375", cratesRequired = 2 }) + ctld.patchCrate(9999.07, { cratesRequired = 5 }) + local e = findCrate(9999.07) + assert.equals(5, e.cratesRequired) + assert.equals("Ural-375", e.unit) + end) + + it("deep-merges one level into a table field", function() + ctld.addCrate("Support", { + weight = 9999.08, desc = "Drone", unit = "MQ-9 Reaper", + specificParams = { alti = 100, speed = 50 }, + }) + ctld.patchCrate(9999.08, { specificParams = { alti = 5000 } }) + local e = findCrate(9999.08) + assert.equals(5000, e.specificParams.alti) + assert.equals(50, e.specificParams.speed) + end) + + it("logs a warning on an unknown weight", function() + local warn = spy.on(ctld, "logWarning") + ctld.patchCrate(123456.99, { cratesRequired = 1 }) + assert.spy(warn).was_called() + ctld.logWarning:revert() + end) + end) + + -- ── addTroopGroup / removeTroopGroup ────────────────────── + describe("ctld.addTroopGroup / removeTroopGroup", function() + + it("adds a troop group to loadableGroups", function() + local before = #cfg.settings["loadableGroups"] + ctld.addTroopGroup({ name = "Test Squad", inf = 3 }) + assert.equals(before + 1, #cfg.settings["loadableGroups"]) + end) + + it("removes a troop group by name", function() + ctld.addTroopGroup({ name = "Removable Squad", inf = 2 }) + ctld.removeTroopGroup("Removable Squad") + for _, g in ipairs(cfg.settings["loadableGroups"]) do + assert.not_equals("Removable Squad", g.name) + end + end) + + it("logs a warning removing an unknown name", function() + local warn = spy.on(ctld, "logWarning") + ctld.removeTroopGroup("No Such Group 42") + assert.spy(warn).was_called() + ctld.logWarning:revert() + end) + end) + + -- ── addTo ───────────────────────────────────────────────── + describe("ctld.addTo", function() + + it("appends to a named array setting", function() + local before = #cfg.settings["transportPilotNames"] + ctld.addTo("transportPilotNames", "TestPilot #1") + assert.equals(before + 1, #cfg.settings["transportPilotNames"]) + assert.equals("TestPilot #1", cfg.settings["transportPilotNames"][#cfg.settings["transportPilotNames"]]) + end) + + it("logs a warning on an unknown setting", function() + local warn = spy.on(ctld, "logWarning") + ctld.addTo("noSuchArraySetting", "x") + assert.spy(warn).was_called() + ctld.logWarning:revert() + end) + end) + + -- ── logDefaults ─────────────────────────────────────────── + describe("ctld.logDefaults", function() + + it("logs a serialised representation of a known setting", function() + local captured = "" + local logSpy = spy.on(ctld.utils, "log") + ctld.logDefaults("spawnableCrates") + assert.spy(logSpy).was_called() + ctld.utils.log:revert() + end) + end) +end) diff --git a/tools/build/listToMerge.txt b/tools/build/listToMerge.txt index 244d063..5294a85 100644 --- a/tools/build/listToMerge.txt +++ b/tools/build/listToMerge.txt @@ -1,6 +1,7 @@ -- Core foundations (no business state) core/class.lua CTLD_config.lua +CTLD_userSetup.lua CTLD_i18n.lua CTLD_i18n_en.lua CTLD_i18n_fr.lua