diff options
Diffstat (limited to 'crates/fparkan-runtime')
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 317 | ||||
| -rw-r--r-- | crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs | 39 |
2 files changed, 333 insertions, 23 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 7ba114d..447e024 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -2240,6 +2240,32 @@ pub fn update_loaded_mission_control_node_stage( update_control_node_stage(table, key, update_effects) } +/// Updates one loaded Control node and then walks its provider descendants in +/// native depth-first order (`Control.dll+0x11130`). +/// +/// Unlike [`update_loaded_mission_control_node_stage`], this also propagates +/// depletion and stage flags through descendants. The whole provider table is +/// committed only if every node can be updated; this CPU projection does not +/// run unsupported visibility, FX, or host callbacks. +pub fn propagate_loaded_mission_control_node_stage( + engine: &mut Engine, + handle: ObjectHandle, + key: u32, + update_effects: bool, +) -> Result<(), ControlResourceAccessError> { + transform_state(&engine.world, handle).map_err(ControlResourceAccessError::World)?; + let loaded = engine + .loaded + .as_mut() + .ok_or(ControlResourceAccessError::MissionUnavailable)?; + let table = loaded + .control_device_tables + .iter_mut() + .find(|table| table.handle == handle) + .ok_or(ControlResourceAccessError::ObjectUnavailable { handle })?; + propagate_control_node_stage(table, key, update_effects) +} + fn update_control_node_stage( table: &mut MissionControlDeviceTableState, key: u32, @@ -2346,6 +2372,120 @@ fn update_control_node_stage( Ok(changed) } +fn propagate_control_node_stage( + table: &mut MissionControlDeviceTableState, + key: u32, + update_effects: bool, +) -> Result<(), ControlResourceAccessError> { + let mut updated = table.clone(); + propagate_control_node_stage_inner(&mut updated, key, update_effects)?; + *table = updated; + Ok(()) +} + +fn propagate_control_node_stage_inner( + table: &mut MissionControlDeviceTableState, + key: u32, + update_effects: bool, +) -> Result<(), ControlResourceAccessError> { + let (index, row_count, previous_stage) = match &table.node_provider_status { + MissionControlNodeProviderTableStatus::Available(rows) => { + let row_count = rows.len(); + let index = usize::try_from(key) + .ok() + .filter(|index| *index < row_count) + .ok_or(ControlResourceAccessError::NodeProviderKeyOutOfRange { key, row_count })?; + (index, row_count, rows[index].current_stage) + } + MissionControlNodeProviderTableStatus::Unavailable(reason) => { + return Err(ControlResourceAccessError::NodeProviderTableUnavailable( + reason.clone(), + )) + } + }; + + update_control_node_stage(table, key, update_effects)?; + + let (stage, flags) = match &table.node_provider_status { + MissionControlNodeProviderTableStatus::Available(rows) => { + let row = &rows[index]; + let flags = row + .current_flags + .ok_or(ControlResourceAccessError::UnsupportedField { + factory_tag: 0, + field: "provider flags", + })?; + (row.current_stage, flags) + } + MissionControlNodeProviderTableStatus::Unavailable(reason) => { + return Err(ControlResourceAccessError::NodeProviderTableUnavailable( + reason.clone(), + )) + } + }; + let depleted_or_worsened = previous_stage < stage || flags & 0x10 != 0; + + for child_index in index + 1..row_count { + let (metadata, child_flags) = match &table.node_provider_status { + MissionControlNodeProviderTableStatus::Available(rows) => { + let row = &rows[child_index]; + let metadata = + row.initial_mesh_state + .ok_or(ControlResourceAccessError::UnsupportedField { + factory_tag: 0, + field: "provider mesh metadata", + })?; + let child_flags = row.current_flags; + (metadata, child_flags) + } + MissionControlNodeProviderTableStatus::Unavailable(reason) => { + return Err(ControlResourceAccessError::NodeProviderTableUnavailable( + reason.clone(), + )) + } + }; + if i64::from(metadata.parent_index) != i64::from(key) { + continue; + } + + let child_key = u32::try_from(child_index) + .map_err(|_| ControlResourceAccessError::InvalidNodeStageState { key })?; + let child_flags = child_flags.ok_or(ControlResourceAccessError::UnsupportedField { + factory_tag: 0, + field: "provider flags", + })?; + if child_flags & 0x140 == 0 { + let child_flags = if flags & 0xc0 == 0 { + (child_flags | if depleted_or_worsened { 0x100 } else { 0 }) & !0x80 + } else { + child_flags | 0x80 + }; + if let MissionControlNodeProviderTableStatus::Available(rows) = + &mut table.node_provider_status + { + rows[child_index].current_flags = Some(child_flags); + } + } + + if depleted_or_worsened { + let child_amount = match &table.node_provider_status { + MissionControlNodeProviderTableStatus::Available(rows) => { + f32::from_bits(rows[child_index].current_amount_bits) + } + MissionControlNodeProviderTableStatus::Unavailable(reason) => { + return Err(ControlResourceAccessError::NodeProviderTableUnavailable( + reason.clone(), + )) + } + }; + apply_control_node_delta(table, child_key, -child_amount)?; + } + + propagate_control_node_stage_inner(table, child_key, update_effects)?; + } + Ok(()) +} + fn control_stage_product(count: u32, ratio: f32) -> f32 { if ratio == 0.0 { return 0.0; @@ -5278,6 +5418,10 @@ mod tests { update_loaded_mission_control_node_stage(&mut engine, handle, 1, false), Err(ControlResourceAccessError::MissionUnavailable) ); + assert_eq!( + propagate_loaded_mission_control_node_stage(&mut engine, handle, 1, false), + Err(ControlResourceAccessError::MissionUnavailable) + ); let stale = ObjectHandle { generation: handle.generation.wrapping_add(1), @@ -5321,6 +5465,12 @@ mod tests { fparkan_world::WorldError::StaleHandle )) ); + assert_eq!( + propagate_loaded_mission_control_node_stage(&mut engine, stale, 1, false), + Err(ControlResourceAccessError::World( + fparkan_world::WorldError::StaleHandle + )) + ); fparkan_world::request_delete(&mut engine.world, handle).expect("delete object"); assert_eq!( @@ -5329,6 +5479,12 @@ mod tests { fparkan_world::WorldError::StaleHandle )) ); + assert_eq!( + propagate_loaded_mission_control_node_stage(&mut engine, handle, 1, false), + Err(ControlResourceAccessError::World( + fparkan_world::WorldError::StaleHandle + )) + ); assert_eq!( apply_loaded_mission_control_node_delta(&mut engine, handle, 1, -1.0), @@ -5697,6 +5853,132 @@ mod tests { } #[test] + fn control_node_stage_descendants_follow_native_walk_and_rollback() { + let make_rows = |parents: [i32; 5]| { + parents + .into_iter() + .enumerate() + .map(|(index, parent_index)| { + let flags = if index == 0 { 0xd2 } else { 2 }; + let mut row = synthetic_node_provider_row(0, index, 1.0_f32.to_bits()); + row.current_flags = Some(flags); + row.initial_mesh_state = Some(MissionControlNodeInitialMeshState { + parent_index, + flags, + stage_count: 2, + }); + row.mesh_state = Some(MissionControlNodeMeshState { + type1_value_bits: 1.0_f32.to_bits(), + visible_stage: 0, + }); + row + }) + .collect::<Vec<_>>() + }; + + let mut rows = make_rows([-1, 0, 0, 1, 3]); + rows[1].current_flags = Some(0x140); + rows[1].initial_mesh_state.as_mut().unwrap().flags = 0x140; + let mut table = synthetic_control_device_table_with_providers( + MissionControlDeviceTableStatus::Available(Vec::new()), + MissionControlNodeProviderTableStatus::Available(rows), + ); + propagate_control_node_stage(&mut table, 0, false).unwrap(); + let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status + else { + panic!() + }; + assert_eq!(rows[0].current_flags, Some(0xd2)); + for index in [1, 3, 4, 2] { + assert_eq!(rows[index].current_amount_bits, 0.0_f32.to_bits()); + assert_eq!( + rows[index].current_flags, + Some(if index == 1 { 0x150 } else { 0x92 }) + ); + assert_eq!(rows[index].processed_amount_bits, 100.0_f32.to_bits()); + } + assert_eq!(table.node_provider_amount_bits, Some(100.0_f32.to_bits())); + + let mut rows = make_rows([-1, 0, 0, 1, 3]); + rows[0].current_flags = Some(2); + rows[0].current_type1_value_bits = 0.0_f32.to_bits(); + rows[1].current_flags = Some(0x82); + rows[1].initial_mesh_state.as_mut().unwrap().flags = 0x82; + let mut table = synthetic_control_device_table_with_providers( + MissionControlDeviceTableStatus::Available(Vec::new()), + MissionControlNodeProviderTableStatus::Available(rows), + ); + propagate_control_node_stage(&mut table, 0, false).unwrap(); + let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status + else { + panic!() + }; + assert_eq!(rows[0].current_stage, 2); + assert_eq!(rows[0].current_flags, Some(0x202)); + for index in [1, 3, 4, 2] { + assert_eq!(rows[index].current_amount_bits, 0.0_f32.to_bits()); + assert_eq!(rows[index].current_stage, 2); + assert_eq!(rows[index].current_flags, Some(0x312)); + } + + let mut rows = make_rows([-1, 0, 0, 1, 3]); + rows[0].current_flags = Some(2); + rows[0].current_stage = 1; + rows[0].current_type1_value_bits = 0.5_f32.to_bits(); + rows[0].initial_mesh_state.as_mut().unwrap().stage_count = 2; + rows[1].current_flags = Some(0x8a); + rows[1].current_stage = 0; + rows[1].current_type1_value_bits = 0.5_f32.to_bits(); + rows[1].initial_mesh_state.as_mut().unwrap().flags = 0x8a; + rows[1].initial_mesh_state.as_mut().unwrap().stage_count = 2; + rows[1].mesh_state.as_mut().unwrap().type1_value_bits = 0.5_f32.to_bits(); + let mut table = synthetic_control_device_table_with_providers( + MissionControlDeviceTableStatus::Available(Vec::new()), + MissionControlNodeProviderTableStatus::Available(rows), + ); + let before = table.clone(); + assert_eq!( + propagate_control_node_stage(&mut table, 0, true), + Err(ControlResourceAccessError::UnsupportedNodeEffects { key: 1 }) + ); + assert_eq!(table, before); + propagate_control_node_stage(&mut table, 0, false).unwrap(); + let MissionControlNodeProviderTableStatus::Available(rows) = &table.node_provider_status + else { + panic!() + }; + assert_eq!(rows[1].current_flags, Some(0x20a)); + assert_eq!(rows[1].current_stage, 1); + assert_eq!(rows[1].current_amount_bits, 100.0_f32.to_bits()); + let before = table.clone(); + assert_eq!( + propagate_control_node_stage(&mut table, u32::MAX, false), + Err(ControlResourceAccessError::NodeProviderKeyOutOfRange { + key: u32::MAX, + row_count: 5, + }) + ); + assert_eq!(table, before); + + let mut rows = make_rows([-1, 0, 0, 1, 3]); + rows[2].mesh_state = None; + rows[4].current_flags = None; + let mut table = synthetic_control_device_table_with_providers( + MissionControlDeviceTableStatus::Available(Vec::new()), + MissionControlNodeProviderTableStatus::Available(rows), + ); + let before = table.clone(); + assert_eq!( + propagate_control_node_stage(&mut table, 0, false), + Err(ControlResourceAccessError::UnsupportedField { + factory_tag: 0, + field: "provider flags", + }) + ); + assert_eq!(table, before); + } + + #[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 [ @@ -6848,11 +7130,38 @@ mod tests { let draft = &loaded_mission_object_drafts(&engine).expect("object drafts")[0]; assert_eq!(draft.unit_components.len(), 6); assert_eq!(draft.unit_components[0].immediate_child_count, 5); - let tables = loaded_mission_control_device_tables(&engine).expect("Control tables"); + let tables = loaded_mission_control_device_tables(&engine) + .expect("Control tables") + .to_vec(); assert_eq!(tables.len(), 1); assert_eq!(tables[0].original_id, Some(OriginalObjectId(0))); assert!(tables[0].node_provider_dirty); let control_handle = tables[0].handle; + let node_provider_count = match &tables[0].node_provider_status { + MissionControlNodeProviderTableStatus::Available(rows) => rows.len(), + MissionControlNodeProviderTableStatus::Unavailable(reason) => { + panic!("licensed Control provider rows unavailable: {reason:?}") + } + }; + let before_invalid_node_key = loaded_mission_control_device_tables(&engine) + .unwrap() + .to_vec(); + assert_eq!( + propagate_loaded_mission_control_node_stage( + &mut engine, + control_handle, + u32::MAX, + false, + ), + Err(ControlResourceAccessError::NodeProviderKeyOutOfRange { + key: u32::MAX, + row_count: node_provider_count, + }) + ); + assert_eq!( + loaded_mission_control_device_tables(&engine), + Some(before_invalid_node_key.as_slice()) + ); assert_eq!( query_loaded_mission_control_resource(&engine, control_handle, 1) .expect("licensed selector 1") @@ -7219,13 +7528,13 @@ mod tests { ) .unwrap() .unwrap(); - assert!(update_loaded_mission_control_node_stage( + propagate_loaded_mission_control_node_stage( &mut mine_engine, mine_profile.handle, stage_key, - false + false, ) - .unwrap()); + .expect("licensed Control descendant stage propagation"); let MissionControlNodeProviderTableStatus::Available(rows) = &loaded_mission_control_device_tables(&mine_engine).unwrap()[0].node_provider_status else { 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 e7756c1..7b82ad3 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -1131,7 +1131,7 @@ internal static class BehaviorDistributorTickOracle { new float[] { 25.0f, 10.0f, 5.0f, 12.0f, 7.0f }, new byte[] { 0x02, 0x02, 0x02, 0x02, 0x02 }); const int rowCount = 5; - int[] parents = { -1, 0, 1, 0, 0 }; + int[] parents = { -1, 0, 0, 1, 3 }; uint[] stages = { 4, 2, 3, 2, 2 }; uint[] flags = { 0x02, 0x42, 0x02, 0x102, 0x02 }; for (int index = 0; index < rowCount; index++) { @@ -1169,14 +1169,14 @@ internal static class BehaviorDistributorTickOracle { IntPtr parent = table.Rows; IntPtr protected40 = IntPtr.Add(table.Rows, 0x2c); - IntPtr grandchild = IntPtr.Add(table.Rows, 2 * 0x2c); + IntPtr sibling = IntPtr.Add(table.Rows, 2 * 0x2c); IntPtr protected100 = IntPtr.Add(table.Rows, 3 * 0x2c); - IntPtr sibling = IntPtr.Add(table.Rows, 4 * 0x2c); + IntPtr grandchild = IntPtr.Add(table.Rows, 4 * 0x2c); IntPtr parentMeshRow = meshRows; IntPtr bit40MeshRow = IntPtr.Add(meshRows, 0x130); - IntPtr grandchildMeshRow = IntPtr.Add(meshRows, 2 * 0x130); + IntPtr siblingMeshRow = IntPtr.Add(meshRows, 2 * 0x130); IntPtr bit100MeshRow = IntPtr.Add(meshRows, 3 * 0x130); - IntPtr siblingMeshRow = IntPtr.Add(meshRows, 4 * 0x130); + IntPtr grandchildMeshRow = IntPtr.Add(meshRows, 4 * 0x130); Expect(ReadFloat(parent, 0x08) == 25.0f && ReadFloat(parent, 0x0c) == 25.0f && unchecked((uint)Marshal.ReadInt32(parent, 0x18)) == 3, "Control 0x11130 updates the parent stage and processed amount first"); @@ -1184,40 +1184,41 @@ internal static class BehaviorDistributorTickOracle { unchecked((uint)Marshal.ReadInt32(protected40, 0x18)) == 0 && (unchecked((uint)Marshal.ReadInt32(protected40, 0x28)) & 0x50) == 0x50, "Protected bit 0x40 does not stop child depletion, while its depleted-stage early return remains visible"); - Expect(ReadFloat(grandchild, 0x08) == 0.0f && ReadFloat(grandchild, 0x0c) == 125.0f && - unchecked((uint)Marshal.ReadInt32(grandchild, 0x18)) == 0 && - (unchecked((uint)Marshal.ReadInt32(grandchild, 0x28)) & 0x90) == 0x90, - "Control 0x11130 drains a protected child's descendant but its inherited 0x80 flag preserves the depleted stage snapshot"); Expect(ReadFloat(protected100, 0x08) == 0.0f && ReadFloat(protected100, 0x0c) == 0.0f && - unchecked((uint)Marshal.ReadInt32(protected100, 0x18)) == 2, - "Protected bit 0x100 does not stop child depletion or its later stage update"); + unchecked((uint)Marshal.ReadInt32(protected100, 0x18)) == 2 && + (unchecked((uint)Marshal.ReadInt32(protected100, 0x28)) & 0x100) != 0, + "Protected bit 0x100 skips flag rewriting but does not stop drain or descendant traversal"); + Expect(ReadFloat(grandchild, 0x08) == 0.0f && ReadFloat(grandchild, 0x0c) == 0.0f && + unchecked((uint)Marshal.ReadInt32(grandchild, 0x18)) == 2 && + (unchecked((uint)Marshal.ReadInt32(grandchild, 0x28)) & 0x100) != 0, + "The protected row's grandchild is visited before the root's next sibling"); Expect(ReadFloat(sibling, 0x08) == 0.0f && ReadFloat(sibling, 0x0c) == 0.0f && - unchecked((uint)Marshal.ReadInt32(sibling, 0x18)) == 2 && + unchecked((uint)Marshal.ReadInt32(sibling, 0x18)) == 3 && (unchecked((uint)Marshal.ReadInt32(sibling, 0x28)) & 0x100) != 0, "An unprotected sibling retains the native damaged flag while updating its stage"); Expect(unchecked((uint)Marshal.ReadInt32(parentMeshRow, 0x1c)) == 3 && unchecked((uint)Marshal.ReadInt32(bit40MeshRow, 0x1c)) == unchecked((uint)-99) && - unchecked((uint)Marshal.ReadInt32(grandchildMeshRow, 0x1c)) == unchecked((uint)-99) && + unchecked((uint)Marshal.ReadInt32(siblingMeshRow, 0x1c)) == 2 && unchecked((uint)Marshal.ReadInt32(bit100MeshRow, 0x1c)) == 1 && - unchecked((uint)Marshal.ReadInt32(siblingMeshRow, 0x1c)) == 1, + unchecked((uint)Marshal.ReadInt32(grandchildMeshRow, 0x1c)) == 1, "ANI slot 0x44 reaches native AniMesh RVA 0x5810 for changed visible stages only"); Expect(controlNodeUpdateCount == 4 && - String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "1,2,3,4" && - String.Join(",", controlNodeUpdateFlags.ConvertAll(value => value.ToString("x")).ToArray()) == "42,82,102,102" && + String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "1,3,4,2" && + String.Join(",", controlNodeUpdateFlags.ConvertAll(value => value.ToString("x")).ToArray()) == "42,102,102,102" && !controlNodeUpdateOwnerMismatch, "Control 0x11130 traverses descendants depth-first, checking callback owners and protected flags"); - Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,3:1,4:1" && + Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,3:1,4:1,2:2" && aniMeshNodeLifecycleCallbacks.Count == 0 && !aniMeshCallbackOwnerMismatch && !nativeAniMeshStageCallbackMissing, "Control descendant update preserves the native callback order and stage bounds"); Expect(String.Join(",", controlProviderCallbackTrace.ToArray()) == - "stage:0:3,amount:1,amount:2,amount:3,stage:3:1,amount:4,stage:4:1", + "stage:0:3,amount:1,amount:3,stage:3:1,amount:4,stage:4:1,amount:2,stage:2:2", "Control 0x11130 keeps amount, lifecycle, and visual-stage callbacks in native depth-first order"); Expect(ReadFloat(table.Owner, 0x590) == 25.0f, "Control 0x10f30 keeps the aggregate amount in sync after recursive child depletion"); Console.WriteLine("native-control-parent-propagation: controlHash=" + ExpectedControlSha256 + - " rva=0x11130 childOrder=[1,2,3,4] protected40/100=depleted bit40-stage-return=preserved" + + " rva=0x11130 childOrder=[1,3,4,2] protected40/100=depleted bit40-stage-return=preserved" + " callbacks=" + String.Join(",", controlProviderCallbackTrace.ToArray())); activeControlProviderRows = IntPtr.Zero; activeAniMeshInterface = IntPtr.Zero; |
