{$mode objfpc}{$H+}
unit uThreadComm;

// CC 20260323 shared state and synchronization between HC thread and MapWindow form thread

interface

uses
  Windows, SysUtils,
  uHCTypeDefs, uHCDefines, uHCGCI, uHCAnnexDefs, uHCAnnexHelpers,
  uGroupSnapshot, uFormationMath;

const
  WM_APPLY_SNAPSHOT = WM_USER + 100;
  WM_APPLY_CHUNK    = WM_USER + 103;   // CC 20260323 chunked processing; wParam = start index
  WM_SCEN_LOAD      = WM_USER + 101;
  WM_SHOW_FORM      = WM_USER + 102;
  CHUNK_SIZE         = 25;              // CC 20260323 groups per chunk
  BATCH_COOLDOWN_MS  = 100;             // CC 20260323 minimum ms between batch completions
  TIMER_NEXT_BATCH   = 1;               // CC 20260323 timer ID for batch cooldown

type
  THCCallbackKind = (
    cbkAppendCoursepoint,
    cbkInsertCoursepoint,
    cbkDeleteCoursepoint,
    cbkClearCourse,
    cbkDragCommit,
    // CC 20260924 formation editing (phase 2), queued as one batch per Apply
    cbkSetFormationZones,    // Zones/RingCount -> group zones[]
    cbkSlowGroupForReform,   // halve the group's speed like the GE editor's OK
    cbkSetUnitStation,       // UnitUUID -> XOff/YOff, Rings/Sectors via FORMATION_EVENT
    cbkSetPatrolArea,        // CC 20260924 PatrolUUID (AIR_PATROL_EVENT uuid) -> Rings/Sectors
    cbkCreatePatrol,         // CC 20260924 phase 4: new AIR_PATROL_EVENT from a deck asset
    cbkCancelPatrol          // CC 20260924 phase 4: PatrolUUID -> return now
  );

  THCCallbackRequest = record
    Kind: THCCallbackKind;
    GroupUUID: string;
    DestLon, DestLat: LongInt;
    WaypointIdx: Integer;       // used for insert-before, delete, drag
    // CC 20260924 formation editing
    UnitUUID: string;
    PatrolUUID: string;           // CC 20260924 cbkSetPatrolArea / cbkCancelPatrol
    // CC 20260924 phase 4 cbkCreatePatrol
    PlaneID, LoadID: Word;
    PatrolType: ShortInt;
    PatrolNumber: Byte;
    DelaySec: Integer;
    Zones: TFormationZones;
    RingCount: Integer;
    XOff, YOff: LongInt;
    Rings, Sectors: UInt64;
  end;

var
  // Synchronization
  gSnapshotCS: TRTLCriticalSection;
  gPendingCS: TRTLCriticalSection;

  // Pending HC callback requests (form thread writes, HC thread executes)
  gPendingRequests: array of THCCallbackRequest;
  gPendingCount: Integer;

  // Form thread window handle for PostMessage
  gFormHWND: HWND;

  // Shared snapshot buffer (HC thread writes under gSnapshotCS, form thread reads)
  gSharedSnapshot: TGameTimeSnapshot;
  gSnapshotGeneration: LongInt;       // CC 20260323 incremented each capture; form uses to coalesce
  gLastAppliedGeneration: LongInt;    // CC 20260323 last generation the form processed
  gSnapshotPending: LongInt;          // CC 20260323 0/1 flag — prevents WM_APPLY_SNAPSHOT flooding

  // Relocated from TfrmMain / mainform globals — accessible from both threads
  gPDLLBusy: Pointer;
  gPStartGroup: PHCGroup;
  gDLLPathFileName: string;
  gGameTime: LongInt;
  gGameTimeLeft: LongInt;
  gSide: byte;
  gTimeFactor: LongInt;               // CC 20260323 current time compression from HC

  // HC callback function pointers (called only from HC's thread)
  gNewPtr: THCNewPtr;
  gDisposPtr: THCDisposPtr;
  gHCQueueHarpoonEvent: THCQueueHarpoonEvent;
  gHCdllCheckLatLonDepth: THCdllCheckLatLonDepth;
  gHCMessageLogLine: THCMessageLogLine;
  gAnnex_id_to_disk_id: THCannex_id_to_disk_id; // CC 20260403 memory to disk ID

  // CC 20260403 Annex helper for endurance calculations (created in ScenLoad)
  gAnnexHelper: IHCAnnexHelper;
  gVerSaveGame: LongWord;
  gVerInterface: LongWord;

  // CC 20260924 head of HC's time-sorted HarpoonEvent list, kept current by the
  //  HarpoonEventHead export. HC thread only: written by that export, read by the
  //  capture inside GameTime. Never dereference it from the form thread.
  gHarpoonEventHead: PHCHarpoonEvent;

  // CC 20260924 phase 4: HC's game time, updated at the start of every GameTime
  //  (HC thread); an event moved to run "now" at the head gets this Time
  gGameTimeNow: LongInt;

  // CC 20260924 event type and group of gHarpoonEventHead, read when HC reported
  //  it (while it was certainly valid). HarpoonEventQueued compares against these
  //  instead of dereferencing a head HC may already have freed. HC thread only.
  gHeadEventType: LongWord;
  gHeadGroup: Pointer;

  // CC 20260924 UUID of the group whose units, deck assets and patrols are
  //  captured each tick ('' = none). Guarded by gSnapshotCS; written by the form
  //  thread through SetFocusGroupUUID, read once per capture on HC's thread.
  gFocusGroupUUID: string;

