Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Lot POST-FULLGAS-FIXES — apply FullGas's review answers

Status: 🚧 in progress
Branch: fix/post-fullgas-corrections → PR → develop
Program: re-tooling CTLD on the VMCT model (see `.backlog/README.md`). Follows
`REINTEGRATE-ORPHAN-TESTS`; sibling to the (still-open) `CLEANUP-LEGACY-DCS-TESTS`.

## Problem Statement

FullGas reviewed our `dev/fullgas-report.md` findings and answered each one. Several are not
"design questions to defer" but concrete corrections we can now apply — one is a real product
regression, the rest are stale tests to rewrite against the current design, plus a mission-content
gap and doc touch-ups.

## Tickets

| # | Scope | Nature |
|---|-------|--------|
| 01 | F-Q whole-vehicle regression: restore Feature Q `loadableList` in `refreshRequestEquipmentSection` (dropped by commit 0a15814, ref b1ddfe4) | **src** + rebuild |
| 02 | U-108 ProbeOffMap + ProbeLifeCheck: run-once guard (FullGas option A) | test |
| 03 | U-108 WarnAndSkip: rewrite — assert `Farp_FG_Petit_Helipad` (probeSkip) excluded, 3 stock heliports present as HELIPORT, INFO "skipped" emitted | test |
| 04 | F-117/F-118 recon: `reconEnabled`→`reconF10Menu` + keyword; `nil`→`~=nil` (scan stays active) + targets empty | test |
| 05 | fr_ai_zones: impl `pickMaxStock = entry.isPickup and 0 or nil` + G3 extended (WARN on scalar/empty-table troopStock, incl. -1 legacy) + rewrite the 7 checks | src + test |
| 06 | Re-add static `coord_farp-1` (group "coord_farp", M92 barrel) to `Test_CTLDNEXT_01.miz` | mission |
| 07 | README doc: title "DCS-CTLD Next"→"CTLD"; "Pack Equipt" under "Crate Operations"; "Developer documentation"→"Guides" (link to the guides site) | doc |

## Decisions

- **F-R-3** (ticket 05): `troopStock = -1` legacy scalar → emit the G3 WARN "invalid format" (guide
the mission-maker to `{All=-1}`), rather than tolerate silently. Confirmed by David.

## Non-goals

- Porting/purging the 194 relics — that's `CLEANUP-LEGACY-DCS-TESTS` (port-to-VEAF then delete all).
- Any commit reference from FullGas's fork (0a15814, b1ddfe4): those objects exist in our repo but
are NOT in develop's lineage — used as read-only parity references, never cherry-picked.
11 changes: 11 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/01-fq-regression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 01 — F-Q whole-vehicle regression (Feature Q)

Status: ✅ done
Type: AFK

Commit 0a15814 (DCS-cargo fix, FullGas fork) dropped the `loadableList` computation in
`CTLDCrateManager:refreshRequestEquipmentSection` and hardcoded `spawnAsVehicle = false`,
silently disabling whole-vehicle spawn from Request Equipment. Restored the logic from the
b1ddfe4 reference (both objects exist in our repo, read-only). src + rebuild CTLD.lua.

Validated live: scenario_fq_vehicle_whole_transport PASS 9/9 (was 1/9).
10 changes: 10 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/02-u108-runonce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 02 — U-108 ProbeOffMap + ProbeLifeCheck

Status: ✅ done
Type: AFK

