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
54 changes: 54 additions & 0 deletions .backlog/FIX-FARP-PACK-FALSE-SUCCESS/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# FIX-FARP-PACK-FALSE-SUCCESS — packing a FARP announces "packed successfully" without checking that a crate was created

**Status:** ✅ done (PR #268)

GitHub issue #263 (automated review of `develop` at `673b064`), decided with the maintainer on 2026-10-20 (message only; the order of the
operations is unchanged).

## Problem Statement

The *Pack <scene>* command of a deployed FARP destroys every object of the scene first (`CTLDSceneManager:packScene`), then creates the
crates. The creation can produce nothing or only part of the crates (DCS refuses an object, no position was computed). The first return value
of the crate creation, which is exactly the number of crates created, is thrown away: the pilot always reads "FARP packed successfully!".
A FARP can therefore disappear with no crate at all, and the warehouse share that should have travelled in the missing crates is lost, with
no trace except a server-side log line.

## Solution

The command reads the number of crates created. None: the pilot is told the pack failed, and an ERROR is logged. Fewer than expected: the
pilot is told how many were created, and a WARNING is logged. All of them: the message does not change. The creation is still attempted
after the scene is destroyed, as today.

## User Stories

1. As a pilot, I want to be told when packing a FARP produced no crate, so that I know the FARP is lost and why nothing is on the ground.
2. As a pilot, I want to be told how many crates were created when only some were, so that I know the pack is incomplete.
3. As a pilot, I want the "packed successfully" message when every crate was created, unchanged.
4. As a CTLD developer, I want a failed or partial pack logged (ERROR / WARNING), so that the failure leaves a trace.
5. As a CTLD developer, I want the same wording style as Request Equipment, so that failures read alike.
6. As a maintainer, I want the question of creating the crates before destroying the scene left to a separate decision.

## Implementation Decisions

- `refreshPackEquiptSection`'s Pack callback keeps the first return value of the crate creation and announces by its value: 0 → failure
message + ERROR log; below the expected number → partial message (`%1` created, `%2` expected) + WARNING log; otherwise the existing message.
- Two new messages, through the translation function, translated FR/ES/KO (proposal for the maintainer to review): "Pack failed: the crates
could not be created." and "FARP packed: %1 of %2 crates created.".
- Unchanged: the order (the scene is destroyed, then the crates are created), the warehouse snapshot carried by the crates that exist, the
menu refresh after the pack, the scene manager, the catalogue.

## Testing Decisions

- A good test observes what the pilot reads and what is logged, not how the command is organised.
- The existing pack spec (`crate_scene_pack_placement_spec`, prior art: the menu builder and the real command with a double of the crate
creation): new cases with a creation that makes none, only some, or all of the crates (control).

## Out of Scope

- Creating the crates before destroying the scene (a safer order, but it changes the order of a destructive action; own decision and ADR if
wanted).
- Giving the FARP back when the creation fails.

## Further Notes

None.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 01 — Report how many crates the FARP pack created

