unit uHCUtils;

{$ifdef FPC}
{$mode objfpc}{H+}
{$endif}

interface

uses
  Classes, SysUtils, {MathLib,} uHCTypeDefs, uHCDefines,
  uHCAnnexDefs, uHCAnnexHelpers;

function HCIsValidGroup(pGroup: PHCGroup; pGroupStart: PHCGroup): CBOOL;
function HCIsValidUnit(pUnit: PHCUnit; pUnitTop: PHCUnit): CBOOL;
function HCUTIsAir(pType: ShortInt): CBOOL; deprecated 'Use HCMTIsAir instead';
function HCMTIsAir(pType: HCMT): Boolean;
function HCMTIsShip(pType: HCMT): Boolean;
function HCMTIsVessel(pType: HCMT): Boolean;
function HCMTIsSub(pType: HCMT): Boolean;
function HCMTIsMissile(pType: HCMT): Boolean;
function HCMTIsWeapon(pType: HCMT): Boolean;
function HCIsSinking(pUnit: PHCUnit): Boolean;
function HCMTIsBase(pType: HCMT): Boolean;
function HCMTIsFixedBase(pType: HCMT): Boolean;
function HCIsFixedBase(const pType: ShortInt): Boolean; deprecated 'Use HCMTIsFixedBase instead';
function HCMTIsLandDet(pType: HCMT): Boolean;
function HCMTIsLandMob(pType: HCMT): Boolean;
function HCUnitDetection2String(const pPUnit: PHCUnit): String;
function HCAltitudeToAltBand(const pAltitude: HCALTITUDE_M): HCALTITUDE;
function HCAltBand2String(const pAltBand: HCALTITUDE): string;
function HCFormationRings2String(const pRings: ULONGLONG): string;
function HCFormationZoneInner(const pRings: ULONGLONG): short; experimental;
function HCFormationZoneOuter(const pRings: ULONGLONG): short; experimental;
function HCFormationSectors2String(const pSectors: ULONGLONG): string;
function HCFormationSectorCount(const pSectors: ULONGLONG): Byte; experimental;
procedure HCFormationSectors2Deg(const pSectors: ULONGLONG;
  out pSectDegStart: short; out pSectDegEnd: short); experimental;
function HCget_group_id_type(const pPGroup: PHCGroup): AnsiChar;
procedure HCset_group_id_type(pPGroup: PHCGroup; const pType: AnsiChar);
function HCVictoryConditionLevel(const pVicLevel: uchar): String;
function HCLat2Deg(const pHCLat: long): Double;
function HCLon2Deg(const pHCLon: long): Double;
function HCuuid2UnitPtr(const pUuid: String; const pStartGroup: PHCGroup; const pGroup: PHCGroup): PHCUnit;
function GenerateUUID(): String;
function GenerateUUIDv7: String;  // CC 20260924 RFC 9562 time-ordered UUID
function HCGroupId2GroupPtr(const pGroupId: string; const pPStartGroup: PHCGroup): PHCGroup; // DDT 20230622
function HCSaveGameType2String(const pSaveGameType: THCSaveGameTypes): String;

// CC 20260403 Airborne endurance and closest-base calculations for ExportDLLs.
// Pascal port of the GE's GetAirborneEndurance and FindClosestBase.

/// <summary>Find the closest friendly base that can accept the given aircraft group</summary>
/// <param name="pAirGroup">Aircraft group seeking a base</param>
/// <param name="pStartGroup">Head of the global group linked list</param>
/// <param name="pAnnexHelper">Annex helper for A1/B1/P1 lookups</param>
/// <remarks>
///  <para>Searches all groups on the map for the nearest friendly base (airfield
///   or carrier) whose runway can accommodate the group's largest aircraft.
///   Carriers are only considered if every aircraft in the group is carrier-capable
///   (UF_B1_carrier). Sinking units and airborne units are excluded.</para>
///  <para>Port of the GE's FindClosestBase.</para>
///  <para>History:
///   20260403 CC Created
///  </para>
/// </remarks>
/// <returns>Pointer to the closest suitable base unit, or nil if none found</returns>
function HCFindClosestBase(pAirGroup: PHCGroup;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper): PHCUnit;

/// <summary>Calculate remaining airborne range for an aircraft group</summary>
/// <param name="pGroup">Aircraft group to evaluate (iterates all units)</param>
/// <param name="pStartGroup">Head of the global group linked list (for FindClosestBase)</param>
/// <param name="pAnnexHelper">Annex helper for B1 speed lookup</param>
/// <param name="aBingo">Output: bingo fuel percentage (0-100)</param>
/// <remarks>
///  <para>Returns the minimum remaining range across all units in the group.
///   Uses the slowest cruise speed from Annex B1 for a best-case (maximum) range
///   estimate. Drop tanks are included in total range but excluded from bingo
///   calculation. Resolves each unit's home base via HCFindClosestBase if not
///   already cached.</para>
///  <para>Port of the GE's GetAirborneEndurance.</para>
///  <para>History:
///   20260403 CC Created
///  </para>
/// </remarks>
/// <returns>Minimum remaining range in nautical miles, or 0 if no valid aircraft
///  units found or no fuel remaining</returns>
function HCGetAirborneEndurance(pGroup: PHCGroup;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper;
  out aBingo: SmallInt): LongInt;