- ProbeLifeCheck: run-once guard (FullGas option A). PASS 4/4 live.
- ProbeOffMap: run-once guard + C3 rewritten as option C — verify the valid
probe's own ghost (CTLD_MVP_H<idx>) exists by name instead of a before/after
count-diff (which collided with the ghosts CTLD init pre-creates). PASS 4/4 live.
Resolved ourselves (David's call), no FullGas decision needed.
9 changes: 9 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/03-u108-warnandskip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 03-u108-warnandskip

Status: ✅ done
Type: AFK

U-108 WarnAndSkip rewrite vs current heliport registry

## Blocked by
Ticket 01 (branch established).
9 changes: 9 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/04-recon-f117-f118.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 04-recon-f117-f118

Status: ✅ done
Type: AFK

F-117/F-118 recon: reconF10Menu gate + scan-stays-active rewrite

## Blocked by
Ticket 01 (branch established).
9 changes: 9 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/05-fr-ai-zones.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 05-fr-ai-zones

Status: ✅ done
Type: AFK

fr_ai_zones pickMaxStock impl + G3 extended WARN + 7 checks

## Blocked by
Ticket 01 (branch established).
9 changes: 9 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/06-coord-farp-miz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 06-coord-farp-miz

Status: ✅ done
Type: AFK

Re-add static coord_farp-1 to Test_CTLDNEXT_01.miz

## Blocked by
Ticket 01 (branch established).
9 changes: 9 additions & 0 deletions .backlog/POST-FULLGAS-FIXES/tickets/07-readme-doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 07 — README doc touch-ups

Status: ⏸ deferred
Type: doc

FullGas's README observations (title "DCS-CTLD Next"→"CTLD", "Pack Equipt" under
"Crate Operations", "Developer documentation"→"Guides") target the monolithic
root README.md. Per David: DON'T patch it piecemeal — the whole README will be
rewritten later. Deferred out of this lot.
19 changes: 14 additions & 5 deletions .claude/skills/integration-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,27 @@ Once these are up, `exec_lua` is available as an MCP tool.
## The injection loop

```
Modify src/ → Rebuild (if src/ changed) → exec_lua CTLD.lua → wait ~3-5s for init
Modify src/ → Rebuild (if src/ changed) → reload mission (Shift+R, if src/ changed)
→ exec_lua the scenario → read the verdict → iterate on FAIL
```

1. **Rebuild** if `src/` changed:
`powershell -ExecutionPolicy Bypass -File tools\build\merge_CTLD.ps1`
2. **Inject CTLD.lua**: `exec_lua(code=<contents of CTLD.lua>)`, then wait 3-5s (CTLD init is
async — `CTLDCoreManager` and friends aren't available immediately).
3. **Inject the scenario**: `exec_lua(code=<contents of the scenario file>)`.
2. **Load the new CTLD code** — how depends on whether `src/` changed:
- **Scenario-only change** (no `src/` change): nothing to reload — CTLD is already in memory,
just inject the scenario (step 3).
- **`src/` changed** (so `CTLD.lua` was rebuilt): **ask the user to reload the mission with
`Shift+R`** in DCS. This restarts the mission from a clean state and re-`dofile`s the fresh
`CTLD.lua` from disk (the MISSION START trigger does this).
⚠️ Do **NOT** inject the whole `CTLD.lua` via `exec_lua`/`--inject-ctld`: the file is ~1.2 MB
and a single exec **times out (HTTP 504)**. And do **NOT** `dofile("…/CTLD.lua")` over a live
mission — it re-runs CTLD init (scheduler, event handlers, timers) on top of the existing
one and **freezes the DCS Lua thread**. Reloading the mission is the only safe way.
3. **Inject the scenario**: `exec_lua(code=<contents of the scenario file>)` (small file — fine).
4. **Read the verdict** — see the return contract below.
5. **Iterate**: on FAIL, fix and re-inject without waiting for the user (autonomous debug loop —
the user does not need to be in the loop between injections).
the user does not need to be in the loop between injections). If the fix is in `src/`, a
mission reload (`Shift+R`) is needed again before re-testing.

You (the AI) drive this loop end to end. Only pause for the user when a scenario requires a
human F10 action it cannot itself take (see pilotActive scenarios).
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ Versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Bug fixes (FullGas review round)

- **Fix**: whole-vehicle spawn from the **Request Equipment** menu (Feature Q) was silently
disabled — `refreshRequestEquipmentSection` hardcoded `spawnAsVehicle=false` after a DCS-cargo
refactor dropped the loadable-vehicle detection. Restored: a transport with
`canTransportWholeVehicle` again spawns a whole vehicle for its loadable types.
- **Fix**: AI-zone stock validation — dropoff-only zones no longer receive a bogus
`pickMaxStock`, and an invalid `troopStock` (a legacy scalar like `0`/`-1`/`10`, or an empty
table, instead of a `{[templateName]=N}` table) now emits a clear config WARN.

### DCS integration testing — first live validation

