Skip to content

fix(fob): reconnect troopPickupAtFOB to the F10 troop pickup path - #136

Merged
FullGas1 merged 6 commits into
developfrom
fix/fob-troop-pickup
Aug 26, 2026
Merged

fix(fob): reconnect troopPickupAtFOB to the F10 troop pickup path#136
FullGas1 merged 6 commits into
developfrom
fix/fob-troop-pickup

Conversation

@FullGas1

@FullGas1 FullGas1 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • troopPickupAtFOB (default true, schema: "Allow troop pickup at built FOBs") has had no in-game effect since the v2 rewrite: it sets a per-FOB flag read only by CTLDFOBManager:isInFOBTroopZone, which has zero callers anywhere else in src/ — the F10 "Load Troops" menu only consults CTLDZoneManager's TRZ_ zones, never FOBs. Legacy (migration/source/CTLD.lua:9676-9677, :10750-10761) fully wires the equivalent check. Undeclared legacy-parity regression, invisible to every test level (grepped — none exercised the behavior).
  • Fix mirrors the existing registerFOBAsLogistic/unregisterLogistic pattern: new CTLDZoneManager:registerFOBAsTroopZone/unregisterTroopZone, wired into CTLDFOBManager:_registerDeployedFOB (same troopPickupAtFOB guard, existing fob._troopPickup flag left untouched) and _destroyFOB (no ghost zone). Reuses the existing fobTroopPickupRadius setting — no new config.
  • Grilled with the user 2026-08-26 (.backlog/FIX-FOB-TROOP-PICKUP/PRD.md); a related generic "zone ↔ owner link" idea was deliberately deferred to dev/roadmap.md rather than bundled here.

Test plan

  • New busted coverage extending tests/ci/unit/deploy_managers_spec.lua's existing CTLDFOBManager deploy + destroy seam (TDD: written red against the missing methods, confirmed green after the fix): zone registered when troopPickupAtFOB=true, none when false, removed on FOB destruction (no ghost zone), isInFOBTroopZone behavior unchanged.
  • Full suite: busted --pattern=_spec --helper=tests/ci/helpers/init.lua tests/ci → 1328 passed / 0 failed / 1 pending (pre-existing, DCS-live gated).
  • luac -p CTLD.lua — Lua 5.1 syntax OK (rebuilt via merge_CTLD.ps1).
  • luacheck not installed locally (per CLAUDE.md, relies on CI).

Summary by Sourcery

Reconnect built FOB troop pickup to the standard F10 troop-zone path while preserving existing FOB APIs and preventing stale or conflicting zones.

Bug Fixes:

  • Restore troop pickup at deployed FOBs when troopPickupAtFOB is enabled, including cleanup when FOBs are destroyed.
  • Prevent FOB zone registrations from silently overwriting same-named logistic or troop zones and preserve readable FOB names in the F10 menu.

Enhancements:

  • Reuse the existing troop-zone system and FOB pickup radius for consistent menu and pickup behavior.

Documentation:

  • Document the new FOB troop-zone registration and removal APIs in English and French developer documentation.

Tests:

  • Add coverage for FOB troop-zone registration, configuration gating, destruction cleanup, collision handling, and F10 menu labels.

Chores:

  • Record the restored FOB troop-pickup behavior in the changelog and backlog.

troopPickupAtFOB (default true) sets a per-FOB flag read only by
isInFOBTroopZone, which has zero callers in src/ - the F10 troop
pickup path only consults CTLDZoneManager's TRZ_ zones, never FOBs.
Undeclared legacy-parity regression, invisible to every test level.
Grilled with the user 2026-08-26: fix mirrors the existing
registerFOBAsLogistic/unregisterLogistic pattern for troop zones.
Single AFK ticket: registerFOBAsTroopZone/unregisterTroopZone,
wiring into _registerDeployedFOB/_destroyFOB, extended
deploy_managers_spec.lua tests, CHANGELOG entry, developer docs.
Granularity confirmed with the user - one cohesive slice, no
artificial docs-only split.
troopPickupAtFOB (default true) set a per-FOB flag read only by
isInFOBTroopZone, which had zero callers in src/ - the F10 "Load
Troops" menu only consults CTLDZoneManager's TRZ_ zones, never FOBs.
Undeclared legacy-parity regression, invisible to every test level
(migration/source/CTLD.lua:9676-9677, :10750-10761 fully wired the
equivalent check via ctld.builtFOBS + ctld.inPickupZone()).