/// <summary>Calculate remaining airborne range for a single aircraft unit</summary>
/// <param name="pUnit">Aircraft unit to evaluate</param>
/// <param name="pStartGroup">Head of the global group linked list (for FindClosestBase)</param>
/// <param name="pAnnexHelper">Annex helper for B1 speed lookup</param>
/// <param name="aBingo">Output: bingo fuel percentage (0-100)</param>
/// <remarks>
///  <para>Single-unit variant of HCGetAirborneEndurance. Always re-resolves the
///   closest base rather than using the cached HomeUnit.</para>
///  <para>Port of the GE's GetAirborneEnduranceUnit.</para>
///  <para>History:
///   20260403 CC Created
///  </para>
/// </remarks>
/// <returns>Remaining range in nautical miles, or 0 if not an aircraft unit or
///  no fuel remaining</returns>
function HCGetAirborneEnduranceUnit(pUnit: PHCUnit;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper;
  out aBingo: SmallInt): LongInt;

/// <summary>Find the longest runway required by any aircraft in the group</summary>
/// <param name="pGroup">Aircraft group to evaluate</param>
/// <param name="pAnnexHelper">Annex helper for B1 runway lookup</param>
/// <remarks>
///  <para>Iterates all units in the group and returns the highest B1 Runway value.
///   This is the minimum runway length any landing base must provide.</para>
///  <para>History:
///   20260403 CC Created
///  </para>
/// </remarks>
/// <returns>Maximum runway requirement (uchar index) across all aircraft in the group,
///  or 0 if no aircraft units found</returns>
function HCFindLongestRunwayNeeded(pGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper): uchar;

implementation

uses
  {$IFDEF MSWINDOWS}Windows,{$ELSE}DateUtils,{$ENDIF}
  SyncObjs,
  uHCGCI;

var
  // CC 20260924 UUIDv7 state: last timestamp and the 12-bit counter that keeps
  //  UUIDs made in the same millisecond in creation order (RFC 9562 method 1)
  gUUIDLock: TCriticalSection;
  gUUIDLastMs: Int64 = 0;
  gUUIDSeq: Integer = 0;

{*** Determine if group is still valid ***}
function HCIsValidGroup(pGroup: PHCGroup; pGroupStart: PHCGroup): CBOOL;
var
  lGroupCheck: PHCGroup;
begin
  //------------------------------------------------------------
  //  Scan the active groups to make sure this one is valid
  //------------------------------------------------------------

  lGroupCheck := pGroupStart;
  while (lGroupCheck <> nil) do
  begin
    if (pGroup = lGroupCheck) then
    begin
      Result := CTRUE;
      exit;
    end;
    lGroupCheck := lGroupCheck^.toSouth;
  end;

  Result := CFALSE;
end;

{*** end HCIsValidGroup ***}

{*** Determine if unit is still valid ***}
function HCIsValidUnit(pUnit: PHCUnit; pUnitTop: PHCUnit): CBOOL;
var
  lUnitCheck: PHCUnit;
begin
  //------------------------------------------------------------
  //  Scan the active groups to make sure this one is valid
  //------------------------------------------------------------

  lUnitCheck := pUnitTop;
  while (lUnitCheck <> nil) do
  begin
    if (pUnit = lUnitCheck) then
    begin
      Result := CTRUE;
      exit;
    end;
    lUnitCheck := lUnitCheck^.toSouth;
  end;

  Result := CFALSE;
end;

{*** end HCIsValidUnit ***}

{*** Test to see if unit type is plane or helicopter ***}
function HCUTIsAir(pType: ShortInt): CBOOL;
begin
  if ((pType = Ord(UT_AC)) or (pType = Ord(UT_HELO))) then
    result := CTRUE
  else
    result := CFALSE;
end;
{*** end HCUTIsAir ***}

{*** Test to see if marker type is plane or helicopter ***}
function HCMTIsAir(pType: HCMT): Boolean;
begin
  if ((pType = MT_AC) OR (pType = MT_HELO)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsAir ***}

{*** Test to see if marker type is ship or aircraft carrier ***}
function HCMTIsShip(pType: HCMT): Boolean;
begin
  if ((pType = MT_SHIP) OR (pType = MT_CARRIER)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsShip ***}

function HCUnitDetection2String(const pPUnit: PHCUnit): String;
var
  lStrShow: String;
begin
  lStrShow := ''; // ALE 20230410 should already be empty but we trust nobody
  if (pPUnit^.showing[0] = NotShowing) then
  begin
    lStrShow := lStrShow + ' has no detection';
  end
  else
  begin
    if ((pPUnit^.showing[0] and SideKnown) > 0) then
    begin
      lStrShow := lStrShow + ' knows side';
    end;
    if ((pPUnit^.showing[0] and ShowExact) > 0) then
    begin
      lStrShow := lStrShow + ' has exact fix';
    end;
    if ((pPUnit^.showing[0] and ShowUncertain) > 0) then
    begin
      lStrShow := lStrShow + ' has approx position';
    end;
  end;
  Result := lStrShow;
end;

{*** Convert from an altitude in meters to an altitude band ***}
function HCAltitudeToAltBand(const pAltitude: HCALTITUDE_M): HCALTITUDE;
begin
  if (pAltitude <= ALT_TOODEEP) then
    Result := TooDeep
  else if (pAltitude <= ALT_VDEEP) then
    Result := Vdeep
  else if (pAltitude <= ALT_DEEP) then
    Result := Deep
  else if (pAltitude <= ALT_INTERMED) then
    Result := Intermed
  else if (pAltitude <= ALT_SHALLOW) then
    Result := Shallow
  else if (pAltitude <= ALT_SNORKEL) then
    Result := Snorkel
  else if (pAltitude <= ALT_SEA_LEVEL) then
    Result := SeaLevel
  else if (pAltitude <= ALT_SCOPE_ALT) then
    Result := ScopeAlt
  else if (pAltitude <= ALT_VLOW) then
    Result := Vlow
  else if (pAltitude <= ALT_LOW) then
    Result := Low
  else if (pAltitude <= ALT_MEDIUM) then
    Result := Medium
  else if (pAltitude <= ALT_HIGH) then
    Result := High
  else if (pAltitude <= ALT_VHIGH) then
    Result := Vhigh
  else
    Result := SubOrbit;
end;

{*** end HCAltitudeToAltBand ***}

{*** Convert from an altitude band to display text ***}
function HCAltBand2String(const pAltBand: HCALTITUDE): string;
begin
  case pAltBand of
    TooDeep: Result := 'TooDeep';
    Vdeep: Result := 'Vdeep';
    Deep: Result := 'Deep';
    Intermed: Result := 'Intermed';
    Shallow: Result := 'Shallow';
    Snorkel: Result := 'Snorkel';
    SeaLevel: Result := 'SeaLevel';
    ScopeAlt: Result := 'ScopeAlt';
    Vlow: Result := 'Vlow';
    Low: Result := 'Low';
    Medium: Result := 'Medium';
    High: Result := 'High';
    Vhigh: Result := 'Vhigh';
    SubOrbit: Result := 'SubOrbit';
    else
      Result := '';
  end;
end;

{*** end HCAltBand2String ***}

{*** Convert from a formation rings value to comma delimited strings ***}
function HCFormationRings2String(const pRings: ULONGLONG): string;
var
  lCount: NativeInt;