- **Fix**: `missions/Test_CTLDNEXT_01.miz`'s embedded `beacon.ogg` (420KB) broke the DCS Mission
Expand Down
43 changes: 32 additions & 11 deletions CTLD.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
--[[
CTLD.lua - Combined Transport and Logistics Dispatcher for DCS World
Version : 2.0.0
Built : 2026-07-09
Built : 2026-07-12
Source : https://github.com/VEAF/CTLD
Licence : MIT
DO NOT EDIT - generated by tools/build/merge_CTLD.ps1
Expand Down Expand Up @@ -1888,7 +1888,7 @@ ctld.i18n["en"][" AIZ[%1] ERROR '%2': neither isPickup nor isDropoff — zone d
ctld.i18n["en"][" AIZ[%1] ERROR '%2': cargoType '%3' requires whole-vehicle transport but no aircraft has canTransportWholeVehicle=true — entry ignored"] = " AIZ[%1] ERROR '%2': cargoType '%3' requires whole-vehicle transport but no aircraft has canTransportWholeVehicle=true — entry ignored"
ctld.i18n["en"][" AIZ[%1] WARN '%2': invalid cargoType '%3' — defaulting to T"] = " AIZ[%1] WARN '%2': invalid cargoType '%3' — defaulting to T"
ctld.i18n["en"][" AIZ[%1] WARN '%2': invalid aiDropMode '%3' — defaulting to GP"] = " AIZ[%1] WARN '%2': invalid aiDropMode '%3' — defaulting to GP"
ctld.i18n["en"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock not defined — troop pickup disabled"] = " AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock not defined — troop pickup disabled"
ctld.i18n["en"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock nil/invalid — use a {[templateName]=N} table"] = " AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock nil/invalid — use a {[templateName]=N} table"
ctld.i18n["en"][" AIZ[%1] WARN '%2': isPickup=true with vehicle cargo but vehicleStock not defined — vehicle pickup disabled"] = " AIZ[%1] WARN '%2': isPickup=true with vehicle cargo but vehicleStock not defined — vehicle pickup disabled"
ctld.i18n["en"][" AIZ[%1] WARN '%2': troopTemplates['%3'] not found in loadableGroups"] = " AIZ[%1] WARN '%2': troopTemplates['%3'] not found in loadableGroups"
ctld.i18n["en"][" AIZ[%1] WARN '%2': all troopTemplates are unknown — troop pickup will always be skipped"] = " AIZ[%1] WARN '%2': all troopTemplates are unknown — troop pickup will always be skipped"
Expand Down Expand Up @@ -2365,7 +2365,7 @@ ctld.i18n["fr"][" AIZ[%1] ERROR '%2': neither isPickup nor isDropoff — zone d
ctld.i18n["fr"][" AIZ[%1] ERROR '%2': cargoType '%3' requires whole-vehicle transport but no aircraft has canTransportWholeVehicle=true — entry ignored"] = " AIZ[%1] ERREUR '%2' : cargoType '%3' nécessite un transport de véhicule entier mais aucun aéronef n'a canTransportWholeVehicle=true — entrée ignorée"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': invalid cargoType '%3' — defaulting to T"] = " AIZ[%1] AVERT '%2' : cargoType '%3' invalide — valeur par défaut T"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': invalid aiDropMode '%3' — defaulting to GP"] = " AIZ[%1] AVERT '%2' : aiDropMode '%3' invalide — valeur par défaut GP"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock not defined — troop pickup disabled"] = " AIZ[%1] AVERT '%2' : isPickup=true avec cargo de troupes mais troopStock non défini — pickup de troupes désactivé"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock nil/invalid — use a {[templateName]=N} table"] = " AIZ[%1] AVERT '%2' : isPickup=true avec cargo de troupes mais troopStock nil/invalide — utiliser une table {[nomTemplate]=N}"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': isPickup=true with vehicle cargo but vehicleStock not defined — vehicle pickup disabled"] = " AIZ[%1] AVERT '%2' : isPickup=true avec cargo de véhicule mais vehicleStock non défini — pickup de véhicule désactivé"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': troopTemplates['%3'] not found in loadableGroups"] = " AIZ[%1] AVERT '%2' : troopTemplates['%3'] introuvable dans loadableGroups"
ctld.i18n["fr"][" AIZ[%1] WARN '%2': all troopTemplates are unknown — troop pickup will always be skipped"] = " AIZ[%1] AVERT '%2' : tous les troopTemplates sont inconnus — le pickup de troupes sera toujours ignoré"
Expand Down Expand Up @@ -2843,7 +2843,7 @@ ctld.i18n["es"][" AIZ[%1] ERROR '%2': neither isPickup nor isDropoff — zone d
ctld.i18n["es"][" AIZ[%1] ERROR '%2': cargoType '%3' requires whole-vehicle transport but no aircraft has canTransportWholeVehicle=true — entry ignored"] = " AIZ[%1] ERROR '%2': cargoType '%3' requiere transporte de vehículo completo pero ninguna aeronave tiene canTransportWholeVehicle=true — entrada ignorada"
ctld.i18n["es"][" AIZ[%1] WARN '%2': invalid cargoType '%3' — defaulting to T"] = " AIZ[%1] AVISO '%2': cargoType '%3' inválido — predeterminado a T"
ctld.i18n["es"][" AIZ[%1] WARN '%2': invalid aiDropMode '%3' — defaulting to GP"] = " AIZ[%1] AVISO '%2': aiDropMode '%3' inválido — predeterminado a GP"
ctld.i18n["es"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock not defined — troop pickup disabled"] = " AIZ[%1] AVISO '%2': isPickup=true con carga de tropas pero troopStock no definido — carga de tropas desactivada"
ctld.i18n["es"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock nil/invalid — use a {[templateName]=N} table"] = " AIZ[%1] AVISO '%2': isPickup=true con carga de tropas pero troopStock nil/inválido — usar una tabla {[nombreTemplate]=N}"
ctld.i18n["es"][" AIZ[%1] WARN '%2': isPickup=true with vehicle cargo but vehicleStock not defined — vehicle pickup disabled"] = " AIZ[%1] AVISO '%2': isPickup=true con carga de vehículo pero vehicleStock no definido — carga de vehículo desactivada"
ctld.i18n["es"][" AIZ[%1] WARN '%2': troopTemplates['%3'] not found in loadableGroups"] = " AIZ[%1] AVISO '%2': troopTemplates['%3'] no encontrado en loadableGroups"
ctld.i18n["es"][" AIZ[%1] WARN '%2': all troopTemplates are unknown — troop pickup will always be skipped"] = " AIZ[%1] AVISO '%2': todos los troopTemplates son desconocidos — el pickup de tropas siempre se omitirá"
Expand Down Expand Up @@ -3171,7 +3171,7 @@ ctld.i18n["ko"][" AIZ[%1] ERROR '%2': neither isPickup nor isDropoff — zone d
ctld.i18n["ko"][" AIZ[%1] ERROR '%2': cargoType '%3' requires whole-vehicle transport but no aircraft has canTransportWholeVehicle=true — entry ignored"] = " AIZ[%1] 오류 '%2': cargoType '%3'은(는) 차량 전체 수송이 필요하지만 canTransportWholeVehicle=true인 항공기가 없음 — 항목 무시됨"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': invalid cargoType '%3' — defaulting to T"] = " AIZ[%1] 경고 '%2': cargoType '%3' 잘못됨 — 기본값 T"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': invalid aiDropMode '%3' — defaulting to GP"] = " AIZ[%1] 경고 '%2': aiDropMode '%3' 잘못됨 — 기본값 GP"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock not defined — troop pickup disabled"] = " AIZ[%1] 경고 '%2': isPickup=true이고 병력 화물이지만 troopStock 미정의병력 픽업 비활성화"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock nil/invalid — use a {[templateName]=N} table"] = " AIZ[%1] 경고 '%2': isPickup=true이고 병력 화물이지만 troopStock nil/무효{[templateName]=N} 테이블 사용"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': isPickup=true with vehicle cargo but vehicleStock not defined — vehicle pickup disabled"] = " AIZ[%1] 경고 '%2': isPickup=true이고 차량 화물이지만 vehicleStock 미정의 — 차량 픽업 비활성화"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': troopTemplates['%3'] not found in loadableGroups"] = " AIZ[%1] 경고 '%2': troopTemplates['%3']을(를) loadableGroups에서 찾을 수 없음"
ctld.i18n["ko"][" AIZ[%1] WARN '%2': all troopTemplates are unknown — troop pickup will always be skipped"] = " AIZ[%1] 경고 '%2': 모든 troopTemplates를 알 수 없음 — 병력 픽업이 항상 건너뜀"
Expand Down Expand Up @@ -8089,7 +8089,9 @@ function CTLDZoneManager:_loadAIZonesFromConfig()
isAIPickup = entry.isPickup == true,
isAIDropoff = entry.isDropoff == true,
aiCargoType = entry.cargoType or "T",
pickMaxStock = 0, -- unlimited; per-template stock via _aiTroopStock
-- pickup zones: 0 = unlimited (per-template stock lives in _aiTroopStock);
-- dropoff-only zones: nil, so hasPickup() stays false (FullGas fix, F-R-2/13.8).
pickMaxStock = entry.isPickup and 0 or nil,
troopTemplates = troopTemplates,
vehicleTypes = (entry.vehicleTypes and #entry.vehicleTypes > 0)
and entry.vehicleTypes or nil,
Expand Down Expand Up @@ -8866,10 +8868,14 @@ function CTLDZoneManager:_validateZoneNames()
if entry.aiDropMode and not VALID_DROP_MODE[entry.aiDropMode] then
warns[#warns + 1] = ctld.tr(" AIZ[%1] WARN '%2': invalid aiDropMode '%3' — defaulting to GP", i, tostring(dzn), tostring(entry.aiDropMode))
end
-- G3: isPickup + troop cargo + troopStock not defined → troop pickup disabled
-- G3: isPickup + troop cargo + troopStock nil OR not a {[name]=N} table (incl. a legacy
-- scalar like 0/-1/10, or an empty table) → invalid format, troop pickup disabled.
local effCargoHasTroops = (not entry.cargoType or entry.cargoType == "T" or entry.cargoType == "TV")
if not hasErr and entry.isPickup and effCargoHasTroops and entry.troopStock == nil then
warns[#warns + 1] = ctld.tr(" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock not defined — troop pickup disabled", i, tostring(dzn))
local troopStockInvalid = entry.troopStock == nil
or type(entry.troopStock) ~= "table"
or next(entry.troopStock) == nil
if not hasErr and entry.isPickup and effCargoHasTroops and troopStockInvalid then
warns[#warns + 1] = ctld.tr(" AIZ[%1] WARN '%2': isPickup=true with troop cargo but troopStock nil/invalid — use a {[templateName]=N} table", i, tostring(dzn))
end
-- G6: isPickup + vehicle cargo + vehicleStock not defined → vehicle pickup disabled
local effCargoHasVehicle = (entry.cargoType == "V" or entry.cargoType == "TV")
Expand Down Expand Up @@ -13930,6 +13936,17 @@ function CTLDCrateManager:refreshRequestEquipmentSection(playerObj)
local showSets = ctld.gs("enableAllCrates") ~= false
local processed = self._processedCrates or {}

-- Feature Q: pre-compute loadable whole-vehicle types for this transport (nil when not
-- capable). Restored from b1ddfe4 — commit 0a15814 (DCS-cargo fix) dropped this list and
-- hardcoded spawnAsVehicle=false, silently disabling whole-vehicle spawn from Request
-- Equipment (regression confirmed by FullGas; scenario_fq_vehicle_whole_transport F-Q-5).
local loadableList = nil
if caps.canTransportWholeVehicle then
loadableList = (playerObj.coalition == coalition.side.RED)
and caps.loadableVehiclesRED
or caps.loadableVehiclesBLUE
end

-- Shared spawn callback: handles both single crate (arg.unit) and set (arg.multiple).
local function spawnFn(arg)
local t = Unit.getByName(arg.unitName)
Expand Down Expand Up @@ -13997,9 +14014,13 @@ function CTLDCrateManager:refreshRequestEquipmentSection(playerObj)
local sc = entry.singleCrate
local sideOk = (sc.side == nil) or (sc.side == playerObj.coalition)
if sideOk and (not _crateIsJTAC(sc) or jtacOk) then
-- Request Equipment always spawns crates, never a whole vehicle.
-- Feature Q whole-vehicle spawn is handled by the dedicated "Load Vehicle" menu.
-- Feature Q: detect if this item should spawn a whole vehicle
local spawnAsVehicle = false
if loadableList then
for _, ltype in ipairs(loadableList) do
if ltype == sc.unit then spawnAsVehicle = true; break end
end
end
crateOrder = crateOrder + 1
menu:addCommand({ root, spawnSub, lgzName, category }, sc.desc,
spawnFn,
Expand Down
Loading