# Plan: Formation Editing in MapWindow

## Context

MapWindow v16 draws a group's formation rings, 8 true-north sector spokes and per-ring NM labels, but it can't change any of them. The goal is to edit formations from the globe with the same reach as the GE's formation editor:

1. **Ring sizes:** the NM radius of each of the six formation rings (`THCGroup.zones[0..5]`).
2. **Unit stations:** where each surface/sub unit sits in the formation, which also decides the ring and sector it keeps or patrols.
3. **Formation patrols:** aircraft from the group's own ships flying patrols in a ring and sector. You create them from aircraft ready on deck, move them, and delete them.

This plan comes from reading the GE source: `C:\Prog\Projects\HC\libge\src\formedit.c` (the editor), `Effects.c` (`ProcessFormationEditEvent`, `ProcessAirPatrolEvent`) and `Gci.c` (offset math). MapWindow should make the **same memory changes and queue the same events** the GE does, so HC can't tell the two editors apart.

In the GE, the aircraft of a non-aircraft group are only visible **while its formation is being edited**. MapWindow has no unit-level or aircraft view at all yet, so this work also needs new map capability. That's covered in its own section below.

---

## How HC's formation editor works (verified from GE source)

### Data

| Field | Type | Meaning |
|---|---|---|
| `THCGroup.zones[0..5]` | short, NM | Outer radius of Main Body, AAW, ASW, Picket 1, Picket 2, Picket 3. 64 slots, only `UI_FORMATION_ZONES` = 6 used. |
| `THCUnit.xOff`, `yOff` | long, DU | Station offset from the group centre. **True-north frame, not rotated by course.** ~6211 DU per NM (`NMD10` = 621 per 0.1 NM). `yOff` is a latitude-sliver delta and HC's `sLat` counts down from the north pole, so **positive `yOff` is south**. `xOff` is a longitude delta scaled by cos(lat) (`Gci.c` `GetPositionOffsets` / `PositionOffsetEx`). |
| `THCUnit.rings`, `sectors` | UInt64 bitmask | Ring(s) and 45° sector(s) the unit keeps or patrols (`MainBodyFlag`..`PicketRing3Flag`, `FormationSector000_045`..`315_000`). Main Body units have `sectors = 0`. |
| `THCUnit.independent` | uchar | Formation-keeping state: `FM_SLAVED`, `FM_REFORMING`, `FM_DRIFT_*`, `FM_SPRINT_*`, `FM_POSITION_REACHED`, `FM_INDEPENDENT`, `FM_RTB`. |
| `THCUnit.myPlanes` | list of `THCPlaneRec` | Aircraft aboard a ship or base: `PlaneID` (annex), `LoadID`, `Status` (`READY` etc.), `PlatNow` (count), `PatrolFlag`, `name`. |
| `THCFormationEvent` (`FORMATION_EVENT` = 7) | `Xoffset`, `Yoffset`, `oldSpeed`, `rings`, `sectors` | Order to move one unit, ship or aircraft, to a station. |
| `THCAirPatrolEvent` (`AIR_PATROL_EVENT`) | `PlaneID`, `LoadoutIDX`, `PatrolType`, `Number`, `Status`, `Rings`, `Sectors` | One formation patrol. It tracks the patrolling aircraft by the event's `unit_uuid`. |

### Ring and sector from an offset (`FindShipRingSector`)

- Distance: `sqrt(xOff² + yOff²) / NMD10` gives tenths of NM. The ring is the first `i` with distance < `zones[i]`.
- Bearing: `iatanl(-yOff, xOff)` gives an ordinary compass bearing (north = -yOff). The sector bit is `1 << (bearing / 45°)`.
- A unit at offset (0,0), or whose ring works out to Main Body, gets `rings = MainBodyFlag`, `sectors = 0`.

### Ring size rules (`Dialog04Dlg`)

- Ring `i` must be at least `i + 1` NM.
- Each ring must be at least 1 NM larger than the one inside it.
- Ring `i` must be at most `MAX_FORMATION_RADIUS` (1024) − (5 − `i`) NM.

The dialog **clamps** values to these rules rather than rejecting them. When you resize rings, the editor also **scales each unit's offset with its ring's band** (`ResetFormationPositions`), so units keep their ring and sector.

### What the editor lists (`FormationDlg` WM_INITDIALOG, `AddFormRec`)