procedure InitThreadComm;
procedure DoneThreadComm;
procedure QueueHCCallback(const aRequest: THCCallbackRequest);
// CC 20260924 queue several requests under one lock so HC's thread never runs
//  half a batch (a formation Apply must slow the group before its station orders)
procedure QueueHCCallbacks(const aRequests: array of THCCallbackRequest);
procedure ExecutePendingHCCallbacks;
procedure SetFocusGroupUUID(const aUUID: string);
// CC 20260924 HC thread: an AIR_PATROL_EVENT's LoadoutIDX for a plane type and
//  loadout (port of the GE's GetLoadoutIndex)
function HCLoadoutIndex(aPlaneID, aLoadID: Word): Byte;
// CC 20260924 phase 3, HC thread: re-slave aircraft-group air units that have
//  reached a formation station (see WatchAirReform); cleared on scenario load
procedure WatchAirReform;
procedure ClearAirReformWatch;

implementation

uses
  uHCUtils;  // CC 20260924 GenerateUUID

// CC 20260323 walk the group linked list to find a group by UUID string
function FindGroupByUUID(const aUUID: string): PHCGroup;
var
  pGroup: PHCGroup;
begin
  Result := nil;
  pGroup := gPStartGroup;
  while pGroup <> nil do
  begin
    if String(pGroup^.UUID) = aUUID then
    begin
      Result := pGroup;
      Exit;
    end;
    pGroup := pGroup^.toSouth;
  end;
end;

// CC 20260323 walk the track linked list to the Nth node (0-based)
function WalkTrackToIndex(pGroup: PHCGroup; idx: Integer): PHCPathRec;
var
  pNode: PHCPathRec;
  i: Integer;
begin
  Result := nil;
  if pGroup = nil then
    Exit;
  pNode := pGroup^.track;
  i := 0;
  while (pNode <> nil) and (i < idx) do
  begin
    pNode := PHCPathRec(pNode^.next);
    Inc(i);
  end;
  Result := pNode;
end;

procedure InitThreadComm;
begin
  InitCriticalSection(gSnapshotCS);
  InitCriticalSection(gPendingCS);
  gPendingCount := 0;
  SetLength(gPendingRequests, 0);
  gFormHWND := 0;
end;

procedure DoneThreadComm;
begin
  DoneCriticalSection(gSnapshotCS);
  DoneCriticalSection(gPendingCS);
end;

// CC 20260924 form thread: choose the formation-edit focus group ('' clears)
procedure SetFocusGroupUUID(const aUUID: string);
begin
  EnterCriticalSection(gSnapshotCS);
  try
    gFocusGroupUUID := aUUID;
  finally
    LeaveCriticalSection(gSnapshotCS);
  end;
end;

procedure QueueHCCallback(const aRequest: THCCallbackRequest);
begin
  EnterCriticalSection(gPendingCS);
  try
    SetLength(gPendingRequests, gPendingCount + 1);
    gPendingRequests[gPendingCount] := aRequest;
    Inc(gPendingCount);
  finally
    LeaveCriticalSection(gPendingCS);
  end;
end;

procedure QueueHCCallbacks(const aRequests: array of THCCallbackRequest);
var
  i: Integer;
