Implement native runout animation attributes and reproduce the missing biped transition

This commit is contained in:
chazmm committed 2026-09-06 17:22:51 +01:00
1 parent c818285b4d
commit bdba0c0a41
8 files changed
+163

No files matched your search

+1
View File
@@ -26,6 +26,7 @@ pub(crate) mod motion_push_off;
#[path = "motion_offboard/reset.rs"] #[path = "motion_offboard/reset.rs"]
pub(crate) mod motion_reset; pub(crate) mod motion_reset;
mod motion_riding; mod motion_riding;
pub(crate) mod motion_runout;
pub(crate) mod motion_riding_conditions; pub(crate) mod motion_riding_conditions;
pub(crate) mod motion_shove; pub(crate) mod motion_shove;
mod motion_sliding; mod motion_sliding;
@@ -44,6 +44,7 @@ pub struct MotionPhysical {
pub foot_frame: Option<PushFootFrame>, pub foot_frame: Option<PushFootFrame>,
} }
enum Instance { enum Instance {
Runout(Option<super::motion_runout::Parameters>),
Trick(i32), Trick(i32),
Stateless, Stateless,
IntentFilter(skate_core::animation::intent_filter::State), IntentFilter(skate_core::animation::intent_filter::State),
@@ -70,6 +71,7 @@ enum Instance {
impl Instance { impl Instance {
fn new(operation: &MotionOperation) -> Self { fn new(operation: &MotionOperation) -> Self {
match operation { match operation {
MotionOperation::AddRunoutAttribs => Self::Runout(None),
MotionOperation::Trick(_) => Self::Trick(0), MotionOperation::Trick(_) => Self::Trick(0),
MotionOperation::IntentFilter(_) => Self::IntentFilter(Default::default()), MotionOperation::IntentFilter(_) => Self::IntentFilter(Default::default()),
MotionOperation::Landing(_) => Self::Landing(Default::default()), MotionOperation::Landing(_) => Self::Landing(Default::default()),
@@ -126,6 +128,7 @@ pub struct MotionHost {
pub riding_conditions: Option<super::motion_riding_conditions::RidingConditionInputs>, pub riding_conditions: Option<super::motion_riding_conditions::RidingConditionInputs>,
pub condition_random: super::motion_riding_conditions::MotionRandom, pub condition_random: super::motion_riding_conditions::MotionRandom,
pub native_physical: Option<super::motion_native::Physical>, pub native_physical: Option<super::motion_native::Physical>,
pub runout_physical: Option<super::motion_runout::Physical>,
pub prelanding_physical: Option<super::motion_spin::PrelandingPhysical>, pub prelanding_physical: Option<super::motion_spin::PrelandingPhysical>,
pub air_leg_physical: Option<skate_core::animation::air_leg_extension::Physical>, pub air_leg_physical: Option<skate_core::animation::air_leg_extension::Physical>,
pub gesture_physical: Option<super::motion_native::GesturePhysical>, pub gesture_physical: Option<super::motion_native::GesturePhysical>,
@@ -218,6 +221,7 @@ impl MotionHost {
riding_conditions: None, riding_conditions: None,
condition_random: super::motion_riding_conditions::MotionRandom::new(), condition_random: super::motion_riding_conditions::MotionRandom::new(),
native_physical: None, native_physical: None,
runout_physical: None,
prelanding_physical: None, prelanding_physical: None,
air_leg_physical: None, air_leg_physical: None,
gesture_physical: None, gesture_physical: None,
@@ -94,6 +94,21 @@ impl MotionHost {
.get_mut(behavior) .get_mut(behavior)
.ok_or("Unallocated MotionGraph behavior")?; .ok_or("Unallocated MotionGraph behavior")?;
match (operation, instance) { match (operation, instance) {
(MotionOperation::AddRunoutAttribs, Instance::Runout(state)) => {
if phase == 0 {
*state = Some(super::super::motion_runout::capture(
self.runout_physical.ok_or("AddRunoutAttribs requires completed physical output")?,
self.animation.skater_animation_flags.ok_or("AddRunoutAttribs requires animation stance")? & 0x4000_0000 != 0,
));
} else if phase == 1 {
let values = state.ok_or("AddRunoutAttribs updated before Begin")?;
for (name, value) in [("BipedStartAngle", values.angle_degrees), ("BipedSpeed", values.speed)] {
self.animation.set_attribute(SettableAttribute {
name: encode(name.as_bytes()), value, normalized: false, sequence_id: -1,
});
}
}
}
//Retail ctor82BA58E8 retains only diagnostic text/layout. All three //Retail ctor82BA58E8 retains only diagnostic text/layout. All three
//lifecycle slots in82309664 are82B61BB8 (blr), with no state writes. //lifecycle slots in82309664 are82B61BB8 (blr), with no state writes.
(MotionOperation::PrintText2D, _) => {} (MotionOperation::PrintText2D, _) => {}
@@ -18,6 +18,7 @@ pub enum MotionOperation {
Play(PlayAnimation), Play(PlayAnimation),
///TU3 vtable82309664: Begin/Update/End all point to the empty82B61BB8. ///TU3 vtable82309664: Begin/Update/End all point to the empty82B61BB8.
PrintText2D, PrintText2D,
AddRunoutAttribs,
AttachIntent { AttachIntent {
intent: String, intent: String,
attribute: AttributeName, attribute: AttributeName,
@@ -113,6 +114,7 @@ impl OperationFactory for MotionFactory {
)), )),
"PlayAnimation" => Some(MotionOperation::Play(play(a))), "PlayAnimation" => Some(MotionOperation::Play(play(a))),
"PrintText2D" => Some(MotionOperation::PrintText2D), "PrintText2D" => Some(MotionOperation::PrintText2D),
"AddRunoutAttribs" => Some(MotionOperation::AddRunoutAttribs),
"ApplyingBodyTilt" => Some(MotionOperation::ApplyingBodyTilt), "ApplyingBodyTilt" => Some(MotionOperation::ApplyingBodyTilt),
"SettingBodyTilt" => { "SettingBodyTilt" => {
Some(MotionOperation::SettingBodyTilt(key(a, "tilt_x", "tilt_x"))) Some(MotionOperation::SettingBodyTilt(key(a, "tilt_x", "tilt_x")))
@@ -0,0 +1,57 @@
//! TU3 AddRunoutAttribs: factory82BC9BA8, vtable82320BDC,
//! Begin82BBA4A8, Update82BBA770, End82B61BB8.
use skate_core::{math::Vector3, physics::board_motion_output::length,
player::wipeout_state::orientation::projected_angle};
#[derive(Clone, Copy, Debug)]
pub struct Physical {
/// Skeleton0: effective animation root Z (Fill82BE1AE8 / GetEffectiveRoot).
pub forward: [f32; 4],
/// SystemReckoning16 and96, respectively.
pub velocity: [f32; 4],
pub up: [f32; 4],
/// OffBoard128, selected only when OffBoard331 is set.
pub trajectory_velocity: Option<[f32; 4]>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Parameters {
pub angle_degrees: f32,
pub speed: f32,
}
pub fn capture(physical: Physical, mirrored: bool) -> Parameters {
let velocity = physical.trajectory_velocity.unwrap_or(physical.velocity);
let angle = projected_angle(physical.forward, velocity, physical.up);
let angle = if mirrored { -angle } else { angle };
// Native wrap has a strict >0.5 boundary, preserving positive 180 degrees.
let turns = angle * f32::from_bits(0x3e22_f983);
let fraction = turns - turns.floor();
let radians = (fraction - if fraction > 0.5 { 1.0 } else { 0.0 })
* f32::from_bits(0x40c9_0fdb);
Parameters {
angle_degrees: radians * f32::from_bits(0x4265_2ee1),
speed: length(Vector3::new(velocity[0], velocity[1], velocity[2])),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runout_direction_speed_stance_and_trajectory_selection() {
let mut p = Physical { forward: [0.,0.,1.,0.], velocity: [3.,4.,0.,0.],
up: [0.,1.,0.,0.], trajectory_velocity: None };
let regular = capture(p, false);
let mirrored = capture(p, true);
assert!((regular.angle_degrees - 90.).abs() < 0.001);
assert!((mirrored.angle_degrees + 90.).abs() < 0.001);
assert!((regular.speed - 5.).abs() < 0.00001);
p.trajectory_velocity = Some([0.,0.,-2.,0.]);
assert!((capture(p, false).angle_degrees - 180.).abs() < 0.001);
assert!((capture(p, true).angle_degrees.abs() - 180.).abs() < 0.001);
assert!((capture(p, false).speed - 2.).abs() < 0.00001);
p.trajectory_velocity = Some([0.;4]);
assert_eq!(capture(p, false), Parameters { angle_degrees: 0., speed: 0. });
}
}
@@ -175,6 +175,15 @@ pub(crate) fn advance(
effective_z = effective_z.map(|v| -v); effective_z = effective_z.map(|v| -v);
} }
let mut effective_x = skater.animated_skeleton.roots.animation_to_world[0]; let mut effective_x = skater.animated_skeleton.roots.animation_to_world[0];
skater.animation.motion.runout_physical = Some(crate::graph_host::motion_runout::Physical {
forward: effective_z,
velocity: physical.reckoning.vector_16.map(f32::from_bits),
up: physical.reckoning.vector_96.map(f32::from_bits),
// OffBoard331 remains reset in the currently implemented riding/air states,
// matching the camera publication. Biped trajectory owners must publish
// OffBoard128 here when they enable that alternate native output.
trajectory_velocity: None,
});
if p.flags_2476 & 4 != 0 { if p.flags_2476 & 4 != 0 {
effective_x = effective_x.map(|v| -v); effective_x = effective_x.map(|v| -v);
} }
@@ -219,3 +219,37 @@ fn authored_checkpoint_reply_runs_teleport_state_and_restores_riding() {
} }
assert!(saw_ready && restored_ticks > 60); assert!(saw_ready && restored_ticks > 60);
} }
#[test]
#[ignore = "requires private stock animation banks and collections"]
fn runout_request_enters_stock_motion_graph_in_all_difficulties() {
let root = std::env::var_os("SKATE3_ASSET_ROOT").expect("set SKATE3_ASSET_ROOT");
let root = std::path::Path::new(&root);
let assets = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
for difficulty in crate::difficulty::Difficulty::ALL {
let mut physics = GamePhysics::load_with_difficulty(root, None, difficulty).unwrap();
let mut skater = SkaterRuntime::load(root, &graphs, &physics, difficulty.key()).unwrap();
let mut controls = PlayerControls::default();
let mut input = crate::input::ControllerInput::default();
let mut camera = crate::camera::CameraRuntime::load(root).unwrap();
for tick in 0..120 {
// Inject the completed runout request at the physical -> animation
// boundary; leave stock graph selection and all following ticks live.
input.sample_raw_for_test(XboxState { buttons: 0, triggers: [0;2], left: [0;2], right: [0;2] });
let mut actions = input.player_actions();
controls.update(&mut actions, physics.settings.step.simulation.time_step,
physics.settings.input_magnitude_threshold,
skater.player_input.physical.scoring.capabilities_204);
frame::advance(&mut physics, &mut skater, &mut controls, &graphs,
&mut actions, true, &mut camera)
.unwrap_or_else(|error| panic!("Runout {difficulty:?} tick{tick}: {error}"));
assert_finite(&physics, &skater, tick);
}
// Exercise the logged MotionGraph branch, separately from the still
// unsupported BipedGround physical owner selected by its output.
skater.player_state.state_flags[78 - 52] = true;
animation_phase::advance(&physics, &mut skater, &controls, &graphs,
&physics.animation_profile).unwrap();
}
}
+41
View File
@@ -0,0 +1,41 @@
# Runout crash investigation
The Hardcore 360-flip reports at ticks 583 and 378 terminate at MotionGraph
behavior 86, `AddRunoutAttribs`. This operation was unsupported before the
difficulty-menu changes. Entering the stock runout graph now succeeds in Easy,
Normal and Hardcore. A full-frame regression then exposed the next missing
owner: `PhysicsGround -> BipedGround` has no production adapter yet.
## Reference
TU3 dump `default_82000000_011B0000.bin`, guest base 0x82000000,
SHA256 f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4.
Registration 0x82F89700 selects factory 0x82BC9BA8; vtable 0x82320BDC has
Begin 0x82BBA4A8, Update 0x82BBA770 and empty End 0x82B61BB8.
The initializer pair 0x82F85870/0x82F85888 identifies BipedStartAngle/BipedSpeed.
Addresses are guest addresses; subtract 0x82000000 for dump offsets.
Observed: Begin reads Skeleton0 (effective animation root Z),
SystemReckoning16 (COM velocity), SystemReckoning96 (up), and mirrors through
ISkaterAnim slot28. OffBoard331 selects alternate velocity OffBoard128.
It uses 0x8286CD88 for the projected signed angle, wraps with the strict
fraction > 0.5 condition, converts radians to degrees using word 0x42652EE1,
and captures full three-dimensional speed. Update emits the retained values
with normalized=false and sequence=-1. End performs no writes.
The game currently publishes the ordinary velocity branch; existing riding/air
states leave OffBoard331 at its native reset value. The alternate branch is
implemented and tested but requires the future BipedAir output owner.
The host numerical routines follow recovered arithmetic; no hardware-matched
360-flip replay has been recorded. A test-injected runout request verifies the
stock animation branch in all three modes, not complete on-foot physics.
## Remaining integration
User explicitly selected implementing native on-foot/runout instead of an
error-recovery screen. BipedGround requires the contact toolkit (82D811C8 /
82D81610), ground job producer, controller placement/update, skeleton mode4,
board manager, state publications, and BipedAir/remount transitions. Existing
recovered modules are being compiled and connected; enabling modules alone is
not a working BipedGround implementation.