| State (`FORMEDIT.H`) | Entry |
|---|---|
| `SHIP_FORM` | Each unit of the group that isn't an aircraft **in combat**: ships, subs, and airborne aircraft not on an attack. |
| `AIR_FORM` | An airborne aircraft unit that has an `AIR_PATROL_EVENT`, i.e. a patrol in progress. It shows its patrol type. |
| `ASSET_FORM` | A `READY` plane record aboard a non-aircraft unit, with its available count (`PlatNow`). |
| `PATROL_FORM` | A new patrol staged from an asset: count taken from the asset, and a default patrol type from the plane subtype (ASuW / ASW / AAW / AEW / EW). |
| `EMPTY_ASSET` | An asset whose aircraft are all assigned to staged patrols. |
| `DELETED_AIR` | A patrol in progress staged for cancellation. |

### Commit (`Formation_OnCommand`, IDOK)

1. **Rings:** `group->zones[i] = zones[i]`, a direct memory write.
2. **Speed:** if any unit's station changed, `oldSpeed = group->kts`. If the group is going faster than half its max, it's slowed to half with `SetGroupKts`; otherwise `oldSpeed = 0`.
3. **Each non-plane entry:**
   - **Cancelled patrol** (`DELETED_AIR`):
     - on the patrol: `Status = PATROL_RETURN`, `Number = PATROL_DELETE`;
     - on the aircraft unit: delete its `FORMATION_EVENT`, set `independent = FM_REFORMING`, `CombatFlag = AC_RETURN`;
     - move the patrol event to the **head** of the event list and set its time to now.
   - **Moved unit:**
     - set `independent = FM_REFORMING`;
     - `NewPtr` a `FormationEvent {Xoffset, Yoffset, oldSpeed, rings, sectors}`;
     - if it's an aircraft with a patrol, also update the patrol's `Rings`/`Sectors`;
     - delete the unit's pending `FORMATION_EVENT`;
     - post a new one (`TimeNow`).
4. **New patrols** (`PATROL_FORM`): `NewPtr` an `AirPatrolEvent`:
   - `PlaneID`;
   - `LoadoutIDX = GetLoadoutIndex(PlaneID, LoadID)`;
   - `PatrolType`, `Number`;
   - `Status = PATROL_START`, `Rings`/`Sectors`;
   - posted with an empty unit, `TimeDelta` staggered by 5 s per patrol.

### Carrying out orders

- **`ProcessFormationEditEvent`:** re-queues every 10 s (1 s for aircraft) until the unit is within one tick of `group position + offset`. Then:
  - Main Body / AAW units become `FM_SLAVED`;
  - ASW / picket units become `FM_DRIFT_INSIDE` (patrol cycle);
  - aircraft become `FM_POSITION_REACHED` at loiter speed.

  When the last forming unit arrives and `oldSpeed` ≠ 0, the group's speed is restored.
- **`ProcessAirPatrolEvent`**, a state machine on `Status`:
  - `PATROL_START`: finds a `READY` plane of the right type and loadout aboard the group and posts a `LAUNCH_EVENT`. The launch links back via `patrolling = E`, and the launched aircraft becomes an **air unit inside the ship group**. If nothing is ready, it checks again in 30 min.
  - `PATROL_TRANSIT`: posts a `FORMATION_EVENT` to a random point in the patrol's rings and sectors (`FindRandomPatrol`).
  - `PATROL_ONSTATION`: watches fuel. Near bingo it posts a **relief** patrol (`PATROL_START`, +45 s) and moves to `PATROL_RETURN`.
  - `PATROL_RETURN`: posts a `LANDING_EVENT` and ends.
- **Automatic patrols:** `CreateInitialAirPatrols` sets up formation patrols automatically for AI sides, and for the player when the game option `FORMATION_AIR_OPS` is on.

### The event list (`harpoon.c` `QueHarpoonEvent`, `Effects.c` `ProcessEvents`)

