aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-runtime/tests
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 04:15:11 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 04:15:11 +0300
commit89a0d1da6180767dd20fccc47227580b83f9a874 (patch)
tree8daaa0d1ef4061084b998be09c33d0affaa85172 /crates/fparkan-runtime/tests
parent31fd9d2ea9b7fb01a952eef38a2af31eceebbe86 (diff)
downloadfparkan-89a0d1da6180767dd20fccc47227580b83f9a874.tar.xz
fparkan-89a0d1da6180767dd20fccc47227580b83f9a874.zip
fix(runtime): warm behavior before mission Init
Diffstat (limited to 'crates/fparkan-runtime/tests')
-rw-r--r--crates/fparkan-runtime/tests/behavior_warm_start_native_oracle.cs164
-rw-r--r--crates/fparkan-runtime/tests/handler19_init_native_oracle.cs151
2 files changed, 312 insertions, 3 deletions
diff --git a/crates/fparkan-runtime/tests/behavior_warm_start_native_oracle.cs b/crates/fparkan-runtime/tests/behavior_warm_start_native_oracle.cs
new file mode 100644
index 0000000..3620bf3
--- /dev/null
+++ b/crates/fparkan-runtime/tests/behavior_warm_start_native_oracle.cs
@@ -0,0 +1,164 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+
+internal static class BehaviorWarmStartOracle {
+ private const uint LoadWithAlteredSearchPath = 0x00000008;
+ private const string ExpectedBehaviorSha256 = "ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99";
+ private const int WarmStartRva = 0x1adb0;
+ private const int GetQueueSlotRva = 0x59110;
+ private const int RandomStateRva = 0x63c1c;
+ private static uint currentQueueTime;
+ private static IntPtr queue;
+ private static bool queuePointerMatched;
+ private static QueueGetter queueGetter = GetCurrentQueue;
+ private static QueueTimeGetter queueTimeGetter = ReadQueueTime;
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ private static extern IntPtr LoadLibraryEx(string fileName, IntPtr file, uint flags);
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool FreeLibrary(IntPtr module);
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint protect, out uint oldProtect);
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr QueueGetter();
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint QueueTimeGetter(IntPtr self);
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void WarmStart(IntPtr self);
+
+ private static IntPtr GetCurrentQueue() { return queue; }
+ private static uint ReadQueueTime(IntPtr self) { queuePointerMatched &= self == queue; return currentQueueTime; }
+
+ private static int Main() {
+ if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
+ string path = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_BEHAVIOR_DLL");
+ if (String.IsNullOrEmpty(path)) path = @"C:\GOG Games\Parkan - Iron Strategy\Behavior.dll";
+ if (!File.Exists(path)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_BEHAVIOR_DLL to the installed Behavior.dll."); return 3; }
+ string hash = Hash(path);
+ if (hash != ExpectedBehaviorSha256) { Console.Error.WriteLine("Unexpected Behavior.dll SHA-256: " + hash); return 4; }
+ IntPtr module = LoadLibraryEx(path, IntPtr.Zero, LoadWithAlteredSearchPath);
+ if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Behavior.dll) failed: " + Marshal.GetLastWin32Error()); return 5; }
+ IntPtr slot = IntPtr.Add(module, GetQueueSlotRva);
+ IntPtr oldQueueGetter = Marshal.ReadIntPtr(slot);
+ int oldRandomValue = Marshal.ReadInt32(IntPtr.Add(module, RandomStateRva));
+ IntPtr queueVtable = Marshal.AllocHGlobal(0x40);
+ queue = Marshal.AllocHGlobal(0x80);
+ IntPtr record = Marshal.AllocHGlobal(0x44);
+ IntPtr secondRecord = Marshal.AllocHGlobal(0x44);
+ IntPtr zeroClockRecord = Marshal.AllocHGlobal(0x44);
+ IntPtr wrappingClockRecord = Marshal.AllocHGlobal(0x44);
+ uint oldSlotProtection = 0;
+ bool slotProtectionChanged = false;
+ try {
+ Zero(queueVtable, 0x40); Zero(queue, 0x80);
+ Zero(record, 0x44); Zero(secondRecord, 0x44); Zero(zeroClockRecord, 0x44); Zero(wrappingClockRecord, 0x44);
+ Marshal.WriteIntPtr(queue, 0, queueVtable);
+ Marshal.WriteIntPtr(queueVtable, 0x38, Marshal.GetFunctionPointerForDelegate(queueTimeGetter));
+ // The synthetic queue controls only GetQueue and the +0x38 clock slot.
+ if (!VirtualProtect(slot, new UIntPtr((uint)IntPtr.Size), 0x40, out oldSlotProtection))
+ throw new InvalidOperationException("VirtualProtect(GetQueue IAT) failed: " + Marshal.GetLastWin32Error());
+ slotProtectionChanged = true;
+ Marshal.WriteIntPtr(slot, Marshal.GetFunctionPointerForDelegate(queueGetter));
+ WarmStart prepare = (WarmStart)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, WarmStartRva), typeof(WarmStart));
+
+ uint rngState = 1;
+ Expect(unchecked((uint)oldRandomValue) == rngState, "fresh DLL RNG state starts at the statically observed seed");
+ InitializeRecord(record);
+ currentQueueTime = 100000;
+ queuePointerMatched = true;
+ prepare(record);
+ Expect(queuePointerMatched, "native clock vslot receives the synthetic queue object");
+ uint previousRandomState = rngState;
+ rngState = NextRandomState(rngState);
+ VerifyFirstPreparation(record, currentQueueTime, previousRandomState, rngState, module);
+
+ uint savedDeadline = ReadWord(record, 0x14);
+ for (int offset = 0x30; offset <= 0x40; offset += 4) Marshal.WriteInt32(record, offset, 0x5a5a5a5a);
+ currentQueueTime = ReadWord(record, 0x10);
+ queuePointerMatched = true;
+ prepare(record);
+ Expect(queuePointerMatched, "repeat native clock vslot receives the synthetic queue object");
+ Expect(ReadWord(record, 0x14) == savedDeadline, "repeat before deadline preserves the deadline");
+ Expect(ReadWord(IntPtr.Add(module, RandomStateRva), 0) == rngState, "repeat before deadline consumes no RNG draw");
+ for (int offset = 0x30; offset <= 0x40; offset += 4)
+ Expect(ReadWord(record, offset) == 0x5a5a5a5a, "nonzero first-time field skips the warm-preparation clear pass");
+
+ InitializeRecord(secondRecord);
+ currentQueueTime = 100500;
+ queuePointerMatched = true;
+ prepare(secondRecord);
+ Expect(queuePointerMatched, "second record uses the same synthetic queue");
+ previousRandomState = rngState;
+ rngState = NextRandomState(rngState);
+ VerifyFirstPreparation(secondRecord, currentQueueTime, previousRandomState, rngState, module);
+
+ InitializeRecord(zeroClockRecord);
+ currentQueueTime = 0;
+ queuePointerMatched = true;
+ prepare(zeroClockRecord);
+ Expect(queuePointerMatched, "zero-clock record uses the synthetic queue");
+ previousRandomState = rngState;
+ rngState = NextRandomState(rngState);
+ VerifyFirstPreparation(zeroClockRecord, 0, previousRandomState, rngState, module);
+ for (int offset = 0x30; offset <= 0x40; offset += 4) Marshal.WriteInt32(zeroClockRecord, offset, 0x6b6b6b6b);
+ queuePointerMatched = true;
+ prepare(zeroClockRecord);
+ Expect(queuePointerMatched, "zero-clock record uses the synthetic queue on repeat");
+ previousRandomState = rngState;
+ rngState = NextRandomState(rngState);
+ VerifyFirstPreparation(zeroClockRecord, 0, previousRandomState, rngState, module);
+
+ InitializeRecord(wrappingClockRecord);
+ currentQueueTime = 0xffffff80;
+ queuePointerMatched = true;
+ prepare(wrappingClockRecord);
+ Expect(queuePointerMatched, "wrapping-clock record uses the synthetic queue");
+ previousRandomState = rngState;
+ rngState = NextRandomState(rngState);
+ VerifyFirstPreparation(wrappingClockRecord, currentQueueTime, previousRandomState, rngState, module);
+ Expect(ReadWord(wrappingClockRecord, 0x14) < currentQueueTime, "deadline addition wraps as a DWORD near UINT32_MAX");
+ Console.WriteLine("original Behavior.dll warm-start oracle passed (RVA 0x1adb0; shared RNG, zero-clock repeat, and DWORD wrap).");
+ return 0;
+ }
+ finally {
+ if (slotProtectionChanged) {
+ Marshal.WriteIntPtr(slot, oldQueueGetter);
+ uint ignored;
+ VirtualProtect(slot, new UIntPtr((uint)IntPtr.Size), oldSlotProtection, out ignored);
+ }
+ Marshal.WriteInt32(IntPtr.Add(module, RandomStateRva), oldRandomValue);
+ Marshal.FreeHGlobal(record); Marshal.FreeHGlobal(secondRecord); Marshal.FreeHGlobal(zeroClockRecord); Marshal.FreeHGlobal(wrappingClockRecord);
+ Marshal.FreeHGlobal(queue); Marshal.FreeHGlobal(queueVtable);
+ GC.KeepAlive(queueGetter); GC.KeepAlive(queueTimeGetter);
+ FreeLibrary(module);
+ }
+ }
+
+ private static uint ReadWord(IntPtr address, int offset) { return unchecked((uint)Marshal.ReadInt32(address, offset)); }
+ private static uint NextRandomState(uint state) { return unchecked(state * 0x343fd + 0x269ec3); }
+ private static uint ExpectedDeadline(uint queueTime, uint previousRandomState) {
+ uint random = (NextRandomState(previousRandomState) >> 16) & 0x7fff;
+ uint jitter = ((random & 0xff) * 64) >> 8;
+ return unchecked(queueTime + 3 * 64 + jitter);
+ }
+ private static void InitializeRecord(IntPtr record) {
+ Zero(record, 0x44);
+ Marshal.WriteInt16(record, 0x18, 3); // Three 64-ms base units.
+ Marshal.WriteInt16(record, 0x1a, 1); // One 64-ms jitter range.
+ for (int offset = 0x30; offset <= 0x40; offset += 4)
+ Marshal.WriteInt32(record, offset, unchecked((int)(0xa0a00000u + (uint)offset)));
+ }
+ private static void VerifyFirstPreparation(IntPtr record, uint queueTime, uint previousRandomState, uint randomState, IntPtr module) {
+ Expect(ReadWord(record, 0x10) == queueTime, "warm preparation stores the queue clock in distributor+0x10");
+ Expect(ReadWord(record, 0x14) == ExpectedDeadline(queueTime, previousRandomState), "deadline matches base, configured interval, and native RNG jitter");
+ Expect(ReadWord(IntPtr.Add(module, RandomStateRva), 0) == randomState, "shared RNG advances by exactly one LCG draw");
+ for (int offset = 0x30; offset <= 0x40; offset += 4)
+ Expect(ReadWord(record, offset) == 0, "warm preparation clears distributor field +0x" + offset.ToString("x"));
+ }
+ private static string Hash(string path) {
+ using (SHA256 sha = SHA256.Create()) using (FileStream input = File.OpenRead(path)) {
+ byte[] digest = sha.ComputeHash(input); var hex = new StringBuilder();
+ foreach (byte value in digest) hex.Append(value.ToString("x2")); return hex.ToString();
+ }
+ }
+ private static void Expect(bool value, string message) { if (!value) throw new InvalidOperationException("original Behavior oracle failed: " + message); }
+ private static void Zero(IntPtr buffer, int length) { for (int offset = 0; offset < length; offset++) Marshal.WriteByte(buffer, offset, 0); }
+}
diff --git a/crates/fparkan-runtime/tests/handler19_init_native_oracle.cs b/crates/fparkan-runtime/tests/handler19_init_native_oracle.cs
index 5dc319a..bd2e581 100644
--- a/crates/fparkan-runtime/tests/handler19_init_native_oracle.cs
+++ b/crates/fparkan-runtime/tests/handler19_init_native_oracle.cs
@@ -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);