diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-22 14:30:41 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-22 14:30:41 +0300 |
| commit | 83db371742ad3865bf4dc534ae38429c07a936d3 (patch) | |
| tree | abb39dd412c6dd3bada1125d2a3c7ba78284cc1b /crates/fparkan-runtime/tests | |
| parent | 29b81b87237d0e6a3b7ffac913a0b882738eed7c (diff) | |
| download | fparkan-83db371742ad3865bf4dc534ae38429c07a936d3.tar.xz fparkan-83db371742ad3865bf4dc534ae38429c07a936d3.zip | |
feat(runtime): load Behavior profiles for resource queries
Diffstat (limited to 'crates/fparkan-runtime/tests')
| -rw-r--r-- | crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs | 263 |
1 files changed, 263 insertions, 0 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 5e89934..610e8db 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -23,6 +23,9 @@ internal static class BehaviorDistributorTickOracle { private const int IndexedPropertySetRva = 0x0b470; private const int IndexedPropertyGetRva = 0x0b5b0; private const int OreInitializationRva = 0x08790; + private const int BehaviorProfileMapRva = 0x08a80; + private const int BehaviorVarBlockConstructorRva = 0x22d90; + private const int BehaviorVarFieldRegisterRva = 0x22e20; private const int GetSystemArealMapIatRva = 0x59008; private const int ControlResourceDeviceGetterRva = 0x21d00; private const int ControlResourceAggregateQueryRva = 0x2b410; @@ -45,6 +48,10 @@ internal static class BehaviorDistributorTickOracle { private const int ControlResourceNoOpStringPairRva = 0x0bcd0; private const int ControlResourceTag19VtableRva = 0x3c608; private const int ControlResourceTag19StringPairNoOpRva = 0x0bcd0; + private const int BehaviorVarTableVtableRva = 0x591b0; + private const int BehaviorVarArrayLoadRva = 0x49fa0; + private const int BehaviorVarFieldBindRva = 0x49090; + private const int BehaviorVarApplyRva = 0x49170; private const uint ResourceProperty = 0x02000100; private static Fixture active; @@ -74,6 +81,13 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void IndexedPropertySet(IntPtr self, uint index, uint valueBits); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int IndexedPropertyGet(IntPtr self, uint index, IntPtr resultBits); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeOreInitialization(IntPtr self); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeBehaviorVarArrayLoad(IntPtr self, IntPtr bytes, IntPtr cursorOut); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarFieldBind(IntPtr self, IntPtr typeName, IntPtr fieldName, IntPtr destination); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarApply(IntPtr self); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorProfileMap(IntPtr self); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarBlockConstructor(IntPtr self); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeBehaviorVarFieldRegister(IntPtr self, IntPtr table); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeBehaviorProfileLoadCallback(IntPtr owner, uint selector, IntPtr archive, IntPtr entry); [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); @@ -111,6 +125,15 @@ internal static class BehaviorDistributorTickOracle { private static IntPtr x87ControlWordCell; private static X87ControlWordReader x87ControlWordReader; private static X87ControlWordWriter x87ControlWordWriter; + private static readonly NativeBehaviorProfileLoadCallback BehaviorProfileLoadCallback = CaptureBehaviorProfileLoad; + private static readonly List<BehaviorProfileSelection> behaviorProfileSelections = new List<BehaviorProfileSelection>(); + + private sealed class BehaviorProfileSelection { + public IntPtr Owner; + public uint Selector; + public string Archive; + public string Entry; + } private sealed class NativeObject { public IntPtr Self; @@ -533,6 +556,9 @@ internal static class BehaviorDistributorTickOracle { RunNativeControlResourceDeviceCase(controlModule); RunNativeControlProviderLiveStateCase(controlModule); RunNativeControlAggregateResourceCases(controlModule); + RunNativeBehaviorProfileMapCases(module); + RunNativeBehaviorVarSyntheticCase(module); + RunNativeBehaviorRealProfileCases(module); } finally { SetX87ControlWord(savedControlWord); Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4")); @@ -1092,6 +1118,243 @@ internal static class BehaviorDistributorTickOracle { Console.WriteLine("native-behavior-ore-initialization: classes=80000004,80000008,01002000,01004000,01008000; defaults=verified"); } + private static void CaptureBehaviorProfileLoad(IntPtr owner, uint selector, IntPtr archive, IntPtr entry) { + behaviorProfileSelections.Add(new BehaviorProfileSelection { + Owner = owner, + Selector = selector, + Archive = Marshal.PtrToStringAnsi(archive), + Entry = Marshal.PtrToStringAnsi(entry) + }); + } + + private static void RunNativeBehaviorProfileMapCases(IntPtr module) { + NativeBehaviorProfileMap map = (NativeBehaviorProfileMap)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorProfileMapRva), typeof(NativeBehaviorProfileMap)); + string[][] cases = new string[][] { + new string[] { "80000004", "prof_mine.var" }, + new string[] { "80000008", "prof_storage.var" }, + new string[] { "80000002", "prof_generator.var" }, + new string[] { "80000010", "prof_plant.var" }, + new string[] { "80000040", "prof_angar.var" }, + new string[] { "80000200", "prof_mtp.var" }, + new string[] { "80000400", "prof_institute.var" }, + new string[] { "80010000", "prof_bunker.var" }, + new string[] { "80020000", "prof_bunker.var" }, + new string[] { "80040000", "prof_bunker.var" }, + new string[] { "80100000", "prof_tower.var" }, + new string[] { "80200000", "prof_tower.var" }, + new string[] { "01002000", "prof_trn.var" }, + new string[] { "01004000", "prof_bld.var" }, + new string[] { "01008000", "prof_war.var" }, + new string[] { "01010000", "prof_hq.var" }, + new string[] { "01020000", "prof_hero.var" }, + new string[] { "20000000", "prof_animal.var" } + }; + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x0b00); + IntPtr vtable = fixture.Allocate(0x68); + Marshal.WriteIntPtr(owner, vtable); + Marshal.WriteIntPtr(vtable, 0x64, Marshal.GetFunctionPointerForDelegate(BehaviorProfileLoadCallback)); + foreach (string[] test in cases) { + behaviorProfileSelections.Clear(); + Marshal.WriteInt32(owner, 0x0afc, unchecked((int)Convert.ToUInt32(test[0], 16))); + map(owner); + Expect(behaviorProfileSelections.Count == 1, "profile mapper callback count for class 0x" + test[0]); + BehaviorProfileSelection selection = behaviorProfileSelections[0]; + Expect(selection.Owner == owner && selection.Selector == 2 && selection.Archive == "behpsp.res" && + selection.Entry == test[1], "profile mapper result for class 0x" + test[0]); + } + string[] noMatch = new string[] { "deadbeef", "8000000c" }; + foreach (string test in noMatch) { + behaviorProfileSelections.Clear(); + Marshal.WriteInt32(owner, 0x0afc, unchecked((int)Convert.ToUInt32(test, 16))); + map(owner); + Expect(behaviorProfileSelections.Count == 0, "profile mapper leaves unknown/combined class 0x" + test + " unconfigured"); + } + } + Console.WriteLine("native-behavior-profile-map: mapped-classes=" + cases.Length + " (including three bunker and two tower codes); unknown-and-combined=no callback"); + } + + private static void RunNativeBehaviorVarSyntheticCase(IntPtr module) { + NativeBehaviorVarArrayLoad load = (NativeBehaviorVarArrayLoad)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarArrayLoadRva), typeof(NativeBehaviorVarArrayLoad)); + NativeBehaviorVarFieldBind bind = (NativeBehaviorVarFieldBind)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarFieldBindRva), typeof(NativeBehaviorVarFieldBind)); + NativeBehaviorVarApply apply = (NativeBehaviorVarApply)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarApplyRva), typeof(NativeBehaviorVarApply)); + byte[] bytes = SerializeOneBehaviorVarField("Transfer_Ore_OffBoard", "float", 0x3f000000, 0x41200000); + using (Fixture fixture = NewFixture()) { + IntPtr table = fixture.Allocate(0x18); + IntPtr target = fixture.Allocate(sizeof(uint)); + IntPtr input = fixture.Allocate(bytes.Length); + IntPtr cursorOut = fixture.Allocate(IntPtr.Size); + IntPtr fieldName = fixture.Allocate(32); + IntPtr typeName = fixture.Allocate(8); + CopyBytes(input, bytes); + WriteFixedAnsi(fieldName, 0, "Transfer_Ore_OffBoard", 32); + WriteFixedAnsi(typeName, 0, "float", 8); + Marshal.WriteIntPtr(table, IntPtr.Add(module, BehaviorVarTableVtableRva)); + Marshal.WriteInt32(input, 0, 1); + + Expect(load(table, input, cursorOut) == 1, "native .var parser accepts the synthetic one-row profile"); + Expect(Marshal.ReadInt32(table, 4) == 1 && Marshal.ReadIntPtr(cursorOut) == IntPtr.Add(input, bytes.Length), + "native .var parser consumes the full synthetic profile"); + bind(table, typeName, fieldName, target); + apply(table); + Expect(unchecked((uint)Marshal.ReadInt32(target)) == 0x41200000, + "native .var apply uses the second value blob after binding"); + Console.WriteLine("native-behavior-var-synthetic: first=0x3f000000 second=0x41200000 applied=0x" + + unchecked((uint)Marshal.ReadInt32(target)).ToString("x8")); + } + } + + private static void RunNativeBehaviorRealProfileCases(IntPtr module) { + string archivePath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_BEHAVIOR_PROFILE_RES"); + if (String.IsNullOrEmpty(archivePath)) archivePath = @"C:\GOG Games\Parkan - Iron Strategy\behpsp.res"; + if (!File.Exists(archivePath)) throw new FileNotFoundException( + "Set FPARKAN_ORIGINAL_BEHAVIOR_PROFILE_RES to the installed behpsp.res.", archivePath); + byte[] archiveBytes = File.ReadAllBytes(archivePath); + NativeBehaviorProfileMap map = (NativeBehaviorProfileMap)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorProfileMapRva), typeof(NativeBehaviorProfileMap)); + NativeBehaviorVarArrayLoad load = (NativeBehaviorVarArrayLoad)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarArrayLoadRva), typeof(NativeBehaviorVarArrayLoad)); + NativeBehaviorVarBlockConstructor construct = (NativeBehaviorVarBlockConstructor)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarBlockConstructorRva), typeof(NativeBehaviorVarBlockConstructor)); + NativeBehaviorVarFieldRegister register = (NativeBehaviorVarFieldRegister)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarFieldRegisterRva), typeof(NativeBehaviorVarFieldRegister)); + NativeBehaviorVarApply apply = (NativeBehaviorVarApply)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, BehaviorVarApplyRva), typeof(NativeBehaviorVarApply)); + + string selectedEntry = SelectBehaviorProfile(map, 0x80000004); + Expect(selectedEntry == "prof_mine.var", "mine profile is selected by native class mapper"); + byte[] profile = ReadNresEntry(archiveBytes, selectedEntry); + Expect(ReadUInt32(profile, 0) == 27, "native mine profile contains all 27 rows"); + uint expectedOreOffBoard = FindBehaviorVarSecondBlob(profile, "Transfer_Ore_OffBoard"); + uint expectedPowerOut = FindBehaviorVarSecondBlob(profile, "Transfer_Power_Out"); + + using (Fixture fixture = NewFixture()) { + IntPtr table = fixture.Allocate(0x18); + IntPtr rawProfile = fixture.Allocate(profile.Length); + IntPtr cursorOut = fixture.Allocate(IntPtr.Size); + IntPtr block = fixture.Allocate(0x80); + CopyBytes(rawProfile, profile); + Marshal.WriteIntPtr(table, IntPtr.Add(module, BehaviorVarTableVtableRva)); + + Expect(load(table, rawProfile, cursorOut) == 1, "native parser accepts the licensed mine profile"); + Expect(Marshal.ReadInt32(table, 4) == 27 && Marshal.ReadIntPtr(cursorOut) == IntPtr.Add(rawProfile, profile.Length), + "native parser consumes all rows in the licensed mine profile"); + construct(block); + Expect(unchecked((uint)Marshal.ReadInt32(block, 0x58)) == 0x40a00000 && + unchecked((uint)Marshal.ReadInt32(block, 0x60)) == 0x42c80000, + "native variable block starts with transfer defaults 5 and 100"); + register(block, table); + apply(table); + Expect(unchecked((uint)Marshal.ReadInt32(block, 0x58)) == expectedOreOffBoard && + unchecked((uint)Marshal.ReadInt32(block, 0x60)) == expectedPowerOut, + "native profile bind/apply copies second blobs into Transfer_Ore_OffBoard and Transfer_Power_Out"); + Console.WriteLine("native-behavior-real-profile: class=80000004 entry=" + selectedEntry + " rows=27 Transfer_Ore_OffBoard=" + + Hex(expectedOreOffBoard) + " Transfer_Power_Out=" + Hex(expectedPowerOut) + "; native-bind-and-apply=verified"); + } + } + + private static string SelectBehaviorProfile(NativeBehaviorProfileMap map, uint classCode) { + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x0b00); + IntPtr vtable = fixture.Allocate(0x68); + Marshal.WriteIntPtr(owner, vtable); + Marshal.WriteIntPtr(vtable, 0x64, Marshal.GetFunctionPointerForDelegate(BehaviorProfileLoadCallback)); + behaviorProfileSelections.Clear(); + Marshal.WriteInt32(owner, 0x0afc, unchecked((int)classCode)); + map(owner); + Expect(behaviorProfileSelections.Count == 1, "native mapper returned a single profile for class 0x" + classCode.ToString("x8")); + BehaviorProfileSelection selection = behaviorProfileSelections[0]; + Expect(selection.Owner == owner && selection.Selector == 2 && selection.Archive == "behpsp.res", + "native mapper selected the behavior profile archive and selector"); + return selection.Entry; + } + } + + private static byte[] SerializeOneBehaviorVarField(string fieldName, string typeName, uint firstBits, uint secondBits) { + byte[] field = Encoding.ASCII.GetBytes(fieldName); + byte[] type = Encoding.ASCII.GetBytes(typeName); + using (MemoryStream stream = new MemoryStream()) using (BinaryWriter writer = new BinaryWriter(stream)) { + writer.Write((uint)1); // row count + writer.Write((uint)4); // value size + writer.Write((uint)3); // float type index + writer.Write((uint)0); // flags + writer.Write((uint)field.Length); writer.Write(field); + writer.Write((uint)type.Length); writer.Write(type); + writer.Write(firstBits); writer.Write(secondBits); + writer.Write((uint)0); writer.Write((uint)0); // trailing row metadata + return stream.ToArray(); + } + } + + private static byte[] ReadNresEntry(byte[] archive, string name) { + if (archive.Length < 16) throw new InvalidDataException("NRes archive is shorter than its header"); + uint count = ReadUInt32(archive, 8); + if (count > Int32.MaxValue / 64) throw new InvalidDataException("NRes directory count is too large"); + int directory = checked(archive.Length - (int)count * 64); + if (directory < 16) throw new InvalidDataException("NRes directory falls inside the header"); + for (int index = 0; index < (int)count; index++) { + int record = checked(directory + index * 64); + int nameEnd = record + 20; + while (nameEnd < record + 56 && archive[nameEnd] != 0) nameEnd++; + string entryName = Encoding.ASCII.GetString(archive, record + 20, nameEnd - (record + 20)); + if (!String.Equals(entryName, name, StringComparison.Ordinal)) continue; + uint size = ReadUInt32(archive, record + 12); + uint dataOffset = ReadUInt32(archive, record + 56); + if (size > Int32.MaxValue || dataOffset > Int32.MaxValue || dataOffset + size > archive.Length) + throw new InvalidDataException("NRes entry range is outside the archive"); + byte[] data = new byte[(int)size]; + Buffer.BlockCopy(archive, (int)dataOffset, data, 0, data.Length); + return data; + } + throw new InvalidDataException("NRes archive does not contain entry " + name); + } + + private static uint FindBehaviorVarSecondBlob(byte[] profile, string fieldName) { + if (profile.Length < 4) throw new InvalidDataException("Behavior profile is shorter than its row count"); + uint count = ReadUInt32(profile, 0); + if (count > 1024) throw new InvalidDataException("Behavior profile row count is unreasonable"); + int cursor = 4; + for (int index = 0; index < (int)count; index++) { + EnsureRange(profile, cursor, 12); + uint size = ReadUInt32(profile, cursor); + if (size > Int32.MaxValue) throw new InvalidDataException("Behavior variable size is too large"); + uint type = ReadUInt32(profile, cursor + 4); + cursor += 12; + uint fieldLength = ReadUInt32(profile, cursor); + if (fieldLength > Int32.MaxValue) throw new InvalidDataException("Behavior field name is too large"); + cursor += 4; EnsureRange(profile, cursor, (int)fieldLength); + string name = Encoding.ASCII.GetString(profile, cursor, (int)fieldLength); + cursor += (int)fieldLength; + uint typeLength = ReadUInt32(profile, cursor); + if (typeLength > Int32.MaxValue) throw new InvalidDataException("Behavior variable type name is too large"); + cursor += 4; EnsureRange(profile, cursor, (int)typeLength); + cursor += (int)typeLength; + EnsureRange(profile, cursor, checked((int)size * 2 + 8)); + int secondBlob = checked(cursor + (int)size); + if (String.Equals(name, fieldName, StringComparison.Ordinal)) { + if (type != 3 || size != 4) throw new InvalidDataException("Expected float profile field " + fieldName); + return ReadUInt32(profile, secondBlob); + } + cursor = checked(cursor + (int)size * 2 + 8); + } + throw new InvalidDataException("Behavior profile does not contain field " + fieldName); + } + + private static uint ReadUInt32(byte[] data, int offset) { + EnsureRange(data, offset, 4); + return unchecked((uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24))); + } + + private static void EnsureRange(byte[] data, int offset, int size) { + if (offset < 0 || size < 0 || offset > data.Length - size) throw new InvalidDataException("Behavior profile range is outside its input"); + } + + private static void CopyBytes(IntPtr destination, byte[] source) { Marshal.Copy(source, 0, destination, source.Length); } + private static ushort CurrentX87ControlWord() { if (x87ControlWordReader == null || x87ControlWordCell == IntPtr.Zero) throw new InvalidOperationException("x87 control-word reader is not initialized"); |