begin
  EnterCriticalSection(gPendingCS);
  try
    SetLength(gPendingRequests, gPendingCount + Length(aRequests));
    for i := 0 to Length(aRequests) - 1 do
    begin
      gPendingRequests[gPendingCount] := aRequests[i];
      Inc(gPendingCount);
    end;
  finally
    LeaveCriticalSection(gPendingCS);
  end;
end;

// ---------------------------------------------------------------------------
// CC 20260924 formation editing on HC's thread. Every pointer used here comes
//  from the ExportDLL interface (StartGroup -> groups -> Units; the
//  HarpoonEventHead export -> events), and HC memory is allocated/queued only
//  through the NewPtr and QueueHarpoonEvent funcRefs. Mirrors formedit.c
//  Formation_OnCommand (IDOK).
// ---------------------------------------------------------------------------

// CC 20260924 phase 3: an air unit of an aircraft/helicopter group sent to a new
//  formation station. On arrival HC's ProcessFormationEditEvent sets it to
//  FM_POSITION_REACHED at loiter throttle with speed 0 - right for a patrol point,
//  but MoveUnits (Display.c) only carries FM_SLAVED units along with their group,
//  so the aircraft would stop and fall behind. WatchAirReform puts it back.
type
  TAirReformWatch = record
    GroupUUID, UnitUUID: string;
    Throttle: uchar;   // THCAirRec.Throttle when the order was given
  end;

var
  gAirReform: array of TAirReformWatch;  // HC thread only

function FindUnitByUUID(pGroup: PHCGroup; const aUUID: string): PHCUnit;
begin
  Result := pGroup^.Units;
  while (Result <> nil) and (String(Result^.UUID) <> aUUID) do
    Result := Result^.next;
end;

procedure SetHCUUID(var aDest: HCUUID; const aSrc: string);
var
  n: Integer;
begin
  FillChar(aDest, SizeOf(aDest), 0);
  n := Length(aSrc);
  if n > HCUUID_LEN - 1 then n := HCUUID_LEN - 1;
  if n > 0 then
    Move(aSrc[1], aDest[0], n);
end;

// a TimeNow HarpoonEvent carrying aData, allocated with HC's NewPtr; nil when
//  the funcRefs aren't available
function NewNowEvent(pGroup: PHCGroup; const aUnitUUID: string;
  aEventType: LongWord; aData: Pointer): PHCHarpoonEvent;
begin
  Result := nil;
  if (aData = nil) or not Assigned(gNewPtr) then Exit;
  Result := PHCHarpoonEvent(gNewPtr(SizeOf(THCHarpoonEvent)));
  if Result = nil then Exit;
  FillChar(Result^, SizeOf(THCHarpoonEvent), 0);
  SetHCUUID(Result^.uuid, GenerateUUID);
  SetHCUUID(Result^.unit_uuid, aUnitUUID);
  Result^.ourGroup := pGroup;
  Result^.DataPtr := aData;
  Result^.next := nil;
  Result^.Time := 0;
  Result^.event_type := aEventType;
  Result^.time_type := TimeNow;
  Result^.in_scroll_flag := CFALSE;
end;

procedure ExecSetFormationZones(pGroup: PHCGroup; const aRequest: THCCallbackRequest);
var
  lZones: TFormationZones;
  i: Integer;
begin
  lZones := aRequest.Zones;
  ClampFormationZones(lZones, aRequest.RingCount);  // never trust the form thread's copy
  for i := 0 to MAX_FORMATION_ZONES - 1 do
    pGroup^.zones[i] := lZones[i];
end;

// GE: oldSpeed = group kts; if the group is going faster than half its max it is
//  slowed to half (SetGroupKts), otherwise oldSpeed = 0 (nothing to restore).
//  SetGroupKts isn't a funcRef, so the slowdown is queued as a group-level
//  ALT_SPEED_EVENT, which sets the group's and its slaved units' speed.
//  ProcessFormationEditEvent restores oldSpeed when the last unit arrives.
function ExecSlowGroupForReform(pGroup: PHCGroup): SmallInt;
var
  lData: PHCSpeedAltEvent;
  lEvent: PHCHarpoonEvent;