Mirrors the existing registerFOBAsLogistic/unregisterLogistic pattern:
CTLDZoneManager:registerFOBAsTroopZone/unregisterTroopZone synthesize
and remove an unlimited-stock CTLDTroopZone for the FOB, wired into
CTLDFOBManager:_registerDeployedFOB (gated by troopPickupAtFOB, same
guard as the existing dead flag, which is left untouched for
isInFOBTroopZone's own callers) and _destroyFOB (no ghost zone).
Reuses the existing fobTroopPickupRadius setting - no new config.

FIX-FOB-TROOP-PICKUP, grilled with the user 2026-08-26.
@FullGas1
FullGas1 requested a review from davidp57 as a code owner August 26, 2026 10:54
@sourcery-ai

sourcery-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

Restores the default troopPickupAtFOB setting by representing deployed FOBs as standard unlimited-stock troop pickup zones, registering and cleaning them up through the existing FOB lifecycle, with public-path regression tests and updated documentation.

Sequence diagram for FOB troop pickup zone lifecycle

sequenceDiagram
    participant FOB as CTLDFOBManager
    participant Zones as CTLDZoneManager
    participant F10 as F10 Load Troops

    FOB->>FOB: _registerDeployedFOB(scene)
    alt troopPickupAtFOB is enabled
        FOB->>Zones: registerFOBAsTroopZone(fobName, centroid, fobTroopPickupRadius, coalitionId)
        Zones->>Zones: CTLDTroopZone:new(...)
    end
    F10->>Zones: getTroopZoneAtPoint(point, coalition)
    Zones-->>F10: troop zone or nil
    FOB->>FOB: _destroyFOB(fob, killerUnit, killerCoalition, integrityPercent)
    FOB->>Zones: unregisterTroopZone(fob.name)
    Zones-->>FOB: zone removed or no-op
Loading

File-Level Changes

Change Details Files
Reconnect built FOBs to the standard troop-zone consumer path.
  • Add unlimited-stock FOB troop-zone registration and safe unregistration to the zone manager.
  • Register the zone during FOB deployment only when troopPickupAtFOB is enabled, using the existing pickup radius and coalition.
  • Remove the troop zone during FOB destruction while preserving the existing fob._troopPickup and isInFOBTroopZone behavior.
src/CTLD_zone.lua
src/CTLD_fob.lua
Add regression coverage for FOB troop pickup behavior.
  • Verify registration and pickup capability when enabled.
  • Verify no zone is created when disabled and no ghost zone remains after destruction.
  • Guard the unchanged isInFOBTroopZone public API behavior.
tests/ci/unit/deploy_managers_spec.lua
Document and announce the restored behavior and new zone-manager API.
  • Document the registration methods in English and French developer references.
  • Add an unreleased changelog entry describing legacy-parity restoration and configuration reuse.
  • Record the completed fix and implementation rationale in backlog artifacts.
CHANGELOG.md
docs/developer/api-reference.md
docs/developer/api-reference.fr.md
docs/developer/subsystems/zones.md
docs/developer/subsystems/zones.fr.md
.backlog/FIX-FOB-TROOP-PICKUP/PRD.md
.backlog/FIX-FOB-TROOP-PICKUP/tickets/01-register-fob-troop-zone.md
.backlog/README.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

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/CTLD_zone.lua" line_range="1205-1214" />
<code_context>
+-- @param point     vec3
+-- @param radius    number  (default 150)
+-- @param coalitionId number
+function CTLDZoneManager:registerFOBAsTroopZone(fobName, point, radius, coalitionId)
+    local zone = CTLDTroopZone:new({
+        zoneName     = fobName,
+        coalition    = coalitionId or 0,
+        center       = point,
+        radius       = radius or 150,
+        pickMaxStock = 0,   -- unlimited
+        active       = true,
+    })
+    self._troopZones[fobName] = zone
+    ctld.utils.log("INFO", "CTLDZoneManager: FOB troop zone '%s' r=%dm", fobName, radius or 150)
+end
</code_context>
<issue_to_address>
**issue (broader_impact):** The FOB name is used as the key in `_troopZones` without checking whether a mission-defined or script-created troop zone already has the same name. Registering such an FOB overwrites the existing zone, and destroying the FOB then unconditionally removes that unrelated zone as well.

**Triggers:** When a mission-defined or scripted troop zone is named `Deployed FOB #<n>` and a deployed FOB receives the same generated name.

**Suggested fix:** Reject or namespace colliding FOB troop-zone names, or preserve and restore any pre-existing zone when registering and unregistering the FOB zone.
</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_zone.lua
Comment on lines +1205 to +1214
function CTLDZoneManager:registerFOBAsTroopZone(fobName, point, radius, coalitionId)
local zone = CTLDTroopZone:new({
zoneName = fobName,
coalition = coalitionId or 0,
center = point,
radius = radius or 150,
pickMaxStock = 0, -- unlimited
active = true,
})
self._troopZones[fobName] = zone

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (broader_impact): The FOB name is used as the key in _troopZones without checking whether a mission-defined or script-created troop zone already has the same name. Registering such an FOB overwrites the existing zone, and destroying the FOB then unconditionally removes that unrelated zone as well.

Triggers: When a mission-defined or scripted troop zone is named Deployed FOB #<n> and a deployed FOB receives the same generated name.

Suggested fix: Reject or namespace colliding FOB troop-zone names, or preserve and restore any pre-existing zone when registering and unregistering the FOB zone.

Review findings (code-review, 8-angle pass on PR #136):
- Reuse ctldTestSettings.borrow/:restore instead of a hand-rolled
  ctld.gs monkey-patch (tests/ci/helpers/settings.lua already exists
  for this, added by FIX-SPEC-ISOLATION).
- Share one FOB_CENTROID constant between scene() and the new
  assertions instead of a second, independently-hardcoded literal.
- Reset CTLDZoneManager.getInstance()._troopZones between the two
  halves of the isInFOBTroopZone regression test, matching the
  existing FOB-state reset - no stale zone left in the singleton
  after the file's tests run.
Two findings from the 8-angle review of PR #136, fixed here at the
user's request rather than deferred:

- registerFOBAsLogistic/registerFOBAsTroopZone now refuse (WARN log)
  to overwrite an existing zone sharing the FOB's name, matching the
  guard createExtractZone/createTroopZoneAtObject already have. Both
  gained a boolean return value for this. Previously a Mission-Editor
  zone with the same literal name as an auto-generated FOB could be
  silently clobbered with no log line.

- CTLDTroopZone gained an optional displayName field (F10 label
  override, defaults to "TRZ_"..zoneName when absent, so every
  existing TRZ_/createTroopZoneAtObject/createExtractZone zone is
  unaffected). registerFOBAsTroopZone sets it to fobName, so the F10
  "Load from ..." entry for a FOB shows its own name instead of a
  fabricated "TRZ_Deployed FOB #N".

New tests: two collision-guard cases in deploy_managers_spec.lua, two
menu-label cases in menu_gating_spec.lua (reusing its existing
getTroopZonesForCoalition-stub seam).
@FullGas1
FullGas1 merged commit b018c84 into develop Aug 26, 2026
9 checks passed
@FullGas1
FullGas1 deleted the fix/fob-troop-pickup branch August 26, 2026 11:52
FullGas1 added a commit that referenced this pull request Aug 26, 2026
8-angle review of PR #137, findings applied:

- CTLDSceneManager:packScene destroyed a FARP's objects with no
  cleanup of any troop zone/watcher registered for it, relying on an
  unverified DCS behavior (does destroying a packed static also flip
  a separately-resolved Airbase handle's isExist()?) that this suite
  cannot check. Only Countryside FARP supports packing; its onRepack
  (already called by packScene before destruction) now calls
  unregisterTroopZone/CTLDStaticWatcher:unwatch explicitly first.
- CTLDStaticWatcher:watch now logs a WARN when it silently overwrites
  a still-live entry for the same id - a pre-existing gap in the
  shared cross-feature registry, cheap and consistent with the
  collision guard registerFOBAsTroopZone already has. New
  static_watcher_spec.lua covers watch/unwatch/tick and the WARN.
- registerFARPTroopPickupFromScene: documented why it lives on
  CTLDZoneManager rather than a dedicated CTLDFARPManager (no such
  entity exists or is needed); removed a redundant ab:getName() call;
  added a WARN on its two previously-silent failure branches.
- Tests: replaced a hand-rolled ctld.gs monkey-patch with
  ctldTestSettings.borrow/:restore (the same fix already applied on
  PR #136, missed again here); shared fakeHelipad/fakeAirbase/ctxFor
  across describe blocks instead of duplicating; trimmed unused
  unit/_params fields from the ctx fixture; fixed a miscounted step
  total in a test description string.

FEAT-FARP-TROOP-PICKUP, PRD post-review addendum.
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