diff --git a/.backlog/FIX-FARP-PACK-FALSE-SUCCESS/PRD.md b/.backlog/FIX-FARP-PACK-FALSE-SUCCESS/PRD.md new file mode 100644 index 00000000..67aeddec --- /dev/null +++ b/.backlog/FIX-FARP-PACK-FALSE-SUCCESS/PRD.md @@ -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 * 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. diff --git a/.backlog/FIX-FARP-PACK-FALSE-SUCCESS/tickets/01-report-the-crates-created.md b/.backlog/FIX-FARP-PACK-FALSE-SUCCESS/tickets/01-report-the-crates-created.md new file mode 100644 index 00000000..96118874 --- /dev/null +++ b/.backlog/FIX-FARP-PACK-FALSE-SUCCESS/tickets/01-report-the-crates-created.md @@ -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 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. diff --git a/.backlog/README.md b/.backlog/README.md index 943b9504..d779dfbc 100644 --- a/.backlog/README.md +++ b/.backlog/README.md @@ -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). | — | diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d5f1d4..f7e5f8cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 " unpacked successfully!" whenever the creation did not answer "refused", so a JTAC unpack blocked diff --git a/src/CTLD_crate.lua b/src/CTLD_crate.lua index 969f90f4..84a589e3 100644 --- a/src/CTLD_crate.lua +++ b/src/CTLD_crate.lua @@ -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 }) diff --git a/src/CTLD_i18n_en.lua b/src/CTLD_i18n_en.lua index 77a8942d..0e1484eb 100644 --- a/src/CTLD_i18n_en.lua +++ b/src/CTLD_i18n_en.lua @@ -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" @@ -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." diff --git a/src/CTLD_i18n_es.lua b/src/CTLD_i18n_es.lua index 66f6b989..98db9906 100644 --- a/src/CTLD_i18n_es.lua +++ b/src/CTLD_i18n_es.lua @@ -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" @@ -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." diff --git a/src/CTLD_i18n_fr.lua b/src/CTLD_i18n_fr.lua index a25c8f0a..c06103c3 100644 --- a/src/CTLD_i18n_fr.lua +++ b/src/CTLD_i18n_fr.lua @@ -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" @@ -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." diff --git a/src/CTLD_i18n_ko.lua b/src/CTLD_i18n_ko.lua index 4f5cd945..7c881a5b 100644 --- a/src/CTLD_i18n_ko.lua +++ b/src/CTLD_i18n_ko.lua @@ -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"] = "표준 그룹" @@ -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."] = "포장 실패: 상자를 생성할 수 없습니다." diff --git a/tests/ci/functional/crate_scene_pack_placement_spec.lua b/tests/ci/functional/crate_scene_pack_placement_spec.lua index a0a30a28..f4b6b62c 100644 --- a/tests/ci/functional/crate_scene_pack_placement_spec.lua +++ b/tests/ci/functional/crate_scene_pack_placement_spec.lua @@ -28,9 +28,9 @@ describe("Pack 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 @@ -58,6 +58,11 @@ describe("Pack 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 @@ -88,7 +93,7 @@ describe("Pack 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 @@ -108,6 +113,7 @@ describe("Pack 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 @@ -120,6 +126,7 @@ describe("Pack 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) @@ -166,4 +173,42 @@ describe("Pack 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)