# HC ExportDLL Interface Documentation

## Overview

HC (Harpoon Classic) supports dynamically loaded DLLs that receive game state callbacks during scenario execution. Both the GE (Game Engine, `Winharp32.exe`) and the SE (Scenario Editor, `Wscenedt32.exe`) load ExportDLLs. Each ExportDLL is a Win32 DLL (`cdecl` calling convention) that exports a subset of well-known function names. HC discovers exports by name at load time via `GetProcAddress` — a DLL only needs to export the functions it cares about.

HC scans three directories for DLLs:
- `<HC_DIR>\ExportDLLs\*.dll` — loaded by both GE and SE
- `<HC_DIR>\ExportDLLs\GE\*.dll` — loaded only by the GE
- `<HC_DIR>\ExportDLLs\SE\*.dll` — loaded only by the SE

---

## Table of Contents

- [Quick Start: Minimal ExportDLL](#quick-start-minimal-exportdll)
- [Developer Guide](#developer-guide)
  - [DLLInit — Setup and Globals](#dllinit--setup-and-globals)
  - [Threading](#threading)
  - [Coordinate System](#coordinate-system)
  - [Platform Database (Annex System)](#platform-database-annex-system)
  - [Writing Back to HC — What's Mutable](#writing-back-to-hc--whats-mutable)
- [Reference](#reference)
  - [Shared Pascal Units](#shared-pascal-units)
  - [Export Slot Table](#export-slot-table-68-slots-from-dllexporth)
  - [Export Function Signatures](#export-function-signatures)
    - [Identification & Lifecycle](#identification--lifecycle)
    - [Scenario Events](#scenario-events)
    - [Game Tick](#game-tick)
    - [Group & Unit Pointers](#group--unit-pointers)
    - [Unit-Level Callbacks](#unit-level-callbacks)
    - [Group Management](#group-management)
    - [Harpoon Events](#harpoon-events)
    - [Victory Conditions](#victory-conditions)
    - [Scenario State Pointers](#scenario-state-pointers)
    - [User Interface](#user-interface)
    - [Movement & Altitude Overrides](#movement--altitude-overrides)
    - [Combat](#combat)
    - [Unit Window & Platform Data](#unit-window--platform-data)
    - [Dialog Overrides](#dialog-overrides)
    - [Effect Overrides (GE only)](#effect-overrides-ge-only)
  - [HC Callback Functions (funcRefs)](#hc-callback-functions-funcrefs)
  - [Key Data Structures](#key-data-structures)
    - [THCGroup](#thcgroup-packed-record)
    - [THCUnit](#thcunit-packed-record)
    - [THCPathRec](#thcpathrec-course-waypoints)
    - [THCHarpoonEvent](#thcharpoonevent)
    - [THCDLL_FUNC_REF](#thcdll_func_ref)
    - [IHCAnnexHelper Methods](#ihcannexhelper-methods)
  - [Interface Version History](#interface-version-history)
- [Appendix](#appendix)
  - [Setting Up Lazarus for ExportDLL Development](#setting-up-lazarus-for-exportdll-development)
  - [Existing ExportDLL Projects](#existing-exportdll-projects)
  - [C Quick Start](#c-quick-start)
  - [Sources](#sources)

---

## Quick Start: Minimal ExportDLL

All existing ExportDLLs are written in Object Pascal — either Free Pascal/Lazarus (current) or Delphi (legacy). Any language that can produce a Win32 DLL with `cdecl` exports will work. See [Setting Up Lazarus](#setting-up-lazarus-for-exportdll-development) for install instructions, or [Appendix: C Quick Start](#c-quick-start) for a C equivalent.

**Target interface version 45.** DLLs with interface versions 4-33 are disabled at load time due to an incompatible `HarpoonEvent` structure change at v34. All exports are called on HC's main thread using `cdecl` calling convention — see [Threading](#threading) for details on keeping HC responsive.

```pascal
library MyExportDLL;
uses Windows, uHCTypeDefs, uHCDefines;

const
  DLLVERN = 1;
  DLLVERINTN = 45;

function DLLVer(): word; export; cdecl;
begin Result := DLLVERN; end;

function DLLVerInt(): word; export; cdecl;
begin Result := DLLVERINTN; end;

function DLLDesc(): PAnsiChar; export; cdecl;
begin Result := 'My ExportDLL'; end;

procedure DLLFree(); export; cdecl;
begin { cleanup } end;

procedure DLLInit(hwndHC: HWND; pathHC: PAnsiChar; verHC: PAnsiChar;
  verSaveGame: longword; funcRefs: PHCDLL_FUNC_REF; PGEClosing: Pointer;
  PDLLBusy: Pointer; verInterface: longword; srandSeed: longword;
  harpoon_uuid_session: PAnsiChar; hclicense_dt_expire: Int64;
  hclicense_licensee: PAnsiChar; usblicense_desc: PAnsiChar;
  RESRSRStructureVersion: ulong; AnnexVersion: ushort;
  GameComponent: uchar; pLosTable: PHCLosTable); export; cdecl;
begin
  { capture funcRefs callbacks, store globals }
end;

procedure GameTime(GameTime: Long; GameTimeLeft: Long); export; cdecl;
begin
  { called each game tick — your main logic here }
end;

procedure StartGroup(StartGroup: PHCGroup); export; cdecl;
begin
  { store the group list head pointer }
end;

exports
  DLLVer, DLLVerInt, DLLDesc, DLLFree, DLLInit,
  GameTime, StartGroup;
end.
```

---

## Developer Guide

### DLLInit — Setup and Globals

```pascal
procedure DLLInit(
  hwndHC: HWND;                        // HC main window handle
  pathHC: PAnsiChar;                   // HC installation directory
  verHC: PAnsiChar;                    // HC version string
  verSaveGame: longword;               // save game format version
  funcRefs: PHCDLL_FUNC_REF;          // linked list of HC callback functions
  PGEClosing: Pointer;                 // pointer to GE closing flag
  PDLLBusy: Pointer;                   // pointer to DLLBusy flag (byte)
  verInterface: longword;              // ExportDLL interface version
  srandSeed: longword;                 // random seed for reproducibility
  harpoon_uuid_session: PAnsiChar;     // session UUID
  hclicense_dt_expire: Int64;          // license expiration timestamp
  hclicense_licensee: PAnsiChar;       // license holder name
  usblicense_desc: PAnsiChar;          // USB license description
  RESRSRStructureVersion: ulong;       // database structure version
  AnnexVersion: ushort;                // annex/platform DB version
  GameComponent: uchar;                // GC_GE=1, GC_SE=2 (GE vs Scenario Editor)
  pLosTable: PHCLosTable               // line-of-sight lookup table
); export; cdecl;
```

**`funcRefs`** is a linked list of `THCDLL_FUNC_REF` records. Walk the list to capture callback function pointers by name:

```pascal
funcTmp := funcRefs;
while funcTmp <> nil do begin
  if funcTmp^.funcName = 'NewPtr' then
    gNewPtr := THCNewPtr(funcTmp^.funcPtr)
  else if funcTmp^.funcName = 'DispPtr' then
    gDisposPtr := THCDisposPtr(funcTmp^.funcPtr);
  // ... etc ...
  funcTmp := PHCDLL_FUNC_REF(funcTmp^.next);
end;
```

**`PGEClosing`** — pointer to a `char` (byte) that HC sets to `TRUE` when the GE is shutting down. Check this before starting work that should not happen during shutdown:

```pascal
if byte(gPGEClosing^) = CFALSE then
begin
  // safe to proceed — GE is running normally
end;
```

**`PDLLBusy`** — pointer to a per-DLL `char` (byte) flag. Set to `CTRUE` at entry, `CFALSE` at exit of expensive exports. While any DLL's DLLBusy flag is TRUE, HC's main loop pumps Windows messages and sleeps instead of processing game ticks:

```pascal
byte(gPDLLBusy^) := CTRUE;
try
  // expensive work here — HC remains responsive to Windows messages
finally
  byte(gPDLLBusy^) := CFALSE;
end;
```

### Threading

- All exports are called on **HC's main thread**. Never block this thread for extended periods.
- Use `PDLLBusy` to signal HC to pump messages while your export runs expensive work.
- If you need a responsive UI, spawn a **dedicated form thread** with its own `GetMessage` loop (see MapWindow's `uFormThread.pas`).
- HC data structures (`PHCGroup`, `PHCUnit`, etc.) are **only safe to dereference on HC's thread**. Use snapshot/copy patterns for cross-thread access.
- HC callback functions (`NewPtr`, `DispPtr`, `QueueHarpoonEvent`, etc.) are **only safe to call on HC's thread**.

### Coordinate System

HC uses **slivers** for internal coordinates, not degrees:

- 1 degree = `SLIVER1` = 372,827 slivers
- Latitude: 0 slivers = 90N, `SLIVER90` = equator, `SLIVER180` = 90S
  - `SliverToDegLatF(sLat) = 90 - sLat / SLIVER1`
  - `HCDegSliverLatF(lat) = SLIVER90 - (lat * SLIVER15 + lat/3) / 15`
- Longitude: 0 slivers = 0 (Greenwich), positive = east, negative = west
  - `SliverToDegLonF(sLon) = sLon / SLIVER1`
  - `HCDegSliverLonF(lon) = (lon * SLIVER15 + lon/3) / 15`
- Distance: `NM1` = 6,211 distance units per nautical mile

### Platform Database (Annex System)

HC's platform database is organized into **annexes** — each annex type covers a category of platform or weapon. The annex data is passed to the DLL via `ScenLoad` (`AnnexDataIn: PHCAnnexes_disk`) and `DBLoaded` (`AnnexDataIn` + `AnnexDataMemIn`).

Access is through the `IHCAnnexHelper` and `IHCAnnexMemHelper` interfaces defined in `uHCAnnexHelpers.pas`. Declare globals as the **interface** type (not the class) — this gives reference-counted lifetime management.

**Annex types:**

| Annex | Constant | Platform Type |
|---|---|---|
| A1 | `ANNEX_A1` ($0000) | Ships / MLUs |
| A2 | `ANNEX_A2` ($1000) | Submarines |
| B1 | `ANNEX_B` ($2000) | Aircraft |
| C1 | `ANNEX_C` ($3000) | Sensors |
| D1 | `ANNEX_D` ($4000) | Weapons / Mounts |
| E1 | `ANNEX_E` ($5000) | Torpedoes |
| F1 | `ANNEX_F` ($6000) | Missiles |
| G1 | `ANNEX_G1` ($7000) | Guns |
| G2 | `ANNEX_G2` ($8000) | Bombs |
| I1 | `ANNEX_I1` ($9000) | Loadouts |
| J1 | `ANNEX_J1` ($A000) | Magazines |
| M1 | `ANNEX_M1` ($B000) | Land installations |
| P1 | `ANNEX_P1` ($C000) | Satellites |
| Y1 | `ANNEX_Y1` ($D000) | Sonobuoys |
| Z1 | `ANNEX_Z1` ($E000) | Special weapons |
| Z2 | `ANNEX_Z2` ($F000) | Mines |

Every unit's `Annex` field (`ushort`) encodes both the annex type (upper 4 bits via `ANNEX_MASK = $F000`) and the platform ID within that annex. Use `IHCAnnexHelper.HCAnnexShift` to extract the type flag and the helper methods to look up the full platform record.

**Setup — declare interface globals, create in `ScenLoad` or `DBLoaded`:**

```pascal
uses uHCTypeDefs, uHCAnnexDefs, uHCAnnexHelpers;

var
  gAnnexData: PHCAnnexes_disk;
  gHCAnnexHelper: IHCAnnexHelper;        // interface — ref-counted lifetime
  gHCAnnexMemHelper: IHCAnnexMemHelper;  // for in-memory annex data
  gHCannex_id_to_disk_id: THCannex_id_to_disk_id;  // captured from funcRefs

procedure ScenLoad(...; AnnexDataIn: PHCAnnexes_disk; ...);
begin
  gAnnexData := AnnexDataIn;
  gHCAnnexHelper := THCAnnexHelper.Create(
    AnnexDataIn, gHCannex_id_to_disk_id, verInterface, verSaveGame);
end;

procedure DBLoaded(AnnexDataIn: PHCAnnexes_disk;
  AnnexDataMemIn: PHCAnnexes_mem; ...);
begin
  if AnnexDataMemIn = nil then
  begin
    // First call — disk data only, good place to modify platform data
    gAnnexData := AnnexDataIn;
    gHCAnnexHelper := THCAnnexHelper.Create(
      AnnexDataIn, gHCannex_id_to_disk_id, verInterface, verSaveGame);
  end
  else
  begin
    // Second call — memory data now available
    gHCAnnexMemHelper := THCAnnexMemHelper.Create(
      AnnexDataMemIn, gHCannex_id_to_disk_id);
  end;
end;
```

**Look up a platform by unit Annex ID (via interface):**

```pascal
var
  pShip: PHCAnnexA1_disk;
  pSub: PHCAnnexA2_disk;
  pPlane: PHCAnnexB1_disk;
  pBase: PHCAnnexP1_disk;
begin
  case gHCAnnexHelper.HCAnnexShift(pUnit^.Annex) of
    SHIP_FLAG: begin
      pShip := gHCAnnexHelper.HCGetAnnexA1(pUnit^.Annex);
      // pShip^.ClassName, pShip^.MaxSpeed, pShip^.Displacement, etc.
    end;
    SUB_FLAG: begin
      pSub := gHCAnnexHelper.HCGetAnnexA2(pUnit^.Annex);
      // pSub^.ClassName, pSub^.MaxSpeed, etc.
    end;
    PLANE_FLAG: begin
      pPlane := gHCAnnexHelper.HCGetAnnexB1(pUnit^.Annex);
      // pPlane^.ClassName, pPlane^.MaxSpeed, etc.
    end;
    BASE_FLAG: begin
      pBase := gHCAnnexHelper.HCGetAnnexP1(pUnit^.Annex);
      // pBase^.Name, etc.
    end;
  end;
end;
```

**Iterate all platforms in an annex (e.g., all ships):**

```pascal
var
  i: NativeUInt;
  pShip: PHCAnnexA1_disk;
begin
  if gAnnexData <> nil then
    for i := 0 to gAnnexData^.AnnexDiskRecCount[SHIP_FLAG] - 1 do
    begin
      pShip := gAnnexData^.PA1_DISK_PTR_LIST + i;
      // pShip^.DiskId, pShip^.ClassName, ...
    end;
end;
```

**Convert between disk and memory IDs:**

```pascal
var
  diskId, annexId: ID;
begin
  // Unit's Annex field is an in-memory ID
  diskId := gHCannex_id_to_disk_id(pUnit^.Annex);  // for serialization
  annexId := gHCdisk_id_to_annex_id(diskId);         // back to memory ID
end;
```

### Writing Back to HC — What's Mutable

The pointers HC passes to ExportDLLs point into **live game memory**. Annex records, group fields, and unit fields can be written to directly — changes take effect immediately in the GE. This is powerful but requires care: there are no validation checks, no undo, and corrupted data can crash HC.

**Best practice: use HarpoonEvents when possible.** The GE's event system (`QueueHarpoonEvent`) is the sanctioned way to change game state — it goes through the same code path as player actions in the native UI, so HC handles all the side effects (speed/altitude transitions, AI responses, staff messages, etc.) correctly. Direct field writes bypass this logic and can leave the game in inconsistent states. Reserve direct writes for cases where no suitable event type exists or where you are correcting data before the game loop starts (e.g., annex patching in `DBLoaded`).

**Annex records are writable.** The `HCGetAnnex*` methods return pointers to the actual annex data in memory. Writing to these records modifies the platform database for the current session. The best time to do this is during `DBLoaded` (first call, before `AnnexDataMem` is derived) so the GE's derived data stays consistent. There is no event equivalent for annex changes — direct writes are the only option.

Example from ErrataBS2 — patching a weapon mount and loadout at scenario load:

```pascal
procedure DBLoaded(AnnexDataIn: PHCAnnexes_disk;
  AnnexDataMemIn: PHCAnnexes_mem; intBSNum: shortint; ...);
var
  pMnt: PHCAnnexZ1_disk;
  pLoadout: PHCAnnexY1_disk;
begin
  if AnnexDataMemIn = nil then  // first call — disk data, before mem is derived
  begin
    gHCAnnexHelper := THCAnnexHelper.Create(AnnexDataIn, ...);

    // Patch SA-N-9 launcher to use correct weapon ID
    pMnt := gHCAnnexHelper.HCGetAnnexZ1(59713);
    pMnt^.WepId := 18477;

    // Add EW loadout to Badger J patrol configuration
    pLoadout := gHCAnnexHelper.HCGetAnnexY1(55507);
    pLoadout^.WepId[0] := 34820;   // ALQ-99 EW pod
    pLoadout^.WepQty[0] := 3;
  end;
end;
```

**Group fields are writable.** Direct writes to `PHCGroup` fields change group state. Prefer `QueueHarpoonEvent` for speed, altitude, and sensor changes where a matching event type exists (e.g., `ALT_SPEED_EVENT`, `SENSOR_EVENT`). Direct writes are appropriate for fields that have no event equivalent (side, position) or for effect overrides like `Effect00` where the DLL is replacing HC's own movement logic:

```pascal
// Change group side (Toolbox "defect" and "attack" features)
PGroup^.side := NEUTRALSIDE;

// Set group speed
PGroup^.speed := 0;
PGroup^.kts := newKts;

// Modify course track (allocate via NewPtr, link into track list)
PGroup^.track := PNewPathLeg;
PGroup^.track^.DestLat := targetLat;
PGroup^.track^.DestLon := targetLon;
```

**Unit fields are writable.** Direct writes to `PHCUnit` fields change unit state. The same caveat applies — prefer events when a matching type exists. Direct writes shown here are from the Toolbox which operates as a development/testing tool where bypassing the event system is intentional:

```pascal
// Reposition a unit within its group
PUnit^.sLon := PGroup^.sLon + offset;
PUnit^.sLat := PGroup^.sLat + offset;

// Change altitude band (validated against platform max)
pPlane := gHCAnnexHelper.HCGetAnnexB1(PUnit^.Annex);
if newAltBand <= pPlane^.MaxAlt then
  PUnit^.AltBand := newAltBand;

// Set unit speed (must also update group kts)
PUnit^.kts := pPlane^.SpeedLevels[throttle];
if PHCGroup(PUnit^.Group)^.kts > PUnit^.kts then
  PHCGroup(PUnit^.Group)^.kts := PUnit^.kts;

// Change aircraft throttle
PUnit^.Air^.Throttle := TH_CRUISE;

// Reassign aircraft home unit
PUnit^.Air^.HomeUnit := PNewHomeUnit;
```

**HarpoonEvents are the preferred way to change game state.** Allocate with `NewPtr`, populate the event fields, and inject with `QueueHarpoonEvent`. The GE processes the event through its normal code path — the same path as player UI actions — ensuring all side effects (AI responses, staff messages, state transitions) are handled correctly:

```pascal
var
  PNewEvent: PHCHarpoonEvent;
begin
  PNewEvent := PHCHarpoonEvent(gNewPtr(SizeOf(THCHarpoonEvent)));
  PNewEvent^.event_type := AIR_REFUEL_EVENT;
  PNewEvent^.time_type := TimeDelta;
  PNewEvent^.Time := deltaSeconds;
  PNewEvent^.ourGroup := Pointer(pGroup);
  PNewEvent^.uuid := GenerateUUID;
  PNewEvent^.next := nil;
  gHCQueueHarpoonEvent(PNewEvent);
end;
```

**Summary of mutable state:**

| What | Preferred | Direct Write (fallback) | Example DLL |
|---|---|---|---|
| Speed/altitude/sensors | `QueueHarpoonEvent` (`ALT_SPEED_EVENT`, `SENSOR_EVENT`) | `PGroup^.kts`, `PUnit^.AltBand` | Toolbox |
| Course track | `QueueHarpoonEvent` (`ALT_COURSE_EVENT`) or `NewPtr` + link | `PGroup^.track` | MapWindow, Toolbox |
| Harpoon events (general) | `NewPtr` + `QueueHarpoonEvent` | n/a | Toolbox, lazFormation |
| Annex records (platform DB) | n/a (no event equivalent) | Write to `HCGetAnnex*` fields in `DBLoaded` | ErrataBS2 |
| Group side | n/a (no event equivalent) | `PGroup^.side := ...` | Toolbox, lazTurncoat |
| Group/unit position | Effect override (`Effect00`) | `PGroup^.sLon/sLat`, `PUnit^.sLon/sLat` | lazMoveUnits |
| Aircraft state | n/a | `PUnit^.Air^.Throttle/HomeUnit/...` | Toolbox |

---

## Reference

### Shared Pascal Units

All ExportDLLs share type definitions from `HC/shared/pascal/`:
https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/
| Unit | Role |
|---|---|
| [`uHCDefines.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCDefines.pas) | Constants: sides (`BLUESIDE`=0, `REDSIDE`=1, `GREENSIDE`=2, `UNKNOWNSIDE`=3), marker types (`MT_SHIP`, `MT_SUB`, ...), visibility flags (`SideKnown`, `ShowExact`, `ShowUncertain`), event types, altitude bands, sensor bitmasks, sliver constants |
| [`uHCTypeDefs.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCTypeDefs.pas) | Packed record types: `THCGroup`, `THCUnit`, `THCPathRec`, `THCHarpoonEvent`, `THCVictoryConditions`, `THCDLL_FUNC_REF`, callback function types (`THCNewPtr`, `THCQueueHarpoonEvent`, ...) |
| [`uHCGCI.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCGCI.pas) | Geographic calculations: `HCNewPos`, `HCDegToSliver`, `SliverToDegLatF`, `SliverToDegLonF`, `HCGetRangeBearing`, `HCFindRangeBearing` |
| [`uHCGlobals.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCGlobals.pas) | Global state |
| [`uHCAnnexDefs.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCAnnexDefs.pas) | Platform/unit database record definitions |
| [`uHCAnnexHelpers.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCAnnexHelpers.pas) | `IHCAnnexHelper` / `IHCAnnexMemHelper` interfaces for type-safe annex access |
| [`uHCEventHelpers.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCEventHelpers.pas) | Harpoon event helper functions |
| [`uHCDateTime.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCDateTime.pas) | Date/time utilities |
| [`uHCUtils.pas`](https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk/uHCUtils.pas) | General utilities |

### Export Slot Table (68 slots from DLLExport.h)

HC maintains a fixed array of 68 function slots. Each slot maps to an export name that HC resolves via `GetProcAddress`. All slots are available at v45.

| Slot | Export Name | Type |
|------|-------------|------|
| 0 | `UnitLatLon` | notification |
| 1 | `UnitShowing` | notification |
| 2 | `UnitURegion` | notification |
| 3 | `UnitCourse` | notification |
| 4 | `UnitSpeed` | notification |
| 5 | `UnitAlt` | notification |
| 6 | `UnitShowingSide` | notification (GE) |
| 7 | `UnitSink` | notification |
| 8 | `GameTime` | notification |
| 9 | `TimeFactor` | notification |
| 10 | `MsgLog` | notification (GE) |
| 11 | `UnitToGroup` | notification |
| 12 | `UnitNew` | notification |
| 13 | `UnitFree` | notification |
| 14 | `ScenLoad` | notification |
| 15 | `UnitWinUnit` | notification |
| 16 | `UnitWinSonoBuoy` | notification |
| 17 | `EffMoveUnits` | override (BOOLEAN) |
| 18 | `PDUnitPic` | notification |
| 19 | `NukeRelease` | override (BOOLEAN) |
| 20 | `DialogOpenSave` | override (BOOLEAN) |
| 21 | `StartGroup` | notification |
| 22 | `HarpoonEventHead` | notification |
| 23 | `SonobuoyRegionHead` | notification |
| 24 | `AllDeadUnitsPtr` | notification |
| 25 | `WeatherGroups` | notification (GE) |
| 26 | `VictoryConditionsList` | notification |
| 27-48 | `Effect00`..`Effect21` | override (BOOLEAN, GE) |
| 49 | `ActGroupPtr` | notification |
| 50 | `ActUnitPtr` | notification |
| 51 | `ChangeAlt` | override (BOOLEAN) |
| 52 | `Turn` | override (BOOLEAN) |
| 53 | `UnitTop` | notification |
| 54 | `SaveGame` | notification |
| 55 | `Effect22` | override (BOOLEAN, GE) |
| 56 | `HarpoonEventQueued` | notification |
| 57 | `VictoryConditionAchieved` | notification |
| 58 | `VictoryConditionLevels` | notification |
| 59 | `AssignGroupId` | action |
| 60 | `GroupFree` | override (BOOLEAN) |
| 61 | `ScenLoaded` | notification |
| 62 | `GroupAllocated` | notification |
| 63 | `DBLoaded` | notification |
| 64 | `SaveScen` | notification |
| 65 | `Staff` | override (BOOLEAN) |
| 66 | `ZoomChanged` | notification |
| 67 | `StoreConsumed` | notification |

**Types:**
- **notification** — HC calls every DLL that exports it, in load order
- **override (BOOLEAN)** — DLL returns TRUE to indicate it handled the event; HC may skip its default logic
- **action** — DLL is expected to perform a specific action (e.g., assign a group ID)
- **(GE)** — only available in Game Engine builds, not Scenario Editor

### Export Function Signatures

#### Identification & Lifecycle

```pascal
function DLLVer(): word; export; cdecl;
function DLLVerInt(): word; export; cdecl;
function DLLDesc(): PAnsiChar; export; cdecl;        // 255 chars max
procedure DLLFree(); export; cdecl;
```

#### Scenario Events

```pascal
procedure ScenLoad(strHarpPath: PAnsiChar; intScenNum: smallint;
  strScenFPath: PAnsiChar; intBSNum: shortint; strBSFName: PAnsiChar;
  intSide: byte; intGameTimeIn: Long; intGameTimeLeftIn: Long;
  AnnexDataIn: PHCAnnexes_disk; DatabaseVersionDBIn: PHCDATABASE_VERSION;
  ScenDataIn: PHCSCENARIO_DATA; ScenSaveDataIn: PHCSCENARIO_SAVE_DATA;
  GlobalsIn: PHCGlobals); export; cdecl;

procedure ScenLoaded(intWhichRound: Long); export; cdecl;
  // Called twice: SCENLOADED_INTERNAL_DONE (0), then SCENLOADED_EXPORTDLLS_DONE (1)

procedure DBLoaded(AnnexDataIn: PHCAnnexes_disk; AnnexDataMemIn: PHCAnnexes_mem;
  intBSNum: shortint; DatabaseVersionDBIn: PHCDATABASE_VERSION); export; cdecl;
  // Called twice: first with AnnexDataMemIn=nil (disk only), then with both

procedure SaveGame(FileNameIn: PAnsiChar; ScenSaveDataIn: PHCSCENARIO_SAVE_DATA;
  SaveGameType: uchar); export; cdecl;
  // SaveGameType: Normal=0, AutoSave=1, ExportDLL=2

procedure SaveScen(FileNameIn: PAnsiChar; ScenDataIn: PHCSCENARIO_DATA); export; cdecl;
  // Scenario Editor context
```

#### Game Tick

```pascal
procedure GameTime(GameTime: Long; GameTimeLeft: Long); export; cdecl;
  // Called each game second. Both params in seconds from GAMESTARTTIME (100,000).

procedure TimeFactor(TimeFactor: longint); export; cdecl;
  // Called when time compression changes. 1 = real time, higher = faster.
```

#### Group & Unit Pointers

```pascal
procedure StartGroup(StartGroup: PHCGroup); export; cdecl;
  // Head of group linked list. Walk toSouth to iterate. Live memory — HC thread only.

procedure ActGroupPtr(ActGroup: PHCGroup); export; cdecl;  // nil = deselected
procedure ActUnitPtr(ActUnit: PHCUnit); export; cdecl;      // nil = deselected
procedure WeatherGroups(WeatherGroups: PHCGroup); export; cdecl;
procedure UnitTop(TopUnit: PHCUnit); export; cdecl;
```

#### Unit-Level Callbacks

`UnitID` is the unit's `id` field (byte), `GroupID` is the group's `id` field (PAnsiChar, 8 chars).

```pascal
procedure UnitLatLon(UnitID: byte; GroupID: PAnsiChar;
  Lat: double; Lon: double); export; cdecl;                          // degrees
procedure UnitShowing(UnitID: byte; GroupID: PAnsiChar;
  Showing: byte); export; cdecl;                                      // bitmask: SideKnown|ShowExact|ShowUncertain
procedure UnitShowingSide(UnitID: byte; GroupID: PAnsiChar;
  showing: PTSidesUChar); export; cdecl;                              // per-side visibility array
procedure UnitCourse(UnitID: byte; GroupID: PAnsiChar;
  course: smallint); export; cdecl;                                   // degrees 0-359
procedure UnitSpeed(UnitID: byte; GroupID: PAnsiChar;
  speed: smallint); export; cdecl;                                    // knots
procedure UnitAlt(UnitID: byte; GroupID: PAnsiChar;
  alt: byte; Altitude: Int64); export; cdecl;                         // alt band + meters
procedure UnitURegion(UnitID: byte; GroupID: PAnsiChar;
  pUR: PTURegion); export; cdecl;                                     // HC allocates temp copies
procedure UnitSink(UnitID: byte; GroupID: PAnsiChar;
  UUID: HCUUID; AnnexID: ID); export; cdecl;
procedure UnitFree(UnitID: byte; GroupID: PAnsiChar;
  UUID: HCUUID; AnnexID: ID); export; cdecl;
procedure UnitNew(UnitID: byte; GroupID: PAnsiChar; utype: shortint;
  side: byte; annexID: word; Name: PAnsiChar; UUID: HCUUID); export; cdecl;
procedure UnitToGroup(UnitID: byte; GroupID, GroupIDold: PAnsiChar); export; cdecl;
  // GroupIDold is empty string if no previous group
```

#### Group Management

```pascal
procedure GroupAllocated(pGroup: PHCGroup); export; cdecl;
  // Group allocated, UUID set, but no group ID yet

procedure AssignGroupId(pGroup: PHCGroup); export; cdecl;
  // GE expects an ID to be assigned if this export exists — don't export unless you assign IDs

function GroupFree(pGroup: PHCGroup): CBOOL; export; cdecl;
  // Return CFALSE normally
```

#### Harpoon Events

```pascal
procedure HarpoonEventHead(HarpEventHead: PHCHarpoonEvent); export; cdecl;
  // Updated soonest event pointer. Events are time-ordered.

procedure HarpoonEventQueued(HarpEvent: PHCHarpoonEvent); export; cdecl;
  // Called each time a new event is queued. See uHCDefines.pas for event type constants.
```

#### Victory Conditions

```pascal
procedure VictoryConditionsList(pVictoryConditionsList: PHCVictoryConditions); export; cdecl;
procedure VictoryConditionAchieved(VictoryCondition: PHCVictoryConditions); export; cdecl;
procedure VictoryConditionLevels(whichSide: uchar; enemySide: uchar; vicLevel: uchar); export; cdecl;
  // vicLevel: VIC_MIN=1, VIC_TOT=2, VIC_GAME_OVER=3
```

#### Scenario State Pointers

```pascal
procedure SonobuoyRegionHead(SonobuoyRegionHead: PHCSonobuoyRegion); export; cdecl;
procedure AllDeadUnitsPtr(AllDeadUnitsPtr: PHCDeadListItem); export; cdecl;
```

#### User Interface

```pascal
procedure MsgLog(MsgLine: PAnsiChar); export; cdecl;          // GE only

function Staff(side: short; button: short; picnum: short; message: pchar;
  unit_involved: PHCUnit; outcome: plong): CBOOL; export; cdecl;
  // Return CTRUE if handled (prevents HC from showing Staff Assistant dialog)

procedure ZoomChanged(GroupZoom: short; UnitZoom: short); export; cdecl;
```

#### Movement & Altitude Overrides

```pascal
function Turn(pGroup: PHCGroup; pUnit: PHCUnit;
  oldcourse: short; newcourse: short): CBOOL; export; cdecl;
  // Return CTRUE if handled. Multiple DLLs can handle same turn.

function ChangeAlt(pGroup: PHCGroup; pUnit: PHCUnit;
  newalt: Int64): CBOOL; export; cdecl;
  // Return CTRUE if handled. newalt in meters.

function EffMoveUnits(topGroup: PHCGroup; WeathGroups: PHCGroup;
  BattleSetVars2: Pointer; BattleSetVars3: Pointer): CBOOL; export; cdecl;
  // Override unit movement for one game second. First DLL to return TRUE wins.
```

#### Combat

```pascal
procedure StoreConsumed(pLaunchUnit: PHCUnit; pLaunchMount: PHCMntRec;
  ourMount: ushort; mountIndex: uchar; pQty: ushort; pWeapUnit: PHCUnit;
  pTargetUnit: PHCUnit; pTargetGroup: PHCGroup); export; cdecl;

function NukeRelease(Msg: PAnsiChar): CBOOL; export; cdecl;
  // Called when nuclear release is granted. Return CTRUE to handle notification.
```

#### Unit Window & Platform Data

```pascal
procedure UnitWinUnit(UnitWinUnit: PHCUnitWinUnit); export; cdecl;
procedure UnitWinSonoBuoy(UnitWinSonoBuoy: PHCSonobuoyRegion); export; cdecl;
procedure PDUnitPic(hwndPD: HWND; intPicID: short; strClassName: PAnsiChar); export; cdecl;
  // Only called for HCDB databases in Common DB battlesets
```

#### Dialog Overrides

```pascal
function DialogOpenSave(FileName: PAnsiChar; PathInitial: PAnsiChar;
  Filter: PAnsiChar; DefExt: PAnsiChar; Title: PAnsiChar;
  IsOpenDlg: uchar): CBOOL; export; cdecl;
  // Return CTRUE if handled. First DLL wins.
```

#### Effect Overrides (GE only)

```pascal
function Effect00(): short; export; cdecl;   // MoveUnits()
function Effect01(): short; export; cdecl;   // eff200(FALSE) — navigation by path
function Effect02(): short; export; cdecl;   // eff111() — patrolling & placekeeping
function Effect03(): short; export; cdecl;   // <unused in stock HC>
function Effect04(): short; export; cdecl;   // Search()
function Effect05(): short; export; cdecl;   // eff102() — NewAreaDefense()
function Effect06(): short; export; cdecl;   // eff100() — Missile Guidance
function Effect07(): short; export; cdecl;   // <unused in stock HC> Gun fire
function Effect08(): short; export; cdecl;   // eff103() — Repair checks
function Effect09(): short; export; cdecl;   // eff104() — Fire & Flooding checks
function Effect10(): short; export; cdecl;   // eff107() — Aircraft readying & repair
function Effect11(): short; export; cdecl;   // eff108() — Aircraft VLow check
function Effect12(): short; export; cdecl;   // eff115() — Sensor Intelligence (satellites)
function Effect13(): short; export; cdecl;   // eff114() — Autonomous Submarine AI
function Effect14(): short; export; cdecl;   // eff113() — Ship movement AI
function Effect15(): short; export; cdecl;   // eff112() — Base AI (installations)
function Effect16(): short; export; cdecl;   // eff101()/eff116() — Aircraft AI
function Effect17(): short; export; cdecl;   // eff201(FALSE) — Check weather range
function Effect18(): short; export; cdecl;   // ProcessEvents() — Process Harpoon Events
function Effect19(): short; export; cdecl;   // CheckVictory()
function Effect20(): short; export; cdecl;   // CheckGroupStaff()
function Effect21(): short; export; cdecl;   // CheckGroupFuel()
function Effect22(): short; export; cdecl;   // CheckAirHomeUnit()
```

Return `CTRUE` if the DLL handled the effect (HC skips its default logic). Effect03 and Effect07 are unused in stock HC but available for mods. First DLL to handle takes precedence. Stock implementations are also available as **callbacks** via `funcRefs`.

### HC Callback Functions (funcRefs)

**Memory & Lifecycle:**

| funcName | Type | Purpose |
|---|---|---|
| `NewPtr` | `function(size: DWORD): Pointer; cdecl;` | Allocate memory via HC's heap |
| `DispPtr` | `procedure(toDispos: Pointer); cdecl;` | Free memory allocated by `NewPtr` |
| `free_Unit` | `procedure(pUnit: PHCUnit); cdecl;` | Free a unit structure |
| `free_Group` | `procedure(pGroup: PHCGroup); cdecl;` | Free a group structure |

**Event Queue:**

| funcName | Type | Purpose |
|---|---|---|
| `QueueHarpoonEvent` | `procedure(HarpoonEventPtr: PHCHarpoonEvent); cdecl;` | Inject a game event into HC's event queue |
| `del_HarpoonEvent_list` | `procedure(HarpEventList: Pointer); cdecl;` | Delete a harpoon event list |

**Group & Unit Manipulation:**

| funcName | Type | Purpose |
|---|---|---|
| `CreateGroup` | `function(side: uchar; sLon: long; sLat: long): PHCGroup; cdecl;` | Create a new group (allocs, assigns ID, adds to list) |
| `ResetGroupCenter` | `procedure(pGroup: PHCGroup); cdecl;` | Recalculate group center from its units |
| `SetCourseGroup_Raw` | `procedure(pGroup: PHCGroup; pCourse: short; pCallerName: PChar; pCallerFile: PChar; pCallerFileLine: long); cdecl;` | Set group heading |
| `SetCourseUnit_Raw` | `procedure(pUnit: PHCUnit; pCourse: short; pCallerName: PChar; pCallerFile: PChar; pCallerFileLine: long); cdecl;` | Set unit heading |
| `assign_group_id` | `function(pGroup: PHCGroup; pAllowExportDLL: CBOOL): long; cdecl;` | Assign a group ID |
| `ChangeActGroupPtr` | `function(pGroup: PHCGroup): PHCGroup; cdecl;` | Change the active (selected) group |
| `ChangeActUnitPtr` | `function(pUnit: PHCUnit): PHCUnit; cdecl;` | Change the active (selected) unit |
| `KilledOff` | `procedure(pUnit: PHCUnit); cdecl;` | Mark a unit as killed (GE only) |

**Map & Database:**

| funcName | Type | Purpose |
|---|---|---|
| `dllCheckLatLonDepth` | `function(lat: Double; lon: Double): short; cdecl;` | Get altitude band for a map position. Lat/lon in degrees |
| `annex_id_to_disk_id` | `function(annex_id: ID): ID; cdecl;` | Convert in-memory platform ID to on-disk ID |
| `disk_id_to_annex_id` | `function(disk_id: ID): ID; cdecl;` | Convert on-disk platform ID to in-memory ID |
| `GetLoadoutIndex` | `function(PlaneID: ID; LoadoutID: ID): uchar; cdecl;` | Get loadout index for a plane/loadout pair |

**UI & Logging:**

| funcName | Type | Purpose |
|---|---|---|
| `MessageLogLine` | `procedure(Message: PAnsiChar); cdecl;` | Write a line to HC's message log (GE only) |
| `ExitGame` | `procedure(ExitMessage: PAnsiChar); cdecl;` | Exit the current game with a message |
| `ZoomWindow` | `procedure(ZoomDir: short; GroupUnitWindow: short); cdecl;` | Zoom group or unit window (`GROUP_ACTIVE`=3, `UNIT_ACTIVE`=7) |
| `SaveGame` | `procedure(pSaveName: PAnsiChar; pAutoSave: uchar); cdecl;` | Trigger a save game |

**Scenario Object Creation:**

| funcName | Type | Purpose |
|---|---|---|
| `NewWeatherGroup` | `function(): PHCGroup; cdecl;` | Create a new weather group (GE only) |
| `CreateSonobuoy` | `function(): PHCSonobuoyRegion; cdecl;` | Create a new sonobuoy region (GE only) |
| `CreateVictoryCondition` | `function(): PHCVictoryConditions; cdecl;` | Create a new victory condition |

**Effect Functions (stock implementations, GE only):**

| funcName | Type | Purpose |
|---|---|---|
| `Effect00`..`Effect22` | `procedure(); cdecl;` | Call stock effect implementation directly |

Note: Effect01 and Effect17 take a `short` parameter. Effect03 and Effect07 have no stock implementation.

### Key Data Structures

#### THCGroup (packed record)

The group linked list is walked via `toSouth` (next) / `toNorth` (prev):

| Field | Type | Notes |
|---|---|---|
| `toNorth, toSouth` | `PHCGroup` | Linked list pointers |
| `Units` | `PHCUnit` | Head of unit linked list |
| `track` | `^THCPathRec` | Head of course waypoint linked list |
| `UncertaintyRegion` | `PURegion` | Uncertainty region (4 corner points) |
| `sLon, sLat` | `LongInt` | Position in slivers |
| `slivers` | `LongInt` | Heading in slivers |
| `speed` | `LongInt` | Speed in distance units |
| `kts` | `short` | Speed in knots |
| `course` | `short` | Heading in degrees (0-359) |
| `side` | `uchar` | Side (0=Blue, 1=Red, 2=Green) |
| `showing` | `uchar` | Visibility bitmask (`SideKnown`, `ShowExact`, `ShowUncertain`) |
| `gtype` | `HCMT` | Marker type (`MT_SHIP`, `MT_SUB`, etc.) |
| `id` | `array[0..7] of AnsiChar` | Group ID string (8 chars) |
| `UUID` | `HCUUID` | 37-char UUID |
| `BestWeaponsRange` | `array[0..1] of short` | [0]=anti-surface, [1]=anti-air range |
| `zones` | `array[0..63] of short` | Formation zone assignments |
| `SensorMode` | `uchar` | Active/passive sensor mode |
| `Flag` | `uchar` | Bitfield flags |

#### THCUnit (packed record)

Units within a group are linked via `prev`/`next`:

| Field | Type | Notes |
|---|---|---|
| `Group` | `PHCGroup` | Parent group |
| `prev, next` | `PHCUnit` | Unit linked list |
| `sLon, sLat` | `LongInt` | Position in slivers |
| `kts` | `short` | Speed in knots |
| `course` | `short` | Heading in degrees |
| `showing` | `array[0..2] of uchar` | Per-side visibility |
| `utype` | `HCMT` | Unit marker type |
| `Annex` | `ushort` | Platform database annex ID |
| `name` | `array[0..47] of AnsiChar` | Unit name |
| `UUID` | `HCUUID` | 37-char UUID |
| `Altitude` | `Int64` | Altitude in meters |
| `UncertaintyRegion` | `array[0..2] of PURegion` | Per-side uncertainty regions |

#### THCPathRec (course waypoints)

Singly-linked list from `Group^.track`:

| Field | Type | Notes |
|---|---|---|
| `Next` | `PHCPathRec` | Next waypoint |
| `DestLon, DestLat` | `LongInt` | Destination in slivers |

#### THCHarpoonEvent

Events are time-ordered. Event type is encoded in the first byte (masked with `CommandMask = $3F`). See event constants in `uHCDefines.pas`.

#### THCDLL_FUNC_REF

```pascal
THCDLL_FUNC_REF = packed record
  prev, next: Pointer;                        // linked list
  funcPtr: Pointer;                            // the callback function pointer
  funcName: packed array[0..254] of AnsiChar;  // null-terminated name
end;
```

#### IHCAnnexHelper Methods

| Method | Returns | Purpose |
|---|---|---|
| `HCAnnexShift(annexID)` | `NativeInt` | Extract annex type flag from annex ID |
| `HCIsDisk(annexID)` | `Boolean` | Check if annex ID is a disk ID |
| `HCdisk_id_to_annex_offset(annexID)` | `NativeInt` | Convert disk ID to array offset |
| `HCGetAnnexA1(annexID)` | `PHCAnnexA1_disk` | Ship platform record |
| `HCGetAnnexA2(annexID)` | `PHCAnnexA2_disk` | Submarine platform record |
| `HCGetAnnexB1(annexID)` | `PHCAnnexB1_disk` | Aircraft platform record |
| `HCGetAnnexC1(annexID)` | `PHCAnnexC1_disk` | Sensor record |
| `HCGetAnnexD1(annexID)` | `PHCAnnexD1_disk` | Weapon/mount record |
| `HCGetAnnexE1(annexID)` | `PHCAnnexE1_disk` | Torpedo record |
| `HCGetAnnexF1(annexID)` | `PHCAnnexF1_disk` | Missile record |
| `HCGetAnnexG1(annexID)` | `PHCAnnexG1_disk` | Gun record |
| `HCGetAnnexG2(annexID)` | `PHCAnnexG2_disk` | Bomb record |
| `HCGetAnnexI1(annexID)` | `PHCAnnexI1_disk` | Loadout record |
| `HCGetAnnexJ1(annexID)` | `PHCAnnexJ1_disk` | Magazine record |
| `HCGetAnnexM1(annexID)` | `PHCAnnexM1_disk` | Land installation record |
| `HCGetAnnexP1(annexID)` | `PHCAnnexP1_disk` | Satellite record |
| `HCGetAnnexY1(annexID)` | `PHCAnnexY1_disk` | Sonobuoy record |
| `HCGetAnnexZ1(annexID)` | `PHCAnnexZ1_disk` | Special weapon record |
| `HCGetAnnexZ2(annexID)` | `PHCAnnexZ2_disk` | Mine record |

`IHCAnnexMemHelper` provides the same pattern with `HCGetAnnexMemA1`, `HCGetAnnexMemA2`, etc., returning `_mem` record pointers for the in-memory representation.

### Interface Version History

<details>
<summary>Version history (for reference)</summary>

| Ver | HC Build | Change |
|-----|----------|--------|
| 7 | 2009.097 | Added funcRefs (QueueHarpoonEvent, etc.) |
| 9 | 2015.009 | Effect21 (CheckGroupFuel) |
| 11 | 2015.009 | ScenLoad: added dt_scen_start, GameTimeLeft |
| 12 | 2015.014 | ScenLoad: added AnnexData |
| 13 | 2015.015 | funcRefs: annex_id_to_disk_id |
| 15 | 2016.002 | ActGroupPtr, ActUnitPtr exports |
| 16 | 2016.011 | UnitAlt: added Altitude in meters |
| 17 | 2017.005 | Unit structure Method/emit/detect widened 16→64 bits |
| 18 | 2017.006 | Turn, ChangeAlt, UnitTop exports |
| 19 | 2017.007 | funcRefs: MessageLogLine |
| 20 | 2017.008 | UUIDs added to structures |
| 21 | 2017.011 | ScenData, ScenSaveData, SaveGame export |
| 22 | 2019.001 | Effect22 (CheckAirHomeUnit) |
| 23 | 2020.009 | funcRefs: assign_group_id |
| 24 | 2020.011 | DLLInit: srandSeed |
| 25 | 2020.012 | UnitSink/UnitFree: added UUID + AnnexID; UnitNew: added UUID |
| 26 | 2020.014 | DLLInit: license information |
| 27 | 2020.015 | DLLInit: RESRSRStructureVersion, AnnexVersion |
| 29 | 2021.009 | ScenLoad: added Globals |
| 30 | 2022.00# | AssignGroupId export |
| 31 | 2022.005 | GroupFree export |
| 32 | 2022.006 | ScenLoaded export |
| 33 | 2023.001 | GroupAllocated export |
| 34 | 2023.001 | Re-vamped HarpoonEvent structure (**min version for new DLLs**) |
| 35 | 2023.007 | funcRefs: free_Unit, free_Group; BSStrings: BroadVicType |
| 36 | 2023.009 | SaveGame: added SaveGameType parameter |
| 37 | 2025.002 | DBLoaded export |
| 38 | 2025.006 | funcRefs: ChangeActGroupPtr, ChangeActUnitPtr |
| 39 | 2025.007 | DBLoaded: added AnnexDataMem |
| 40 | 2025.008 | DLLInit: GameComponent |
| 41 | 2025.010 | SaveScen export |
| 42 | 2025.012 | Staff, ZoomChanged exports; DLLInit: pLosTable |
| 43 | 2025.013 | StoreConsumed export; funcRefs: ZoomWindow |
| 44 | 2025.020 | funcRefs: ExitGame |
| 45 | 2025.025 | DBLoaded: added BattleSetNumber |

</details>

---

## Appendix

### Setting Up Lazarus for ExportDLL Development

ExportDLLs are Win32 DLLs. This guide uses **fpcupdeluxe** to install a native 32-bit Free Pascal + Lazarus environment on Windows.

#### 1. Download fpcupdeluxe

Get the latest release from GitHub:
https://github.com/LongDirtyAnimAlf/fpcupdeluxe/releases

Download the Windows executable (e.g., `fpcupdeluxe-x86_64-win64.exe` or `fpcupdeluxe-i386-win32.exe`). Either will work — it's just the installer, not the compiler you'll be building with.

#### 2. Run fpcupdeluxe and Configure

1. Launch `fpcupdeluxe.exe`
2. Set the **Install directory** to `C:\Prog\lazarus` (suggested convention)
3. Under **FPC Version**, select `stable` (or a specific version >= 3.2.2)
4. Under **Lazarus Version**, select `stable` (must be 4.0 or newer; 4.4+ recommended)
5. For **CPU/OS Target**, select: **CPU = i386**, **OS = win32**
6. Click **Install/Update FPC+Laz**

This will download, compile, and install a native 32-bit Free Pascal compiler and Lazarus IDE. The process takes 15-30 minutes depending on your machine.

#### 3. Verify the Installation

1. Open Lazarus from the install directory
2. Go to **Tools > Options > Environment > Files** and confirm the compiler executable points to the 32-bit `fpc.exe`
3. Create a test project: **Project > New Project > Library** — this confirms the IDE can produce DLLs
4. Build it (F9 or Run > Build) — you should get a `.dll` file in the output directory

#### 4. Check Out Source Code

Projects follow this directory structure — the relative path between ExportDLL projects and the shared units matters because `.lpi` project files reference the shared units via `..\..\shared\pascal`:

```
C:\Prog\Projects\StratsimsOSS\HC\
├── shared\
│   └── pascal\          ← shared type definitions (all ExportDLLs depend on this)
└── ExportDLLs\
    ├── TimeCompressUse\ ← example: minimal ExportDLL
    ├── lazGUI\          ← example: comprehensive ExportDLL
    ├── MapWindow\       ← example: globe map visualization
    └── MyNewDLL\        ← your new project goes here
```

Check out the shared units and an example project:

```bash
# Shared Pascal units — required by all ExportDLLs
svn checkout https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk C:\Prog\Projects\StratsimsOSS\HC\shared\pascal

# A minimal example — just TimeFactor + ScenLoad
svn checkout https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/TimeCompressUse/trunk C:\Prog\Projects\StratsimsOSS\HC\ExportDLLs\TimeCompressUse

# The most comprehensive example — all 68 export slots
svn checkout https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazGUI/trunk C:\Prog\Projects\StratsimsOSS\HC\ExportDLLs\lazGUI
```

#### 5. Configure Your Project

Open the `.lpi` file in Lazarus, then verify paths to the shared units:

1. **Project > Project Options > Compiler Options > Paths**
2. Confirm **Other unit files (-Fu)** includes the shared pascal path.
   Existing projects use `..\..\shared\pascal` — this works when your project is at `HC\ExportDLLs\YourDLL\` and shared units are at `HC\shared\pascal\`.
   For a new project, add the relative or absolute path to your shared pascal checkout.
3. Under **Compiler Options > Config and Target**:
   - **Target CPU family**: i386
   - **Target OS**: win32
4. Under **Compiler Options > Compilation and Linking**:
   - Confirm **Generate a DLL** (or that the project type is Library)
5. Set the **Output directory** to your HC ExportDLLs folder:
   e.g., `C:\Games\HC\ExportDLLs\MyDLL`

#### 7. Build and Test

1. Build the project: **Run > Build** (or `Ctrl+Shift+F9`, or `F9`)
2. The compiled `.dll` appears in your output directory
3. Launch HC (`Winharp32.exe`) — it will load your DLL automatically from the ExportDLLs directory
4. Check HC's message log or your DLL's output to confirm it loaded

#### 8. Debugging (Optional)

To debug your DLL running inside HC:

1. **Run > Run Parameters**
2. Set **Host application** to the HC executable (e.g., `C:\Games\HC\Winharp32.exe`)
3. Set **Working directory** to the HC directory (e.g., `C:\Games\HC`)
4. Set breakpoints in your code
5. Press **F9** — Lazarus launches HC, and your breakpoints will hit when HC calls your DLL exports

#### Tips

- **DLL locked during build**: If you get a build error (access denied / error code 5), HC is running with your DLL loaded. Close HC before rebuilding.
- **Multiple Lazarus installs**: You can have both 32-bit and 64-bit Lazarus installed side by side via fpcupdeluxe — just use different install directories.
- **Command-line builds**: Use `lazbuild` for CI or scripted builds:
  ```bash
  /path/to/lazbuild --build-all MyExportDLL.lpi
  ```
- **New project from scratch**: Start with **Project > New Project > Library**, add the shared unit path, and implement the exports from the [Quick Start](#quick-start-minimal-exportdll) template.
- **Existing Delphi projects**: Lazarus can open `.dpr` files. You may need to add `{$mode delphi}` or `{$mode objfpc}{H+}` and adjust uses clauses for LCL vs VCL differences.

---

### Existing ExportDLL Projects

All projects live under `HC/ExportDLLs/` and share types from `HC/shared/pascal/`. "Core" = DLLVer + DLLVerInt + DLLDesc + DLLFree + DLLInit.

#### Visualization & UI

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[lazGUI](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazGUI/trunk)** | 38 | 45 | Development/debugging GUI — most comprehensive | All 68 export slots |
| **[MapWindow](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/MapWindow/trunk)** | 14 | 34 | Globe map with TGlobe4; player/god visibility; waypoint editing | Core + ScenLoad + GameTime + StartGroup + TimeFactor |
| **[USNI](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/USNI/trunk)** | 2 | 22 | USNI platform promotion via embedded Chromium browser | Core + PDUnitPic + ActUnitPtr + ActGroupPtr + ScenLoad |

#### Data Collection & Metrics

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[EduMetrics](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/EduMetrics/trunk)** | 4 | 36 | Education metrics via HTTP POST (SHA3 hashed serials, JSON) | Core + GameTime |
| **[GameStatusDLL](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/GameStatus/trunk)** | 10 | 11 | Kill counts by type/side via memory-mapped file IPC | Core + ScenLoad + GameTime + AllDeadUnitsPtr |
| **[ConflictDirectCost](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/ConflictDirectCost/trunk)** | 1 | 21 | Human/monetary cost of conflict via UDP multicast | Core + ScenLoad + GameTime + AllDeadUnitsPtr |
| **[TimeCompressUse](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/TimeCompressUse/trunk)** | 1 | 2 | Tracks real-time spent at each time compression level | Core + TimeFactor + ScenLoad |

#### Event Publishing & Integration

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[HCMQTTLink](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazHarpoonEventsMQTT/trunk)** | 4 | 25 | Publish events to MQTT broker via WM_COPYDATA to HCMQTTSender.exe | Core + ScenLoad + GameTime + TimeFactor + HarpoonEventQueued + Units + SaveGame |
| **[ActUnitReport](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/ActUnitReport/trunk)** | 3 | 34 | Reports on active unit selection with event details | Core + ScenLoad + GameTime + HarpoonEventHead + ActUnitPtr + StartGroup |

#### Save Game Management

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[AutoSaves](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/AutoSaves/trunk)** | 3 | 36 | Auto-save move/copy/leave management with UUID lineage tracking | Core + ScenLoad + SaveGame + GameTime |

#### Game Mechanics & AI

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[lazHCEffects](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazHCEffects/trunk)** | 3 | 34 | Stock game effect overrides (all 23 effects) | Core + Effect00..Effect22 |
| **[lazFormation](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazFormation/trunk)** | 1 | 21 | Formation keeping with TDictionary-based unit tracking | Core + ScenLoad + GameTime + StartGroup + Effect02 + UnitFree + many state pointers |
| **[lazMoveUnits](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazMoveUnits/trunk)** | 1 | 21 | Custom unit movement override | Core + ScenLoad + GameTime + StartGroup + Effect00 |
| **[lazTurncoat](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazTurncoat/trunk)** | 5 | 34 | Side-switching mechanics | Core + ScenLoad + GameTime + StartGroup + UnitSink + MsgLog + HarpoonEventHead |
| **[GroupIdManager](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/GroupIdManager/trunk)** | 9 | 34 | Custom group ID allocation per side | Core + ScenLoad + GameTime + StartGroup + AssignGroupId + GroupFree + ScenLoaded |

#### Errata / Corrections

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[ErrataGE](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/ErrataGE)** | 1 | 43 | Corrects formation/airPat mismatches at game start | Core + StartGroup + HarpoonEventHead + ScenLoaded |
| **[ErrataBS2](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/ErrataGE)** | 1 | 45 | Corrects unit data and loadouts for specific battle sets | Core + ScenLoad + StartGroup + DBLoaded |
| **[ErrataV](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/ErrataGE)** | 1 | 34 | Corrects event/unit group mismatches via HarpoonEventHead | Core + ScenLoad + StartGroup + HarpoonEventHead |

#### Testing

| Project | DLLVer | IntVer | Purpose | Key Exports |
|---|---|---|---|---|
| **[TestGroupIds](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/TestGroupIds/trunk)** | 2 | 34 | Testbed for group ID creation; uses CreateGroup callback | Core + ScenLoad + GameTime + StartGroup + MsgLog |
| **[Toolbox](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/Toolbox/trunk)** | 26 | 17 | Multi-tool dev interface; air refueling; event inspection | Core + ScenLoad + GameTime + StartGroup + MsgLog + HarpoonEventHead |
| **[CourseRHA](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/CourseRHA/trunk)** | 1 | 16 | Set group heading/altitude/speed/distance | Core + ScenLoad + GameTime + StartGroup |

#### Companion Applications (not ExportDLLs)

| Project | Purpose |
|---|---|
| **[HCMQTTSender](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/lazHarpoonEventsMQTT/trunk)** | Receives WM_COPYDATA from HCMQTTLink, publishes to MQTT broker |
| **[HCGameStatusViewer](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/GameStatus/trunk)** | FMX viewer for GameStatusDLL shared memory data |
| **[HCConflictDirectCostViewer](https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/ConflictDirectCost/trunk)** | Viewer for ConflictDirectCost data |

#### IPC Patterns in Use

| Pattern | Used By |
|---|---|
| Memory-mapped file (`TTGPMemMappedFile`) | GameStatusDLL → HCGameStatusViewer |
| UDP multicast | ConflictDirectCost → viewer |
| WM_COPYDATA to companion .exe | HCMQTTLink → HCMQTTSender → MQTT |
| HTTP POST (JSON) | EduMetrics → harpgamer.com |
| INI/TXT file logging | TimeCompressUse |
| Dedicated form thread + snapshot messaging | MapWindow |

---

### C Quick Start

Equivalent of the Pascal quick start for C/C++ developers. Struct layouts match `DLLExport.h`.

```c
// MyExportDLL.c
// Compile: cl /LD MyExportDLL.c /link /DEF:MyExportDLL.def

#include <windows.h>

#define DLLVERN    1
#define DLLVERINTN 45

// HC type aliases (from DLLExport.h / typedefs.h)
typedef unsigned char  uchar;
typedef unsigned short ushort;
typedef unsigned long  ulong;
typedef short          ID;

#pragma pack(push, 1)
typedef struct S_DLL_FUNC_REF {
    struct S_DLL_FUNC_REF *prev, *next;
    int (FAR __cdecl *funcPtr)();
    char funcName[255];
} DLL_FUNC_REF;
#pragma pack(pop)

typedef void *GroupPtr;

// Globals captured during DLLInit
static void *gPDLLBusy = NULL;
static void *gPGEClosing = NULL;
static GroupPtr gStartGroup = NULL;

// --- Exports ---

__declspec(dllexport) unsigned short __cdecl DLLVer(void) {
    return DLLVERN;
}

__declspec(dllexport) unsigned short __cdecl DLLVerInt(void) {
    return DLLVERINTN;
}

__declspec(dllexport) char * __cdecl DLLDesc(void) {
    return "My C ExportDLL";
}

__declspec(dllexport) void __cdecl DLLFree(void) {
    /* cleanup */
}

__declspec(dllexport) void __cdecl DLLInit(
    HWND hwndHC, char *pathHC, char *verHC,
    unsigned long verSaveGame, DLL_FUNC_REF *funcRefs,
    void *PGEClosing, void *PDLLBusy, unsigned long verInterface,
    unsigned long srandSeed, char *harpoon_uuid_session,
    __int64 hclicense_dt_expire, char *hclicense_licensee,
    char *usblicense_desc, ulong RESRSRStructureVersion,
    ushort AnnexVersion, uchar GameComponent, void *pLosTable)
{
    gPDLLBusy = PDLLBusy;
    gPGEClosing = PGEClosing;

    /* Walk funcRefs linked list to capture callbacks */
    DLL_FUNC_REF *f = funcRefs;
    while (f != NULL) {
        /* if (!strcmp(f->funcName, "NewPtr")) gNewPtr = f->funcPtr; */
        /* if (!strcmp(f->funcName, "QueueHarpoonEvent")) ... */
        f = f->next;
    }
}

__declspec(dllexport) void __cdecl GameTime(long gameTime, long gameTimeLeft) {
    /* called each game tick */
}

__declspec(dllexport) void __cdecl StartGroup(GroupPtr startGroup) {
    gStartGroup = startGroup;
}
```

Module definition file (for clean export names without decoration):

```def
; MyExportDLL.def
LIBRARY MyExportDLL
EXPORTS
    DLLVer
    DLLVerInt
    DLLDesc
    DLLFree
    DLLInit
    GameTime
    StartGroup
```

---

### Sources

- ExportDLL interface definition: `C:\Prog\Projects\HC\Common\Include\DLLExport.h` and `C:\Prog\Projects\HC\Common\SRC\DLLExport.c`
- Shared Pascal types: `https://tarzan.tgp.net/svn/StratsimsOSS/HC/shared/pascal/trunk`
- ExportDLL implementations: `https://tarzan.tgp.net/svn/StratsimsOSS/HC/ExportDLLs/`
