aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-29 16:08:08 +0300
committerValentin Popov <valentin@popov.link>2026-10-29 16:08:08 +0300
commit6eb1dcaf7036e4e1702de15d0dd02181a3577852 (patch)
treeab0e2969e599227810e4de670f5cd1cf4b6729f7 /crates
parentacdf635ffb3868eee4fa383303cac75fdeea8e23 (diff)
downloadfparkan-6eb1dcaf7036e4e1702de15d0dd02181a3577852.tar.xz
fparkan-6eb1dcaf7036e4e1702de15d0dd02181a3577852.zip
feat(runtime): publish provider values into model node state
Diffstat (limited to 'crates')
-rw-r--r--crates/fparkan-runtime/src/lib.rs76
-rw-r--r--crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs44
2 files changed, 109 insertions, 11 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index 9ae4e84..8e8224b 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -222,6 +222,15 @@ pub struct MissionControlNodeInitialMeshState {
pub stage_count: u32,
}
+/// Mutable projection of the provider row's matching AniMesh node fields.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct MissionControlNodeMeshState {
+ /// Node value at AniMesh `+0x124`, written by selector `0x200`.
+ pub type1_value_bits: u32,
+ /// Visible MSH stage at AniMesh `+0x1c`; distinct from Control's exhaustion sentinel.
+ pub visible_stage: u32,
+}
+
/// Source and current state of one loaded global Control node-data provider row.
///
/// The native table is indexed independently of MSH model nodes. Its rows are
@@ -256,12 +265,13 @@ pub struct MissionControlNodeProviderRow {
pub current_flags: Option<u32>,
/// MSH state exposed at the same global ordinal, if the mesh projection exists.
pub initial_mesh_state: Option<MissionControlNodeInitialMeshState>,
+ /// Current model-node fields, when its global ordinal was resolved.
+ pub mesh_state: Option<MissionControlNodeMeshState>,
}
-/// Node-property publication required after a provider amount changes.
+/// Node-property publication applied to CPU model state after an amount change.
///
-/// The model-node callback is native selector `0x200`; the caller must publish
-/// this value separately from the later stage and visibility update.
+/// Records native selector `0x200`; stage and visibility updates are separate.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[must_use]
pub struct ControlNodeProviderPublication {
@@ -2157,15 +2167,16 @@ pub fn set_loaded_mission_control_multiplier(
/// Applies the confirmed non-animation-delta path of native Control `0x10f30`.
///
-/// Updates amount, ratio, aggregate, exhaustion flag, and dirty state, returning
-/// the required selector-`0x200` node publication. It does not run stage propagation,
+/// Updates amount, ratio, aggregate, exhaustion flag, dirty state, and the CPU
+/// model node's selector-`0x200` value, returning the applied publication.
+/// It does not run stage propagation,
/// visibility, or the separate Control clock; the processed amount stays unchanged.
/// The loaded Unit DAT path has unity maximum multipliers.
///
/// # Errors
/// Rejects invalid handles, unavailable state, unsupported numeric ranges, or
/// depletion requiring the object's unimplemented lifecycle. No state is changed
-/// on error. The returned node publication must be applied by the caller.
+/// on error. This updates CPU model state, not GPU uploads or window rendering.
pub fn apply_loaded_mission_control_node_delta(
engine: &mut Engine,
handle: ObjectHandle,
@@ -2262,8 +2273,16 @@ fn apply_control_node_delta(
let change = checked_control_sum(0, "provider amount change", amount, -previous)?;
let total = checked_control_sum(0, "provider aggregate", total, change)?.max(0.0);
let ratio = checked_control_ratio("provider ratio", amount, maximum)?;
+ let mesh = row
+ .mesh_state
+ .as_mut()
+ .ok_or(ControlResourceAccessError::UnsupportedField {
+ factory_tag: 0,
+ field: "model node state",
+ })?;
row.current_amount_bits = amount.to_bits();
row.current_type1_value_bits = ratio.to_bits();
+ mesh.type1_value_bits = ratio.to_bits();
row.current_flags = Some(if amount == 0.0 {
flags | 0x10
} else {
@@ -3755,6 +3774,7 @@ fn append_control_node_provider_rows(
current_type1_value_bits: 0x3f80_0000,
current_flags: None,
initial_mesh_state: None,
+ mesh_state: None,
});
}
}
@@ -3820,6 +3840,10 @@ fn attach_initial_mesh_states_by_global_ordinal(
for (ordinal, row) in provider_rows.iter_mut().enumerate() {
row.initial_mesh_state = global_mesh_states.get(ordinal).copied().flatten();
row.current_flags = row.initial_mesh_state.map(|state| state.flags);
+ row.mesh_state = row.initial_mesh_state.map(|_| MissionControlNodeMeshState {
+ type1_value_bits: row.initial_type1_value_bits,
+ visible_stage: 0,
+ });
}
}
@@ -4321,6 +4345,7 @@ mod tests {
current_type1_value_bits: initial_type1_value_bits,
current_flags: None,
initial_mesh_state: None,
+ mesh_state: None,
}
}
@@ -5157,6 +5182,10 @@ mod tests {
row.current_amount_bits = 40.0_f32.to_bits();
row.processed_amount_bits = 40.0_f32.to_bits();
row.current_flags = Some(0);
+ row.mesh_state = Some(MissionControlNodeMeshState {
+ type1_value_bits: 1.0_f32.to_bits(),
+ visible_stage: 77,
+ });
let mut device = synthetic_effective_control_device(
26,
26,
@@ -5198,6 +5227,13 @@ mod tests {
};
assert_eq!(rows[1].current_amount_bits, amount.to_bits());
assert_eq!(rows[1].processed_amount_bits, 40.0_f32.to_bits());
+ assert_eq!(
+ rows[1].mesh_state,
+ Some(MissionControlNodeMeshState {
+ type1_value_bits: ratio.to_bits(),
+ visible_stage: 77
+ })
+ );
assert_eq!(rows[1].initial_amount_bits, 100.0_f32.to_bits());
assert_eq!(rows[1].current_flags.unwrap() & 0x10 != 0, exhausted);
let resource = (ratio * 2.0) * 3.0;
@@ -5260,6 +5296,27 @@ mod tests {
));
assert_eq!(table, before);
table.node_provider_amount_bits = total;
+ let MissionControlNodeProviderTableStatus::Available(rows) =
+ &mut table.node_provider_status
+ else {
+ panic!()
+ };
+ let mesh = rows[1].mesh_state.take();
+ let before = table.clone();
+ assert!(matches!(
+ apply_control_node_delta(&mut table, 1, 1.0),
+ Err(ControlResourceAccessError::UnsupportedField {
+ field: "model node state",
+ ..
+ })
+ ));
+ assert_eq!(table, before);
+ let MissionControlNodeProviderTableStatus::Available(rows) =
+ &mut table.node_provider_status
+ else {
+ panic!()
+ };
+ rows[1].mesh_state = mesh;
let before = table.clone();
assert_eq!(
apply_control_node_delta(&mut table, 1, -100.0),
@@ -6794,6 +6851,13 @@ mod tests {
};
assert_eq!(mine_rows[key as usize].initial_amount_bits, initial);
assert_eq!(mine_rows[key as usize].processed_amount_bits, initial);
+ assert_eq!(
+ mine_rows[key as usize].mesh_state,
+ Some(MissionControlNodeMeshState {
+ type1_value_bits: 0.5_f32.to_bits(),
+ visible_stage: 0,
+ })
+ );
}
#[test]
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 d35081f..61cbc01 100644
--- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs
+++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs
@@ -125,6 +125,7 @@ internal static class BehaviorDistributorTickOracle {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
+ [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeAniMeshNodeConstructor(IntPtr self, IntPtr model, IntPtr source, uint component, IntPtr meshData);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountDistribute(IntPtr self, float delta, uint mode, uint flags);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlContactRateUpdate(IntPtr self, float deltaSeconds);
@@ -696,7 +697,7 @@ internal static class BehaviorDistributorTickOracle {
RunNativeControlProviderInitialStateCase(controlModule, aniMeshModule);
RunNativeControlIntoOverrideCase(controlModule);
RunNativeControlResourceDeviceCase(controlModule);
- RunNativeControlProviderLiveStateCase(controlModule);
+ RunNativeControlProviderLiveStateCase(controlModule, aniMeshModule);
RunNativeControlProviderStageArithmeticCase(controlModule, aniMeshModule);
RunNativeControlProviderHierarchyPropagationCase(controlModule, aniMeshModule);
RunNativeControlProviderVisibilityCase(controlModule, aniMeshModule);
@@ -861,7 +862,7 @@ internal static class BehaviorDistributorTickOracle {
}
}
- private static void RunNativeControlProviderLiveStateCase(IntPtr module) {
+ private static void RunNativeControlProviderLiveStateCase(IntPtr module, IntPtr aniMeshModule) {
NativeControlProviderRowConstructor constructProviderRow = (NativeControlProviderRowConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva),
typeof(NativeControlProviderRowConstructor));
@@ -890,7 +891,29 @@ internal static class BehaviorDistributorTickOracle {
IntPtr providerInterface = IntPtr.Add(owner, 4);
IntPtr providerVtable = IntPtr.Add(module, ControlProviderInterfaceVtableRva);
- IntPtr callbackObject = fixture.Allocate(IntPtr.Size);
+ IntPtr callbackObject = fixture.Allocate(0x1b0);
+ IntPtr meshRows = fixture.Allocate(2 * 0x130);
+ NativeAniMeshNodeConstructor constructNode = (NativeAniMeshNodeConstructor)Marshal.GetDelegateForFunctionPointer(
+ IntPtr.Add(aniMeshModule, 0x123e0), typeof(NativeAniMeshNodeConstructor));
+ for (int node = 0; node < 2; node++) {
+ constructNode(IntPtr.Add(meshRows, node * 0x130), IntPtr.Zero, IntPtr.Zero, 0, IntPtr.Zero);
+ Expect(Marshal.ReadInt32(meshRows, node * 0x130 + 0x124) == FloatBits(1.0f) &&
+ Marshal.ReadInt32(meshRows, node * 0x130 + 0x1c) == 0,
+ "original AniMesh node constructor initializes provider value and visible stage");
+ }
+ Marshal.WriteIntPtr(callbackObject, 0x1a8, meshRows);
+ Marshal.WriteInt32(callbackObject, 0x1ac, 2);
+ Marshal.WriteInt32(meshRows, 0x124, FloatBits(-7.0f));
+ Marshal.WriteInt32(meshRows, 0x130 + 0x1c, 77);
+ NativeControlNodeUpdateCallback setNodeValue = (NativeControlNodeUpdateCallback)Marshal.GetDelegateForFunctionPointer(
+ IntPtr.Add(aniMeshModule, 0x5750), typeof(NativeControlNodeUpdateCallback));
+ NativeControlNodeUpdateCallback forwardNodeValue = delegate(IntPtr self, uint node, uint selector, uint bits) {
+ OnControlNodeUpdate(self, node, selector, bits);
+ setNodeValue(self, node, selector, bits);
+ };
+ setNodeValue(callbackObject, 1, 0x201, unchecked((uint)FloatBits(99.0f)));
+ Expect(ReadFloat(meshRows, 0x130 + 0x124) == 1.0f,
+ "AniMesh node-value setter ignores selectors other than 0x200");
IntPtr callbackVtable = fixture.Allocate(0x38);
IntPtr device = fixture.Allocate(0x94);
IntPtr row = IntPtr.Add(rows, 0x2c); // Keep the root row untouched.
@@ -909,7 +932,7 @@ internal static class BehaviorDistributorTickOracle {
Marshal.WriteInt32(row, 0x14, FloatBits(0.4f));
Marshal.WriteIntPtr(callbackObject, 0, callbackVtable);
- Marshal.WriteIntPtr(callbackVtable, 0x34, Marshal.GetFunctionPointerForDelegate(ControlNodeUpdateCallback));
+ Marshal.WriteIntPtr(callbackVtable, 0x34, Marshal.GetFunctionPointerForDelegate(forwardNodeValue));
Marshal.WriteIntPtr(owner, 0x20, callbackObject);
Marshal.WriteInt32(device, 0x04, 1);
@@ -922,6 +945,9 @@ internal static class BehaviorDistributorTickOracle {
Expect(ReadFloat(row, 0x08) == 60.0f && ReadFloat(row, 0x14) == 0.6f &&
ReadFloat(owner, 0x590) == 60.0f,
"Control native amount update clamps the node amount, updates the normalized provider value, and keeps the aggregate amount in sync");
+ Expect(ReadFloat(meshRows, 0x130 + 0x124) == 0.6f && ReadFloat(meshRows, 0x124) == -7.0f &&
+ Marshal.ReadInt32(meshRows, 0x130 + 0x1c) == 77,
+ "Control amount update reaches original AniMesh setter and changes only its target node value");
Expect(controlNodeUpdateCount == 1 && controlNodeUpdateIndex == 1 &&
controlNodeUpdateSelector == 0x200 && controlNodeUpdateValueBits == unchecked((uint)FloatBits(0.6f)),
"Control native amount update publishes the normalized provider state through the existing node callback");
@@ -960,11 +986,17 @@ internal static class BehaviorDistributorTickOracle {
update(owner, 1, -1.0f, 0);
Expect(Marshal.ReadInt32(row, 8) == 0 && ReadFloat(row, 0x14) == 0.25f &&
ReadFloat(row, 0x0c) == 40.0f && ReadFloat(owner, 0x590) == 20.0f &&
- controlNodeUpdateCount == 5 && (Marshal.ReadInt32(row, 0x28) & 0x10) == 0,
+ controlNodeUpdateCount == 5 && (Marshal.ReadInt32(row, 0x28) & 0x10) == 0 &&
+ ReadFloat(meshRows, 0x130 + 0x124) == 0.1f,
"clamping signed zero writes amount bits but leaves ratio, aggregate, flags and publication unchanged");
Console.WriteLine("native-control-live-ndp-provider: controlHash=" + ExpectedControlSha256 +
" rowCtorRva=0xb240 initialType1=1 rowStride=0x2c node=1 max=100 amount=0..100 tag26Selector300=live");
+ Console.WriteLine("native-control-to-animesh-node-value: AniMeshCtor=0x123e0 setter=0x5750 rowStride=0x130 value=+0x124" +
+ " selector=0x200 other-node/stage=unchanged signed-zero=no-publication");
+ GC.KeepAlive(constructNode);
+ GC.KeepAlive(setNodeValue);
+ GC.KeepAlive(forwardNodeValue);
GC.KeepAlive(constructProviderRow);
GC.KeepAlive(update);
GC.KeepAlive(getter);
@@ -1046,6 +1078,8 @@ internal static class BehaviorDistributorTickOracle {
unchecked((uint)Marshal.ReadInt32(rows, 0x2c + 0x14)) == 0x3e800000 &&
unchecked((uint)Marshal.ReadInt32(rows, 3 * 0x2c + 0x14)) == 0x3e800000,
"Control flag 4 copies the parent type-1 ratio before stage calculation");
+ Expect(ReadFloat(meshRows, 0x130 + 0x124) == 0.0f && ReadFloat(rows, 0x2c + 0x14) == 0.25f,
+ "inheriting a parent provider ratio does not rewrite the separate AniMesh node value");
Expect(String.Join(",", aniMeshStageTransitions.ToArray()) == "0:3,1:3,2:1,3:3,4:2,5:1,6:1,7:0,8:2" &&
String.Join(",", aniMeshNodeLifecycleCallbacks.ToArray()) == "7:101:4" && !aniMeshCallbackOwnerMismatch &&
!controlNodeUpdateOwnerMismatch && !nativeAniMeshStageCallbackMissing,