diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-20 08:06:17 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-20 08:06:17 +0300 |
| commit | 374db10daf5db88a52596edebc6cf5f2d560065f (patch) | |
| tree | b77e7bc5a09e4a4496c1ad555336cdac5079f9aa /crates/fparkan-runtime/src | |
| parent | caef8f8e6c0ae81b46fcab39cd4f2b74e1b29235 (diff) | |
| download | fparkan-374db10daf5db88a52596edebc6cf5f2d560065f.tar.xz fparkan-374db10daf5db88a52596edebc6cf5f2d560065f.zip | |
fix(runtime): model distributor share normalizationtrunk
Diffstat (limited to 'crates/fparkan-runtime/src')
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 240 |
1 files changed, 234 insertions, 6 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 8d42958..3964f78 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -116,7 +116,7 @@ pub struct MissionObjectDraft { } /// A mission object registered with one clan's native Behavior distributor. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct BehaviorRegisteredObject { /// Stable identity from the source TMA object order. pub original_id: OriginalObjectId, @@ -126,10 +126,16 @@ pub struct BehaviorRegisteredObject { pub selector: u32, /// Class word recovered from the root Unit DAT header. pub class_flags: u32, + /// Native share field at record offset `+0x04`. + pub share: f32, + /// Raw DWORDs at offsets `+0x08..+0x18`; the first controls whether + /// normalization treats this record as fixed, and the remaining words are + /// not yet assigned semantics. A normal native Add initializes all five to 0. + pub native_tail: [u32; 5], } /// Resource distributor state associated with one TMA clan's constructed AI. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct BehaviorDistributorState { /// TMA clan index; it is not an index into the filtered script bundle list. pub clan_index: usize, @@ -1022,14 +1028,17 @@ fn load_mission_with_options( let Some(original_id) = draft.original_id else { continue; }; - distributors[clan_index] - .registered_objects - .push(BehaviorRegisteredObject { + add_behavior_registered_object( + &mut distributors[clan_index], + BehaviorRegisteredObject { original_id, handle: *handle, selector: draft.raw_after_resource, class_flags, - }); + share: 0.0, + native_tail: [0; 5], + }, + ); } distributors } else { @@ -1409,6 +1418,69 @@ fn has_behavior_registration_key(class_flags: u32) -> bool { class_flags & BEHAVIOR_REGISTRATION_KEY_FLAG != 0 } +fn add_behavior_registered_object( + distributor: &mut BehaviorDistributorState, + mut object: BehaviorRegisteredObject, +) { + let existing_index = distributor + .registered_objects + .iter() + .position(|entry| entry.handle == object.handle); + let count = distributor.registered_objects.len() + usize::from(existing_index.is_none()); + + // Native Add/normalize ran with x87 control word 0x007f. Keep the list-order + // arithmetic in f32 so each operation has the observed 24-bit precision. + let share = 1.0_f32 / count as f32; + let handle = object.handle; + if let Some(index) = existing_index { + distributor.registered_objects[index].share = share; + } else { + object.share = share; + object.native_tail = [0; 5]; + distributor.registered_objects.push(object); + } + + normalize_behavior_distributor(distributor, handle); +} + +fn normalize_behavior_distributor( + distributor: &mut BehaviorDistributorState, + fixed_handle: ObjectHandle, +) { + let mut fixed_sum = 0.0_f32; + let mut free_sum = 0.0_f32; + let mut adjustable_count = 0_usize; + + for object in &distributor.registered_objects { + if object.handle == fixed_handle || object.native_tail[0] != 0 { + fixed_sum = fixed_sum + object.share; + } else { + free_sum = free_sum + object.share; + adjustable_count += 1; + } + } + + if adjustable_count == 0 { + return; + } + + if free_sum <= 0.0 { + let share = (1.0_f32 - fixed_sum) / adjustable_count as f32; + for object in &mut distributor.registered_objects { + if object.handle != fixed_handle && object.native_tail[0] == 0 { + object.share = share; + } + } + } else { + let factor = (1.0_f32 - fixed_sum) / free_sum; + for object in &mut distributor.registered_objects { + if object.handle != fixed_handle && object.native_tail[0] == 0 { + object.share = object.share * factor; + } + } + } +} + fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> { normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| { EngineError::Path { @@ -2421,6 +2493,8 @@ mod tests { draft.resource_name_raw.as_slice(), object.selector, object.class_flags, + object.share.to_bits(), + object.native_tail, ) }) .collect::<Vec<_>>(), @@ -2431,6 +2505,8 @@ mod tests { &b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..], 0, 0x8000_0010, + 0x3f00_0000, + [0; 5], ), ( 0, @@ -2438,6 +2514,8 @@ mod tests { &b"UNITS\\BUILDS\\GENER\\gener01.dat"[..], 0, 0x8000_0002, + 0x3f00_0000, + [0; 5], ), ( 1, @@ -2445,6 +2523,8 @@ mod tests { &b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..], 1, 0x8000_0010, + 0x3f00_0000, + [0; 5], ), ( 1, @@ -2452,6 +2532,8 @@ mod tests { &b"UNITS\\BUILDS\\GENER\\gener01.dat"[..], 1, 0x8000_0002, + 0x3f00_0000, + [0; 5], ), ] ); @@ -2519,6 +2601,152 @@ mod tests { ); } + fn behavior_test_object( + slot: u32, + share: f32, + native_tail: [u32; 5], + ) -> BehaviorRegisteredObject { + BehaviorRegisteredObject { + original_id: OriginalObjectId(slot), + handle: ObjectHandle { + generation: 0, + slot, + }, + selector: 0, + class_flags: 0, + share, + native_tail, + } + } + + #[test] + fn behavior_distributor_add_uses_handle_and_preserves_duplicate_tail() { + let mut distributor = BehaviorDistributorState { + clan_index: 0, + super_ai_visible: true, + registered_objects: Vec::new(), + }; + + add_behavior_registered_object(&mut distributor, behavior_test_object(4, 0.0, [0; 5])); + add_behavior_registered_object(&mut distributor, behavior_test_object(6, 0.0, [0; 5])); + assert_eq!( + distributor + .registered_objects + .iter() + .map(|object| object.share.to_bits()) + .collect::<Vec<_>>(), + vec![0x3f00_0000, 0x3f00_0000] + ); + + distributor.registered_objects[0].share = 0.2; + distributor.registered_objects[0].native_tail = [0xa1, 0xa2, 0xa3, 0xa4, 0xa5]; + add_behavior_registered_object(&mut distributor, behavior_test_object(4, 0.0, [0; 5])); + assert_eq!( + distributor.registered_objects[0].share.to_bits(), + 0x3f00_0000 + ); + assert_eq!( + distributor.registered_objects[0].native_tail, + [0xa1, 0xa2, 0xa3, 0xa4, 0xa5] + ); + + add_behavior_registered_object(&mut distributor, behavior_test_object(8, 0.0, [0; 5])); + assert_eq!( + distributor + .registered_objects + .iter() + .map(|object| object.share.to_bits()) + .collect::<Vec<_>>(), + vec![0x3f00_0000, 0x3e2a_aaa8, 0x3eaa_aaab] + ); + assert_eq!( + distributor + .registered_objects + .iter() + .map(|object| object.native_tail) + .collect::<Vec<_>>(), + vec![[0xa1, 0xa2, 0xa3, 0xa4, 0xa5], [0; 5], [0; 5]] + ); + } + + #[test] + fn behavior_distributor_normalize_matches_native_24_bit_vectors() { + let mut pinned = BehaviorDistributorState { + clan_index: 0, + super_ai_visible: true, + registered_objects: vec![ + behavior_test_object(0x11, 0.2, [0; 5]), + behavior_test_object(0x22, 0.3, [1, 0x11, 0x22, 0x33, 0x44]), + behavior_test_object(0x33, 0.3, [0; 5]), + ], + }; + normalize_behavior_distributor( + &mut pinned, + ObjectHandle { + generation: 0, + slot: 0x11, + }, + ); + assert_eq!( + pinned + .registered_objects + .iter() + .map(|object| object.share.to_bits()) + .collect::<Vec<_>>(), + vec![0x3e4c_cccd, 0x3e99_999a, 0x3f00_0000] + ); + + let mut zero_sum = BehaviorDistributorState { + clan_index: 0, + super_ai_visible: true, + registered_objects: vec![ + behavior_test_object(0x41, 0.25, [0; 5]), + behavior_test_object(0x42, 0.0, [0; 5]), + behavior_test_object(0x43, 0.0, [0; 5]), + ], + }; + normalize_behavior_distributor( + &mut zero_sum, + ObjectHandle { + generation: 0, + slot: 0x41, + }, + ); + assert_eq!( + zero_sum + .registered_objects + .iter() + .map(|object| object.share.to_bits()) + .collect::<Vec<_>>(), + vec![0x3e80_0000, 0x3ec0_0000, 0x3ec0_0000] + ); + + let mut near_rounding = BehaviorDistributorState { + clan_index: 0, + super_ai_visible: true, + registered_objects: vec![ + behavior_test_object(0x51, 0.5, [0; 5]), + behavior_test_object(0x52, 1.0, [0; 5]), + behavior_test_object(0x53, f32::from_bits(0x3380_0000), [0; 5]), + ], + }; + normalize_behavior_distributor( + &mut near_rounding, + ObjectHandle { + generation: 0, + slot: 0x51, + }, + ); + assert_eq!( + near_rounding + .registered_objects + .iter() + .map(|object| object.share.to_bits()) + .collect::<Vec<_>>(), + vec![0x3f00_0000, 0x3f00_0000, 0x3300_0000] + ); + } + #[test] #[ignore = "requires licensed corpus"] fn licensed_campaign_preview_keeps_unsupported_init_without_partial_writes() { |
