From 38ff0ca6828ce83dea376f288959a9ab2242d662 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 29 Oct 2026 16:26:54 +0400 Subject: fix(runtime): load robot Control device tables --- crates/fparkan-runtime/src/lib.rs | 93 +++++++++++++++++++++- .../behavior_distributor_tick_native_oracle.cs | 93 +++++++++++++++++++--- 2 files changed, 171 insertions(+), 15 deletions(-) (limited to 'crates/fparkan-runtime') diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 180533a..d15d7ea 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -3154,16 +3154,17 @@ fn apply_effective_control_override( fn control_factory_uses_common_setter(factory_tag: u32) -> bool { matches!( factory_tag, - 1 | 2 | 4 | 5 | 8 | 9 | 10 | 15 | 17 | 19 | 25 | 26 | 30 + 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 15 | 17 | 19 | 21 | 25 | 26 | 27 | 30 ) } fn native_control_factory_defaults(factory_tag: u32) -> (Option, Option) { match factory_tag { 1 | 4 => (Some(0x3f80_0000), Some(0x200)), - 5 | 8 | 26 => (Some(0x3f80_0000), Some(0x5)), + 3 | 5 | 8 | 21 | 26 => (Some(0x3f80_0000), Some(0x5)), 10 => (Some(0x3f80_0000), Some(0x2000)), 17 | 19 => (Some(0x3f80_0000), Some(0x20)), + 2 | 27 => (Some(0x3f80_0000), Some(0)), 9 | 15 | 25 => (Some(0x3f80_0000), None), _ => (None, None), } @@ -3534,7 +3535,10 @@ fn build_control_device_projection( target_index, }, )?; - if !matches!(device.factory_tag, 9 | 15 | 19 | 25) { + if !matches!( + device.factory_tag, + 2 | 3 | 4 | 5 | 8 | 9 | 10 | 15 | 19 | 21 | 25 | 27 + ) { return Err(Unavailable::UnsupportedOverrideReceiver { factory_tag: device.factory_tag, }); @@ -4960,6 +4964,57 @@ mod tests { ); } + #[test] + fn common_factory_into_overrides_preserve_native_constructor_defaults() { + const INTO: u32 = u32::from_le_bytes(*b"INTO"); + for (factory_tag, word_50) in [ + (2, 0), + (3, 5), + (4, 0x200), + (5, 5), + (8, 5), + (10, 0x2000), + (21, 5), + (27, 0), + ] { + let mut root = synthetic_control_record(factory_tag, 2, 0x20, None); + root.fixed_words[6] = u32::MAX; + let mut replacement = synthetic_control_record(19, 99, 0x30, None); + replacement.fixed_words[6] = u32::MAX; + let status = synthetic_control_table( + &[None, Some(INTO)], + &[1, 1], + &[1, 0], + &[0, 0], + &[1, 1], + vec![ + Some(synthetic_component_control( + 0, + vec![root], + PreparedControlEffectSection::Parsed(Vec::new()), + )), + Some(synthetic_component_control( + 1, + vec![replacement], + PreparedControlEffectSection::Parsed(Vec::new()), + )), + ], + ); + let MissionControlDeviceTableStatus::Available(devices) = status else { + panic!("{status:?}") + }; + assert_eq!(devices.len(), 1); + let device = &devices[0]; + assert_eq!( + (device.factory_tag, device.descriptor.tag, device.key), + (factory_tag, 19, 2) + ); + assert_eq!(device.multiplier_bits, Some(1.0_f32.to_bits())); + assert_eq!(device.native_word_50_bits, Some(word_50)); + assert_eq!(device.coefficient_bits.unwrap()[0], 0x30); + } + } + #[test] fn control_factory_multiplier_defaults_preserve_unknown_word_50() { for factory_tag in [9, 15, 25] { @@ -6970,6 +7025,38 @@ mod tests { ); } + #[test] + #[ignore = "requires licensed corpus"] + fn full_load_builds_control_tables_for_autodemo_robots_and_buildings() { + let mut engine = create(EngineServices::new(Arc::new(DirectoryVfs::new( + licensed_root("IS"), + )))) + .unwrap(); + load_mission( + &mut engine, + MissionRequest { + key: "MISSIONS/Autodemo.00/data.tma".into(), + mode: MissionMode::AutoDemo, + }, + 100_000, + ) + .unwrap(); + let tables = loaded_mission_control_device_tables(&engine).unwrap(); + let expected_counts = [14, 12, 15, 13, 12, 12, 11, 11]; + assert_eq!(tables.len(), expected_counts.len()); + for (table, expected_count) in tables.iter().zip(expected_counts) { + let MissionControlDeviceTableStatus::Available(devices) = &table.status else { + panic!("object {:?}: {:?}", table.original_id, table.status); + }; + assert_eq!( + devices.len(), + expected_count, + "object {:?}", + table.original_id + ); + } + } + #[test] #[ignore = "requires licensed corpus"] fn headless_step_runs_resource_schedule_and_rolls_back_both_error_paths() { 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 1383c1a..677a867 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -121,6 +121,7 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceRate(IntPtr self, float deltaSeconds); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeControlResourceDeviceGate(IntPtr self); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceAvailability(IntPtr self, byte flags); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlDeviceFactory(IntPtr self, uint componentId, uint keyOffset, IntPtr record); [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); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value); @@ -2253,6 +2254,22 @@ internal static class BehaviorDistributorTickOracle { ControlResourceTag15VtableRva, ControlResourceNoOpStringPairRva, applyOverride); RunNativeControlOverrideReceiverCase(module, 25, 0xa4, ControlResourceTag25ConstructorRva, ControlResourceTag25VtableRva, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 4, 0xdc, 0x233d0, + 0x3c720, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 10, 0x94, 0x26470, + 0x3ca20, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 27, 0x94, 0x218b0, + 0x3c528, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 5, 0xa4, 0x20800, + 0x3cdf8, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 8, 0xbc, 0x24310, + 0x3c800, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 21, 0xa4, 0x20800, + 0x3c448, ControlResourceNoOpStringPairRva, applyOverride); + RunNativeControlOverrideReceiverCase(module, 2, 0x184, 0x294c0, + 0x3cbd8, 0x29940, applyOverride); + RunNativeControlOverrideReceiverCase(module, 3, 0xa4, 0x20800, + 0x3c448, ControlResourceNoOpStringPairRva, applyOverride); GC.KeepAlive(applyOverride); } @@ -2262,17 +2279,37 @@ internal static class BehaviorDistributorTickOracle { NativeControlResourceDeviceConstructor construct = (NativeControlResourceDeviceConstructor)Marshal.GetDelegateForFunctionPointer( IntPtr.Add(module, constructorRva), typeof(NativeControlResourceDeviceConstructor)); IntPtr expectedVtable = IntPtr.Add(module, vtableRva); - Expect(Marshal.ReadIntPtr(expectedVtable, 0x1c) == IntPtr.Add(module, ControlResourceCommonSetterRva), - "tag-" + tag + " vtable common setter slot resolves to native RVA 0x21d50"); + Expect(Marshal.ReadIntPtr(expectedVtable, 0x1c) == IntPtr.Add(module, tag == 2 ? 0x29650 : ControlResourceCommonSetterRva), + "tag-" + tag + " vtable reader resolves to its native implementation"); Expect(Marshal.ReadIntPtr(expectedVtable, 0x24) == IntPtr.Add(module, stringPairRva), "tag-" + tag + " vtable string-pair slot matches its native callback"); using (Fixture fixture = NewFixture()) { - IntPtr owner = fixture.Allocate(0x5c4); - IntPtr deviceArray = fixture.Allocate(IntPtr.Size); - IntPtr targetDevice = fixture.Allocate(objectSize); - IntPtr typedRecord = fixture.Allocate(0xb4); - Expect(construct(targetDevice) == targetDevice, - "native tag-" + tag + " constructor returns its exact-size device object"); + IntPtr owner = fixture.Allocate(0x5c8); + IntPtr deviceArray = fixture.Allocate(2 * IntPtr.Size); + IntPtr targetDevice = (tag == 3 || tag == 5) ? IntPtr.Zero : fixture.Allocate(objectSize); + bool variableTail = tag == 2 || tag == 3 || tag == 4 || tag == 5 || tag == 8 || tag == 10 || tag == 21 || tag == 27; + IntPtr typedRecord = fixture.Allocate(variableTail ? 0xc0 : 0xb4); + if (tag == 3 || tag == 5) { + // Tag 3 follows the default factory; case 5 also assigns a derived vtable. + Marshal.WriteIntPtr(owner, 0x5bc, deviceArray); + Marshal.WriteInt32(owner, 0x5c4, 2); + Marshal.WriteInt32(typedRecord, 0, tag); + Marshal.WriteInt32(typedRecord, 0x18, -1); + NativeControlDeviceFactory create = (NativeControlDeviceFactory)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, 0x2d4b0), typeof(NativeControlDeviceFactory)); + Expect(create(owner, 0, 0, typedRecord) == IntPtr.Add(typedRecord, 0xb4), "native tag-" + tag + " factory cursor"); + targetDevice = Marshal.ReadIntPtr(deviceArray); + GC.KeepAlive(create); + } else { + Expect(construct(targetDevice) == targetDevice, + "native tag-" + tag + " constructor returns its exact-size device object"); + } + if (variableTail) { + int expectedWord = tag == 4 ? 0x200 : (tag == 3 || tag == 5 || tag == 8 || tag == 21) ? 5 : tag == 10 ? 0x2000 : 0; + Expect(Marshal.ReadInt32(targetDevice, 0x50) == expectedWord && + Marshal.ReadInt32(targetDevice, 0x4c) == FloatBits(1.0f), + "native extended factory constructor defaults"); + } Marshal.WriteIntPtr(deviceArray, 0, targetDevice); Marshal.WriteIntPtr(owner, 0x5bc, deviceArray); Marshal.WriteInt32(owner, 0x5c0, 1); @@ -2282,7 +2319,7 @@ internal static class BehaviorDistributorTickOracle { Marshal.WriteInt32(targetDevice, 0x4c, unchecked((int)0x40200000)); // multiplier 2.5 for (int i = 0; i < 16; i++) { Marshal.WriteInt32(targetDevice, 0x54 + i * 4, unchecked((int)(0xb2000000u + (uint)i))); - Marshal.WriteInt32(typedRecord, 0x2c + i * 4, unchecked((int)(0xc3000000u + (uint)i))); + Marshal.WriteInt32(typedRecord, 0x2c + i * 4, unchecked((int)(tag == 2 && i < 2 ? (uint)FloatBits(i == 0 ? 3.5f : 1.25f) : 0xc3000000u + (uint)i))); } Marshal.WriteInt32(typedRecord, 0x00, 19); Marshal.WriteInt32(typedRecord, 0x04, unchecked((int)0x77777777)); // differs from the existing device key @@ -2292,10 +2329,19 @@ internal static class BehaviorDistributorTickOracle { // This synthetic receiver has no FX host and the descriptor has no map rows. Marshal.WriteIntPtr(targetDevice, 0x3c, IntPtr.Zero); } + if (variableTail) { + Marshal.WriteInt32(typedRecord, 0xac, 2); + Marshal.WriteInt32(typedRecord, 0xb0, 7); + Marshal.WriteInt32(typedRecord, 0xb4, 9); + Marshal.WriteInt32(typedRecord, 0xb8, 2); + Marshal.WriteByte(typedRecord, 0xbc, (byte)'f'); + Marshal.WriteByte(typedRecord, 0xbd, (byte)'x'); + Marshal.WriteByte(typedRecord, 0xbe, 0); + } int priorWord50 = Marshal.ReadInt32(targetDevice, 0x50); IntPtr result = applyOverride(owner, 0, typedRecord); - Expect(result == IntPtr.Add(typedRecord, 0xb4), + Expect(result == IntPtr.Add(typedRecord, variableTail ? 0xbf : 0xb4), "Control indexed override completes one tag-" + tag + " typed record"); Expect(Marshal.ReadInt32(owner, 0x5c0) == 1 && Marshal.ReadIntPtr(owner, 0x5bc) == deviceArray && Marshal.ReadIntPtr(deviceArray, 0) == targetDevice, @@ -2311,13 +2357,36 @@ internal static class BehaviorDistributorTickOracle { Expect(Marshal.ReadIntPtr(targetDevice, 0x3c) == IntPtr.Zero, "tag-9 string callback runs with no FX host on the synthetic receiver"); for (int i = 0; i < 16; i++) { - Expect(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x54 + i * 4)) == 0xc3000000u + (uint)i, + Expect(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x54 + i * 4)) == (tag == 2 && i < 2 ? (uint)FloatBits(i == 0 ? 3.5f : 1.25f) : 0xc3000000u + (uint)i), "Control replaces selected tag-" + tag + " device coefficient " + i); } + if (variableTail) { + IntPtr map = Marshal.ReadIntPtr(targetDevice, 0x1c); + Expect(Marshal.ReadInt32(targetDevice, 0x20) == 2 && Marshal.ReadInt32(map) == 7 && + Marshal.ReadInt32(map, 0x14) == 9, "native common reader retains both variable map rows"); + } + if (tag == 2) { + IntPtr rowState = Marshal.ReadIntPtr(targetDevice, 0x12c); + Expect(Marshal.ReadInt32(targetDevice, 0x120) == FloatBits(1.25f) && + Marshal.ReadInt32(targetDevice, 0x124) == 4 && + Marshal.ReadInt32(rowState) == 0 && Marshal.ReadInt32(rowState, 4) == 0, + "tag-2 wrapper caches selector 1, rounds selector 0 with x87, and zeros map state"); + Marshal.WriteInt32(rowState, 1); + Marshal.WriteInt32(rowState, 4, 2); + Expect(applyOverride(owner, 0, typedRecord) == result && + Marshal.ReadInt32(targetDevice, 0x20) == 4, + "tag-2 repeated override retains common cursor and appends map rows"); + rowState = Marshal.ReadIntPtr(targetDevice, 0x12c); + for (int i = 0; i < 4; i++) + Expect(Marshal.ReadInt32(rowState, i * 4) == 0, + "tag-2 repeated override clears state for every accumulated map row"); + Console.WriteLine("native-control-tag2-wrapper: selector1=1.25 selector0=3.5 integer=4 rows=4 state=zero"); + } Console.WriteLine("native-control-tag" + tag + "-into-override: size=0x" + objectSize.ToString("x") + " index=0 count=1 key=22222222 component=12345678 multiplier=" + Float(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x4c))) + - " original-class=preserved descriptor=attached coefficients=replaced"); + " original-class=preserved descriptor=attached coefficients=replaced cursor=" + + (variableTail ? "0xbf map=[7,9] string=fx" : "0xb4")); GC.KeepAlive(construct); } } -- cgit v1.2.3