Skip to content

feat(config): safe Mission Maker userSetup API (FEAT-USERCONFIG-API)#45

Merged
davidp57 merged 2 commits into
developfrom
feature/userconfig-api
Jul 20, 2026
Merged

feat(config): safe Mission Maker userSetup API (FEAT-USERCONFIG-API)#45
davidp57 merged 2 commits into
developfrom
feature/userconfig-api

Conversation

@davidp57

@davidp57 davidp57 commented Jul 20, 2026

Copy link
Copy Markdown
Member

Implements the FEAT-USERCONFIG-API lot (see .backlog/FEAT-USERCONFIG-API/PRD.md).

What

  • 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 defined it). Helpers: addCrate, removeCrate, patchCrate
    (deep-merge one level), addTroopGroup, removeTroopGroup, addTo, logDefaults. Each
    callback runs guarded — a failing one warns without aborting the others or the mission.
  • injectAACrates relocated from CTLDCrateManager:_processSpawnableCrates() to
    ctld.initialize() (before the userSetup callbacks). initialize() is now the single place
    that materialises the full config: defaults → AA injection → userSetup → managers.
  • CTLD_userConfig.lua template rewritten — broken Section 2 replaced by documented
    ctld.userSetup examples + per-table field schemas; test-only debug block removed; Section 1
    defaults corrected (parachuteMinAltitude* = 152, JTAC_droneAltitude = 4000).
  • Docs mission-maker/configuration.md (EN+FR) updated to the callback-based flow.

Tests

  • New usersetup_spec.lua (7 helpers); extended config_spec.lua (dispatch: order, nil-safe,
    mutations, failure isolation) and aasystem_spec.lua (injection relocation + _weightIndex
    non-regression).
  • Validated locally under lua5.1 (busted not installable locally → CI); CTLD.lua rebuilt,
    parses clean, no BOM.

Notes

  • No behavioural change beyond the new API; CTLD_config.lua defaults were already correct.
  • First lot of the ctld-tools program; the YAML/exe authoring layer builds on this API (ADR 0009).

Summary by Sourcery

Introduce a safe, callback-based Mission Maker configuration API via ctld.userSetup, centralize config materialization in initialization, update the user configuration template and documentation, and expand tests around the new API and AA crate injection ordering.

New Features:

  • Add ctld.userSetup callback API and helper functions for modifying live CTLD configuration tables without replacing defaults.
  • Allow Mission Makers to inspect runtime default configuration values via ctld.logDefaults.

Enhancements:

  • Relocate AA-system crate injection into ctld.initialize so the final config is materialized in a single, ordered place before managers read it.
  • Rewrite CTLD_userConfig.lua Section 2 into a documented callback-based template with corrected scalar defaults for parachute altitudes and JTAC drone altitude.

Documentation:

  • Document the new ctld.userSetup callback workflow and helpers in English and French Mission Maker configuration guides, replacing direct CTLDConfig editing examples.

Tests:

  • Add unit tests for ctld.userSetup helpers, callback dispatch behavior, and AA crate injection ordering and indexing.

…ation

Implements FEAT-USERCONFIG-API: ctld.userSetup callbacks with add/remove/patch
helpers, injectAACrates moved into ctld.initialize(), userConfig template rewrite,
Section 1 default fixes. Docs (EN+FR) + CHANGELOG updated.
@sourcery-ai

sourcery-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a guarded Mission Maker configuration callback API ctld.userSetup, centralises config materialisation order (defaults → AA crate injection → userSetup → managers), rewrites the CTLD_userConfig.lua template and docs to use the new API, fixes a few scalar defaults, and adds unit tests around the helpers and ordering.

Sequence diagram for config materialisation with ctld.userSetup

sequenceDiagram
    participant ctld
    participant CTLDConfig
    participant CTLDCrateAssemblyManager
    participant CTLDCrateManager
    participant userSetupCallback

    ctld->>CTLDConfig: get()
    CTLDConfig-->>ctld: configInstance
    ctld->>CTLDConfig: load()

    ctld->>CTLDConfig: gs(spawnableCrates)
    CTLDConfig-->>ctld: spawnableCrates
    ctld->>CTLDCrateAssemblyManager: injectAACrates(spawnableCrates)

    ctld->>ctld: runUserSetup()
    loop for each ctld.userSetup callback
        ctld->>userSetupCallback: pcall(callback, configInstance)
        userSetupCallback->>CTLDConfig: mutate settings via helpers
    end

    CTLDCrateManager->>CTLDConfig: gs(spawnableCrates)
    CTLDConfig-->>CTLDCrateManager: spawnableCrates (defaults + AA + user patches)
    CTLDCrateManager->>CTLDCrateManager: _processSpawnableCrates()
Loading

File-Level Changes

Change Details Files
Add CTLD_userSetup.lua helper module and wire it into initialization and tests
  • Introduce ctld helper functions (addCrate, removeCrate, patchCrate, addTroopGroup, removeTroopGroup, addTo, logDefaults, runUserSetup) operating on CTLDConfig.get().settings
  • Ensure ctld.runUserSetup executes userSetup callbacks in order with pcall-based failure isolation and logs warnings for invalid entries
  • Wire CTLD_userSetup.lua into the merged CTLD.lua build, CI test loader, and bump the CTLD.lua build timestamp