- `QueueHarpoonEvent` changes `TimeNow`/`TimeDelta` to an absolute time and inserts the event **before the first event with a later time** (strict `<`). A `TimeNow` event therefore lands **after every event already due this second** and before the first `GameTime+1` event. It becomes the head only when nothing earlier or equal is queued; HC then calls the DLL's `HarpoonEventHead` export with the new head.
- **There's no tail pointer.** The list is always walked from `HarpoonEventHead`, and a last-place event is linked through the final node's `next`.
- `ProcessEvents` (effect slot 18, after `dllexGameTime` in each tick's effect cycle) runs events from the head until it reaches one that isn't due yet.
- So a DLL can **move any event except the head**: unlink it (`prev->next := E->next`, safe even for the last node) and re-queue the **same** event, keeping its address valid for anything that points to it (e.g. `LAUNCH_EVENT.patrolling`). A head event is already next in line, so it's changed in place. The one thing a DLL must never write is HC's `HarpoonEventHead` variable.

---

## Constraints for an ExportDLL

| Need | Available to the DLL? | Plan |
|---|---|---|
| Allocate HC memory | `NewPtr` / `DispPtr` ✔ (already captured) | Same as the course editor |
| Queue an event | `QueueHarpoonEvent` ✔ (captured, unused until now) | Pattern already working in Toolbox (`RflBtnQueueClick`) and lazGUI |
| Read the event list (find patrols, find pending formation orders) | Optional `HarpoonEventHead` export ✔ | **Needed from Phase 1.** MapWindow implements the export and keeps the head pointer. It walks the list only on HC's thread. |
| `del_Unit_effects` (drop a unit's pending formation order) | ✘ not exported | A non-head pending `FORMATION_EVENT` is **unlinked and freed** (`DispPtr` data then event). A head one is **updated in place**. Then the new order is queued. |
| Cancel a patrol immediately | ✘ the GE relinks the event to the list head | **Decision 4:** set `Status = PATROL_RETURN`, `Number = PATROL_DELETE`. Then if the event is not the head, unlink it and re-queue it with `TimeNow`; if it is the head, set its `Time` to now. It runs on the next pass of the event processor, with no GE change. |
| `GetLoadoutIndex(PlaneID, LoadID)` | ✘ not exported, not in shared Pascal | Port it to shared Pascal using annex B1 loadout data; `uHCAnnexHelpers` already reads B1. |
| `SetGroupKts` (slow group while reforming) | ✘ not exported | **Decision 2:** a "Slow group while units reform" checkbox on Apply, default on. When on, the DLL slows the group to half its max speed, either with a queued `ALT_SPEED_EVENT` or by mirroring `SetGroupKts`' field writes (decide in phase 3 after reading `ALT_SPEED_EVENT` processing), and posts the real `oldSpeed`. **HC restores the speed itself** when the last unit arrives. When off, it posts `oldSpeed = 0`. |
| Run on HC's thread | Only inside `GameTime` | **`GameTime` isn't called while paused** (`Command.c`: it's inside `if (!ExportDLLBusy) { GameTime++; … }`). **Decision 5:** Apply works while paused. The commit is queued via `QueueHCCallback`, and the globe and editor show it as **pending** (dashed rings and stations, a "pending" status line) until the first tick carries it out. |

**Known limitation (event-list head):** the GE promises to call the `HarpoonEventHead` export every time the head changes, and 12 of its 13 head-changing sites do (checked 2026-09-24). The exception is **Orders > Refuel**: `Command.c:1029` drops the group's existing refuel event with `free_HarpoonEvent(&HarpoonEventHead, E)`, and `free_HarpoonEvent` (`Common/SRC/Memfs.c:1213`) reassigns `*event_head` without calling `dllexHarpoonEventHead`. If that refuel event was the head, MapWindow's stored head would point at the freed event until the next head change (`ProcessEvents` makes the call as soon as it consumes the head).

**Mitigated (2026-09-24):** `Command.c` always queues a replacement refuel event for the same group straight after freeing the old one, which calls the **`HarpoonEventQueued`** export. MapWindow records the head's `event_type` and `ourGroup` whenever HC reports a new head, while the pointer is known to be valid. When `HarpoonEventQueued` sees an `AIR_REFUEL_EVENT` for the same group as a recorded refuel-event head (and the new event isn't itself the head), it drops the head to nil until HC's next head notification. The freed event is never read. The cost is that patrols can disappear from the Formation Editor for a moment after a Refuel order. A DLL that queues an extra refuel for the group without removing one (e.g. Toolbox) causes the same brief blank, never a bad read. The DLL can't take over command handling (`Command.c` has no export hook; overriding `Effect18` replaces event processing, not orders), so this is the fix available from the DLL side. See *GE findings*.

**Threading:** writing `zones[]`, unit fields or event data from the form thread would race HC's thread. All reads of live pointers and all writes happen on HC's thread through `QueueHCCallback` / `ExecutePendingHCCallbacks` and the snapshot, same as waypoint edits.

---

## Map enhancements needed (MapWindow's group window)

Today MapWindow draws **one icon per group**. It has no idea of units, aircraft aboard, or airborne patrol aircraft inside a ship group. Formation editing needs:

1. **Unit-level capture:** for the focus group, per unit: position, offsets, rings/sectors, `independent`, type, name, UUID, `CombatFlag`.
2. **Deck assets:** per ship or base, its `myPlanes` records (type name, count, status, loadout). These feed the patrol list, and later a "what's aboard" readout for the selected group.
3. **Formation patrol state:** each `AIR_PATROL_EVENT` for the group (type, count, status START/TRANSIT/ONSTATION/RETURN, rings/sectors, linked aircraft UUID), read from the event list. This covers patrols waiting to launch and relief patrols, not just airborne ones.
4. **Airborne patrol aircraft on the globe:** air units inside a ship group, drawn at their **actual** position with an aircraft icon and a line back to the patrol region. As in the GE, **only in formation-edit mode**.
5. **Patrol region highlight:** ring ∩ sector drawn as an annular wedge (the GE's `FramePatrolRegion`), for the selected unit or patrol.
6. **Station markers:** each ship's staged station, draggable, with a line to its actual position while it's reforming.

Items 1–3 extend the snapshot for the **focus group only**, to keep capture cheap. Items 4–6 are new overlays that follow the existing create/update/remove lifecycle in `UpdateGroupFromSnapshot`.

---

## Design

### Entry and exit
- A right-click context menu item on an own-side group icon: **Edit Formation…**
  - **Decision 1:** in Player view, own side only. In God view, any group (enemy and neutral included).
- Opens a non-modal **Formation Editor** window. The globe goes into formation-edit mode for that group: the zoom fits the outer ring, and units, aircraft and patrol regions become visible.
- **Apply** commits staged changes, **Cancel** discards them. Closing the window counts as Cancel.
- Staged-then-Apply, because HC acts on each change by moving ships and launching aircraft.

### Snapshot additions (HC thread)
- The form thread publishes a **focus group UUID** under `gSnapshotCS`. `CaptureGameTimeSnapshot` walks units, plane records and the event list only for that group.
- `TGroupSnapshot.Units: array of TUnitSnapshot`:
  - UUID, name, id, type, `is_air`, `CombatFlag`;
  - `sLon`/`sLat`, `xOff`/`yOff`, `rings`/`sectors`, `independent`;
  - whether it has a pending `FORMATION_EVENT`.
- `TGroupSnapshot.Assets: array of TPlaneAssetSnapshot`:
  - owning unit UUID;
  - `PlaneID`, `LoadID`, type name;
  - `Status`, `PlatNow`, `PatrolFlag`.
- `TGroupSnapshot.Patrols: array of TPatrolSnapshot`:
  - event UUID;
  - `PlaneID`, `LoadoutIDX`, `PatrolType`, `Number`, `Status`, `Rings`/`Sectors`;
  - linked aircraft UUID.

### Formation Editor window
- **Ring sizes:** six spin edits (NM) with HC's clamping rules and a note when a value was clamped.
- **Units and patrols list**, grouped like the GE's:
  - Ships/subs: ring, sector(s) and state.
  - Patrols in progress: type, count, status, region.
  - Deck assets: type and available count.
  - Staged new patrols.
- **Patrol actions:**
  - **Set Air Patrol…** on an asset: choose a count (≤ available) and a patrol type (defaulting from the plane subtype, as in the GE).
  - **Delete Patrol** on a patrol in progress or a staged one.
- **Apply** / **Cancel**, the **"Slow group while units reform"** checkbox (default on), and a status line showing pending changes while paused.
- **Automatic patrols** (Decision 6), created by HC for AI sides and under `FORMATION_AIR_OPS`, are listed and edited like any other patrol. HC doesn't tell them apart either.

### Editing on the globe
- **Ships:** drag the station marker. Offset = `GetPositionOffsets(group centre, drop)`, and ring/sector are recomputed with a Pascal port of `FindShipRingSector` using the staged zone sizes.
- **Patrols:** click inside a ring ∩ sector wedge to set the patrol's `Rings`/`Sectors`. **Decision 3:** Shift-click extends the area using the GE's own rules (`MoveAndSetPosition`, `AdjustSector`):
  - rings are OR'd; extending from Main Body fills every ring from AAW out to the clicked one; Main Body is always masked out (`&= 0xFE`);
  - sectors are OR'd, then the gap is filled on the **shorter** side, so the sectors always form one continuous arc.
- **Ship stations and deck assets:** moving a ship also moves the offsets of that ship's deck assets, as the GE does.
- **Ring resize:** scale staged ship stations with their ring band, as the GE does. We index the band by ring number; see *GE finding* below.

### Commit (HC thread, one batch per Apply)

New `THCCallbackKind` values. Every lookup is by UUID on HC's thread, and anything that has vanished is skipped and logged through `MessageLogLine`.
- **`hcbSetFormationZones(group, zones[6])`:** validate again against the rules, then write `zones[]`.
- **`hcbSetUnitStation(group, unit, xOff, yOff, rings, sectors)`:**
  - set `independent := FM_REFORMING`;
  - replace the pending `FORMATION_EVENT` (unlink and free it if it isn't the head, update it in place if it is), otherwise `NewPtr` and queue a new one (`TimeNow`, `oldSpeed` from the reform-speed checkbox);
  - for an aircraft with a patrol, also update the patrol's `Rings`/`Sectors`.
- **`hcbCreatePatrol(group, PlaneID, LoadID, PatrolType, Number, Rings, Sectors, delaySec)`:** `NewPtr` a `THCAirPatrolEvent` (`PATROL_START`, `LoadoutIDX` from the ported `GetLoadoutIndex`) plus its event, and queue it (`TimeDelta` = delaySec, staggered 5 s).
- **`hcbCancelPatrol(group, patrolEventUUID)`:**
  - on the patrol: `Status := PATROL_RETURN`, `Number := PATROL_DELETE`;
  - on the airborne unit, if any: `independent := FM_REFORMING`, `CombatFlag := AC_RETURN`, and remove its pending `FORMATION_EVENT`;
  - move the patrol event to run now: unlink and re-queue it with `TimeNow` if it isn't the head, set its `Time` to now if it is.
- **`hcbSlowGroupForReform(group)`:** issued first in the batch when the checkbox is on. It returns `oldSpeed` for the station commands in the same batch.

---

## Phases

1. **Read-only formation view**
   - `HarpoonEventHead` export; focus-group capture of units, deck assets and patrols.
   - Edit mode with station markers, airborne patrol aircraft, patrol wedges, and the editor list.
   - No writes.
   - **Verification:** compare with the GE formation editor for the same group, especially sector orientation, DU↔NM scale and patrol regions.
2. **Ring sizes**
   - Spin edits with HC's rules, staged preview, `hcbSetFormationZones`, station scaling.
   - Low risk: a direct write, exactly what the GE does.
3. **Ship stations**
   - Drag, stage, Apply; replace or queue `FORMATION_EVENT`s.
   - "Slow group while units reform" checkbox, with HC restoring the speed.
   - **Verification:** ships sail to new stations and settle as slaved or drifting; the group slows while they reform and returns to its old speed after.
4. **Formation patrols**
   - Port `GetLoadoutIndex`.
   - Create patrols from deck assets, move patrol areas (multi ring/sector with the GE's rules), cancel patrols immediately via event relinking.
   - **Verification:** a patrol launches, transits, goes on station, is relieved at bingo and recovers, all the same as a GE-created patrol.
5. **Polish**
   - Snap and keyboard nudging, undo within a staging session.
   - A "what's aboard" readout for the selected group outside edit mode.

---

## Extended rings (decision 7, 2026-09-24)

MapWindow uses formation rings up to the **data-structure and GE runtime limits**, not just the six the GE's editor shows.

| | Data limit | GE runtime | MapWindow |
|---|---|---|---|
| Rings | 64 (`zones[MAX_FORMATION_ZONES]`, 64-bit `rings`) | `Effect2.c` `FindRandomPatrol` handles ring 0–63, with a band from `zones[ring-1]` to `zones[ring]`. Only the editors and `effect4.c` `OutsidePatrolArea` stop at `UI_FORMATION_ZONES`. | **Up to 64** |
| Sectors | 64-bit mask (`MAX_FORMATION_SECTORS` = 64) | Bearing = `45 × sector`, so sector bits 8 and up only repeat sectors 0–7 | **8** |

- **Enabled ring:** part of the unbroken run of rings, starting from Main Body, whose radii are above zero and strictly increasing. The first ring that is zero or not larger than the one inside it ends the run, because the GE builds every band from `zones[i-1]..zones[i]`. Rings 0–5 keep the GE editor's clamping rules; later rings need only be larger than the one inside them, up to `MAX_FORMATION_RADIUS`.
- **Drawn rings:** a ring is drawn when it's **enabled or populated**. Populated means a unit or patrol of the focus group has that ring bit set. For other groups only "enabled" is known. This keeps extended formations from cluttering the globe.
- **Runtime behaviour in extended rings (index ≥ 6), from the GE source:**
  - `ProcessFormationEditEvent` puts a ship into the drift/patrol cycle only for ASW/Picket 1–3. A ship given an extended ring therefore settles **slaved at its offset**, a fixed station.
  - `OutsidePatrolArea` treats everything past the sixth ring as index 6, so a *drifting* unit in ring index ≥ 7 would always count as outside its area. MapWindow never puts a ship into a drift state in those rings (it only sends `FORMATION_EVENT`s), so this doesn't come up, but the editor should say that extended-ring ships hold a fixed station.
  - Aircraft patrols use `FindRandomPatrol`, which handles all 64 rings.
- **Labels:** each ring's label goes on its own spoke, clockwise from NE and repeating after 8 rings.

## Decisions (2026-09-24)

1. **God mode:** Player view edits the player's side only; God view can edit any group.
2. **Reform speed:** Apply has a "Slow group while units reform" checkbox, default on. It mirrors the GE's half-speed slowdown and HC's own restore.
3. **Patrol areas:** multiple rings and sectors, built with the GE's Shift-click rules (ring OR with fill from Main Body; sectors kept as one continuous arc).
4. **Cancelling a patrol:** immediate, without a GE change. Unlink a non-head event and re-queue it with `TimeNow`, or change a head event in place (see *The event list*). The GE itself can't be modified.
5. **Paused editing:** Apply works while paused and shows the change as pending until the first tick carries it out.
6. **Automatic patrols:** treated like any other patrol.

8. **Which groups (2026-09-24):** Edit Formation is offered for the group types the GE's own menu gives a formation (ships, subs, carriers, convoys, armor, bases, ports, airfields, SAM sites), **plus aircraft and helicopter groups**, which the GE's editor doesn't allow. For those groups the editor shows the group's **air units** as the formation's members: each air unit gets a station marker and is placed like a ship. The airborne-patrol markers are only for patrol air units flying over a surface group.
   - **Phase 3 caveat for air groups:** when an aircraft *reaches* its station, `ProcessFormationEditEvent` sets it to `FM_POSITION_REACHED` at loiter speed with `speed = 0`. That suits a patrol aircraft holding a point, but an air unit that is a member of a **moving** aircraft group would stop and drop behind. Phase 3 must handle this for those air units, e.g. by watching for `FM_POSITION_REACHED` on HC's thread and returning them to `FM_SLAVED` at group speed, the way ships end up. Verify in play before relying on it.

Still to settle while building:
- **HCDataBroker:** this is built in the DLL now; the broker's command channel later carries the same commands. See the 2026-09-24 section of `Plan-HCDataBroker-20260324.md`.

---

## Phase 2 as built (2026-09-24)

Phase 2 matches the GE, so ring resizing also moves ships (decision 9: user chose "match the GE" over "rings only").

- **Editor:**
  - The six ring edits are live.
  - **Add ring** / **Remove ring** manage rings 7–64 in a scrolling grid.
  - **Slow group while units reform** is on by default.
  - **Apply** / **Revert** / **Close**.
  - Edits are staged and clamped with `ClampFormationZones` (the GE's `Dialog04Dlg` rules, generalised to N rings). The fields show the clamped values once you leave them.
- **Preview:** `StagedFocusView` gives the focus group the staged zones and scales every station with `ScaleStationForZones` (the GE's `ResetFormationPositions`, with the Picket 2 band index corrected). Rings, labels, spokes and station markers all preview before Apply.
- **Apply** is one batch queued under a single lock (`QueueHCCallbacks`), carried out on HC's thread in the next `GameTime`:
  1. `cbkSetFormationZones`: clamps again, then writes `zones[0..63]`.
  2. `cbkSlowGroupForReform`, when the box is ticked and a ship moves. Following the GE's rule (only if faster than half max speed), it queues a group-level `ALT_SPEED_EVENT` at half max speed (`SetGroupKts` isn't a funcRef) and passes the old speed to the station orders, so HC restores it when the last unit arrives.
  3. `cbkSetUnitStation` per unit whose scaled station changed:
     - it sets `independent := FM_REFORMING`;
     - it **retargets the unit's pending `FORMATION_EVENT` in place**, or queues a new `TimeNow` one if none is pending, instead of the GE's delete-and-repost; HC re-runs it within 10 s either way;
     - a patrolling aircraft's `AIR_PATROL_EVENT` keeps the same rings and sectors.
- **Pending:** the editor shows "Applied – waiting for HC" until HC reports the staged sizes, including while paused.
- **Air-unit groups:** their rings resize, but their air units don't move yet (the decision 8 caveat, handled in phase 3).
- **Tests:** `ClampFormationZones` and `ScaleStationForZones` are covered in `tests/FormationMathTest.lpr` (216 checks).

## Phase 3 as built (2026-09-24)

- **Dragging stations:** in formation edit mode, pressing on a unit's station marker and dragging it stages a new station.
  - The drag works on ships, and on the air units of an aircraft/helicopter group.
  - It doesn't work on patrol aircraft over a surface group, whose patrol area is phase 4, or on aircraft on an attack.
  - Globe panning is off during the drag, and **Esc** puts the station back.
  - Only the overlay is redrawn while dragging.
- **Staged station record:** each staged station keeps the ring sizes it was placed against. If the rings are resized before Apply, the station is rescaled with its band, as the GE's `ResetFormationPositions` does for staged positions.
- **Ring and sector:** a dragged unit's ring and sector come from `FindRingSector` at its new position (the GE's `MoveAndSetPosition`).
- **Apply:** uses phase 2's batch, now built from the staged view. Every unit whose station differs from HC's gets a `cbkSetUnitStation`, whether it was scaled by a ring change or dragged. The editor counts staged ring sizes **or** staged stations as a change, and **Revert** clears both.
- **Shift+click area (decision 10, 2026-09-24):** with a unit selected, Shift+click inside the formation adds that ring/sector cell to the unit's area (`ExtendPatrolArea`: rings OR'd, filled from AAW when extending from Main Body; sectors OR'd with the GE's `AdjustSector` gap fill so the arc stays contiguous). **Unlike the GE, it doesn't move the station**, which is the user's choice. A drag resets the area to the new position's cell. It works on ships, which drift inside their ASW/picket area, and on aircraft-group air units.
  - `ProcessFormationEditEvent` only applies an order's rings and sectors while the unit is still moving. A unit already on its station takes the arrival branch and keeps its old area. `ExecSetUnitStation` therefore also writes `unit->rings`/`sectors` directly (through the interface's unit pointer), then queues the order so HC restarts the drift patrol in the new area.
  - The editor list now shows the staged view's ring and sector.
  - **Ctrl+click** sets the selected area to just the clicked cell. The GE has no equivalent, since its plain click also moves the unit.
  - **Patrol areas were brought forward from phase 4** at the user's request ("the more important functionality"). With a patrol selected, from the list or by clicking its airborne aircraft, Shift/Ctrl+click edits the patrol's area. On Apply:
    - `cbkSetPatrolArea` sets the `AIR_PATROL_EVENT`'s `Rings`/`Sectors`; relief patrols copy them.
    - An airborne patrol aircraft also gets a `cbkSetUnitStation` to a `RandomPointInArea` in the new area. That's the same kind of point HC's `FindRandomPatrol` picks, kept 0.2 NM inside band edges because `FindRingSector` truncates each axis.
    - Patrol changes don't trigger the group slowdown.
  - Area outlines use `PatrolOutlineCells`: consecutive rings merge into one band, so a multi-ring area shows only its outer border.
  - Pressing on a marker only starts a drag after the mouse moves 4 px. Before this, a plain click staged a "move" to the same spot and reset the unit's area.
- **Switching groups:** choosing Edit Formation on another group discards the old group's staged sizes and stations. Before this, staged ring sizes could carry over to the new group.
- **Air-unit arrival fix (decision 8):** `ExecSetUnitStation` remembers each aircraft-group air unit it orders, with its throttle. Each `GameTime`, `WatchAirReform` looks for those units reaching `FM_POSITION_REACHED` (HC's `ProcessFormationEditEvent` stops an arriving aircraft at loiter throttle, `speed = 0`), then:
  - sets it `FM_SLAVED`, which is the state `MoveUnits` carries along with the group each tick (`Display.c`);
  - gives it the group's `kts`, `speed` and `course`, and its throttle back;
  - snaps it to *group centre + offset*, to undo the tick it lagged behind.

  Entries are dropped if the unit is gone, goes into combat, or is taken over by something else, and the list is cleared on scenario load.

## Phase 4 as built (2026-09-24)

Patrol **areas** were done in the phase 3 follow-up (r1104). This adds creating and cancelling patrols.

- **Create:**
  - Select an "On deck" row. The patrol toolbar offers a type and an aircraft count, up to the aircraft still unassigned on that asset.
  - The type defaults from the loadout's Y1 `MissionID` (`DefaultPatrolTypeForMission`: ATA→AAW, ASW→ASW, strike missions→ASuW, otherwise AEW). That's what the GE's `HandleDialog27` intends; its mission variable is never set, so the GE itself always lands on AEW/EW.
  - **Add patrol** stages it over Main Body, the GE's `CreateAirPatrol` default, and selects it, so Shift/Ctrl+click sets its area. Its aircraft come off the asset's count in the view.
  - On Apply, `cbkCreatePatrol` queues an `AIR_PATROL_EVENT`: `PATROL_START`, staged rings/sectors, staggered 5 s apart as in the GE.
  - `LoadoutIDX` comes from `HCLoadoutIndex`, a port of `Plane.c` `GetLoadoutIndex`: `MAX_PLANE_LOADOUTS` + the position of the plane's loadout (converted with the `annex_id_to_disk_id` funcRef) in the aircraft's B1 disk record `LoadId[]`. The offset matters: `GetLoadoutFromIndex` treats smaller values as legacy positional indexes and remaps them by mission.
- **Cancel:** select a patrol and press **Cancel patrol**. A new patrol is removed; an existing one toggles between cancel and keep. On Apply, `cbkCancelPatrol`:
  - sets `PATROL_RETURN` with `Number = PATROL_DELETE`;
  - for an airborne aircraft, sets `FM_REFORMING` and `CombatFlag = AC_RETURN`, and drops its pending `FORMATION_EVENT`s by **blanking their `unit_uuid`**: `ProcessFormationEditEvent` then finds no unit and returns FALSE, and `ProcessEvents` disposes of the event (`Effects.c`). That's the `del_Unit_effects` result without unlinking anything, head included;
  - makes the patrol run now: a non-head event is unlinked and re-queued with `TimeNow` (same address, so `LAUNCH_EVENT.patrolling` stays valid), and a head event gets `Time = gGameTimeNow`.

  A patrol still on deck just ends (`PATROL_RETURN` with no aircraft returns FALSE). The GE's own editor can't cancel those.

## GE findings (for the GE, not MapWindow)

In `formedit.c` `ResetFormationPositions`, `index = (short)(ring >> 2)` turns a ring **bitmask** into a band index. It works for AAW (`$02`→0), ASW (`$04`→1), Picket 1 (`$08`→2) and Picket 3 (`$20`→8, clamped to 4). For **Picket 2** (`$10`) it gives 4 instead of 3. Picket 2 units are therefore scaled by the Picket 2/3 band instead of the Picket 1/2 band when you resize rings. The fix is to take the index from the bit position (log2(ring) − 1).

In `formedit.c` `MoveAndSetPosition`, Shift-clicking from a Main Body area fills rings with `theRing |= (2 << ring)` for `ring` from the clicked ring's index down to 0. That sets bits index+1 down to 1, one ring **beyond** the clicked one (then `&= 0xFE` drops Main Body). The comment says the intent is to fill up to the clicked ring, so the shift should be `1 << ring`. MapWindow's `ExtendPatrolArea` fills AAW through the clicked ring.

`free_HarpoonEvent` (`Common/SRC/Memfs.c:1213`) changes the list head without calling `dllexHarpoonEventHead` when it removes the head of `HarpoonEventHead`. Its only caller on the main event list is Orders > Refuel (`Command.c:1029`). ExportDLLs that track the head (MapWindow does) are left holding a freed pointer until the next head change. The fix is to notify DLLs when `event_head == &HarpoonEventHead` and the head changed.

The formation editor's OK (`formedit.c`) only slows a group that is going faster than half its maximum speed. It deletes each moved unit's pending FORMATION_EVENT and re-posts it with that Apply's `oldSpeed`. A second OK while units are still reforming finds the group already at half speed, so it re-posts with `oldSpeed = 0`. `ProcessFormationEditEvent` (`Effects.c`) restores speed only from the event of the last unit to arrive, so if that unit was re-posted, the group stays at half speed. MapWindow instead carries the largest pending `oldSpeed` forward (2026-09-24 code review).

## Key reuse

| Existing | Used for |
|---|---|
| `uThreadComm` `QueueHCCallback` / `ExecutePendingHCCallbacks` | Carrying commits to HC's thread |
| `ReapplyLastSnapshot` | Staged-edit preview without waiting for a tick |
| `uRangeRings` rings/spokes, `uZoneLabels` | Drawing from staged zone sizes |
| `uWaypointDrag` hit-test/drag pattern | Station marker dragging |
| `uHCAnnexHelpers` (B1 data) | Plane type names, subtype for default patrol type, porting `GetLoadoutIndex` |
| Toolbox `RflBtnQueueClick` | Event construction example |
