feat(runtime): project initial Control mesh metadata
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s

This commit is contained in:
Valentin Popov committed 2026-10-23 13:20:59 +04:00
1 parent ce8e65c8ae
commit 4e16902661
8 files changed
+655 -6

No files matched your search

@@ -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));