src/CTLD_userSetup.lua
CTLD.lua
tests/ci/helpers/loader.lua
tools/build/listToMerge.txt
Relocate AA crate injection and integrate userSetup into the bootstrap sequence
  • Move CTLDCrateAssemblyManager.injectAACrates call from CTLDCrateManager:_processSpawnableCrates to ctld.initialize so spawnableCrates already contains AA entries before userSetup runs
  • Call ctld.runUserSetup from ctld.initialize after AA injection and before manager bootstrapping, both in source and merged CTLD.lua
  • Adjust CTLDCrateManager:_processSpawnableCrates to rely on pre-injected AA crates and update comments accordingly
src/CTLD_bootstrap.lua
src/CTLD_crate.lua
CTLD.lua
Replace broken direct-config Section 2 with documented userSetup-based template and corrected scalar defaults
  • Remove the old Section 2 in CTLD_userConfig.lua that directly called CTLDConfig.get() and contained debug-only aiZones and large commented example tables
  • Introduce a new Section 2 that documents the ctld.userSetup callback pattern, describes each helper and which settings are edited directly, and gives example usage
  • Correct Section 1 scalar defaults for parachuteMinAltitude* (152) and JTAC_droneAltitude (4000)
src/CTLD_userConfig.lua
Update Mission Maker configuration docs to describe the callback-based flow
  • Change EN and FR configuration docs from direct CTLDConfig.get() table replacement to using ctld.userSetup callbacks and helper functions
  • Document that helpers operate on the live config and that some dictionaries (e.g. capabilitiesByType, aiZones) are still edited via the cfg argument
  • Clarify how to patch individual capability fields and how to use ctld.logDefaults to inspect runtime defaults
docs/mission-maker/configuration.md
docs/mission-maker/configuration.fr.md
Add unit tests for userSetup helpers and ordering, and AA crate injection relocation
  • Add usersetup_spec.lua to cover all ctld_userSetup helpers’ observable effects on the live config (crates, troop groups, array settings, logDefaults)
  • Extend config_spec.lua to verify ctld.runUserSetup dispatch semantics (order, nil-safety, mutation visibility, failure isolation)
  • Extend aasystem_spec.lua to validate that injectAACrates populates AA sections, that later addCrate appends after injected entries, and that early injection still feeds CTLDCrateManager._weightIndex
tests/ci/unit/usersetup_spec.lua
tests/ci/unit/config_spec.lua
tests/ci/unit/aasystem_spec.lua
Document the new feature in the changelog
  • Add an Unreleased section entry describing ctld.userSetup, AA crate injection relocation, CTLD_userConfig.lua template rewrite, scalar default fixes, and documentation updates
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In ctld.addCrate, consider explicitly validating that entry and entry.weight are non-nil and of the expected type, and logging a clear warning when they are not, to avoid silently inserting crates that can never be referenced or removed later.
  • For ctld.addTo, you might want to assert or guard that the target setting is a numerically indexed array (e.g. via # and ipairs) before table.insert, so that accidental use with a dictionary-like table is caught early rather than mutating arbitrary config structures.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ctld.addCrate`, consider explicitly validating that `entry` and `entry.weight` are non-nil and of the expected type, and logging a clear warning when they are not, to avoid silently inserting crates that can never be referenced or removed later.
- For `ctld.addTo`, you might want to assert or guard that the target setting is a numerically indexed array (e.g. via `#` and `ipairs`) before `table.insert`, so that accidental use with a dictionary-like table is caught early rather than mutating arbitrary config structures.

## Individual Comments

### Comment 1
<location path="src/CTLD_userSetup.lua" line_range="43-64" />
<code_context>
+---@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
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Validate `entry.weight` presence/type before inserting crates to avoid hard-to-debug catalogue issues.

As written, crates with `weight = nil` or a non-number still get added to `spawnableCrates`, but later logic (e.g. `_findCrate` and the crate manager) assumes `weight` is a valid numeric key. That mismatch will manifest as odd runtime behaviour instead of a clear configuration error. Consider enforcing `type(entry.weight) == "number"` before insertion and logging/rejecting any invalid entries.

```suggestion
---@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 type(entry) ~= "table" then
        ctld.logWarning("ctld.addCrate: invalid entry (expected table, got %s) — entry rejected", type(entry))
        return false
    end

    local weight = entry.weight
    if type(weight) ~= "number" then
        local msg = string.format(
            "CTLD userConfig: crate weight must be a number (got %s) — entry rejected",
            tostring(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

    if _findCrate(weight) then
        local msg = string.format(
            "CTLD userConfig: duplicate crate weight %s — entry rejected",
            tostring(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
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/CTLD_userSetup.lua
Comment on lines +43 to +64
---@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Validate entry.weight presence/type before inserting crates to avoid hard-to-debug catalogue issues.

As written, crates with weight = nil or a non-number still get added to spawnableCrates, but later logic (e.g. _findCrate and the crate manager) assumes weight is a valid numeric key. That mismatch will manifest as odd runtime behaviour instead of a clear configuration error. Consider enforcing type(entry.weight) == "number" before insertion and logging/rejecting any invalid entries.

Suggested change
---@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
---@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 type(entry) ~= "table" then
ctld.logWarning("ctld.addCrate: invalid entry (expected table, got %s) — entry rejected", type(entry))
return false
end
local weight = entry.weight
if type(weight) ~= "number" then
local msg = string.format(
"CTLD userConfig: crate weight must be a number (got %s) — entry rejected",
tostring(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
if _findCrate(weight) then
local msg = string.format(
"CTLD userConfig: duplicate crate weight %s — entry rejected",
tostring(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

@davidp57
davidp57 merged commit 63b245b into develop Jul 20, 2026
6 checks passed
@davidp57
davidp57 deleted the feature/userconfig-api branch July 20, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant