diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-23 12:20:59 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-23 12:20:59 +0300 |
| commit | 4e16902661bc659e28f81155887ec63dc5746546 (patch) | |
| tree | 34fad67126402bef5d966eea150276fdf8ce0e07 /crates/fparkan-runtime/tests | |
| parent | ce8e65c8ae585098c0c74507bb2b05f14f44e6d4 (diff) | |
| download | fparkan-4e16902661bc659e28f81155887ec63dc5746546.tar.xz fparkan-4e16902661bc659e28f81155887ec63dc5746546.zip | |
feat(runtime): project initial Control mesh metadata
Diffstat (limited to 'crates/fparkan-runtime/tests')
| -rw-r--r-- | crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs | 103 |
1 files changed, 100 insertions, 3 deletions
diff --git a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs index 247d08d..b650d78 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; -// Calls selected original x86 Behavior.dll, ArealMap.dll, and Control.dll paths +// Calls selected original x86 Behavior.dll, ArealMap.dll, AniMesh.dll, and Control.dll paths // against synthetic storage. Only host callbacks, GetSystemArealMap/GetQueue IAT // slots, and bounded vector backing are substituted; tested module code remains installed. internal static class BehaviorDistributorTickOracle { @@ -17,6 +17,7 @@ internal static class BehaviorDistributorTickOracle { private const string ExpectedBehaviorSha256 = "ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99"; private const string ExpectedControlSha256 = "bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e"; private const string ExpectedArealMapSha256 = "a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea"; + private const string ExpectedAniMeshSha256 = "65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e"; private const int TickRva = 0x19e80; private const int DistributorScheduleRva = 0x1adb0; private const int AddObjectRva = 0x1aac0; @@ -40,6 +41,8 @@ internal static class BehaviorDistributorTickOracle { private const int ControlContactRateUpdateRva = 0x12a40; private const int ControlTimedUpdateRva = 0x0bcf0; private const int ArealMapUnitDatComponentBuilderRva = 0x14b90; + private const int AniMeshNodeMetadataQueryRva = 0x051f0; + private const int AniMeshNodeStageCountRva = 0x05840; private const int ControlResourceDeviceConstructorRva = 0x22980; private const int ControlResourceComponentOverrideRva = 0x2d890; private const int ControlResourceCommonSetterRva = 0x21d50; @@ -99,6 +102,8 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceGet(IntPtr self, uint selector); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeFastcallComponentBuilder(IntPtr owner, IntPtr node, uint parentId); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeArealMapEvent(IntPtr owner, uint message, uint subcode, IntPtr descriptor); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeAniMeshNodeMetadataQuery(IntPtr owner, uint nodeIndex, uint selector, IntPtr result); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint NativeAniMeshNodeStageCount(IntPtr owner, uint nodeIndex); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceDeviceConstructor(IntPtr self); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result); @@ -522,6 +527,11 @@ internal static class BehaviorDistributorTickOracle { if (!File.Exists(arealMapPath)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_AREALMAP_DLL to the installed ArealMap.dll."); return 10; } string arealMapHash = Hash(arealMapPath); if (arealMapHash != ExpectedArealMapSha256) { Console.Error.WriteLine("Unexpected ArealMap.dll SHA-256: " + arealMapHash); return 11; } + string aniMeshPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_ANIMESH_DLL"); + if (String.IsNullOrEmpty(aniMeshPath)) aniMeshPath = @"C:\GOG Games\Parkan - Iron Strategy\AniMesh.dll"; + if (!File.Exists(aniMeshPath)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_ANIMESH_DLL to the installed AniMesh.dll."); return 13; } + string aniMeshHash = Hash(aniMeshPath); + if (aniMeshHash != ExpectedAniMeshSha256) { Console.Error.WriteLine("Unexpected AniMesh.dll SHA-256: " + aniMeshHash); return 14; } IntPtr controlModule = LoadLibraryEx(controlPath, IntPtr.Zero, LoadWithAlteredSearchPath); if (controlModule == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Control.dll) failed: " + Marshal.GetLastWin32Error()); return 9; } string path = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_BEHAVIOR_DLL"); @@ -536,6 +546,11 @@ internal static class BehaviorDistributorTickOracle { Console.Error.WriteLine("LoadLibraryEx(ArealMap.dll) failed: " + Marshal.GetLastWin32Error()); FreeLibrary(module); FreeLibrary(controlModule); return 12; } + IntPtr aniMeshModule = LoadLibraryEx(aniMeshPath, IntPtr.Zero, LoadWithAlteredSearchPath); + if (aniMeshModule == IntPtr.Zero) { + Console.Error.WriteLine("LoadLibraryEx(AniMesh.dll) failed: " + Marshal.GetLastWin32Error()); + FreeLibrary(arealMapModule); FreeLibrary(module); FreeLibrary(controlModule); return 15; + } IntPtr import = IntPtr.Add(module, GetSystemArealMapIatRva); IntPtr queueImport = IntPtr.Add(module, GetQueueIatRva); IntPtr originalImport = Marshal.ReadIntPtr(import); @@ -560,7 +575,7 @@ internal static class BehaviorDistributorTickOracle { protectionChanged = true; Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback)); Marshal.WriteIntPtr(queueImport, Marshal.GetFunctionPointerForDelegate(QueueGetterCallback)); - RunSmokeCases(module, controlModule, arealMapModule); + RunSmokeCases(module, controlModule, arealMapModule, aniMeshModule); Console.WriteLine("original native Behavior/ArealMap/Control oracle passed (Behavior Add 0x1aac0, normalize 0x1ac90, scheduler 0x1adb0, tick 0x19e80; ArealMap UnitDAT projection 0x14b90; Control INTO override 0x2d890, provider distribution 0x10ba0, class-4 update 0x12a40 and inactive timing baseline 0xbcf0; synthetic host callbacks, GetSystemArealMap/GetQueue IAT slots, and bounded vector insertion are substituted; Control provider selector vtable is original; no full Control update or Rust parity is claimed)."); return 0; } @@ -587,12 +602,13 @@ internal static class BehaviorDistributorTickOracle { throw new InvalidOperationException("VirtualFree(fastcall callback thunks) failed: " + Marshal.GetLastWin32Error()); if (x87ControlWordCell != IntPtr.Zero) { Marshal.FreeHGlobal(x87ControlWordCell); x87ControlWordCell = IntPtr.Zero; } FreeLibrary(arealMapModule); + FreeLibrary(aniMeshModule); FreeLibrary(module); FreeLibrary(controlModule); } } - private static void RunSmokeCases(IntPtr module, IntPtr controlModule, IntPtr arealMapModule) { + private static void RunSmokeCases(IntPtr module, IntPtr controlModule, IntPtr arealMapModule, IntPtr aniMeshModule) { ushort savedControlWord = CurrentX87ControlWord(); Console.WriteLine("x87-control-word-before=0x" + savedControlWord.ToString("x4") + " (x86 oracle process)"); try { @@ -601,6 +617,7 @@ internal static class BehaviorDistributorTickOracle { RunNativeAddCases(module); RunNativeNormalizeCases(module); RunNativeArealMapComponentBuilderCase(arealMapModule); + RunNativeAniMeshNodeMetadataCases(aniMeshModule); RunNativeControlIntoOverrideCase(controlModule); RunNativeControlResourceDeviceCase(controlModule); RunNativeControlProviderLiveStateCase(controlModule); @@ -1219,6 +1236,86 @@ internal static class BehaviorDistributorTickOracle { } } + private static void RunNativeAniMeshNodeMetadataCases(IntPtr module) { + NativeAniMeshNodeMetadataQuery query = (NativeAniMeshNodeMetadataQuery)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, AniMeshNodeMetadataQueryRva), typeof(NativeAniMeshNodeMetadataQuery)); + NativeAniMeshNodeStageCount stageCount = (NativeAniMeshNodeStageCount)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, AniMeshNodeStageCountRva), typeof(NativeAniMeshNodeStageCount)); + + using (Fixture fixture = new Fixture()) { + IntPtr owner = fixture.Allocate(0x1b0); + IntPtr meshRows = fixture.Allocate(3 * 0x130); + IntPtr nodeData = fixture.Allocate(3 * 0x26); + IntPtr slotData = fixture.Allocate(3 * 0x44); + IntPtr meshData = fixture.Allocate(3 * IntPtr.Size); + IntPtr result = fixture.Allocate(IntPtr.Size); + Marshal.WriteIntPtr(meshData, 0, nodeData); + Marshal.WriteIntPtr(meshData, 2 * IntPtr.Size, slotData); + Marshal.WriteIntPtr(owner, 0x1a8, meshRows); + Marshal.WriteInt32(owner, 0x1ac, 3); + + ushort[] headerFlags = new ushort[] { 0x8201, 0x0180, 0x0400 }; + int[] rawParentIndices = new int[] { 3, 0, 1 }; + int[] runtimeParentIndices = new int[] { -1, 22, 7 }; + for (int nodeIndex = 0; nodeIndex < 3; nodeIndex++) { + IntPtr rawNode = IntPtr.Add(nodeData, nodeIndex * 0x26); + for (int word = 0; word < 19; word++) Marshal.WriteInt16(rawNode, word * 2, -1); + Marshal.WriteInt16(rawNode, 0, unchecked((short)headerFlags[nodeIndex])); + Marshal.WriteInt16(rawNode, 2, unchecked((short)rawParentIndices[nodeIndex])); + + IntPtr meshRow = IntPtr.Add(meshRows, nodeIndex * 0x130); + Marshal.WriteInt32(meshRow, 8, nodeIndex); + Marshal.WriteInt32(meshRow, 0x18, runtimeParentIndices[nodeIndex]); + Marshal.WriteIntPtr(meshRow, 0x12c, meshData); + } + + // Node 0: state0 present, state1 absent, state2 present. Native + // GetStateCount stops at the first missing state, so it returns 1. + WriteNode38Slot(nodeData, 0, 0, 0); + WriteNode38Slot(nodeData, 0, 2, 2); + // Node 1: the prefix is empty even though later states are present. + WriteNode38Slot(nodeData, 1, 1, 1); + WriteNode38Slot(nodeData, 1, 2, 2); + // Node 2: all three consecutive state slots are present. + WriteNode38Slot(nodeData, 2, 0, 0); + WriteNode38Slot(nodeData, 2, 1, 1); + WriteNode38Slot(nodeData, 2, 2, 2); + + for (uint nodeIndex = 0; nodeIndex < 3; nodeIndex++) { + Marshal.WriteIntPtr(result, IntPtr.Zero); + Expect(NativeBoolean(query(owner, nodeIndex, 0x0d, result)) == 1, + "AniMesh selector D returns runtime parent metadata"); + IntPtr parent = Marshal.ReadIntPtr(result); + Expect(runtimeParentIndices[nodeIndex] != rawParentIndices[nodeIndex], + "AniMesh runtime parent fixture differs from the raw Node38 parent"); + Expect(Marshal.ReadInt32(parent) == runtimeParentIndices[nodeIndex], + "AniMesh selector D copies the indexed global runtime parent, not the raw Node38 link"); + + Marshal.WriteIntPtr(result, IntPtr.Zero); + Expect(NativeBoolean(query(owner, nodeIndex, 0x0e, result)) == 1, + "AniMesh selector E returns the Node38 header word"); + IntPtr flags = Marshal.ReadIntPtr(result); + Expect(unchecked((uint)Marshal.ReadInt32(flags)) == headerFlags[nodeIndex], + "AniMesh selector E points to the indexed Node38 hdr0"); + } + + uint[] expectedStageCounts = new uint[] { 1, 0, 3 }; + for (uint nodeIndex = 0; nodeIndex < expectedStageCounts.Length; nodeIndex++) { + Expect(stageCount(owner, nodeIndex) == expectedStageCounts[nodeIndex], + "AniMesh stage count is the consecutive state0-2 prefix"); + } + Console.WriteLine("native-anim-mesh-control-node-metadata: aniMeshHash=" + ExpectedAniMeshSha256 + + " D=runtime-parent E=Node38-hdr0 stage-prefix=[1,0,3]"); + } + GC.KeepAlive(query); + GC.KeepAlive(stageCount); + } + + private static void WriteNode38Slot(IntPtr nodeData, int nodeIndex, int state, ushort slotIndex) { + int wordIndex = nodeIndex * 19 + 4 + state * 5; + Marshal.WriteInt16(nodeData, wordIndex * 2, unchecked((short)slotIndex)); + } + private static void RunNativeControlIntoOverrideCase(IntPtr module) { NativeControlResourceComponentOverride applyOverride = (NativeControlResourceComponentOverride)Marshal.GetDelegateForFunctionPointer( IntPtr.Add(module, ControlResourceComponentOverrideRva), typeof(NativeControlResourceComponentOverride)); |
