aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-runtime/src/lib.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 08:06:17 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 08:06:17 +0300
commit374db10daf5db88a52596edebc6cf5f2d560065f (patch)
treeb77e7bc5a09e4a4496c1ad555336cdac5079f9aa /crates/fparkan-runtime/src/lib.rs
parentcaef8f8e6c0ae81b46fcab39cd4f2b74e1b29235 (diff)
downloadfparkan-374db10daf5db88a52596edebc6cf5f2d560065f.tar.xz
fparkan-374db10daf5db88a52596edebc6cf5f2d560065f.zip
fix(runtime): model distributor share normalizationtrunk
Diffstat (limited to 'crates/fparkan-runtime/src/lib.rs')
-rw-r--r--crates/fparkan-runtime/src/lib.rs240
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() {