diff options
Diffstat (limited to 'crates/fparkan-runtime/tests')
| -rw-r--r-- | crates/fparkan-runtime/tests/behavior_warm_start_native_oracle.cs | 164 | ||||
| -rw-r--r-- | crates/fparkan-runtime/tests/handler19_init_native_oracle.cs | 151 |
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); |
