aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-runtime/tests
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 02:20:20 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 02:20:20 +0300
commitdc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2 (patch)
tree998ad76276ac1a942ccd784bdd9ea2203d40b8a1 /crates/fparkan-runtime/tests
parent65f1298d936eb8d8cd2ea9c5578c17d6ec75ba36 (diff)
downloadfparkan-dc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2.tar.xz
fparkan-dc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2.zip
fix(runtime): preserve native mission outcome guards
Diffstat (limited to 'crates/fparkan-runtime/tests')
-rw-r--r--crates/fparkan-runtime/tests/original_host_callback_oracle.cs185
1 files changed, 185 insertions, 0 deletions
diff --git a/crates/fparkan-runtime/tests/original_host_callback_oracle.cs b/crates/fparkan-runtime/tests/original_host_callback_oracle.cs
new file mode 100644
index 0000000..0fd953e
--- /dev/null
+++ b/crates/fparkan-runtime/tests/original_host_callback_oracle.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+
+internal static class HostCallbackOracle {
+ private const uint LoadWithAlteredSearchPath = 0x00000008;
+ private const uint MemCommitReserve = 0x00003000;
+ private const uint PageExecuteReadWrite = 0x40;
+ private const int CallbackRva = 0x611d0;
+ private const int GameGlobalRva = 0x10a5f8;
+ private const int SoundServerIatRva = 0xe42cc;
+ private const string ExpectedSha256 = "d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa";
+
+ [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 IntPtr VirtualAlloc(IntPtr address, UIntPtr size, uint allocationType, uint protect);
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualFree(IntPtr address, UIntPtr size, uint freeType);
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualProtect(IntPtr address, UIntPtr size, uint newProtect, out uint oldProtect);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate int HostCallback(int gameMode, int command, int payload);
+
+ private static int Main() {
+ if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
+ string dllPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_IRON3D_DLL");
+ if (String.IsNullOrEmpty(dllPath) || !File.Exists(dllPath)) {
+ Console.Error.WriteLine("Set FPARKAN_ORIGINAL_IRON3D_DLL to the original GOG iron3d.dll."); return 3;
+ }
+ string hash;
+ using (SHA256 sha = SHA256.Create()) using (FileStream input = File.OpenRead(dllPath)) {
+ byte[] digest = sha.ComputeHash(input); StringBuilder hex = new StringBuilder();
+ foreach (byte value in digest) hex.Append(value.ToString("x2")); hash = hex.ToString();
+ }
+ if (hash != ExpectedSha256) { Console.Error.WriteLine("Unexpected iron3d.dll SHA-256: " + hash); return 4; }
+ IntPtr module = LoadLibraryEx(dllPath, IntPtr.Zero, LoadWithAlteredSearchPath);
+ if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx failed: " + Marshal.GetLastWin32Error()); return 5; }
+
+ IntPtr game = IntPtr.Zero, app = IntPtr.Zero, manager = IntPtr.Zero, sound = IntPtr.Zero, vtable = IntPtr.Zero;
+ IntPtr callStub = IntPtr.Zero, returnStub = IntPtr.Zero;
+ IntPtr gameGlobal = IntPtr.Zero; int originalGameGlobal = 0; bool gameGlobalPatched = false;
+ uint oldProtect = 0; bool iatPatched = false; int originalSoundPointer = 0;
+ try {
+ game = Marshal.AllocHGlobal(0x200); app = Marshal.AllocHGlobal(0x200); manager = Marshal.AllocHGlobal(0x40);
+ sound = Marshal.AllocHGlobal(4); vtable = Marshal.AllocHGlobal(0x14);
+ IntPtr observations = Marshal.AllocHGlobal(56);
+ try {
+ Zero(game, 0x200); Zero(app, 0x200); Zero(manager, 0x40); Zero(sound, 4); Zero(vtable, 0x14); Zero(observations, 56);
+ Marshal.WriteInt32(game, 4, app.ToInt32()); Marshal.WriteInt32(game, 0x2c, manager.ToInt32());
+ Marshal.WriteInt32(manager, 0x10, IntPtr.Zero.ToInt32());
+ Marshal.WriteInt32(sound, vtable.ToInt32());
+ IntPtr codePage = VirtualAlloc(IntPtr.Zero, new UIntPtr(0x1000), MemCommitReserve, PageExecuteReadWrite);
+ if (codePage == IntPtr.Zero) throw new InvalidOperationException("VirtualAlloc failed: " + Marshal.GetLastWin32Error());
+ callStub = codePage;
+ byte[] soundMethod = BuildSoundMethod(observations, game, app);
+ Marshal.Copy(soundMethod, 0, callStub, soundMethod.Length);
+ Marshal.WriteInt32(vtable, 0x10, callStub.ToInt32());
+ returnStub = IntPtr.Add(codePage, 0x100);
+ byte[] returnCode = { 0xb8, 0, 0, 0, 0, 0xc3 };
+ WriteImm32(returnCode, 1, sound.ToInt32()); Marshal.Copy(returnCode, 0, returnStub, returnCode.Length);
+
+ IntPtr iat = IntPtr.Add(module, SoundServerIatRva);
+ originalSoundPointer = Marshal.ReadInt32(iat);
+ if (!VirtualProtect(iat, new UIntPtr(4), PageExecuteReadWrite, out oldProtect))
+ throw new InvalidOperationException("VirtualProtect(IAT) failed: " + Marshal.GetLastWin32Error());
+ Marshal.WriteInt32(iat, returnStub.ToInt32());
+ iatPatched = true;
+ uint ignored;
+ if (!VirtualProtect(iat, new UIntPtr(4), oldProtect, out ignored))
+ throw new InvalidOperationException("VirtualProtect(IAT restore protection) failed: " + Marshal.GetLastWin32Error());
+
+ gameGlobal = IntPtr.Add(module, GameGlobalRva);
+ originalGameGlobal = Marshal.ReadInt32(gameGlobal);
+ Marshal.WriteInt32(gameGlobal, game.ToInt32());
+ gameGlobalPatched = true;
+ var callback = (HostCallback)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, CallbackRva), typeof(HostCallback));
+ RunAllowedFailure(callback, game, app, observations);
+ RunFailureRepeat(callback, game, app, observations);
+ RunOppositeCompletion(callback, game, app, observations);
+ RunCompletionRepeat(callback, game, app, observations);
+ RunFailureAfterCompletion(callback, game, app, observations);
+ RunAutoDemoGate(callback, game, app, observations);
+ RunNonZeroGameMode(callback, game, app, observations);
+ RunUnknownPayload(callback, game, app, observations);
+ Console.WriteLine("native callback oracle passed; audio endpoint is an isolated no-op recorder");
+ return 0;
+ }
+ finally { Marshal.FreeHGlobal(observations); }
+ }
+ finally {
+ if (gameGlobalPatched) Marshal.WriteInt32(gameGlobal, originalGameGlobal);
+ if (iatPatched) {
+ IntPtr iat = IntPtr.Add(module, SoundServerIatRva); uint restore;
+ if (!VirtualProtect(iat, new UIntPtr(4), PageExecuteReadWrite, out restore))
+ throw new InvalidOperationException("VirtualProtect(IAT cleanup) failed: " + Marshal.GetLastWin32Error());
+ Marshal.WriteInt32(iat, originalSoundPointer);
+ uint ignored;
+ if (!VirtualProtect(iat, new UIntPtr(4), restore, out ignored))
+ throw new InvalidOperationException("VirtualProtect(IAT cleanup restore protection) failed: " + Marshal.GetLastWin32Error());
+ }
+ if (callStub != IntPtr.Zero) VirtualFree(callStub, UIntPtr.Zero, 0x8000);
+ if (vtable != IntPtr.Zero) Marshal.FreeHGlobal(vtable);
+ if (sound != IntPtr.Zero) Marshal.FreeHGlobal(sound);
+ if (manager != IntPtr.Zero) Marshal.FreeHGlobal(manager);
+ if (app != IntPtr.Zero) Marshal.FreeHGlobal(app);
+ FreeLibrary(module);
+ if (game != IntPtr.Zero) Marshal.FreeHGlobal(game);
+ }
+ }
+
+ private static byte[] BuildSoundMethod(IntPtr observations, IntPtr game, IntPtr app) {
+ var code = new List<byte>();
+ code.AddRange(new byte[] { 0x50, 0x52, 0x51, 0x56, 0x57 });
+ code.Add(0xa1); AddImm32(code, observations.ToInt32()); code.Add(0x40); code.Add(0xa3); AddImm32(code, observations.ToInt32());
+ code.AddRange(new byte[] { 0x8b, 0x74, 0x24, 0x18, 0x8b, 0x06, 0x8b, 0x56, 0x04, 0x2b, 0xd0, 0x89, 0x15 }); AddImm32(code, observations.ToInt32() + 4);
+ code.AddRange(new byte[] { 0x83, 0xfa, 0x17, 0x76, 0x09, 0xc6, 0x05 }); AddImm32(code, observations.ToInt32() + 48);
+ code.AddRange(new byte[] { 0x01, 0xeb, 0x0f, 0xbf }); AddImm32(code, observations.ToInt32() + 12);
+ code.AddRange(new byte[] { 0x8b, 0xca, 0x8b, 0xf0, 0xfc, 0xf3, 0xa4, 0xc6, 0x07, 0x00 });
+ code.AddRange(new byte[] { 0x8b, 0x44, 0x24, 0x1c, 0xa3 }); AddImm32(code, observations.ToInt32() + 8);
+ code.Add(0xb8); AddImm32(code, game.ToInt32());
+ code.AddRange(new byte[] { 0x8b, 0x40, 0x08, 0xa3 }); AddImm32(code, observations.ToInt32() + 40);
+ code.Add(0xb8); AddImm32(code, app.ToInt32());
+ code.AddRange(new byte[] { 0x0f, 0xb6, 0x80, 0x57, 0x01, 0x00, 0x00, 0xa3 }); AddImm32(code, observations.ToInt32() + 44);
+ code.AddRange(new byte[] { 0x5f, 0x5e, 0x59, 0x5a, 0x58, 0xc2, 0x08, 0x00 });
+ return code.ToArray();
+ }
+
+ private static void RunAllowedFailure(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ Setup(game, app, obs, 0, 5, 0x7f); int result = callback(0, 0, 0);
+ Expect(result == 1 && Marshal.ReadInt32(game, 8) == 1 && Marshal.ReadByte(app, 0x157) == 0, "failure state writes");
+ Expect(Marshal.ReadInt32(obs, 0) == 1 && Marshal.ReadInt32(obs, 40) == 5 && Marshal.ReadInt32(obs, 44) == 0x7f, "failure sound before writes");
+ Console.WriteLine("fail: status=1 mission_flag=0 sound={0}", SoundId(obs));
+ Expect(SoundId(obs) == "VOICE_MISSION_FAIL" && Marshal.ReadInt32(obs, 8) == 0, "failure voice id and argument");
+ }
+ private static void RunFailureRepeat(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ Marshal.WriteByte(app, 0x157, 0x7f);
+ int result = callback(0, 0, 0);
+ Expect(result == 1 && Marshal.ReadInt32(game, 8) == 1 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 1, "repeat failure preserves mismatched mission flag");
+ }
+ private static void RunOppositeCompletion(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ int result = callback(0, 0, 1); Expect(result == 1 && Marshal.ReadInt32(game, 8) == 0 && Marshal.ReadByte(app, 0x157) == 1 && Marshal.ReadInt32(obs, 0) == 2, "completion after failure");
+ Expect(Marshal.ReadInt32(obs, 40) == 1 && Marshal.ReadInt32(obs, 44) == 0x7f && SoundId(obs) == "VOICE_MISSION_COMPLETE" && Marshal.ReadInt32(obs, 8) == 0, "completion sound before writes");
+ }
+ private static void RunCompletionRepeat(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ Marshal.WriteByte(app, 0x157, 0x7f);
+ int result = callback(0, 0, 1);
+ Expect(result == 1 && Marshal.ReadInt32(game, 8) == 0 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 2, "repeat completion preserves mismatched mission flag");
+ }
+ private static void RunFailureAfterCompletion(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ int result = callback(0, 0, 0);
+ Expect(result == 1 && Marshal.ReadInt32(game, 8) == 1 && Marshal.ReadByte(app, 0x157) == 0 && Marshal.ReadInt32(obs, 0) == 3, "failure after completion");
+ Expect(Marshal.ReadInt32(obs, 40) == 0 && Marshal.ReadInt32(obs, 44) == 0x7f && SoundId(obs) == "VOICE_MISSION_FAIL" && Marshal.ReadInt32(obs, 8) == 0, "reverse transition sound before writes");
+ Console.WriteLine("fail-after-complete: status=1 mission_flag=0 sound={0}", SoundId(obs));
+ }
+ private static void RunAutoDemoGate(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ Setup(game, app, obs, 1, 5, 0x7f); callback(0, 0, 0);
+ Expect(Marshal.ReadInt32(game, 8) == 5 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 0, "AutoDemo gate suppresses callback");
+ }
+ private static void RunNonZeroGameMode(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ Setup(game, app, obs, 0, 5, 0x7f); callback(1, 0, 0);
+ Expect(Marshal.ReadInt32(game, 8) == 5 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 0, "nonzero callback mode suppressed");
+ }
+ private static void RunUnknownPayload(HostCallback callback, IntPtr game, IntPtr app, IntPtr obs) {
+ Setup(game, app, obs, 0, 5, 0x7f); callback(0, 0, 2);
+ Expect(Marshal.ReadInt32(game, 8) == 5 && Marshal.ReadByte(app, 0x157) == 0x7f && Marshal.ReadInt32(obs, 0) == 0, "unknown payload suppressed");
+ }
+ private static void Setup(IntPtr game, IntPtr app, IntPtr obs, byte gate, int status, byte missionFlag) {
+ Marshal.WriteByte(game, 0xe5, gate); Marshal.WriteInt32(game, 8, status); Marshal.WriteByte(app, 0x157, missionFlag);
+ Zero(obs, 56);
+ }
+ private static void Expect(bool value, string description) { if (!value) throw new InvalidOperationException("native oracle assertion failed: " + description); }
+ private static string SoundId(IntPtr observations) {
+ int length = Marshal.ReadInt32(observations, 4);
+ if (Marshal.ReadByte(observations, 48) != 0) throw new InvalidOperationException("native sound identifier exceeded recorder capacity");
+ if (length < 0 || length > 23) throw new InvalidOperationException("unexpected sound identifier length: " + length);
+ byte[] bytes = new byte[length]; Marshal.Copy(IntPtr.Add(observations, 12), bytes, 0, length);
+ return Encoding.ASCII.GetString(bytes);
+ }
+ private static void Zero(IntPtr buffer, int length) { for (int offset = 0; offset < length; offset++) Marshal.WriteByte(buffer, offset, 0); }
+ private static void AddImm32(List<byte> code, int value) { byte[] bytes = BitConverter.GetBytes(value); code.AddRange(bytes); }
+ private static void WriteImm32(byte[] bytes, int offset, int value) { byte[] immediate = BitConverter.GetBytes(value); Buffer.BlockCopy(immediate, 0, bytes, offset, 4); }
+}