{$mode delphi}{$H+} library MapWindow; // CC 20260310 Lazarus/FPC port of MapWindow.dpr // CC 20260323 Threaded architecture — form runs on its own thread uses Classes, Windows, Forms, Interfaces, ShellAPI, SysUtils, Types, Dialogs, Controls, DateUtils, uHCDefines in '..\..\shared\pascal\uHCDefines.pas', uHCGCI in '..\..\shared\pascal\uHCGCI.pas', uHCGlobals in '..\..\shared\pascal\uHCGlobals.pas', uHCTypeDefs in '..\..\shared\pascal\uHCTypeDefs.pas', uHCEventHelpers in '..\..\shared\pascal\uHCEventHelpers.pas', uHCDateTime in '..\..\shared\pascal\uHCDateTime.pas', uHCAnnexDefs in '..\..\shared\pascal\uHCAnnexDefs.pas', uHCAnnexHelpers in '..\..\shared\pascal\uHCAnnexHelpers.pas', uHCUtils in '..\..\shared\pascal\uHCUtils.pas', mainform in 'mainform.pas' {frmMain}, uCICG4Presenters in '..\..\..\shared\pascal\uCICG4Presenters.pas', uGroupTypes, uGroupSnapshot, uThreadComm, uFormThread {$IFDEF DEBUG}, LazLogger{$ENDIF}; {$R *.res} const DLLVERN = 16; DLLVERINTN = 34; var gFormThread: TFormThread; {*** Return DLL internal version, i.e. revision 5 of my DLL ***} function DLLVer(): word; export; cdecl; begin Result := DLLVERN; end; {*** Return DLL interface version, i.e. revision 1 of my HCE interface spec ***} function DLLVerInt(): word; export; cdecl; begin Result := DLLVERINTN; end; {*** Return DLL description, 255 characters max ***} function DLLDesc(): PAnsiChar; export; cdecl; begin Result := 'Map Window test DLL by Tony Eischens'; end; {*** Free DLL ***} procedure DLLFree(); export; cdecl; begin // CC 20260323 terminate the form thread's message loop — WM_CLOSE alone just // hides the form; PostThreadMessage(WM_QUIT) causes GetMessage to return 0 if gFormThread <> nil then begin PostThreadMessage(gFormThread.ThreadID, WM_QUIT, 0, 0); gFormThread.WaitFor; gFormThread.Free; gFormThread := nil; end; DoneThreadComm; end; {*** Init DLL ***} 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); export; cdecl; var funcTmp: PHCDLL_FUNC_REF; buf: array[0..MAX_PATH] of Char; begin // Store DLL path GetModuleFileName(hInstance, buf, SizeOf(buf)); gDLLPathFileName := buf; // Store HC callback pointers in thread comm globals gPDLLBusy := PDLLBusy; gVerSaveGame := verSaveGame; gVerInterface := verInterface; 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) else if (funcTmp^.funcName = 'QueueHarpoonEvent') then gHCQueueHarpoonEvent := THCQueueHarpoonEvent(funcTmp^.funcPtr) else if (funcTmp^.funcName = 'dllCheckLatLonDepth') then gHCdllCheckLatLonDepth := THCdllCheckLatLonDepth(funcTmp^.funcPtr) else if (funcTmp^.funcName = 'MessageLogLine') then gHCMessageLogLine := THCMessageLogLine(funcTmp^.funcPtr) else if (funcTmp^.funcName = 'annex_id_to_disk_id') then // CC 20260403 gAnnex_id_to_disk_id := THCannex_id_to_disk_id(funcTmp^.funcPtr); funcTmp := funcTmp^.Next; end; // Initialize thread communication InitThreadComm; // Spawn form thread with its own message loop gFormThread := TFormThread.Create( 'MapWindow Export DLL v' + IntToStr(DLLVERN) + ', ExportDLL Interface v' + IntToStr(DLLVERINTN), DLLVERN, DLLVERINTN); gFormThread.WaitReady(5000); // wait up to 5s for form creation end; 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; begin byte(gPDLLBusy^) := CTRUE; // CC 20260403 create annex helper for endurance calculations if AnnexDataIn <> nil then gAnnexHelper := THCAnnexHelper.Create( AnnexDataIn, gAnnex_id_to_disk_id, gVerInterface, gVerSaveGame) else gAnnexHelper := nil; ClearAirReformWatch; // CC 20260924 phase 3 // Clear pending callback queue EnterCriticalSection(gPendingCS); gPendingCount := 0; SetLength(gPendingRequests, 0); LeaveCriticalSection(gPendingCS); // CC 20260922 invalidate the old scenario's snapshot so a queued apply or // cooldown timer on the form thread can't render it after the clear EnterCriticalSection(gSnapshotCS); gSharedSnapshot.Ready := False; gSharedSnapshot.Count := 0; SetLength(gSharedSnapshot.Groups, 0); LeaveCriticalSection(gSnapshotCS); // Tell form thread to clear all groups (synchronous) if gFormHWND <> 0 then SendMessage(gFormHWND, WM_SCEN_LOAD, 0, 0); gGameTime := intGameTimeIn; gGameTimeLeft := intGameTimeLeftIn; gSide := intSide; // CC 20260924 gHarpoonEventHead is deliberately not reset here: HC already // sent the new head (see HarpoonEventHead below) // Show form if gFormHWND <> 0 then PostMessage(gFormHWND, WM_SHOW_FORM, 0, 0); byte(gPDLLBusy^) := CFALSE; end; procedure GameTime(GameTime: Long; GameTimeLeft: Long); export; cdecl; var lLocalSnap: TGameTimeSnapshot; begin byte(gPDLLBusy^) := CTRUE; gGameTimeNow := GameTime; // CC 20260924 phase 4 // CC 20260323 Execute any pending HC callback requests from the form thread ExecutePendingHCCallbacks; WatchAirReform; // CC 20260924 phase 3: re-slave arrived aircraft-group air units // CC 20260323 Capture to local (no lock needed — only this thread reads HC data) CaptureGameTimeSnapshot(gPStartGroup, GameTime, GameTimeLeft, gSide, gAnnexHelper, lLocalSnap); // CC 20260323 Swap into shared buffer under lock (fast — just record assignment) EnterCriticalSection(gSnapshotCS); try gSharedSnapshot := lLocalSnap; Inc(gSnapshotGeneration); finally LeaveCriticalSection(gSnapshotCS); end; // CC 20260323 Post only if no WM_APPLY_SNAPSHOT already pending — prevents flooding if (gFormHWND <> 0) and (InterlockedExchange(gSnapshotPending, 1) = 0) then PostMessage(gFormHWND, WM_APPLY_SNAPSHOT, 0, 0); byte(gPDLLBusy^) := CFALSE; end; procedure StartGroup(StartGroup: PHCGroup); export; cdecl; begin gPStartGroup := StartGroup; end; // CC 20260924 HC calls this whenever the head of its HarpoonEvent list changes // (harpoon.c QueHarpoonEvent, Effects.c ProcessEvents, formedit.c, ...). With // DLLVerInt >= 34 it passes the current THCHarpoonEvent layout (DLLExport.c // dllexHarpoonEventHead). This export is the only source of the head: HC // exports no getter and THCGlobals (ScenLoad's GlobalsIn) holds no event // pointer. It is reliable across loads because delete_old_memory (Initscen.c) // sends nil before any new events exist, and every event is then queued // through QueHarpoonEvent, which reports each new head. ScenLoad must NOT // reset gHarpoonEventHead: for a saved game dllexScenLoad runs after the event // list has been rebuilt (Loadgame.c RestoreGame), so a reset would lose it. // Known gap: Command.c (Orders > Refuel) frees an AIR_REFUEL_EVENT with // free_HarpoonEvent without notifying; if that event was the head, the stored // pointer is stale until HC's next head change. Called on HC's thread only. procedure HarpoonEventHead(HarpEventHead: PHCHarpoonEvent); export; cdecl; begin gHarpoonEventHead := HarpEventHead; // CC 20260924 remember what the head is while HC vouches for it if HarpEventHead <> nil then begin gHeadEventType := HarpEventHead^.event_type; gHeadGroup := HarpEventHead^.ourGroup; end else begin gHeadEventType := 0; gHeadGroup := nil; end; end; // CC 20260924 the one place the GE changes the event head without calling the // HarpoonEventHead export is Orders > Refuel: Command.c:1029 frees the group's // existing refuel event with free_HarpoonEvent (Memfs.c:1213), which reassigns // the head silently, then immediately queues a replacement AIR_REFUEL_EVENT for // the same group. That replacement arrives here. If the head HC last reported is // a refuel event of that same group (and the replacement didn't itself become the // head, which HC would have reported first), our head was just freed: forget it // until HC reports the next head. Only values recorded while the head was valid // are compared - the old head is never dereferenced. A DLL queueing an extra // refuel for the group (e.g. Toolbox) gives a harmless false alarm: the head is // dropped briefly, never read after being freed. procedure HarpoonEventQueued(HarpEvent: PHCHarpoonEvent); export; cdecl; begin if (HarpEvent = nil) or (gHarpoonEventHead = nil) then Exit; if HarpEvent = gHarpoonEventHead then Exit; if (HarpEvent^.event_type = AIR_REFUEL_EVENT) and (gHeadEventType = AIR_REFUEL_EVENT) and (gHeadGroup = HarpEvent^.ourGroup) then begin gHarpoonEventHead := nil; gHeadEventType := 0; gHeadGroup := nil; end; end; // CC 20260323 receive time compression setting from HC procedure TimeFactor(TimeFactor: longint); export; cdecl; begin gTimeFactor := TimeFactor; end; exports DLLVer, DLLVerInt, DLLDesc, DLLFree , DLLInit , ScenLoad { , UnitLatLon, UnitShowing, UnitURegion, UnitCourse, UnitSpeed, UnitAlt, UnitShowingSide, UnitSink, UnitToGroup, UnitNew, UnitFree } , TimeFactor // CC 20260323 receive time compression , GameTime , StartGroup // ALE 20130205 Sends updated GroupPtr whenever that changes , HarpoonEventHead // ALE 20130805 Sends updated soonest HarpoonEvent pointer whenever it changes , HarpoonEventQueued // ALE 20200825 Sends HarpoonEvent pointer when an event is added { , SonobuoyRegionHead // ALE 20130812 Sends updated pointer to all sonobuoys when it changes , AllDeadUnitsPtr // ALE 20130812 Sends updated pointer to the list of killed units used for victory condition evaluation , WeatherGroups // ALE 20130812 Sends update GroupPtr whenever the top weather group changes , VictoryConditionsList // ALE 20130812 Sends a pointer to the list of victory conditions when a scenario is loaded , ActGroupPtr // ALE 20160305 Sends pointer to the Group the player just selected or NULL , ActUnitPtr // ALE 20160305 Sends pointer to the Unit the player just selected or NULL , SaveGame // ALE 20170923 UUIDs to associate any data with the right saved game file , VictoryConditionAchieved // ALE 20211212 Lets us know when a VC is achieved , VictoryConditionLevels // ALE 20211213 Lets us know when a VC level is reached by a side } ; // CC 20260314 prevent RunError(101) on DLL unload — no console attached // HeapTrc and LCL logger both WriteLn to stderr/stdout during finalization. // In a DLL hosted by a GUI app there is no console, so the writes raise // RunError(101). Fix: close invalid handles under {$I-} to suppress IO errors, // and in debug builds redirect heaptrc + LCL logger output to files. var buf: array[0..MAX_PATH] of Char; dllDir: string; begin IsConsole := False; {$I-} Close(Output); Close(ErrOutput); Close(StdOut); Close(StdErr); {$I+} {$IFDEF DEBUG} GetModuleFileName(hInstance, buf, SizeOf(buf)); dllDir := ExtractFilePath(buf); SetHeapTraceOutput(dllDir + 'MapWindow_heaptrc.log'); GetDebugLogger.LogName := dllDir + 'MapWindow_debug.log'; GetDebugLogger.UseStdOut := False; {$ENDIF} end.