mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Implement native runout animation attributes and reproduce the missing biped transition
This commit is contained in:
1 parent
c818285b4d
commit
bdba0c0a41
8 files changed
+163
No files matched your search
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
Reference in new issue
Block a user