aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-runtime
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-runtime')
-rw-r--r--crates/fparkan-runtime/src/lib.rs157
-rw-r--r--crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs128
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");