begin
  Result := pGroup^.kts;
  if Result <= (pGroup^.maxspeed shr 1) then
    Exit(0);
  if not Assigned(gNewPtr) or not Assigned(gHCQueueHarpoonEvent) then
    Exit(0);
  lData := PHCSpeedAltEvent(gNewPtr(SizeOf(THCSpeedAltEvent)));
  if lData = nil then Exit(0);
  lData^.newSpeed := pGroup^.maxspeed shr 1;
  lData^.newAltBand := -1;
  lData^.newThrottle := -1;
  lData^.newAltitude := INVALID_ALTITUDE;
  lEvent := NewNowEvent(pGroup, '', ALT_SPEED_EVENT, lData);
  if lEvent = nil then
  begin
    if Assigned(gDisposPtr) then gDisposPtr(lData);
    Exit(0);
  end;
  gHCQueueHarpoonEvent(lEvent);
end;

// CC 20260924 review: the speed a group's pending FORMATION_EVENTs will restore
//  (the largest oldSpeed among them, 0 if none). A later Apply that doesn't slow
//  the group itself (it is already at half speed) carries this on, so the group
//  still gets its speed back whichever unit arrives last.
function PendingReformSpeed(pGroup: PHCGroup): SmallInt;
var
  E: PHCHarpoonEvent;
  lSteps: Integer;
