# Plan: HCDataBroker — Decoupled ExportDLL Data Broker

## Context

Currently each HC ExportDLL (MapWindow, lazGUI, etc.) receives callbacks directly from HC and handles its own rendering/logic. This couples data reception with visualization and limits extensibility. The goal: a single **HCDataBroker.dll** that receives all HC callbacks and publishes game state to multiple external client applications via IPC.

---

## Architecture

```
HC (Winharp32.exe)
    |
    | DLLInit, GameTime, ScenLoad, StartGroup, TimeFactor, ...
    v
HCDataBroker.dll (ExportDLL in HC process)
    |
    |-- Writes snapshots to shared memory (Local\HCDataBroker)
    |-- Named mutex (Local\HCDataBrokerMutex) for sync
    |-- Named pipe (\\.\pipe\HCDataBroker) for notifications + commands
    |
    +---> Client: MapWindow.exe (reads shared mem, sends commands via pipe)
    +---> Client: Future tool (same protocol)
    +---> Client: WebSocket relay (for browser UIs)
```

### IPC: Memory-Mapped File + Named Pipes (hybrid)

- **Shared memory** for bulk snapshot data (~54KB for 86 groups). All clients read from the same physical pages — zero per-client copy cost. Protected by named mutex.
- **Named pipe** (duplex, multi-instance) for signaling ("snapshot N ready") and commands (waypoint ops). One pipe instance per client. Non-blocking overlapped I/O.

### Serialization: Binary packed records

Fixed-size `TBrokerGroupRec` (~626 bytes, max 64 waypoints inline). No dynamic arrays in shared memory. Version header for forward compatibility.

```
Shared Memory Layout (1 MB):
  Header (64 bytes) — magic, version, generation, game time, group count
  ScenInfo (256 bytes) — scenario metadata
  Group[] (variable) — array of TBrokerGroupRec
```

### Publish model: push notification, pull data

Broker writes snapshot to shared memory, sends 12-byte notification to each pipe. Client acquires mutex, reads shared memory, releases mutex. Coalescing by generation.

### Command channel

Clients write `TBrokerCommand` records to their pipe. Broker reads during `GameTime` and executes via HC callbacks.

```pascal
TBrokerCommand = packed record
  Magic: DWORD;          // $48434443 ('HCDC')
  Kind: Byte;            // command type enum
  GroupUUID: HCUUID;     // 37 bytes
  Param1: LongInt;       // DestLon, speed, altitude, etc.
  Param2: LongInt;       // DestLat, etc.
  Param3: Integer;       // WaypointIdx, sensor mode, etc.
  Reserved: array[0..15] of Byte;
end;
```

Two execution paths based on command kind:
- **Direct memory ops** (coursepoints, drag) — broker uses `NewPtr`/`DisposPtr` to manipulate `PHCGroup^.track` linked list directly
- **Game commands** (speed, altitude, formation, sensors) — broker uses `QueueHarpoonEvent` to inject proper HC events, same as player clicking in the native UI

#### Command examples

**Append coursepoint** — add waypoint to group's course:
```
Kind=cbkAppendCoursepoint  GroupUUID='{abc-123}'
Param1=DestLon(slivers)  Param2=DestLat(slivers)  Param3=unused
Execution: NewPtr → allocate PHCPathRec → link at end of track list
```

**Insert coursepoint before waypoint 2:**
```
Kind=cbkInsertCoursepoint  GroupUUID='{abc-123}'
Param1=DestLon  Param2=DestLat  Param3=2 (insert before index)
Execution: NewPtr → allocate PHCPathRec → insert before node at index 2
```

**Delete waypoint 3:**
```
Kind=cbkDeleteCoursepoint  GroupUUID='{abc-123}'
Param3=3 (waypoint index)
Execution: unlink node at index 3 → DisposPtr
```

**Drag commit — move waypoint 1 to new position:**
```
Kind=cbkDragCommit  GroupUUID='{abc-123}'
Param1=DestLon  Param2=DestLat  Param3=1 (waypoint index)
Execution: walk to node 1 → update DestLon/DestLat in place
```

**Clear course — remove all waypoints:**
```
Kind=cbkClearCourse  GroupUUID='{abc-123}'
Execution: walk track list → DisposPtr each node → set track=nil
```

**Set group speed (future):**
```
Kind=cbkSetSpeed  GroupUUID='{abc-123}'
Param1=15 (knots)
Execution: QueueHarpoonEvent → inject speed change event into HC
```