**Status:** ✅ done (PR #268) · **Type:** AFK

## Parent

[PRD — FIX-FARP-PACK-FALSE-SUCCESS](../PRD.md). Stories 1-6. GitHub issue #263.

## What to build

In the Pack <scene> command, read the number of crates created and announce it: failure (none), partial (some), or the existing message (all),
with an ERROR / WARNING log. Add the two messages and translate FR, ES, KO. Tests first in the pack spec. Then CHANGELOG, backlog index, PR.

## Acceptance criteria

- [ ] New cases fail before the change and pass after it; the existing pack cases pass unchanged.
- [ ] No crate created: failure message and ERROR log, no "packed successfully".
- [ ] Some crates created: the partial message with the counts and a WARNING log.
- [ ] Dictionaries regenerated by `merge_CTLD.ps1`, FR/ES/KO translated; luacheck clean; `busted` green; CHANGELOG entry.

## Blocked by

None - can start immediately.
1 change: 1 addition & 0 deletions .backlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ authored **per lot, when the lot is started** (not in batch).

| Lot | Status | Description | Branch |
|-----|--------|-------------|--------|
| [`FIX-FARP-PACK-FALSE-SUCCESS`](FIX-FARP-PACK-FALSE-SUCCESS/PRD.md) | merged (PR #268) — 1 ticket ([report the crates created](FIX-FARP-PACK-FALSE-SUCCESS/tickets/01-report-the-crates-created.md)) | Packing a FARP now reports a pack that created no crate, or only some, instead of always announcing a success. |
| [`FIX-UNPACK-FALSE-SUCCESS`](FIX-UNPACK-FALSE-SUCCESS/PRD.md) | merged (PR #267) — 1 ticket ([announce success only on true](FIX-UNPACK-FALSE-SUCCESS/tickets/01-announce-success-only-on-true.md)) | The Unpack Crate click no longer announces a success when a JTAC limit or a missing spawn position produced nothing. |
| [`FEAT-JTAC-LOG-REAL-LASER-CODE`](FEAT-JTAC-LOG-REAL-LASER-CODE/PRD.md) | ready — 1 ticket ([01 log the code, then measure the LSS](FEAT-JTAC-LOG-REAL-LASER-CODE/tickets/01-log-the-code-then-measure-the-lss.md)) | Reported by VMCT from the Sinai Open Training v6 (2026-10-12): Reaper 1 (imposed 1511) claimed nine targets in an hour, yet an A-10C's TGP LSS on 1511 at 15 nm found no spot. The log holds only claim and release, never the code or the spot, so the report cannot be settled. Log both, then test the hypotheses (code, LSS range, line of sight, shared code). | — |
| [`FIX-MENU-STABLE-ENTRIES`](FIX-MENU-STABLE-ENTRIES/PRD.md) | merged (PR #261) — 04 waiting-human (live DCS check, Zip) — D1-D4 confirmed as recommended (2026-10-09) — 4 tickets (01 ✅ [measure DCS id reuse](FIX-MENU-STABLE-ENTRIES/tickets/01-measure-dcs-id-reuse.md), 02 ✅ [render by difference](FIX-MENU-STABLE-ENTRIES/tickets/02-render-by-difference.md), 03 ✅ [drop the ambient delay + ADR 0027](FIX-MENU-STABLE-ENTRIES/tickets/03-drop-ambient-delay-and-adr.md), 04 [live check](FIX-MENU-STABLE-ENTRIES/tickets/04-live-check.md)) | Issue #257: wrong F10 commands persist after ADR 0015. DCS reuses a removed entry's id for the next entry created, so the full wipe + rebuild of each refresh (immediate or 4 s later) repoints every entry of a screen left open. Render by difference so an unchanged entry keeps its id, and park each freed id on an inert command for a group no player holds (measured 2026-10-09, shipped in VMCT #1113). | — |
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Versioning follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed — Packing a FARP reports how many crates were created (FIX-FARP-PACK-FALSE-SUCCESS)

- Packing a FARP destroys the scene first, then creates its crates, and always announced "FARP packed successfully!" whatever the creation
produced: a FARP could disappear with no crate (DCS refused the objects) and the warehouse share of the missing crates was lost without a
word. The pack now reads how many crates were created: none gives "Pack failed: the crates could not be created." and an ERROR in the log,
only some gives "FARP packed: N of M crates created." and a WARNING, all of them keeps the existing message. The order of the operations
is unchanged (issue #263). FR/ES/KO texts are a proposal for review.

### Fixed — Unpack Crate no longer announces a success when nothing was created (FIX-UNPACK-FALSE-SUCCESS)

- The Unpack Crate click announced "<equipment> unpacked successfully!" whenever the creation did not answer "refused", so a JTAC unpack blocked
Expand Down
16 changes: 14 additions & 2 deletions src/CTLD_crate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1018,14 +1018,26 @@ function CTLDCrateManager:refreshPackEquiptSection(playerObj, overrideInAir, noR
-- crates of the FARP just packed.
local descriptors = {}
for i = 1, (cd.cratesRequired or 1) do descriptors[i] = desc end
local _, _, crates = mgr_c:spawnCratesAligned(descriptors, t, t:getCoalition(), t:getName(),
local createdCount, _, crates = mgr_c:spawnCratesAligned(descriptors, t, t:getCoalition(), t:getName(),
CTLDCrate.SPAWN_METHOD.CRATE_SPAWN)
if repackData and repackData.warehouseSnapshot then
for _, crate in ipairs(crates) do
crate.metadata.warehouseSnapshot = repackData.warehouseSnapshot
end
end
trigger.action.outTextForGroup(gid, ctld.tr("FARP packed successfully!"), 10)
-- The scene is already destroyed: a pack that produced nothing, or only part of its crates, must say so.
if createdCount == 0 then
ctld.utils.log("ERROR", "Pack %s: scene '%s' packed but no crate was created (unit=%s)",
tostring(sc._modelName), tostring(arg.sceneName), tostring(arg.unitName))
trigger.action.outTextForGroup(gid, ctld.tr("Pack failed: the crates could not be created."), 10)
elseif createdCount < #descriptors then
ctld.utils.log("WARNING", "Pack %s: only %d of %d crates created for scene '%s' (unit=%s)",
tostring(sc._modelName), createdCount, #descriptors, tostring(arg.sceneName), tostring(arg.unitName))
trigger.action.outTextForGroup(gid,
ctld.tr("FARP packed: %1 of %2 crates created.", createdCount, #descriptors), 10)
else
trigger.action.outTextForGroup(gid, ctld.tr("FARP packed successfully!"), 10)
end
mgr_c:refreshUnpackSectionForUnit(arg.unitName)
end,
{ unitName = playerObj.unitName, sceneName = scene._name })
Expand Down
6 changes: 5 additions & 1 deletion src/CTLD_i18n_en.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if not ctld then ctld = {} end
if not ctld.i18n then ctld.i18n = {} end

ctld.i18n["en"] = {}
ctld.i18n["en"].translation_version = "1.26"
ctld.i18n["en"].translation_version = "1.27"

--- groups names
ctld.i18n["en"]["Standard Group"] = "Standard Group"
Expand Down Expand Up @@ -593,3 +593,7 @@ ctld.i18n["en"]["The cargo bay doors are closed: open them, then use Fit parachu

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-09
ctld.i18n["en"]["Unpack failed: the equipment could not be created."] = "Unpack failed: the equipment could not be created."

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20
ctld.i18n["en"]["FARP packed: %1 of %2 crates created."] = "FARP packed: %1 of %2 crates created."
ctld.i18n["en"]["Pack failed: the crates could not be created."] = "Pack failed: the crates could not be created."
6 changes: 5 additions & 1 deletion src/CTLD_i18n_es.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if not ctld then ctld = {} end
if not ctld.i18n then ctld.i18n = {} end

ctld.i18n["es"] = {}
ctld.i18n["es"].translation_version = "1.26"
ctld.i18n["es"].translation_version = "1.27"

--- groups names
ctld.i18n["es"]["Standard Group"] = "Grupo estándar"
Expand Down Expand Up @@ -600,3 +600,7 @@ ctld.i18n["es"]["The cargo bay doors are closed: open them, then use Fit parachu

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-09
ctld.i18n["es"]["Unpack failed: the equipment could not be created."] = "Fallo al desempaquetar: no se pudo crear el equipo."

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20
ctld.i18n["es"]["FARP packed: %1 of %2 crates created."] = "FARP reempacado: %1 de %2 cajas creadas."
ctld.i18n["es"]["Pack failed: the crates could not be created."] = "Error al reempacar: no se pudieron crear las cajas."
6 changes: 5 additions & 1 deletion src/CTLD_i18n_fr.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ if not ctld then ctld = {} end
if not ctld.i18n then ctld.i18n = {} end

ctld.i18n["fr"] = {}
ctld.i18n["fr"].translation_version = "1.26"
ctld.i18n["fr"].translation_version = "1.27"

--- groups names
ctld.i18n["fr"]["Standard Group"] = "Groupe standard"
Expand Down Expand Up @@ -625,3 +625,7 @@ ctld.i18n["fr"]["The cargo bay doors are closed: open them, then use Fit parachu

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-09
ctld.i18n["fr"]["Unpack failed: the equipment could not be created."] = "Échec du déballage : l'équipement n'a pas pu être créé."

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20
ctld.i18n["fr"]["FARP packed: %1 of %2 crates created."] = "FARP remballé : %1 caisse(s) sur %2 créée(s)."
ctld.i18n["fr"]["Pack failed: the crates could not be created."] = "Remballage impossible : les caisses n'ont pas pu être créées."
6 changes: 5 additions & 1 deletion src/CTLD_i18n_ko.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if not ctld then ctld = {} end
if not ctld.i18n then ctld.i18n = {} end

ctld.i18n["ko"] = {}
ctld.i18n["ko"].translation_version = "1.26"
ctld.i18n["ko"].translation_version = "1.27"

--- groups names
ctld.i18n["ko"]["Standard Group"] = "표준 그룹"
Expand Down Expand Up @@ -479,3 +479,7 @@ ctld.i18n["ko"]["The cargo bay doors are closed: open them, then use Fit parachu

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-09
ctld.i18n["ko"]["Unpack failed: the equipment could not be created."] = "풀기 실패: 장비를 생성할 수 없습니다."

--- Keys added by generate_i18n_dicts.ps1 on 2026-10-20
ctld.i18n["ko"]["FARP packed: %1 of %2 crates created."] = "FARP 포장됨: 상자 %2개 중 %1개 생성됨."
ctld.i18n["ko"]["Pack failed: the crates could not be created."] = "포장 실패: 상자를 생성할 수 없습니다."
51 changes: 48 additions & 3 deletions tests/ci/functional/crate_scene_pack_placement_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ describe("Pack <scene> places its crates by the common rule", function()
local CX, CZ = 1000, 2000 -- aircraft position (heading 0: nose to +x, right side to +z)
local UH1H_BOX = { min = { x = -8.86, y = -1.63, z = -1.59 }, max = { x = 3.95, y = 1.59, z = 1.56 } }

local cm, sm, transport, created, avoid, avoidSeen, broadcasts, caps
local cm, sm, transport, created, avoid, avoidSeen, broadcasts, caps, messages, logs, createLimit
local origGs, origGetByName, origInAir, origRandom, origGroupId, origOutTextGroup, origOutText
local origPositions, origActive, origModels
local origPositions, origActive, origModels, origLog

local function lateral(p) return p.z - CZ end -- + = right of the aircraft
local function ahead(p) return p.x - CX end -- + = in front of the nose
Expand Down Expand Up @@ -58,6 +58,11 @@ describe("Pack <scene> places its crates by the common rule", function()

caps = { cratesEnabled = true, crateSpawnSector = "side", crateSpawnDistance = 3.0 }
created, avoid, avoidSeen, broadcasts = {}, {}, nil, {}
messages, logs, createLimit = {}, {}, math.huge
origLog = ctld.utils.log
ctld.utils.log = function(level, fmt, ...)
logs[#logs + 1] = { level = level, text = select("#", ...) > 0 and string.format(fmt, ...) or fmt }
end

ctld.gs = function(k)
if k == "capabilitiesByType" then return { ["UH-1H"] = caps } end
Expand Down Expand Up @@ -88,7 +93,7 @@ describe("Pack <scene> places its crates by the common rule", function()
ctld.utils.inAir = function() return false end
ctld.utils.getGroupId = function() return 9904 end
ctld.utils.RandomReal = function() return 0.1 end -- the right side, whatever the range
trigger.action.outTextForGroup = function() end
trigger.action.outTextForGroup = function(_, text) messages[#messages + 1] = text end
trigger.action.outText = function(text) broadcasts[#broadcasts + 1] = text end
ctld.utils.getSpawnObjectPositions = function(unit, n, dist, spacing, axis, avoidList)
avoidSeen = avoidList
Expand All @@ -108,6 +113,7 @@ describe("Pack <scene> places its crates by the common rule", function()
cm._getDynamicBBoxes = function() return avoid end
cm.findDescriptorByUnitType = function() return { desc = "FARP crate", unit = MODEL, weight = 1000 } end
cm.spawnCrate = function(_, _, pos)
if #created >= createLimit then return nil end
local crate = { position = pos, metadata = {} }
created[#created + 1] = crate
return crate
Expand All @@ -120,6 +126,7 @@ describe("Pack <scene> places its crates by the common rule", function()
ctld.utils.RandomReal, ctld.utils.getGroupId = origRandom, origGroupId
trigger.action.outTextForGroup, trigger.action.outText = origOutTextGroup, origOutText
ctld.utils.getSpawnObjectPositions = origPositions
ctld.utils.log = origLog
sm._active, sm._models = origActive, origModels
resetAll()
end)
Expand Down Expand Up @@ -166,4 +173,42 @@ describe("Pack <scene> places its crates by the common rule", function()

end)

describe("reports how many crates the pack produced", function()

local function said(text)
for _, m in ipairs(messages) do if m == text then return true end end
return false
end

local function logged(level, pattern)
for _, l in ipairs(logs) do
if l.level == level and l.text:find(pattern, 1, true) then return true end
end
return false
end

it("announces the success when every crate was created (control)", function()
pack()
assert.is_true(said("FARP packed successfully!"))
assert.is_false(logged("ERROR", "packed but no crate"))
end)

it("tells the pilot and logs an ERROR when no crate was created", function()
createLimit = 0
pack()
assert.is_true(said("Pack failed: the crates could not be created."))
assert.is_false(said("FARP packed successfully!"))
assert.is_true(logged("ERROR", "packed but no crate"))
end)

it("tells the pilot how many crates were created and logs a WARNING when only some were", function()
createLimit = 2
pack()
assert.is_true(said("FARP packed: 2 of 3 crates created."))
assert.is_false(said("FARP packed successfully!"))
assert.is_true(logged("WARNING", "only 2 of 3 crates"))
end)

end)

end)
Loading