begin
  Result := 0;
  E := gHarpoonEventHead;
  lSteps := 0;
  while (E <> nil) and (lSteps < 200000) do
  begin
    if (E^.event_type = FORMATION_EVENT) and (E^.ourGroup = Pointer(pGroup)) and
       (E^.DataPtr <> nil) and (E^.unit_uuid[0] <> #0) and
       (PHCFormationEvent(E^.DataPtr)^.oldSpeed > Result) then
      Result := PHCFormationEvent(E^.DataPtr)^.oldSpeed;
    E := PHCHarpoonEvent(E^.next);
    Inc(lSteps);
  end;
end;

procedure RememberAirReform(pGroup: PHCGroup; pUnit: PHCUnit);
var
  i: Integer;
begin
  i := 0;
  while (i < Length(gAirReform)) and (gAirReform[i].UnitUUID <> String(pUnit^.UUID)) do
    Inc(i);
  if i = Length(gAirReform) then
  begin
    SetLength(gAirReform, i + 1);
    gAirReform[i].GroupUUID := String(pGroup^.UUID);
    gAirReform[i].UnitUUID := String(pUnit^.UUID);
    if pUnit^.Air <> nil then
      gAirReform[i].Throttle := pUnit^.Air^.Throttle
    else
      gAirReform[i].Throttle := 0;
  end;
  // an existing entry keeps the throttle from before the first order
end;

procedure ExecSetUnitStation(pGroup: PHCGroup; const aRequest: THCCallbackRequest;
  aOldSpeed: SmallInt);
var
  lUnit: PHCUnit;
  E: PHCHarpoonEvent;
  lData: PHCFormationEvent;
  lAP: PHCAirPatrolEvent;
  lEvent: PHCHarpoonEvent;
  lRetargeted: Boolean;
  lSteps: Integer;
begin
  lUnit := FindUnitByUUID(pGroup, aRequest.UnitUUID);
  if lUnit = nil then Exit;
  if HCMTIsAir(lUnit^.utype) and HCMTIsAir(pGroup^.gtype) then
    RememberAirReform(pGroup, lUnit);  // CC 20260924 phase 3
  lUnit^.independent := FM_REFORMING;
  // CC 20260924 phase 3: ProcessFormationEditEvent only copies the order's
  //  rings/sectors while the unit is still moving; a unit already on its station
  //  takes the arrival branch, which doesn't. An area-only change (Shift+click
  //  doesn't move the station) therefore sets them here as well.
  lUnit^.rings := aRequest.Rings;
  lUnit^.sectors := aRequest.Sectors;

  // The GE deletes the unit's pending FORMATION_EVENT (del_Unit_effects, not a
  //  funcRef) and posts a new one. Retargeting the pending one in place has the
  //  same effect - HC re-runs it within 10 s (1 s for aircraft) - without any list
  //  surgery. A new event is queued only when none is pending.
  lRetargeted := False;
  E := gHarpoonEventHead;
  lSteps := 0;
  while (E <> nil) and (lSteps < 200000) do
  begin
    if (E^.event_type = FORMATION_EVENT) and (E^.ourGroup = Pointer(pGroup)) and
       (E^.DataPtr <> nil) and (String(E^.unit_uuid) = aRequest.UnitUUID) then
    begin
      lData := PHCFormationEvent(E^.DataPtr);
      lData^.Xoffset := aRequest.XOff;
      lData^.Yoffset := aRequest.YOff;
      if aOldSpeed <> 0 then  // CC 20260924 review: never drop a pending restore
        lData^.oldSpeed := aOldSpeed;
      lData^.rings := aRequest.Rings;
      lData^.sectors := aRequest.Sectors;
      lRetargeted := True;
    end
    else if (E^.event_type = AIR_PATROL_EVENT) and (E^.ourGroup = Pointer(pGroup)) and
       (E^.DataPtr <> nil) and (String(E^.unit_uuid) = aRequest.UnitUUID) then
    begin
      // an aircraft flying a patrol keeps its patrol in step, as the GE does
      lAP := PHCAirPatrolEvent(E^.DataPtr);
      lAP^.Rings := aRequest.Rings;
      lAP^.Sectors := aRequest.Sectors;
    end;
    E := PHCHarpoonEvent(E^.next);
    Inc(lSteps);
  end;
  if lRetargeted then Exit;

  if not Assigned(gNewPtr) or not Assigned(gHCQueueHarpoonEvent) then Exit;
  lData := PHCFormationEvent(gNewPtr(SizeOf(THCFormationEvent)));
  if lData = nil then Exit;
  FillChar(lData^, SizeOf(THCFormationEvent), 0);
  lData^.Xoffset := aRequest.XOff;
  lData^.Yoffset := aRequest.YOff;
  lData^.oldSpeed := aOldSpeed;
  lData^.rings := aRequest.Rings;
  lData^.sectors := aRequest.Sectors;
  lEvent := NewNowEvent(pGroup, aRequest.UnitUUID, FORMATION_EVENT, lData);
  if lEvent = nil then
  begin
    if Assigned(gDisposPtr) then gDisposPtr(lData);
    Exit;
  end;
  gHCQueueHarpoonEvent(lEvent);
end;

// CC 20260924 a formation patrol's area: the AIR_PATROL_EVENT (found through the
//  HarpoonEventHead list) gets the new rings/sectors, as the GE editor's OK sets
//  AP->Rings/Sectors. Its relief patrols copy it, so they use the new area too.
//  An airborne patrol aircraft is sent into the new area by a separate
//  cbkSetUnitStation in the same batch.
procedure ExecSetPatrolArea(pGroup: PHCGroup; const aRequest: THCCallbackRequest);
var
  E: PHCHarpoonEvent;
  lAP: PHCAirPatrolEvent;
  lSteps: Integer;
begin
  E := gHarpoonEventHead;
  lSteps := 0;
  while (E <> nil) and (lSteps < 200000) do
  begin
    if (E^.event_type = AIR_PATROL_EVENT) and (E^.ourGroup = Pointer(pGroup)) and
       (E^.DataPtr <> nil) and (String(E^.uuid) = aRequest.PatrolUUID) then
    begin
      lAP := PHCAirPatrolEvent(E^.DataPtr);
      lAP^.Rings := aRequest.Rings;
      lAP^.Sectors := aRequest.Sectors;
      Exit;
    end;
    E := PHCHarpoonEvent(E^.next);
    Inc(lSteps);
  end;
end;

// CC 20260924 phase 4: port of the GE's Plane.c GetLoadoutIndex. An
//  AIR_PATROL_EVENT stores its loadout as MAX_PLANE_LOADOUTS + the position of the
//  loadout in the aircraft type's B1 disk record (LoadId[], disk IDs, as
//  Initutil.c GetPlaneLoadouts reads them); GetLoadoutFromIndex treats anything
//  below MAX_PLANE_LOADOUTS as a legacy positional index and remaps it by mission,
//  so the offset matters. A loadout not in the list falls back to position 0, as
//  in the GE ("hope for the best").
function HCLoadoutIndex(aPlaneID, aLoadID: Word): Byte;
var
  lB1: PHCAnnexB1_disk;
  lDiskLoad: Word;
  i: Integer;
begin
  Result := MAX_PLANE_LOADOUTS;
  if (gAnnexHelper = nil) or (aPlaneID = INVALID_ID) or (aLoadID = INVALID_ID) then Exit;
  try
    lB1 := gAnnexHelper.HCGetAnnexB1(aPlaneID);
  except
    Exit;
  end;
  if lB1 = nil then Exit;
  lDiskLoad := aLoadID;
  if Assigned(gAnnex_id_to_disk_id) then
    lDiskLoad := gAnnex_id_to_disk_id(aLoadID);
  for i := 0 to MAX_PLANE_LOADOUTS - 1 do
    if lB1^.LoadId[i] = lDiskLoad then
      Exit(MAX_PLANE_LOADOUTS + i);
end;

// CC 20260924 phase 4: a new formation patrol, as the GE editor's OK posts one:
//  PATROL_START, rings/sectors as staged, staggered by DelaySec (GE: 5 s apart).
//  HC's ProcessAirPatrolEvent then finds a READY plane of this type and loadout
//  aboard the group and launches it.
procedure ExecCreatePatrol(pGroup: PHCGroup; const aRequest: THCCallbackRequest);
var
  lAP: PHCAirPatrolEvent;
  lEvent: PHCHarpoonEvent;
begin
  if not Assigned(gNewPtr) or not Assigned(gHCQueueHarpoonEvent) then Exit;
  lAP := PHCAirPatrolEvent(gNewPtr(SizeOf(THCAirPatrolEvent)));
  if lAP = nil then Exit;
  FillChar(lAP^, SizeOf(THCAirPatrolEvent), 0);
  lAP^.PlaneID := aRequest.PlaneID;
  lAP^.LoadoutIDX := HCLoadoutIndex(aRequest.PlaneID, aRequest.LoadID);
  lAP^.PatrolType := aRequest.PatrolType;
  lAP^.Numer := aRequest.PatrolNumber;
  lAP^.Status := PATROL_START;
  lAP^.Rings := aRequest.Rings;
  lAP^.Sectors := aRequest.Sectors;
  lEvent := NewNowEvent(pGroup, '', AIR_PATROL_EVENT, lAP);
  if lEvent = nil then
  begin
    if Assigned(gDisposPtr) then gDisposPtr(lAP);
    Exit;
  end;
  if aRequest.DelaySec > 0 then
  begin
    lEvent^.time_type := TimeDelta;
    lEvent^.Time := aRequest.DelaySec;
  end;
  gHCQueueHarpoonEvent(lEvent);
end;

// CC 20260924 phase 4: cancel a formation patrol now, as the GE editor's OK does
//  for a deleted patrol (Status PATROL_RETURN, Number PATROL_DELETE; an airborne
//  aircraft set FM_REFORMING / AC_RETURN with its formation orders dropped), then
//  make the patrol event run now. The GE moves it to the list head; a DLL mustn't
//  write HC's head variable, so a non-head event is unlinked and re-queued with
//  TimeNow (it lands after everything already due - the same tick), and a head
//  event just gets Time = now. Its pending FORMATION_EVENTs are dropped by
//  blanking their unit_uuid: ProcessFormationEditEvent then finds no unit and
//  returns FALSE, and ProcessEvents disposes of the event (Effects.c) - the
//  del_Unit_effects effect without unlinking anything. A patrol still on deck
//  just ends: PATROL_RETURN with no aircraft returns FALSE too.
procedure ExecCancelPatrol(pGroup: PHCGroup; const aRequest: THCCallbackRequest);
const
  AC_RETURN_FLAG = 6;  // GE Combat.h AC_RETURN
var
  E, lPrev, lFound, lFoundPrev: PHCHarpoonEvent;
  lAP: PHCAirPatrolEvent;
  lUnit: PHCUnit;
  lAircraft: string;
  lSteps: Integer;
begin
  lFound := nil;
  lFoundPrev := nil;
  lPrev := nil;
  E := gHarpoonEventHead;
  lSteps := 0;
  while (E <> nil) and (lSteps < 200000) do
  begin
    if (E^.event_type = AIR_PATROL_EVENT) and (E^.ourGroup = Pointer(pGroup)) and
       (E^.DataPtr <> nil) and (String(E^.uuid) = aRequest.PatrolUUID) then
    begin
      lFound := E;
      lFoundPrev := lPrev;
      Break;
    end;
    lPrev := E;
    E := PHCHarpoonEvent(E^.next);
    Inc(lSteps);
  end;
  if lFound = nil then Exit;

  lAP := PHCAirPatrolEvent(lFound^.DataPtr);
  lAP^.Status := PATROL_RETURN;
  lAP^.Numer := Byte(PATROL_DELETE);

  lAircraft := String(lFound^.unit_uuid);
  if lAircraft <> '' then
  begin
    lUnit := FindUnitByUUID(pGroup, lAircraft);
    if lUnit <> nil then
    begin
      lUnit^.independent := FM_REFORMING;
      lUnit^.CombatFlag := AC_RETURN_FLAG;
    end;
    E := gHarpoonEventHead;
    lSteps := 0;
    while (E <> nil) and (lSteps < 200000) do
    begin
      if (E^.event_type = FORMATION_EVENT) and (E^.ourGroup = Pointer(pGroup)) and
         (String(E^.unit_uuid) = lAircraft) then
        FillChar(E^.unit_uuid, SizeOf(E^.unit_uuid), 0);
      E := PHCHarpoonEvent(E^.next);
      Inc(lSteps);
    end;
  end;

  if lFoundPrev = nil then
  begin
    // the head: already next in line; make it due now
    lFound^.time_type := TimeAbs;
    lFound^.Time := gGameTimeNow;
  end
  else if Assigned(gHCQueueHarpoonEvent) then
  begin
    lFoundPrev^.next := lFound^.next;
    lFound^.next := nil;
    lFound^.time_type := TimeNow;
    lFound^.Time := 0;
    gHCQueueHarpoonEvent(lFound);  // same event, same address: LAUNCH_EVENT.patrolling stays valid
  end;
end;

procedure DropAirReform(i: Integer);
var
  n: Integer;
begin
  n := Length(gAirReform);
  if i < n - 1 then
    gAirReform[i] := gAirReform[n - 1];
  SetLength(gAirReform, n - 1);
end;

// CC 20260924 phase 3: once an aircraft-group air unit reaches its station, make
//  it follow the group again: FM_SLAVED at the group's speed and course, its own
//  throttle back, and snapped to group centre + offset (it lagged one tick)
procedure WatchAirReform;
var
  i: Integer;
  pGroup: PHCGroup;
  lUnit: PHCUnit;
  lLat, lLon: LongInt;
begin
  i := 0;
  while i < Length(gAirReform) do
  begin
    lUnit := nil;
    pGroup := FindGroupByUUID(gAirReform[i].GroupUUID);
    if pGroup <> nil then
      lUnit := FindUnitByUUID(pGroup, gAirReform[i].UnitUUID);
    if (lUnit = nil) or (lUnit^.CombatFlag <> 0) then
      DropAirReform(i)             // gone, or off on combat business
    else if lUnit^.independent = FM_POSITION_REACHED then
    begin
      lUnit^.independent := FM_SLAVED;
      lUnit^.kts := pGroup^.kts;
      lUnit^.speed := pGroup^.speed;
      lUnit^.course := pGroup^.course;
      if lUnit^.Air <> nil then
        lUnit^.Air^.Throttle := gAirReform[i].Throttle;
      FormationOffsetToLatLon(pGroup^.sLat, pGroup^.sLon, lUnit^.xOff, lUnit^.yOff,
        lLat, lLon);
      lUnit^.sLat := lLat;
      lUnit^.sLon := lLon;
      DropAirReform(i);
    end
    else if lUnit^.independent <> FM_REFORMING then
      DropAirReform(i)             // something else took over the unit
    else
      Inc(i);
  end;
end;

procedure ClearAirReformWatch;
begin
  SetLength(gAirReform, 0);
end;

procedure ExecutePendingHCCallbacks;
var
  lLocalRequests: array of THCCallbackRequest;
  lLocalCount: Integer;
  i: Integer;
  request: THCCallbackRequest;
  pGroup: PHCGroup;
  track: PHCPathRec;
  pNew: PHCPathRec;
  pNode: PHCPathRec;
  pPrev: PHCPathRec;
  pNext: PHCPathRec;
  lReformGroupUUID: string;   // CC 20260924 group slowed earlier in this batch
  lReformOldSpeed: SmallInt;  //  and the speed HC should restore
  lStationSpeed: SmallInt;
begin
  lReformGroupUUID := '';
  lReformOldSpeed := 0;
  // Acquire lock, copy pending list, clear shared list
  EnterCriticalSection(gPendingCS);
  try
    lLocalCount := gPendingCount;
    SetLength(lLocalRequests, lLocalCount);
    if lLocalCount > 0 then
    begin
      for i := 0 to lLocalCount - 1 do
        lLocalRequests[i] := gPendingRequests[i];
    end;
    gPendingCount := 0;
    SetLength(gPendingRequests, 0);
  finally
    LeaveCriticalSection(gPendingCS);
  end;

  if lLocalCount = 0 then
    Exit;

  for i := 0 to lLocalCount - 1 do
  begin
    request := lLocalRequests[i];

    // Find the group by UUID
    pGroup := FindGroupByUUID(request.GroupUUID);
    if pGroup = nil then
      Continue;

    track := pGroup^.track;

    case request.Kind of

      cbkAppendCoursepoint:
      begin
        if not Assigned(gNewPtr) then
          Continue;
        pNew := PHCPathRec(gNewPtr(SizeOf(THCPathRec)));
        pNew^.DestLon    := request.DestLon;
        pNew^.DestLat    := request.DestLat;
        pNew^.next       := nil;
        pNew^.pathEvents := nil;

        if track = nil then
        begin
          // Empty list: new node is the only node
          pNew^.prev := nil;
          pGroup^.track := pNew;
        end
        else
        begin
          // Walk to last node
          pNode := track;
          while PHCPathRec(pNode^.next) <> nil do
            pNode := PHCPathRec(pNode^.next);
          // Link pNew after pNode
          pNew^.prev  := pNode;
          pNode^.next := pNew;
        end;
      end;

      cbkInsertCoursepoint:
      begin
        if not Assigned(gNewPtr) then
          Continue;
        // Walk to node at WaypointIdx (the node to insert before)
        pNode := WalkTrackToIndex(pGroup, request.WaypointIdx);
        pNew := PHCPathRec(gNewPtr(SizeOf(THCPathRec)));
        pNew^.DestLon    := request.DestLon;
        pNew^.DestLat    := request.DestLat;
        pNew^.pathEvents := nil;

        if pNode = nil then
        begin
          // Index beyond end — append to end of list
          if track = nil then
          begin
            pNew^.prev    := nil;
            pNew^.next    := nil;
            pGroup^.track := pNew;
          end
          else
          begin
            // Walk to last node
            pPrev := track;
            while PHCPathRec(pPrev^.next) <> nil do
              pPrev := PHCPathRec(pPrev^.next);
            pNew^.prev  := pPrev;
            pNew^.next  := nil;
            pPrev^.next := pNew;
          end;
        end
        else
        begin
          // Insert pNew before pNode
          pPrev := PHCPathRec(pNode^.prev);
          pNew^.prev  := pPrev;
          pNew^.next  := pNode;
          pNode^.prev := pNew;
          if pPrev <> nil then
            pPrev^.next := pNew
          else
            pGroup^.track := pNew; // pNode was the head
        end;
      end;

      cbkDeleteCoursepoint:
      begin
        if not Assigned(gDisposPtr) then
          Continue;
        pNode := WalkTrackToIndex(pGroup, request.WaypointIdx);
        if pNode = nil then
          Continue;

        pPrev := PHCPathRec(pNode^.prev);
        pNext := PHCPathRec(pNode^.next);

        if pPrev <> nil then
          pPrev^.next := pNext
        else
          pGroup^.track := pNext; // deleted head node

        if pNext <> nil then
          pNext^.prev := pPrev;

        gDisposPtr(pNode);
      end;

      cbkClearCourse:
      begin
        if not Assigned(gDisposPtr) then
          Continue;
        pNode := track;
        while pNode <> nil do
        begin
          pNext := PHCPathRec(pNode^.next);
          gDisposPtr(pNode);
          pNode := pNext;
        end;
        pGroup^.track := nil;
      end;

      cbkDragCommit:
      begin
        pNode := WalkTrackToIndex(pGroup, request.WaypointIdx);
        if pNode = nil then
          Continue;
        pNode^.DestLon := request.DestLon;
        pNode^.DestLat := request.DestLat;
      end;

      // CC 20260924 formation editing
      cbkSetFormationZones:
        ExecSetFormationZones(pGroup, request);

      cbkSlowGroupForReform:
      begin
        lReformGroupUUID := request.GroupUUID;
        lReformOldSpeed := ExecSlowGroupForReform(pGroup);
      end;

      cbkSetPatrolArea:
        ExecSetPatrolArea(pGroup, request);  // CC 20260924

      cbkCreatePatrol:
        ExecCreatePatrol(pGroup, request);   // CC 20260924 phase 4

      cbkCancelPatrol:
        ExecCancelPatrol(pGroup, request);   // CC 20260924 phase 4

      cbkSetUnitStation:
      begin
        if request.GroupUUID = lReformGroupUUID then
          lStationSpeed := lReformOldSpeed
        else
          lStationSpeed := 0;
        if lStationSpeed = 0 then  // CC 20260924 review: keep an earlier slowdown's
          lStationSpeed := PendingReformSpeed(pGroup);
        ExecSetUnitStation(pGroup, request, lStationSpeed);
      end;

    end; // case
  end; // for
end;

end.
