diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 157 | ||||
| -rw-r--r-- | crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs | 128 |
2 files changed, 281 insertions, 4 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 3964f78..baba3b3 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -134,6 +134,21 @@ pub struct BehaviorRegisteredObject { pub native_tail: [u32; 5], } +/// Native `CurrentOre`/`MaximumOre` state for one full Unit DAT mission object. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BehaviorOreState { + /// Stable identity from the source TMA object order. + pub original_id: OriginalObjectId, + /// Corresponding live Rust world object. + pub handle: ObjectHandle, + /// Class word recovered from the root Unit DAT header. + pub class_flags: u32, + /// Raw bits of the `CurrentOre` float property. + pub current_ore_bits: u32, + /// Raw bits of the read-only `MaximumOre` float property. + pub maximum_ore_bits: u32, +} + /// Resource distributor state associated with one TMA clan's constructed AI. #[derive(Clone, Debug, PartialEq)] pub struct BehaviorDistributorState { @@ -464,6 +479,7 @@ struct LoadedMissionState { asset_plan: MissionAssetPlan, object_drafts: Vec<MissionObjectDraft>, behavior_distributors: Vec<BehaviorDistributorState>, + behavior_ore_states: Vec<BehaviorOreState>, script_bundles: Vec<MissionScriptBundle>, script_varset: Option<MissionScriptVarSet>, script_init_states: Vec<MissionScriptInitState>, @@ -1045,6 +1061,24 @@ fn load_mission_with_options( Vec::new() }; + let behavior_ore_states = if options.asset_scope == MissionAssetScope::Full { + // Property callbacks also run for Unit DAT objects that do not join a distributor. + object_drafts + .iter() + .zip(&handles) + .filter_map(|(draft, handle)| { + Some(initialize_behavior_ore_state( + draft.original_id?, + *handle, + draft.unit_dat_header_flags?, + &draft.properties, + )) + }) + .collect() + } else { + Vec::new() + }; + let mut next_behavior_rng_state = engine.behavior_rng_state; if options.asset_scope == MissionAssetScope::Full { let queue_time_ms = queue_time_ms.expect("full mission loading requires a queue time"); @@ -1120,6 +1154,7 @@ fn load_mission_with_options( asset_plan: mission_asset_plan, object_drafts, behavior_distributors, + behavior_ore_states, script_bundles: loaded_scripts.bundles, script_varset: loaded_scripts.varset, script_init_states: loaded_scripts.init_states, @@ -1402,6 +1437,15 @@ pub fn loaded_mission_behavior_distributors( .map(|state| state.behavior_distributors.as_slice()) } +/// Returns native ore properties attached to full Unit DAT mission objects. +#[must_use] +pub fn loaded_mission_behavior_ore_states(engine: &Engine) -> Option<&[BehaviorOreState]> { + engine + .loaded + .as_ref() + .map(|state| state.behavior_ore_states.as_slice()) +} + fn behavior_clan_for_selector( selector: u32, clan_count: usize, @@ -1418,6 +1462,38 @@ fn has_behavior_registration_key(class_flags: u32) -> bool { class_flags & BEHAVIOR_REGISTRATION_KEY_FLAG != 0 } +fn native_behavior_ore_defaults(class_flags: u32) -> (u32, u32) { + match class_flags { + 0x8000_0004 => (0, 0x43fa_0000), + 0x8000_0008 => (0, 0x457a_0000), + 0x0100_2000 => (0, 0x44fa_0000), + 0x0100_4000 => (0x44fa_0000, 0x44fa_0000), + _ => (0, 0), + } +} + +fn initialize_behavior_ore_state( + original_id: OriginalObjectId, + handle: ObjectHandle, + class_flags: u32, + properties: &[MissionObjectProperty], +) -> BehaviorOreState { + let (mut current_ore_bits, maximum_ore_bits) = native_behavior_ore_defaults(class_flags); + if let Some(property) = properties + .iter() + .find(|property| property.name_raw.as_slice() == b"CurrentOre") + { + current_ore_bits = property.raw_value[1]; + } + BehaviorOreState { + original_id, + handle, + class_flags, + current_ore_bits, + maximum_ore_bits, + } +} + fn add_behavior_registered_object( distributor: &mut BehaviorDistributorState, mut object: BehaviorRegisteredObject, @@ -2470,6 +2546,26 @@ mod tests { .collect::<Vec<_>>(), vec![(0, true), (1, true)] ); + let ore_states = loaded_mission_behavior_ore_states(&engine).expect("Behavior ore state"); + let unit_dat_drafts = drafts + .iter() + .filter(|draft| draft.unit_dat_header_flags.is_some()) + .collect::<Vec<_>>(); + assert_eq!(ore_states.len(), unit_dat_drafts.len()); + assert_eq!(ore_states.len(), 8); + for state in ore_states { + let draft = unit_dat_drafts + .iter() + .find(|draft| draft.original_id == Some(state.original_id)) + .expect("ore state has a Unit DAT source"); + assert_eq!( + state.handle, + handle_by_original_id(&engine.world, state.original_id).unwrap() + ); + assert_eq!(state.class_flags, draft.unit_dat_header_flags.unwrap()); + assert_eq!(state.current_ore_bits, 0); + assert_eq!(state.maximum_ore_bits, 0); + } let registered = distributors .iter() .flat_map(|distributor| { @@ -2538,6 +2634,15 @@ mod tests { ] ); assert_eq!(registered.len(), 4); + assert!(ore_states.iter().any(|state| { + state.class_flags == 0x0100_8000 + && !distributors.iter().any(|distributor| { + distributor + .registered_objects + .iter() + .any(|object| object.handle == state.handle) + }) + })); for (_, object) in registered { assert_eq!( handle_by_original_id(&engine.world, object.original_id), @@ -2620,6 +2725,55 @@ mod tests { } #[test] + fn behavior_ore_initialization_uses_first_exact_current_value_and_read_only_maximum() { + let handle = ObjectHandle { + generation: 7, + slot: 19, + }; + let state = initialize_behavior_ore_state( + OriginalObjectId(19), + handle, + 0x0100_2000, + &[ + MissionObjectProperty { + name_raw: b"currentore".to_vec(), + raw_value: [0x1111_1111, 0xdead_beef, 0, 0], + }, + MissionObjectProperty { + name_raw: b"CurrentOre".to_vec(), + raw_value: [0x3f80_0000, 0x7fc1_2345, 0, 0], + }, + MissionObjectProperty { + name_raw: b"CurrentOre".to_vec(), + raw_value: [0, 0x4400_0000, 0, 0], + }, + MissionObjectProperty { + name_raw: b"MaximumOre".to_vec(), + raw_value: [0, 0x4123_4567, 0, 0], + }, + ], + ); + + assert_eq!(state.original_id, OriginalObjectId(19)); + assert_eq!(state.handle, handle); + assert_eq!(state.class_flags, 0x0100_2000); + assert_eq!(state.current_ore_bits, 0x7fc1_2345); + assert_eq!(state.maximum_ore_bits, 0x44fa_0000); + } + + #[test] + fn behavior_ore_defaults_follow_native_class_dispatch() { + assert_eq!(native_behavior_ore_defaults(0x8000_0004), (0, 0x43fa_0000)); + assert_eq!(native_behavior_ore_defaults(0x8000_0008), (0, 0x457a_0000)); + assert_eq!(native_behavior_ore_defaults(0x0100_2000), (0, 0x44fa_0000)); + assert_eq!( + native_behavior_ore_defaults(0x0100_4000), + (0x44fa_0000, 0x44fa_0000) + ); + assert_eq!(native_behavior_ore_defaults(0x0100_8000), (0, 0)); + } + + #[test] fn behavior_distributor_add_uses_handle_and_preserves_duplicate_tail() { let mut distributor = BehaviorDistributorState { clan_index: 0, @@ -2767,6 +2921,9 @@ mod tests { assert!(loaded_mission_behavior_distributors(&engine) .expect("preview Behavior distributors") .is_empty()); + assert!(loaded_mission_behavior_ore_states(&engine) + .expect("preview Behavior ore state") + .is_empty()); assert!(bundles .iter() .all(|bundle| bundle.behavior_warm_start.is_none())); 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 5c7c4f2..8532d1a 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -5,9 +5,10 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; -// Calls the original x86 Behavior.dll distributor Add, normalize, and tick paths -// against synthetic storage. Only GetSystemArealMap and the bounded vector backing -// are substituted; Behavior's corresponding instructions remain the installed DLL. +// Calls the original x86 Behavior.dll distributor Add, normalize, indexed resource +// property, and tick paths against synthetic storage. Only GetSystemArealMap and the +// bounded vector backing are substituted; Behavior's corresponding instructions +// remain the installed DLL. internal static class BehaviorDistributorTickOracle { private const uint LoadWithAlteredSearchPath = 0x00000008; private const uint MemCommitReserve = 0x00003000; @@ -18,6 +19,9 @@ internal static class BehaviorDistributorTickOracle { private const int TickRva = 0x19e80; private const int AddObjectRva = 0x1aac0; private const int NormalizeRva = 0x1ac90; + private const int IndexedPropertySetRva = 0x0b470; + private const int IndexedPropertyGetRva = 0x0b5b0; + private const int OreInitializationRva = 0x08790; private const int GetSystemArealMapIatRva = 0x59008; private const uint ResourceProperty = 0x02000100; private static Fixture active; @@ -45,6 +49,9 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void ObjectSetProperty(IntPtr self, uint property, float value, IntPtr result); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceGetProperty(IntPtr self, uint selector, IntPtr result); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceSetProperty(IntPtr self, uint selector, IntPtr value); + [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); private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap; private static readonly VectorInsert VectorInsertCallback = InsertVector; @@ -158,7 +165,7 @@ internal static class BehaviorDistributorTickOracle { Marshal.WriteInt32(entry, 0x04, BitConverter.ToInt32(BitConverter.GetBytes(1.0f), 0)); } } - private IntPtr Allocate(int size) { + public IntPtr Allocate(int size) { IntPtr value = Marshal.AllocHGlobal(size); Zero(value, size); Allocations.Add(value); @@ -365,6 +372,8 @@ internal static class BehaviorDistributorTickOracle { SetX87ControlWord(savedControlWord); Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4")); } + RunNativeIndexedPropertyCases(module); + RunNativeOreInitializationCases(module); TickDelegate tick = (TickDelegate)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, TickRva), typeof(TickDelegate)); using (Fixture empty = NewFixture()) { empty.MakeDistributor(); active = empty; tick(empty.Distributor, 0.0f); @@ -387,6 +396,117 @@ internal static class BehaviorDistributorTickOracle { RunSkippedCase(tick); } + private static void RunNativeIndexedPropertyCases(IntPtr module) { + IndexedPropertySet set = (IndexedPropertySet)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, IndexedPropertySetRva), typeof(IndexedPropertySet)); + IndexedPropertyGet get = (IndexedPropertyGet)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, IndexedPropertyGetRva), typeof(IndexedPropertyGet)); + using (Fixture fixture = NewFixture()) { + // The handlers receive the embedded interface at owner+4. Its descriptor + // vector is at interface+0xa3c and its keyed property store at interface+0x5e4. + IntPtr owner = fixture.Allocate(0x0b20); + IntPtr iface = IntPtr.Add(owner, 4); + IntPtr descriptors = fixture.Allocate(6 * 0x18); + IntPtr propertyEntry = fixture.Allocate(0x18); + IntPtr maximumOutput = fixture.Allocate(sizeof(uint)); + IntPtr currentOutput = fixture.Allocate(sizeof(uint)); + + Marshal.WriteInt32(iface, 0xa3c, 6); + Marshal.WriteIntPtr(iface, 0xa44, descriptors); + Marshal.WriteInt32(IntPtr.Add(descriptors, 4 * 0x18), 4, 4); + Marshal.WriteInt32(IntPtr.Add(descriptors, 5 * 0x18), 4, 5); + + Marshal.WriteInt32(iface, 0x5e4, 0); // vector vtable is not used by these callbacks + Marshal.WriteInt32(iface, 0x5e8, 1); // count + Marshal.WriteInt32(iface, 0x5ec, 1); // capacity + Marshal.WriteIntPtr(iface, 0x5f0, propertyEntry); + Marshal.WriteInt32(propertyEntry, 0x00, unchecked((int)ResourceProperty)); + Marshal.WriteInt32(propertyEntry, 0x04, unchecked((int)0x11223344)); + Marshal.WriteInt32(propertyEntry, 0x08, FloatBits(2500.0f)); // auxiliary maximum + Marshal.WriteInt32(propertyEntry, 0x0c, unchecked((int)0x55667788)); + Marshal.WriteInt32(propertyEntry, 0x10, unchecked((int)0x99aabbcc)); + Marshal.WriteInt32(propertyEntry, 0x14, FloatBits(12.5f)); // primary current amount + + Expect(get(iface, 4, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f), + "descriptor 4 reads keyed auxiliary value as MaximumOre"); + Expect(get(iface, 5, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(12.5f), + "descriptor 5 reads keyed primary float as CurrentOre"); + + set(iface, 5, unchecked((uint)FloatBits(21.75f))); + Expect(Marshal.ReadInt32(propertyEntry, 0x14) == FloatBits(21.75f) && + Marshal.ReadInt32(propertyEntry, 0x08) == FloatBits(2500.0f), + "descriptor 5 updates CurrentOre and preserves MaximumOre auxiliary value"); + set(iface, 4, unchecked((uint)FloatBits(999.0f))); + Expect(Marshal.ReadInt32(propertyEntry, 0x14) == FloatBits(21.75f) && + Marshal.ReadInt32(propertyEntry, 0x08) == FloatBits(2500.0f), + "descriptor 4 is read-only through the indexed setter"); + Expect(get(iface, 4, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f) && + get(iface, 5, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(21.75f), + "indexed resource values remain independently readable after CurrentOre write"); + + IntPtr reorderedDescriptors = fixture.Allocate(2 * 0x18); + Marshal.WriteInt32(iface, 0xa3c, 2); + Marshal.WriteIntPtr(iface, 0xa44, reorderedDescriptors); + Marshal.WriteInt32(reorderedDescriptors, 4, 5); + Marshal.WriteInt32(IntPtr.Add(reorderedDescriptors, 0x18), 4, 4); + Expect(get(iface, 0, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(21.75f), + "descriptor index zero follows its CurrentOre code 5 row"); + Expect(get(iface, 1, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f), + "descriptor index one follows its MaximumOre code 4 row"); + set(iface, 0, unchecked((uint)FloatBits(31.25f))); + set(iface, 1, unchecked((uint)FloatBits(999.0f))); + Expect(Marshal.ReadInt32(propertyEntry, 0x14) == FloatBits(31.25f) && + Marshal.ReadInt32(propertyEntry, 0x08) == FloatBits(2500.0f), + "reordered descriptor rows preserve CurrentOre write and MaximumOre read-only behavior"); + Expect(get(iface, 0, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(31.25f) && + get(iface, 1, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f), + "reordered descriptor rows remain readable after their indexed writes"); + Console.WriteLine("native-indexed-resource-property: maximum=" + Float(unchecked((uint)Marshal.ReadInt32(maximumOutput))) + + " current=" + Float(unchecked((uint)Marshal.ReadInt32(currentOutput))) + + "; descriptor4-write=no-op descriptor5-write=primary-only; reordered-indices=verified"); + } + } + + private static void RunNativeOreInitializationCases(IntPtr module) { + NativeOreInitialization initialize = (NativeOreInitialization)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, OreInitializationRva), typeof(NativeOreInitialization)); + uint[][] cases = new uint[][] { + new uint[] { 0x80000004, 0x00000000, 0x43fa0000 }, + new uint[] { 0x80000008, 0x00000000, 0x457a0000 }, + new uint[] { 0x01002000, 0x00000000, 0x44fa0000 }, + new uint[] { 0x01004000, 0x44fa0000, 0x44fa0000 }, + new uint[] { 0x01008000, 0x00000000, 0x00000000 } + }; + IntPtr vtable = IntPtr.Add(module, 0x59250); + foreach (uint[] test in cases) { + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x0b20); + IntPtr propertyEntry = fixture.Allocate(0x18); + Marshal.WriteIntPtr(owner, vtable); + Marshal.WriteIntPtr(owner, 0x30, owner); + Marshal.WriteInt32(owner, 0xa04, 2); + Marshal.WriteInt32(owner, 0xa64, 1); // suppress the wrapper's deferred side effect + Marshal.WriteInt32(owner, 0xafc, unchecked((int)test[0])); + Marshal.WriteInt32(owner, 0x70c, FloatBits(500.0f)); + Marshal.WriteInt32(owner, 0x710, FloatBits(4000.0f)); + Marshal.WriteInt32(owner, 0x720, FloatBits(2000.0f)); + Marshal.WriteInt32(propertyEntry, 0x00, unchecked((int)ResourceProperty)); + Marshal.WriteInt32(propertyEntry, 0x08, unchecked((int)0xbf800000)); // native add default auxiliary + Marshal.WriteInt32(propertyEntry, 0x14, FloatBits(123.0f)); // prove initialization overwrites primary + Marshal.WriteInt32(owner, 0x5ec, 1); // property count + Marshal.WriteInt32(owner, 0x5f0, 1); // property capacity + Marshal.WriteIntPtr(owner, 0x5f4, propertyEntry); + + initialize(owner); + + Expect(unchecked((uint)Marshal.ReadInt32(propertyEntry, 0x14)) == test[1] && + unchecked((uint)Marshal.ReadInt32(propertyEntry, 0x08)) == test[2], + "native ore defaults for class 0x" + test[0].ToString("x8")); + } + } + Console.WriteLine("native-behavior-ore-initialization: classes=80000004,80000008,01002000,01004000,01008000; defaults=verified"); + } + private static ushort CurrentX87ControlWord() { if (x87ControlWordReader == null || x87ControlWordCell == IntPtr.Zero) throw new InvalidOperationException("x87 control-word reader is not initialized"); |