begin
  Result := '';
  lCount := 0;

  if ((pRings and MainBodyFlag) > 0) then
  begin
    Inc(lCount);
    Result := 'Main Body';
  end;
  if ((pRings and AAWRingFlag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
      Result := Result + ', ';
    Result := Result + 'AAW';
  end;
  if ((pRings and ASWRingFlag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
      Result := Result + ', ';
    Result := Result + 'ASW';
  end;
  if ((pRings and PicketRingFlag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
      Result := Result + ', ';
    Result := Result + 'Picket 1';
  end;
  if ((pRings and PicketRing2Flag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
      Result := Result + ', ';
    Result := Result + 'Picket 2';
  end;
  if ((pRings and PicketRing3Flag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
      Result := Result + ', ';
    Result := Result + 'Picket 3';
  end;
  { the below really means Any Ring rather than All Rings
  if ((pRings AND AllRingsFlag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
       result := result + ', ';
    result := result + 'All Rings';
  end;
  }
end;

{*** end HCFormationRings2String ***}

{*** Find the first (innermost) selected formation ring ***}
function HCFormationZoneInner(const pRings: ULONGLONG): short; experimental;
begin
  result := 0;

  if ((pRings and MainBodyFlag) > 0) then
    result := MainBodyZone
  else if ((pRings and AAWRingFlag) > 0) then
    result := AAWRingZone
  else if ((pRings and ASWRingFlag) > 0) then
    result := ASWRingZone
  else if ((pRings and PicketRingFlag) > 0) then
    result := PicketRingZone
  else if ((pRings and PicketRing2Flag) > 0) then
    result := PicketRing2Zone
  else if ((pRings and PicketRing3Flag) > 0) then
    result := PicketRing3Zone;
end;
{*** end HCFormationZoneInner ***}

{*** Find the last (outermost) selected formation ring ***}
function HCFormationZoneOuter(const pRings: ULONGLONG): short; experimental;
begin
  result := 0;

  if ((pRings and PicketRing3Flag) > 0) then
    result := PicketRing3Zone
  else if ((pRings and PicketRing2Flag) > 0) then
    result := PicketRing2Zone
  else if ((pRings and PicketRingFlag) > 0) then
    result := PicketRingZone
  else if ((pRings and ASWRingFlag) > 0) then
    result := ASWRingZone
  else if ((pRings and AAWRingFlag) > 0) then
    result := AAWRingZone
  else if ((pRings and MainBodyFlag) > 0) then
    result := MainBodyZone;
end;
{*** end HCFormationZoneOuter ***}

{*** Convert from a formation sectors value to comma delimited strings ***}
function HCFormationSectors2String(const pSectors: ULONGLONG): string;
var
  lCount: NativeInt;
begin
  Result := '';
  lCount := 0;

  if (pSectors = FormationSectorNone) then
  begin
    Result := '';
  end
  else
  begin
    if ((pSectors and FormationSector000_045) > 0) then
    begin
      Inc(lCount);
      Result := '000 to 045';
    end;
    if ((pSectors and FormationSector045_090) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '045 to 090';
    end;
    if ((pSectors and FormationSector090_135) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '090 to 135';
    end;
    if ((pSectors and FormationSector135_180) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '135 to 180';
    end;
    if ((pSectors and FormationSector180_225) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '180 to 225';
    end;
    if ((pSectors and FormationSector225_270) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '225 to 270';
    end;
    if ((pSectors and FormationSector270_315) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '270 to 315';
    end;
    if ((pSectors and FormationSector315_000) > 0) then
    begin
      Inc(lCount);
      if (lCount > 1) then
        Result := Result + ', ';
      Result := Result + '315 to 360';
    end;
  { the below really means Any Ring rather than All Rings
  if ((pSectors AND AllRingsFlag) > 0) then
  begin
    Inc(lCount);
    if (lCount > 1) then
       result := result + ', ';
    result := result + 'All Rings';
  end;
  }
  end;
end;

{*** end HCFormationSectors2String ***}

{*** Convert from a formation sectors value to comma delimited strings ***}
procedure HCFormationSectors2Deg(const pSectors: ULONGLONG;
  out pSectDegStart: short; out pSectDegEnd: short); experimental;
begin
  if (pSectors = FormationSectorNone) then
  begin
    // ALE 20180322 Main Body
    pSectDegStart := -1;
    pSectDegEnd := -1;
  end
  else if (pSectors = FormationSectorAll) then
  begin
    // ALE 20180326 All sectors in ring(s) selected
    pSectDegStart := 0;
    pSectDegEnd := 360;
  end
  else
  begin
    // ALE 20180826 Go clockwise, first sector that doesn't have an
    //  empty sector before it becomes the start.
    if ((pSectors and FormationSector000_045) > 0) then
    begin
      if ((pSectors and FormationSector315_000) = 0) then
        pSectDegStart := 0;
    end;
    if ((pSectors and FormationSector045_090) > 0) then
    begin
      if ((pSectors and FormationSector000_045) = 0) then
        pSectDegStart := 45;
    end;
    if ((pSectors and FormationSector090_135) > 0) then
    begin
      if ((pSectors and FormationSector045_090) = 0) then
        pSectDegStart := 90;
    end;
    if ((pSectors and FormationSector135_180) > 0) then
    begin
      if ((pSectors and FormationSector090_135) = 0) then
        pSectDegStart := 135;
    end;
    if ((pSectors and FormationSector180_225) > 0) then
    begin
      if ((pSectors and FormationSector135_180) = 0) then
        pSectDegStart := 180;
    end;
    if ((pSectors and FormationSector225_270) > 0) then
    begin
      if ((pSectors and FormationSector180_225) = 0) then
        pSectDegStart := 225;
    end;
    if ((pSectors and FormationSector270_315) > 0) then
    begin
      if ((pSectors and FormationSector225_270) = 0) then
        pSectDegStart := 270;
    end;
    if ((pSectors and FormationSector315_000) > 0) then
    begin
      if ((pSectors and FormationSector270_315) = 0) then
        pSectDegStart := 315;
    end;
    // ALE 20180826 Go counter-clockwise, first sector that doesn't have an
    //  empty sector before it becomes the start.
    if ((pSectors and FormationSector315_000) > 0) then
    begin
      if ((pSectors and FormationSector000_045) = 0) then
        pSectDegEnd := 360;
    end;
    if ((pSectors and FormationSector270_315) > 0) then
    begin
      if ((pSectors and FormationSector315_000) = 0) then
        pSectDegEnd := 315;
    end;
    if ((pSectors and FormationSector225_270) > 0) then
    begin
      if ((pSectors and FormationSector270_315) = 0) then
        pSectDegEnd := 270;
    end;
    if ((pSectors and FormationSector180_225) > 0) then
    begin
      if ((pSectors and FormationSector225_270) = 0) then
        pSectDegEnd := 225;
    end;
    if ((pSectors and FormationSector135_180) > 0) then
    begin
      if ((pSectors and FormationSector180_225) = 0) then
        pSectDegEnd := 180;
    end;
    if ((pSectors and FormationSector090_135) > 0) then
    begin
      if ((pSectors and FormationSector135_180) = 0) then
        pSectDegEnd := 135;
    end;
    if ((pSectors and FormationSector045_090) > 0) then
    begin
      if ((pSectors and FormationSector090_135) = 0) then
        pSectDegEnd := 90;
    end;
    if ((pSectors and FormationSector000_045) > 0) then
    begin
      if ((pSectors and FormationSector045_090) = 0) then
        pSectDegEnd := 45;
    end;
  end;
end;
{*** end HCFormationSectors2Deg ***}

{*** Test to see if marker type is ship or aircraft carrier or submarine ***}
function HCMTIsVessel(pType: HCMT): Boolean;
begin
  if ((pType = MT_SHIP) OR (pType = MT_CARRIER) OR (pType = MT_SUB)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsVessel ***}

{*** Test to see if marker type is a submarine ***}
function HCMTIsSub(pType: HCMT): Boolean;
begin
  if (pType = MT_SUB) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsSub ***}

{*** Test to see if marker type is a missile ***}
function HCMTIsMissile(pType: HCMT): Boolean;
begin
  if (pType = MT_MISSILE) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsMissile ***}

{*** Test to see if marker type is a missile or torpedo ***}
function HCMTIsWeapon(pType: HCMT): Boolean;
begin
  if ((pType = MT_MISSILE) OR (pType = MT_TORP)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsWeapon ***}

{*** Test to see if unit is sinking (damage >= original DP) ***}
function HCIsSinking(pUnit: PHCUnit): Boolean;
begin
  Result := pUnit^.CurrDp >= pUnit^.OrigDp;
end;
{*** end HCIsSinking ***}

{*** Test to see if marker type is a fixed or mobile base ***}
function HCMTIsBase(pType: HCMT): Boolean;
begin
  if ((pType = MT_BASE) OR (pType = MT_AIRFIELD) OR (pType = MT_PORT)
      OR (pType = MT_CITY) OR (pType = MT_SAMFIXED)
      OR (pType = MT_SAMMOBILE) OR (pType = MT_CONVOY) OR (pType = MT_ARMORED)
      ) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsBase ***}

{*** Test to see if marker type is a land detectable unit, not always detected ***}
function HCMTIsLandDet(pType: HCMT): Boolean;
begin
  if ((pType = MT_SAMFIXED) OR (pType = MT_SAMMOBILE) OR (pType = MT_CONVOY)
      OR (pType = MT_ARMORED)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsLandDet ***}

{*** Test to see if marker type is a mobile land unit ***}
function HCMTIsLandMob(pType: HCMT): Boolean;
begin
  if ((pType = MT_SAMMOBILE) OR (pType = MT_CONVOY) OR (pType = MT_ARMORED)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsLandMob ***}

{*** Test to see if marker type is a fixed base ***}
function HCMTIsFixedBase(pType: HCMT): Boolean;
begin
  if ((pType = MT_BASE) OR (pType = MT_AIRFIELD) OR (pType = MT_PORT)
      OR (pType = MT_CITY)) then
    result := True
  else
    result := False;
end;
{*** end HCMTIsFixedBase ***}

function HCIsFixedBase(const pType: ShortInt): Boolean;
begin
  if ((pType = Ord(UT_BASE)) OR (pType = Ord(UT_AIRFIELD))
   OR (pType = Ord(UT_PORT)) OR (pType = Ord(UT_CITY))) then
    result := TRUE
  else
    result := FALSE;
end;

function HCget_group_id_type(const pPGroup: PHCGroup): AnsiChar;
var
  lIdLen: NativeInt;
begin
  // ALE 20200816 Length gives 8, Pos #0 gives 0 so do it the old fashioned way
  for lIdLen:=0 to Length(pPGroup^.id) - 1 do
  begin
    if (pPGroup^.id[lIdLen] = #0) then
      break;
  end;
  result := pPGroup^.id[lIdLen - 1];
end;

procedure HCset_group_id_type(pPGroup: PHCGroup; const pType: AnsiChar);
begin
  pPGroup^.id[2]:=pType;
  pPGroup^.id[2 + 1]:=#0;
end;

function HCVictoryConditionLevel(const pVicLevel: uchar): String;
begin
  case (pVicLevel) of
    VIC_NONE: result := 'none';
    VIC_MIN: result := 'minimum';
    VIC_TOT: result := 'total';
    VIC_GAME_OVER: result := 'game over';
  else
    result := '';
  end;
end;

function HCLat2Deg(const pHCLat: long): Double;
begin
  result := 90-(pHCLat*360.0/SLIVER360);
end;

function HCLon2Deg(const pHCLon: long): Double;
begin
  result := ((pHCLon*360.0)/SLIVER360);
end;

{*** Provide a Unit's uuid as well as StartGroup OR single Group to check ***}
function HCuuid2UnitPtr(const pUuid: String; const pStartGroup: PHCGroup; const pGroup: PHCGroup): PHCUnit;
var
  lPHCGThis: PHCGroup;
  lPHCUThis: PHCUnit;
begin
  lPHCGThis := nil;
  lPHCUThis := nil;
  Result := nil;
  if (pUuid = '') then
  begin
    exit;  // ALE 20230406 Result set to nil already above
  end;
  if ((pGroup <> nil) AND (pGroup^.Units <> nil)) then
  begin
    // ALE 20230405 we only need to check the specified group
    lPHCGThis := pGroup;
  end
  else if (pStartGroup <> nil) then
  begin
    lPHCGthis := pStartGroup;
  end
  else
  begin
    // ALE 20230405 we have no valid group to start from so can't find a unit
    // ALE 20230406 Result set to nil already above Result := nil;
    exit;
  end;

  while (lPHCGthis <> nil) do // ALE 20230405 we can't check for nil unit here since we may be iterating over entire list of groups which might have empty groups
  begin
    lPHCUthis := lPHCGthis^.Units;
    while (lPHCUthis <> nil) do
    begin
      if (lPHCUthis^.UUID = pUuid) then
      begin
        Result := lPHCUthis;
        exit;
      end;
      lPHCUthis := lPHCUthis^.next;
    end;
    if (pGroup <> nil) then
    begin
      lPHCGthis := lPHCGthis^.toSouth;
    end
    else
    begin
      lPHCGthis := nil;
    end;
  end;
end;

// CC 20260924 Unix epoch milliseconds, UTC
function UnixMsNow: Int64;
{$IFDEF MSWINDOWS}
var
  ft: TFileTime;
begin
  GetSystemTimeAsFileTime(ft);
  // FILETIME counts 100 ns ticks since 1601-01-01
  Result := ((Int64(ft.dwHighDateTime) shl 32) or ft.dwLowDateTime);
  Result := (Result - 116444736000000000) div 10000;
end;
{$ELSE}
begin
  Result := MilliSecondsBetween(LocalTimeToUniversal(Now), UnixDateDelta);
end;
{$ENDIF}

// CC 20260924 UUID version 7 (RFC 9562): 48-bit big-endian Unix milliseconds,
//  version 7, a 12-bit per-millisecond counter (rand_a), the RFC variant, then 62
//  random bits. Same text form as before (36 lowercase hex chars, no braces), so
//  HC and existing callers are unaffected; v7 UUIDs sort by creation time.
//  Randomness comes from CreateGUID (the OS's cryptographic generator).
function GenerateUUIDv7: String;
const
  HEXDIGITS: array[0..15] of Char = '0123456789abcdef';
var
  lRand: TGUID;
  lRandBytes: array[0..15] of Byte absolute lRand;
  b: array[0..15] of Byte;
  lMs: Int64;
  lSeq, i: Integer;
begin
  Result := '';
  if CreateGUID(lRand) <> S_OK then
    Exit;

  gUUIDLock.Enter;
  try
    lMs := UnixMsNow;
    if lMs <= gUUIDLastMs then
    begin
      // same (or an earlier, clock-stepped) millisecond: keep order by counting
      lMs := gUUIDLastMs;
      Inc(gUUIDSeq);
      if gUUIDSeq > $FFF then
      begin
        Inc(lMs);            // counter exhausted: borrow the next millisecond
        gUUIDSeq := lRandBytes[0] and $3FF;
      end;
    end
    else
      gUUIDSeq := lRandBytes[0] and $3FF;  // fresh ms: random start, headroom to count up
    gUUIDLastMs := lMs;
    lSeq := gUUIDSeq;
  finally
    gUUIDLock.Leave;
  end;

  for i := 0 to 5 do
    b[i] := Byte(lMs shr (8 * (5 - i)));
  b[6] := $70 or ((lSeq shr 8) and $0F);   // version 7 + counter high bits
  b[7] := lSeq and $FF;
  b[8] := $80 or (lRandBytes[8] and $3F);   // RFC 9562 variant 10xx
  for i := 9 to 15 do
    b[i] := lRandBytes[i];

  SetLength(Result, 36);
  lSeq := 1;  // reused as the output position
  for i := 0 to 15 do
  begin
    if (i = 4) or (i = 6) or (i = 8) or (i = 10) then
    begin
      Result[lSeq] := '-';
      Inc(lSeq);
    end;
    Result[lSeq] := HEXDIGITS[b[i] shr 4];
    Result[lSeq + 1] := HEXDIGITS[b[i] and $0F];
    Inc(lSeq, 2);
  end;
end;

// CC 20260924 now UUIDv7 (was version 4 from CreateGUID); same text form
function GenerateUUID: String;
begin
  Result := GenerateUUIDv7;
end;

{*** Count the number of selected sectors in the unit's formation entry ***}
function HCFormationSectorCount(const pSectors: ULONGLONG): byte; experimental;
begin
  // ALE 20180409 count how many bits are set, that will be
  //  how many sectors are selected.
  Result := 0;
  if (pSectors = FormationSectorNone) then
  begin
    // ALE 20180322 Main Body
    Result := 0;
  end
  else if (pSectors = FormationSectorAll) then
  begin
    // ALE 20180326 All sectors in ring(s) selected
    Result := UI_FORMATION_SECTORS;
  end
  else
  begin
    if ((pSectors and FormationSector000_045) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector045_090) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector090_135) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector135_180) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector180_225) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector225_270) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector270_315) > 0) then
      Inc(Result);
    if ((pSectors and FormationSector315_000) > 0) then
      Inc(Result);
  end;
end;
{*** end HCFormationSectorCount ***}

/// <summary>HC Search group list for the group with the provided id</summary>
/// <param name="pGroupId">Group Id of group to find</param>
/// <param name="pPStartGroup">Northernmost group from which to start the search.
/// </param>
/// <remarks>
///  <para>Does not check group validity, only finds and returns the GroupPtr</para>
///  <para>History:
///   20230622 ALE Code case cleanup, parameter checking, documentation
///   20230622 DDT Created
///  </para>
/// </remarks>
/// <returns>PHCGroup pointer to the found group or nil if not found</returns>
function HCGroupId2GroupPtr(const pGroupId: string; const pPStartGroup: PHCGroup): PHCGroup; // DDT 20230622
var
  lPThisGroup: PHCGroup;
  lStrId: string;
begin
  result := nil;
  if (pPStartGroup = nil) then
    exit
  else if (pGroupId = '') then
    exit;

  lPThisGroup:= pPStartGroup;
  lStrId:= Format('%2.2s',[pGroupId]);
  while (lPThisGroup <> nil) do
    begin
      if (lStrId = Copy(lPThisGroup^.id,1,2)) then
       exit(lPThisGroup);
      lPThisGroup := lPThisGroup^.toSouth;
    end;
end;

/// <summary>Turns a save game approach/type into a string</summary>
/// <param name="pSaveGameType">Type of save [Manual, AutoSave, ExportDLL, ...]</param>
/// <remarks>
///  <para>History:
///   20250528 ALE Created
///  </para>
/// </remarks>
/// <returns>String representation of pSaveGameType</returns>
function HCSaveGameType2String(const pSaveGameType: THCSaveGameTypes): String;
begin
  if (pSaveGameType = THCSaveGameTypes.Normal) then
    result := 'Normal/Manual'
  else if (pSaveGameType = THCSaveGameTypes.AutoSave) then
    result := 'AutoSave'
  else if (pSaveGameType = THCSaveGameTypes.ExportDLL) then
    result := 'ExportDLL'
  else
    result := 'Undefined';
end;

// CC 20260403 Airborne endurance and closest-base calculations

/// <summary>Determine the slowest non-zero cruise speed for an aircraft platform</summary>
/// <param name="pB1">Pointer to the aircraft's B1 annex data</param>
/// <remarks>
///  <para>Scans SpeedLevels[TH_CRUISE..TH_FULLMP-1] and returns the lowest
///   non-zero value. This gives a best-case (maximum) range estimate.</para>
///  <para>History:
///   20260403 CC Created
///  </para>
/// </remarks>
/// <returns>Slowest cruise speed in knots, or 0 if all speed levels are zero</returns>
function HCSlowestCruiseSpeed(pB1: PHCAnnexB1_disk): ushort;
var
  i: Integer;
begin
  Result := pB1^.SpeedLevels[TH_CRUISE];
  for i := TH_CRUISE + 1 to TH_FULLMP - 1 do
  begin
    if (pB1^.SpeedLevels[i] <> 0) and (pB1^.SpeedLevels[i] < Result) then
      Result := pB1^.SpeedLevels[i];
  end;
end;

/// <summary>Calculate endurance for a single aircraft unit</summary>
/// <param name="pUnit">Unit to evaluate</param>
/// <param name="pStartGroup">Head of the global group linked list</param>
/// <param name="pAnnexHelper">Annex helper for B1 speed lookup</param>
/// <param name="aBingo">Tracks the minimum bingo percentage across units</param>
/// <param name="aResolveBase">When True, always re-resolves the closest base instead
///  of using the cached HomeUnit</param>
/// <param name="aIsAir">Output: set to True if the unit has a valid AirRec and B1 annex</param>
/// <remarks>
///  <para>Internal helper shared by HCGetAirborneEndurance and
///   HCGetAirborneEnduranceUnit. Computes range from fuel endurance and slowest
///   cruise speed. Subtracts distance to home base from internal-fuel-only range
///   for bingo calculation.</para>
///  <para>History:
///   20260403 CC Created
///  </para>
/// </remarks>
/// <returns>Remaining range in nautical miles, or 0 if not an aircraft unit or
///  no fuel remaining</returns>
function HCCalcAirUnitEndurance(pUnit: PHCUnit;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper;
  var aBingo: SmallInt;
  aResolveBase: Boolean;
  out aIsAir: Boolean): LongInt;
var
  pAir: PHCAirRec;
  pB1: PHCAnnexB1_disk;
  lSpeed: ushort;
  lTime, lBingoTime: LongInt;
  lDist, lBingoDist: LongInt;
  lHomeUnit: PHCUnit;
  lRangeDU, lBearing: long;  // lBearing required by HCFindRangeBearing but unused
  lRangeNM: LongInt;
  lBingoPct: LongInt;
begin
  Result := 0;
  aIsAir := False;

  pAir := pUnit^.Air;
  if pAir = nil then
    Exit;

  // CC 20260403 only process aircraft — skip non-B1 annexes
  if pAnnexHelper.HCAnnexShift(pUnit^.Annex) <> PLANE_FLAG then
    Exit;

  pB1 := pAnnexHelper.HCGetAnnexB1(pUnit^.Annex);
  if pB1 = nil then
    Exit;

  aIsAir := True;

  // Step 3a: compute total endurance time (quarter-seconds -> seconds)
  lTime := (pAir^.Endurance + pAir^.DropTanks) shr 2;
  lBingoTime := pAir^.Endurance shr 2;

  // Step 3b: determine slowest cruise speed (knots)
  lSpeed := HCSlowestCruiseSpeed(pB1);
  if lSpeed = 0 then
    Exit;

  // Step 3c: convert to range in NM (speed in knots * time in seconds / 3600)
  lDist := (LongInt(lSpeed) * lTime) div 3600;
  lBingoDist := (LongInt(lSpeed) * lBingoTime) div 3600;

  // Step 3d: bingo calculation — resolve home base
  lHomeUnit := PHCUnit(pAir^.HomeUnit);

  if aResolveBase or (lHomeUnit = nil) then
  begin
    lHomeUnit := HCFindClosestBase(PHCGroup(pUnit^.Group),
      pStartGroup, pAnnexHelper);
    // Cache on the AirRec (same behavior as GE original)
    if not aResolveBase then
      pAir^.HomeUnit := lHomeUnit;
  end;

  if lHomeUnit = nil then
  begin
    aBingo := 0;
    Result := 0;
    Exit;
  end;

  // Compute distance to base (DU), convert to NM
  HCFindRangeBearing(pUnit^.sLat, pUnit^.sLon,
    lHomeUnit^.sLat, lHomeUnit^.sLon, lRangeDU, lBearing);
  lRangeNM := lRangeDU div NM1;

  // Subtract distance to base from internal-fuel range
  lDist := lBingoDist - lRangeNM;

  // Insufficient fuel check
  if (lBingoDist <= lRangeNM) or (lBingoDist <= 0) then
  begin
    aBingo := 0;
    Result := 0;
    Exit;
  end;

  // Compute bingo percentage — track minimum across units
  lBingoPct := ((lBingoDist - lRangeNM) * 100) div lBingoDist;
  if lBingoPct < aBingo then
    aBingo := SmallInt(lBingoPct);

  Result := lDist;
end;

/// <summary>Find the longest runway required by any aircraft in the group</summary>
function HCFindLongestRunwayNeeded(pGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper): uchar;
var
  pUnit: PHCUnit;
  pB1: PHCAnnexB1_disk;
begin
  Result := 0;
  pUnit := pGroup^.Units;
  while pUnit <> nil do
  begin
    if pAnnexHelper.HCAnnexShift(pUnit^.Annex) = PLANE_FLAG then
    begin
      pB1 := pAnnexHelper.HCGetAnnexB1(pUnit^.Annex);
      if (pB1 <> nil) and (pB1^.Runway > Result) then
        Result := pB1^.Runway;
    end;
    pUnit := pUnit^.next;
  end;
end;

/// <summary>Find the closest friendly base that can accept the aircraft group</summary>
function HCFindClosestBase(pAirGroup: PHCGroup;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper): PHCUnit;
var
  pBaseGroup: PHCGroup;
  pBaseUnit: PHCUnit;
  lMaxRunway: uchar;
  lCarrierFlag: Boolean;
  pUnit: PHCUnit;
  pB1: PHCAnnexB1_disk;
  lRange, lBearing: long;  // lBearing required by HCFindRangeBearing but unused
  lMinRange: LongInt;
  lBaseRunway: ShortInt;
  lAnnexShift: NativeInt;
  pA1: PHCAnnexA1_disk;
begin
  Result := nil;
  if pAirGroup = nil then
    Exit;

  // Step 1: determine group requirements
  lMaxRunway := HCFindLongestRunwayNeeded(pAirGroup, pAnnexHelper);

  // Check carrier capability — all units must be carrier-capable
  lCarrierFlag := True;
  pUnit := pAirGroup^.Units;
  while pUnit <> nil do
  begin
    if pAnnexHelper.HCAnnexShift(pUnit^.Annex) = PLANE_FLAG then
    begin
      pB1 := pAnnexHelper.HCGetAnnexB1(pUnit^.Annex);
      if (pB1 <> nil) and ((pB1^.UnitFlags and UF_B1_carrier) = 0) then
      begin
        lCarrierFlag := False;
        Break;
      end;
    end;
    pUnit := pUnit^.next;
  end;

  // Step 2: search all groups
  lMinRange := MaxLongint;
  pBaseGroup := pStartGroup;
  while pBaseGroup <> nil do
  begin
    // Side filter — must be same side
    if pBaseGroup^.side = pAirGroup^.side then
    begin
      // Validity check
      if HCIsValidGroup(pBaseGroup, pStartGroup) = CTRUE then
      begin
        // Step 3: evaluate each unit in candidate group
        pBaseUnit := pBaseGroup^.Units;
        while pBaseUnit <> nil do
        begin
          // Not sinking, not airborne
          if (not HCIsSinking(pBaseUnit)) and
             (not HCMTIsAir(pBaseUnit^.utype)) then
          begin
            lAnnexShift := pAnnexHelper.HCAnnexShift(pBaseUnit^.Annex);

            // Must be a ship/carrier (A1) or land facility (P1/BASE_FLAG)
            if (lAnnexShift = SHIP_FLAG) or (lAnnexShift = BASE_FLAG) then
            begin
              // Compute range
              HCFindRangeBearing(pAirGroup^.sLat, pAirGroup^.sLon,
                pBaseUnit^.sLat, pBaseUnit^.sLon, lRange, lBearing);

              // Determine available runway
              lBaseRunway := 0;
              if lAnnexShift = SHIP_FLAG then
              begin
                pA1 := pAnnexHelper.HCGetAnnexA1(pBaseUnit^.Annex);
                if pA1 <> nil then
                begin
                  lBaseRunway := pA1^.Runway;
                  // Carrier without carrier-capable group: force runway to 0
                  if (pBaseUnit^.utype = MT_CARRIER) and (not lCarrierFlag) then
                    lBaseRunway := 0;
                end;
              end
              else if lAnnexShift = BASE_FLAG then
              begin
                // P1 land facility: runway stored in unit's flooding field
                lBaseRunway := ShortInt(pBaseUnit^.flooding);
              end;

              // Selection: runway adequate, closer than previous, not self
              if (lBaseRunway >= ShortInt(lMaxRunway)) and
                 (lRange < lMinRange) and
                 (String(pBaseGroup^.id) <> String(pAirGroup^.id)) then
              begin
                lMinRange := lRange;
                Result := pBaseUnit;
              end;
            end;
          end;
          pBaseUnit := pBaseUnit^.next;
        end; // unit loop
      end; // valid group
    end; // same side
    pBaseGroup := pBaseGroup^.toSouth;
  end; // group loop
end;

/// <summary>Calculate remaining airborne range for an aircraft group</summary>
function HCGetAirborneEndurance(pGroup: PHCGroup;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper;
  out aBingo: SmallInt): LongInt;
var
  pUnit: PHCUnit;
  lUnitDist: LongInt;
  lIsAir, lFirst: Boolean;
begin
  Result := 0;
  aBingo := 100;
  lFirst := True;

  pUnit := pGroup^.Units;
  while pUnit <> nil do
  begin
    lUnitDist := HCCalcAirUnitEndurance(pUnit, pStartGroup, pAnnexHelper,
      aBingo, False, lIsAir);

    if lIsAir and (lFirst or (lUnitDist < Result)) then
    begin
      Result := lUnitDist;
      lFirst := False;
    end;

    pUnit := pUnit^.next;
  end;
end;

/// <summary>Calculate remaining airborne range for a single aircraft unit</summary>
function HCGetAirborneEnduranceUnit(pUnit: PHCUnit;
  pStartGroup: PHCGroup;
  pAnnexHelper: IHCAnnexHelper;
  out aBingo: SmallInt): LongInt;
var
  lIsAir: Boolean;
begin
  aBingo := 100;

  // Always re-resolve base (aResolveBase=True), matching
  // GetAirborneEnduranceUnit behavior
  Result := HCCalcAirUnitEndurance(pUnit, pStartGroup, pAnnexHelper,
    aBingo, True, lIsAir);
end;

initialization
  gUUIDLock := TCriticalSection.Create;  // CC 20260924

finalization
  gUUIDLock.Free;

end.
