fix(runtime): warm behavior before mission Init
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s

This commit is contained in:
Valentin Popov committed 2026-10-20 05:15:11 +04:00
1 parent 31fd9d2ea9
commit 89a0d1da61
6 files changed
+866 -48

No files matched your search

@@ -8,10 +8,13 @@ using System.Text;
internal static class Handler19DispatchOracle {
private const uint LoadWithAlteredSearchPath = 0x00000008;
private const string ExpectedAiSha256 = "c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc";
private const string ExpectedIron3dSha256 = "d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa";
private const string ExpectedScriptSha256 = "db1c6fde57aff43181619f19e19e1cacf7301ebb859e8cb883db6b4b7181eced";
private const int EventLookupRva = 0x11f40;
private const int EventDispatchRva = 0x11f10;
private const int Handler19Rva = 0x0aa38;
private const int AiWarmStartRva = 0x01ae0;
private const int Iron3dClanRefreshRva = 0x39440;
private const int EventStride = 0x14;
private const int InstructionStride = 0x20;
private const int VarSetStride = 0x30;
@@ -24,6 +27,32 @@ internal static class Handler19DispatchOracle {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate uint DispatchEvent(IntPtr vm, int eventIndex);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate uint ReadVariable(IntPtr record);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void SetDwordPosition(IntPtr record, uint x, uint y);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void RefreshClan(IntPtr record, uint requestedMode);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void HostInterfaceRefresh(IntPtr host);
private sealed class WarmStartObservation {
public int Calls;
public IntPtr Host;
public IntPtr Record;
public IntPtr Ai;
public IntPtr Cells;
public int EntryMode;
public int AiMode;
public byte Latch;
public uint XCellBeforeDispatch;
}
private static WarmStartObservation CurrentWarmStartObservation;
private static HostInterfaceRefresh WarmStartRecorder = RecordWarmStart;
private static void RecordWarmStart(IntPtr host) {
WarmStartObservation observation = CurrentWarmStartObservation;
observation.Calls++;
observation.Host = host;
observation.EntryMode = Marshal.ReadInt32(observation.Record, 0x18);
observation.AiMode = Marshal.ReadInt32(observation.Ai, 0x3fc);
observation.Latch = Marshal.ReadByte(observation.Ai, 0x8a8);
observation.XCellBeforeDispatch = ReadWord(observation.Cells, 0);
}
private sealed class Instruction {
public readonly uint[] Words;
@@ -59,11 +88,13 @@ internal static class Handler19DispatchOracle {
private static int Main() {
if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
string aiPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_AI_DLL");
string iron3dPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_IRON3D_DLL");
string scriptPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_DEFAULT_SCR");
if (String.IsNullOrEmpty(aiPath) || !File.Exists(aiPath) || String.IsNullOrEmpty(scriptPath) || !File.Exists(scriptPath)) {
Console.Error.WriteLine("Set FPARKAN_ORIGINAL_AI_DLL and FPARKAN_ORIGINAL_DEFAULT_SCR to the installed GOG files."); return 3;
if (String.IsNullOrEmpty(aiPath) || !File.Exists(aiPath) || String.IsNullOrEmpty(iron3dPath) || !File.Exists(iron3dPath) || String.IsNullOrEmpty(scriptPath) || !File.Exists(scriptPath)) {
Console.Error.WriteLine("Set FPARKAN_ORIGINAL_AI_DLL, FPARKAN_ORIGINAL_IRON3D_DLL, and FPARKAN_ORIGINAL_DEFAULT_SCR to the installed GOG files."); return 3;
}
if (Hash(aiPath) != ExpectedAiSha256) { Console.Error.WriteLine("Unexpected ai.dll SHA-256: " + Hash(aiPath)); return 4; }
if (Hash(iron3dPath) != ExpectedIron3dSha256) { Console.Error.WriteLine("Unexpected iron3d.dll SHA-256: " + Hash(iron3dPath)); return 4; }
if (Hash(scriptPath) != ExpectedScriptSha256) { Console.Error.WriteLine("Unexpected default.scr SHA-256: " + Hash(scriptPath)); return 5; }
Package package = ReadPackage(scriptPath);
if (package.HandlerCount != 73 || package.Events.Length != 9) throw new InvalidDataException("Unexpected default.scr header.");
@@ -84,12 +115,126 @@ internal static class Handler19DispatchOracle {
RunInstance(package, find, dispatch, readVariable, module, 500.2857f, 752.0f, 0, 500, 752);
RunInstance(package, find, dispatch, readVariable, module, 728.0f, 449.0f, 1, 728, 449);
RunDwordToHandlerRoundTrip(package, find, dispatch, readVariable, setDwordPosition, module);
Console.WriteLine("original ai.dll dispatcher passed for default.scr Init instances and DWORD-to-handler round-trip; code bytes were not modified");
RunOriginalClanRefreshPath(package, module, iron3dPath);
Console.WriteLine("original ai.dll dispatcher passed for default.scr Init instances and DWORD round-trip; original iron3d clan-refresh to AI-vtable Init path also passed; code bytes were not modified");
return 0;
}
finally { FreeLibrary(module); }
}
private static void RunOriginalClanRefreshPath(Package package, IntPtr aiModule, string iron3dPath) {
IntPtr iron3d = LoadLibraryEx(iron3dPath, IntPtr.Zero, LoadWithAlteredSearchPath);
if (iron3d == IntPtr.Zero) throw new InvalidOperationException("LoadLibraryEx(iron3d.dll) failed: " + Marshal.GetLastWin32Error());
try {
RefreshClan refresh = (RefreshClan)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(iron3d, Iron3dClanRefreshRva), typeof(RefreshClan));
IntPtr method = IntPtr.Add(aiModule, AiWarmStartRva);
RunClanRefreshCase(package, aiModule, refresh, method, 1, 2, 2, "forward requested mode");
RunClanRefreshCase(package, aiModule, refresh, method, 0, 2, 1, "zero flag forces mode one");
RunClanRefreshCase(package, aiModule, refresh, method, 1, 1, 1, "forward mode one");
RunClanRefreshMissingCase(package, aiModule, refresh, method);
RunClanRefreshBusyCase(package, aiModule, refresh, method);
RunClanRefreshNullAiCase(refresh);
}
finally { FreeLibrary(iron3d); }
}
private static void RunClanRefreshCase(Package package, IntPtr aiModule, RefreshClan refresh, IntPtr method, int forceFlag, uint requestedMode, uint expectedMode, string label) {
using (Instance instance = BuildInstance(package, 500.0f, 752.0f, 0, IntPtr.Add(aiModule, Handler19Rva))) {
IntPtr host = ConfigureClanRefresh(instance, method);
IntPtr record = PrepareClanRecord(instance, forceFlag);
CurrentWarmStartObservation = new WarmStartObservation { Record = record, Ai = instance.Ai, Cells = instance.Cells };
refresh(record, requestedMode);
WarmStartObservation observed = CurrentWarmStartObservation;
Expect(Marshal.ReadInt32(record, 0x18) == requestedMode, label + ": Iron3D stores the requested per-clan mode");
Expect(Marshal.ReadInt32(instance.Ai, 0x3fc) == expectedMode, label + ": AI callback receives the forced/forwarded mode");
Expect(observed.Calls == 1 && observed.Host == host, label + ": original AI slot called its host interface first");
Expect(observed.EntryMode == requestedMode && observed.AiMode == expectedMode && observed.Latch == 0 && observed.XCellBeforeDispatch == 0,
label + ": record and AI mode are stored before host callback, and Init dispatch follows it");
Expect(Marshal.ReadByte(instance.Ai, 0x78) == 0, label + ": normal Init dispatch clears the recursion guard");
Expect(Marshal.ReadByte(instance.Ai, 0x8a8) == 1, label + ": successful Init lookup latches after dispatch");
Expect(ReadWord(instance.Cells, 0) == 500 && ReadWord(instance.Cells, 1) == 752 && ReadWord(instance.Cells, 2) == 0,
label + ": original Init handler writes X/Y/clan after host callback");
Console.WriteLine("iron3d 39440 -> ai 01ae0 ({0}): entry_mode={1}, ai_mode={2}, latch={3}", label, requestedMode, expectedMode, Marshal.ReadByte(instance.Ai, 0x8a8));
Marshal.WriteInt32(instance.Cells, 0, unchecked((int)0xfeedface));
refresh(record, requestedMode);
Expect(observed.Calls == 2 && Marshal.ReadInt32(instance.Ai, 0x3fc) == expectedMode, label + ": repeat still reaches interface before latch check");
Expect(ReadWord(instance.Cells, 0) == 0xfeedface, label + ": Init latch suppresses a second event dispatch");
}
}
private static void RunClanRefreshMissingCase(Package package, IntPtr aiModule, RefreshClan refresh, IntPtr method) {
Event[] events = (Event[])package.Events.Clone();
bool replaced = false;
for (int i = 0; i < events.Length; i++) {
if (Encoding.ASCII.GetString(events[i].Name) == "Init\0") {
events[i] = new Event(Encoding.ASCII.GetBytes("Missing\0"), events[i].EventWord, events[i].Instructions);
replaced = true; break;
}
}
Expect(replaced, "missing-path package contains Init to remove");
using (Instance instance = BuildInstance(new Package(package.HandlerCount, events), 500.0f, 752.0f, 0, IntPtr.Add(aiModule, Handler19Rva))) {
ConfigureClanRefresh(instance, method);
IntPtr record = PrepareClanRecord(instance, 1);
CurrentWarmStartObservation = new WarmStartObservation { Record = record, Ai = instance.Ai, Cells = instance.Cells };
refresh(record, 2);
Expect(Marshal.ReadByte(instance.Ai, 0x8a8) == 0 && CurrentWarmStartObservation.Calls == 1, "missing Init leaves the native latch clear after the host callback");
Expect(ReadWord(instance.Cells, 0) == 0 && ReadWord(instance.Cells, 1) == 0 && ReadWord(instance.Cells, 2) == 0, "missing Init has no Handler(19) writes");
}
}
private static void RunClanRefreshBusyCase(Package package, IntPtr aiModule, RefreshClan refresh, IntPtr method) {
using (Instance instance = BuildInstance(package, 500.0f, 752.0f, 0, IntPtr.Add(aiModule, Handler19Rva))) {
ConfigureClanRefresh(instance, method);
Marshal.WriteByte(instance.Ai, 0x78, 1);
IntPtr record = PrepareClanRecord(instance, 1);
CurrentWarmStartObservation = new WarmStartObservation { Record = record, Ai = instance.Ai, Cells = instance.Cells };
refresh(record, 2);
Expect(CurrentWarmStartObservation.Calls == 1 && ReadWord(instance.Cells, 0) == 0, "busy recursion guard suppresses Init dispatch");
Expect(Marshal.ReadByte(instance.Ai, 0x78) == 1 && Marshal.ReadByte(instance.Ai, 0x8a8) == 1, "found Init latches even when the preexisting busy guard suppresses dispatch");
Marshal.WriteByte(instance.Ai, 0x78, 0);
Marshal.WriteInt32(instance.Cells, 0, unchecked((int)0xfeedface));
refresh(record, 2);
Expect(CurrentWarmStartObservation.Calls == 2 && Marshal.ReadByte(instance.Ai, 0x78) == 0 && Marshal.ReadByte(instance.Ai, 0x8a8) == 1,
"clearing the busy guard cannot bypass the already-set Init latch");
Expect(ReadWord(instance.Cells, 0) == 0xfeedface, "busy-path latch suppresses a later Init dispatch after the guard clears");
}
}
private static void RunClanRefreshNullAiCase(RefreshClan refresh) {
IntPtr record = Marshal.AllocHGlobal(0x68);
try {
Zero(record, 0x68);
Marshal.WriteInt32(record, 0x0c, 1);
Marshal.WriteInt32(record, 0x18, -1);
refresh(record, 2);
Expect(Marshal.ReadInt32(record, 0x18) == 2, "null AI still stores the requested record mode");
}
finally { Marshal.FreeHGlobal(record); }
}
private static IntPtr ConfigureClanRefresh(Instance instance, IntPtr method) {
IntPtr vtable = instance.Allocate(0x18);
IntPtr host = instance.Allocate(4);
IntPtr hostVtable = instance.Allocate(0x30);
Marshal.WriteInt32(vtable, 0x14, method.ToInt32());
Marshal.WriteInt32(instance.Ai, 0, vtable.ToInt32());
Marshal.WriteInt32(host, 0, hostVtable.ToInt32());
Marshal.WriteInt32(hostVtable, 0x2c, Marshal.GetFunctionPointerForDelegate(WarmStartRecorder).ToInt32());
Marshal.WriteInt32(instance.Ai, 0x438, host.ToInt32());
Marshal.WriteByte(instance.Ai, 0x78, 0);
Marshal.WriteByte(instance.Ai, 0x8a8, 0);
return host;
}
private static IntPtr PrepareClanRecord(Instance instance, int forceFlag) {
IntPtr record = instance.Allocate(0x68);
Marshal.WriteInt32(record, 0x0c, forceFlag);
Marshal.WriteInt32(record, 0x50, instance.Ai.ToInt32());
return record;
}
private static void VerifyEventLookupVectors(Package package, FindEvent find, DispatchEvent dispatch, IntPtr module) {
var duplicateEvents = new Event[package.Events.Length + 1];
Array.Copy(package.Events, duplicateEvents, package.Events.Length);