Skip to content

feat(farp): register a troop pickup zone when a FARP scene completes - #137

Merged
FullGas1 merged 5 commits into
developfrom
feature/farp-troop-pickup
Aug 26, 2026
Merged

feat(farp): register a troop pickup zone when a FARP scene completes#137
FullGas1 merged 5 commits into
developfrom
feature/farp-troop-pickup

Conversation

@FullGas1

@FullGas1 FullGas1 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Follows FIX-FOB-TROOP-PICKUP (PR fix(fob): reconnect troopPickupAtFOB to the F10 troop pickup path #136): a built FARP had no troop pickup capability at all, unlike a FOB. New troopPickupAtFARP (default true) and farpTroopPickupRadius (default 150, independent of the FOB's own radius) settings.
  • All three built-in FARP scenes (farpScene, FARP Alpha, Countryside FARP) gain a final step that resolves the built Airbase and registers a troop pickup zone via the existing registerFOBAsTroopZone/unregisterTroopZone (reused as-is — collision guard and F10 displayName inherited for free).
  • Destruction detection needs no bespoke manager: a FARP registers as a real DCS Airbase, so its death is binary and native (Airbase:isExist()). Reuses CTLDStaticWatcher, already proven for this exact object class by CTLDReconManager's FARP-detection code, instead of a new CTLDFARPManager or the deferred generic zone-link system (dev/roadmap.md).
  • New shared CTLDZoneManager:registerFARPTroopPickupFromScene(ctx) avoids tripling the same logic across the three scene files.
  • Grilled with the user 2026-08-26 (.backlog/FEAT-FARP-TROOP-PICKUP/PRD.md).

Test plan

  • New busted coverage: structure assertions extending tests/ci/unit/scenes_minefields_spec.lua for all three scenes (new describe block for Countryside FARP, previously untested), plus behavior tests driving each scene's final step.func(ctx) directly and asserting through the public CTLDZoneManager:getTroopZoneAtPoint path (zone registered when troopPickupAtFARP=true, none when false, removed after CTLDStaticWatcher:_tick once the stubbed Airbase:isExist() flips false).
  • Full suite: busted --pattern=_spec --helper=tests/ci/helpers/init.lua tests/ci → 1347 passed / 0 failed / 1 pending (pre-existing, DCS-live gated).
  • tests/ci/data/config_defaults.json regenerated via ctld-tools gen (oracle drift guard) after the two new settings.
  • 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

Add automatic troop pickup support to built FARPs with configurable behavior and lifecycle cleanup.

New Features:

  • Enable built FARPs to provide troop pickup zones across all three built-in FARP scenes, controlled by a new setting and independently configurable radius.

Bug Fixes:

  • Automatically remove FARP troop pickup zones when the underlying airbase is destroyed or a Countryside FARP is repacked.

Enhancements:

  • Reuse the existing troop-zone registration and static-watcher lifecycle mechanisms for FARP support, with shared scene registration logic and watcher collision warnings.

Documentation:

  • Document FARP troop pickup configuration, scene behavior, lifecycle, and related API changes in English and French.

Tests:

  • Add structure and behavior coverage for troop pickup registration, destruction cleanup, repack cleanup, and static-watcher behavior across all FARP scenes.

Chores:

  • Record the completed feature in the backlog and update the roadmap.

A built FARP has no troop-pickup capability at all, unlike a FOB
(fixed by FIX-FOB-TROOP-PICKUP, PR #136). Grilled with the user
2026-08-26: reuses registerFOBAsTroopZone/unregisterTroopZone as-is,
detects FARP destruction via Airbase.getByName + CTLDStaticWatcher
(both already proven for this object class by the recon FARP
detection code) instead of a new bespoke manager or the deferred
generic zone-link system.

Also marks the originating roadmap entry ("TRZ_ automatique...") as
formalized for both its FOB and FARP halves.
Single AFK ticket: troopPickupAtFARP + farpTroopPickupRadius
settings, final registration step on all 3 FARP scenes (reusing
registerFOBAsTroopZone/unregisterTroopZone and CTLDStaticWatcher
as-is), tests, CHANGELOG, dev+MM docs. Granularity confirmed with
the user - one cohesive slice, same reasoning as FIX-FOB-TROOP-PICKUP.
Follows FIX-FOB-TROOP-PICKUP (PR #136): a built FARP had no troop
pickup capability at all, unlike a FOB. New troopPickupAtFARP
(default true) and farpTroopPickupRadius (150) settings, plus a
final step on all three FARP scenes (farpScene, FARP Alpha,
Countryside FARP) that resolves the built Airbase and registers a
troop zone via the existing registerFOBAsTroopZone/unregisterTroopZone
(reused as-is, including their collision guard and F10 displayName).

Destruction detection needs no bespoke manager: a FARP is a real DCS
Airbase, so its death is binary and native (Airbase:isExist()).
Reuses CTLDStaticWatcher, already proven for this exact object class
by CTLDReconManager's FARP-detection code, instead of building a
CTLDFARPManager or the deferred generic zone-link system.

New shared CTLDZoneManager:registerFARPTroopPickupFromScene(ctx)
avoids tripling the same ~15 lines across the three scene files.

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

sourcery-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

Built FARP scenes now automatically expose configurable troop pickup zones by resolving their spawned DCS Airbase, registering through the existing CTLD troop-zone machinery, and watching native Airbase existence for cleanup; the change includes shared lifecycle wiring, configuration and documentation updates, generated defaults, and comprehensive tests for all three scenes.

Sequence diagram for FARP troop pickup registration and cleanup

sequenceDiagram
    participant Scene as FARP Scene
    participant Manager as CTLDZoneManager
    participant DCS as DCS Airbase
    participant Watcher as CTLDStaticWatcher
    participant Zone as Troop Zone

    Scene->>Manager: registerFARPTroopPickupFromScene(ctx)
    Manager->>DCS: Airbase.getByName(name)
    DCS-->>Manager: Airbase
    alt troopPickupAtFARP enabled
        Manager->>Zone: registerFOBAsTroopZone(name, point, radius, coalitionId)
        Zone-->>Manager: registration succeeded
        Manager->>Watcher: watch(trz_farp_name, isExist, unregisterTroopZone)
        Watcher->>DCS: isExist()
        DCS-->>Watcher: false
        Watcher->>Manager: unregisterTroopZone(name)
        Manager->>Zone: remove troop zone
    end
Loading

File-Level Changes

Change Details Files
Adds configurable troop pickup support to all built-in FARP scenes.
  • Introduces default-on troopPickupAtFARP and independent farpTroopPickupRadius settings.
  • Adds a final function-only step to each FARP scene that invokes shared registration logic after construction.
  • Documents the new settings and FARP behavior in English and French configuration/scene/API documentation.
src/CTLD_config.yaml
src/CTLD_config_schema.yaml
src/scenes/CTLD_farpScene.lua
src/scenes/CTLD_farpAlphaScene.lua
src/scenes/CTLD_countrysideFarpScene.lua
tests/ci/data/config_defaults.json
docs/mission-maker/configuration.md
docs/mission-maker/configuration.fr.md
docs/mission-maker/scenes-fob.md
docs/mission-maker/scenes-fob.fr.md
docs/developer/api-reference.md
docs/developer/api-reference.fr.md
Centralizes FARP troop-zone lifecycle management by reusing the existing troop-zone and static-watcher infrastructure.
  • Resolves the scene's Heliports-category object to a DCS Airbase and registers it through registerFOBAsTroopZone, preserving collision handling and F10 naming.
  • Registers a namespaced CTLDStaticWatcher callback only after successful zone registration and removes the zone when Airbase:isExist() becomes false.
  • Leaves the existing zone-registration and watcher implementations unchanged while avoiding a dedicated FARP manager or generic owner-link system.
src/CTLD_zone.lua
Adds regression coverage for scene structure and FARP pickup behavior across all three scene variants.
  • Loads and verifies the previously untested Countryside FARP scene and checks the new final steps.
  • Tests enabled/disabled registration and public point-based lookup behavior for each scene.
  • Drives watcher cleanup after simulated Airbase destruction and verifies zones do not persist.
tests/ci/unit/scenes_minefields_spec.lua
Records the feature as shipped and updates project-facing release and roadmap information.
  • Adds an Unreleased changelog entry describing configuration, registration, and destruction behavior.
  • Adds the completed feature lot and rationale to backlog/roadmap documentation.
CHANGELOG.md
.backlog/FEAT-FARP-TROOP-PICKUP/PRD.md
.backlog/FEAT-FARP-TROOP-PICKUP/tickets/01-farp-troop-pickup-step.md
.backlog/README.md
dev/roadmap.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 3 issues

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

## Individual Comments

### Comment 1
<location path="src/CTLD_config_schema.yaml" line_range="438-442" />
<code_context>
     en: Radius (m) within which troops may board at a FOB
     fr: Rayon (m) dans lequel les troops peuvent embarquer à un FOB
+farpTroopPickupRadius:
+  group: fob
+  standard: true
+  label: {en: FARP troop boarding radius, fr: Rayon d'embarquement à une FARP}
+  unit: m
+  description:
+    en: Radius (m) within which troops may board at a FARP
+    fr: Rayon (m) dans lequel les troops peuvent embarquer à une FARP
</code_context>
<issue_to_address>
**nitpick:** `farpTroopPickupRadius` is assigned to the `fob` schema group even though the feature defines it as a FARP troop setting alongside `troopPickupAtFARP` in the `troops` group, so schema-driven configuration UIs and generated documentation place the setting under the wrong section.

**Suggested fix:** Change the schema entry's group from `fob` to `troops` and regenerate any derived configuration artifacts.
</issue_to_address>

### Comment 2
<location path="tests/ci/unit/scenes_minefields_spec.lua" line_range="276-287" />
<code_context>
+                    return model.steps[#model.steps].func
+                end
+
+                it("registers a pickup-capable troop zone when troopPickupAtFARP is true", function()
+                    local pad = fakeHelipad(sceneName .. "-1", { x = 10, y = 0, z = 20 })
+                    local ab  = fakeAirbase(pad)
+                    Airbase.getByName = function(n) return (n == pad:getName()) and ab or nil end
+
+                    lastStepFunc()(ctxFor(pad))
+
+                    local zone = CTLDZoneManager.getInstance():getTroopZoneAtPoint(
+                        { x = 10, y = 0, z = 20 }, coalition.side.BLUE)
+                    assert.is_not_nil(zone)
+                    assert.is_true(zone:hasPickup())
+                end)
+
+                it("registers no troop zone when troopPickupAtFARP is false", function()
</code_context>
<issue_to_address>
**nitpick (testing):** The successful-registration test leaves its `CTLDStaticWatcher` entry and scheduled watcher timer installed after the test; the suite resets `_troopZones` but not the shared watcher registry, so later tests run with stale closures and accumulated watcher state.

**Triggers:** When the test suite continues after any of the three successful-registration cases.

**Suggested fix:** Unwatch the generated FARP watcher or reset the shared `CTLDStaticWatcher` state in `after_each`.
</issue_to_address>

### Comment 3
<location path="CHANGELOG.md" line_range="21-22" />
<code_context>
+  scene completes, discovered through the same `CTLDZoneManager` machinery as a Mission-Editor
+  `TRZ_…` zone or a built FOB. Follows directly from `FIX-FOB-TROOP-PICKUP`: reuses
+  `registerFOBAsTroopZone`/`unregisterTroopZone` as-is.
+- Unlike a FOB, a FARP registers as a real DCS airbase, so its destruction is detected natively
+  (`Airbase:isExist()`) via the existing `CTLDStaticWatcher` — already used for this exact object
+  class by the recon FARP-detection code — rather than a new bespoke manager. The zone disappears
+  the moment DCS considers the FARP destroyed.
+
 ### Fixed — troops can now actually be picked up at a built FOB (FIX-FOB-TROOP-PICKUP)
</code_context>
<issue_to_address>
**nitpick:** The documentation says the troop zone disappears immediately when DCS considers the FARP destroyed, but the implementation only polls `Airbase:isExist()` on the installer's one-second `CTLDStaticWatcher` timer, so the zone remains available until the next poll.

**Triggers:** When a FARP is destroyed between watcher polls.

**Suggested fix:** Document the polling delay, or make the destruction path event-driven if immediate removal is a required contract.

```suggestion
  class by the recon FARP-detection code — rather than a new bespoke manager. The zone is removed
  on the next `CTLDStaticWatcher` poll, within one second of DCS considering the FARP destroyed.
```
</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 on lines +438 to +442
group: fob
standard: true
label: {en: FARP troop boarding radius, fr: Rayon d'embarquement à une FARP}
unit: m
description:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick: farpTroopPickupRadius is assigned to the fob schema group even though the feature defines it as a FARP troop setting alongside troopPickupAtFARP in the troops group, so schema-driven configuration UIs and generated documentation place the setting under the wrong section.

Suggested fix: Change the schema entry's group from fob to troops and regenerate any derived configuration artifacts.

Comment on lines +276 to +287
it("registers a pickup-capable troop zone when troopPickupAtFARP is true", function()
local pad = fakeHelipad(sceneName .. "-1", { x = 10, y = 0, z = 20 })
local ab = fakeAirbase(pad)
Airbase.getByName = function(n) return (n == pad:getName()) and ab or nil end

lastStepFunc()(ctxFor(pad))

local zone = CTLDZoneManager.getInstance():getTroopZoneAtPoint(
{ x = 10, y = 0, z = 20 }, coalition.side.BLUE)
assert.is_not_nil(zone)
assert.is_true(zone:hasPickup())
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.

nitpick (testing): The successful-registration test leaves its CTLDStaticWatcher entry and scheduled watcher timer installed after the test; the suite resets _troopZones but not the shared watcher registry, so later tests run with stale closures and accumulated watcher state.

Triggers: When the test suite continues after any of the three successful-registration cases.

Suggested fix: Unwatch the generated FARP watcher or reset the shared CTLDStaticWatcher state in after_each.

Comment thread CHANGELOG.md
Comment on lines +21 to +22
class by the recon FARP-detection code — rather than a new bespoke manager. The zone disappears
the moment DCS considers the FARP destroyed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick: The documentation says the troop zone disappears immediately when DCS considers the FARP destroyed, but the implementation only polls Airbase:isExist() on the installer's one-second CTLDStaticWatcher timer, so the zone remains available until the next poll.

Triggers: When a FARP is destroyed between watcher polls.

Suggested fix: Document the polling delay, or make the destruction path event-driven if immediate removal is a required contract.

Suggested change
class by the recon FARP-detection code — rather than a new bespoke manager. The zone disappears
the moment DCS considers the FARP destroyed.
class by the recon FARP-detection code — rather than a new bespoke manager. The zone is removed
on the next `CTLDStaticWatcher` poll, within one second of DCS considering the FARP destroyed.

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.
@FullGas1
FullGas1 merged commit d36e640 into develop Aug 26, 2026
9 checks passed
@FullGas1
FullGas1 deleted the feature/farp-troop-pickup branch August 26, 2026 13:24
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