**Change altitude (future):**
```
Kind=cbkChangeAlt  GroupUUID='{abc-123}'
Param1=3 (altitude band)
Execution: QueueHarpoonEvent → inject altitude change event
```

**Set sensor mode (future):**
```
Kind=cbkSetSensorMode  GroupUUID='{abc-123}'
Param1=1 (active/passive)
Execution: QueueHarpoonEvent → inject sensor mode event
```

The generic `Param1`/`Param2`/`Param3` fields accommodate both current coursepoint ops and future game commands without changing the record layout.

---

## Broker DLL Structure

```
HC/ExportDLLs/HCDataBroker/
  HCDataBroker.lpr          — DLL entry, full export surface
  uBrokerTypes.pas          — shared record definitions
  uBrokerSharedMem.pas      — CreateFileMapping/MapViewOfFile, mutex
  uBrokerPipeServer.pas     — named pipe server, client management
  uBrokerCapture.pas        — snapshot capture
  uBrokerCommands.pas       — command execution
```

Runs entirely on HC's thread. All pipe I/O non-blocking.

### Export surface phases

| Phase | Exports |
|---|---|
| 1 (Core) | DLLVer/Init/Free, ScenLoad, GameTime, StartGroup, TimeFactor |
| 2 (Units) | UnitNew/Free/LatLon/Showing/Course/Speed/Alt/ShowingSide/Sink/ToGroup/URegion |
| 3 (Events) | HarpoonEventHead/Queued, ActGroupPtr/UnitPtr, VictoryConditions, SaveGame, etc. |

---

## Client Architecture (MapWindow.exe)

MapWindow converts from DLL to standalone EXE. Form thread architecture already independent.

### What stays the same
- `mainform.pas`, all helper units (`uGroupIcon`, `uArrows`, etc.), `uGroupTypes.pas`

### What changes
- `MapWindow.lpr` → `MapWindowClient.lpr` (program, no exports)
- `uThreadComm.pas` → `uBrokerClient.pas` (pipe I/O)
- Snapshot reads from shared memory instead of `gSharedSnapshot`

---

## Implementation Phases

1. **Broker Core** — DLL with essential exports, shared memory, pipe server
2. **MapWindow Client** — convert to standalone EXE with broker client
3. **Dual Operation** — validate side-by-side, then retire DLL
4. **Extended Data** — unit-level callbacks, scenario events

---

## Key Reuse

| Existing | Reused as |
|---|---|
| `uGroupSnapshot.CaptureGameTimeSnapshot` | `uBrokerCapture` |
| `uThreadComm.ExecutePendingHCCallbacks` | `uBrokerCommands` |
| `mainform.WMApplySnapshot/Chunk` | Client mainform |
| All helper units | Client — unchanged |

---

## Updates since this plan (2026-09-24, MapWindow v16, r1046-r1093)

The plan is still unimplemented. MapWindow has changed underneath it in ways the broker design must absorb:

- **Snapshot record grew.** `TGroupSnapshot` now also carries `AirRangeNM`/`BingoRangeNM`/`BingoPct` (endurance rings, computed on HC's thread via the annex helper), `Zones[0..5]` (formation ring radii, NM, confirmed against the GE display) and `Jammed`. `TBrokerGroupRec` must include these, so the ~626-byte size estimate is low. Endurance needs `annex_id_to_disk_id` and `AnnexDataIn`, so the broker (not the client) should compute it.
- **Scenario load resets state.** `ScenLoad` now invalidates `gSharedSnapshot` before the form clears, and the form orphans any in-flight chunk batch. The broker needs an equivalent: bump a scenario epoch in the shared-memory header and notify clients so they drop groups and ignore stale generations.
- **Settings model.** `uMapConfig` (`TMapSettings`, INI at `MapWindow\MapWindow.ini`) is client-side state and needs no broker support, but it gets `gDLLPathFileName` from `uThreadComm`. The EXE client must supply that path (its own directory) when `uThreadComm` is replaced by `uBrokerClient`.
- **Re-apply without a new tick.** `ReapplyLastSnapshot` re-renders the last applied snapshot on settings, selection and zoom changes. It only needs the client's last snapshot copy, so it works unchanged with shared memory.
- **Future command kinds.** Formation editing in MapWindow is planned: ring ranges (`zones[]`) and unit placement within the formation. Expect command kinds such as `cbkSetZoneRange` and `cbkSetUnitFormationZone`. Whether these can write `THCGroup`/unit fields directly or must go through `QueueHarpoonEvent` (`FORMATION_EVENT`) still needs to be worked out.
