From 4ddc99959b0385d9613e50b60d977c8efa056687 Mon Sep 17 00:00:00 2001 From: chazmm Date: Mon, 7 Sep 2026 18:38:01 +0100 Subject: [PATCH] Connect live fifty-fifty grinds, stock animation and rail exits --- crates/skate-core/src/physics/grind_forces.rs | 78 ++++- .../src/player/input_phase/types.rs | 4 + crates/skate-game/src/graph_host/mod.rs | 1 + crates/skate-game/src/graph_host/motion.rs | 6 + .../src/graph_host/motion_animation.rs | 17 + .../src/graph_host/motion_conditions.rs | 4 + .../src/graph_host/motion_execute.rs | 6 + .../skate-game/src/graph_host/motion_grind.rs | 131 +++++++ .../skate-game/src/graph_host/motion_nodes.rs | 5 +- crates/skate-game/src/grind_world.rs | 6 +- crates/skate-game/src/physics.rs | 3 + .../skate-game/src/physics/animation_phase.rs | 28 +- crates/skate-game/src/physics/frame.rs | 2 + crates/skate-game/src/physics/grind.rs | 330 ++++++++++++++++++ crates/skate-game/src/physics/input_phase.rs | 12 + .../src/physics/offboard/air_trajectory.rs | 14 +- .../src/physics/player_input/grind.rs | 10 +- .../src/physics/player_input/reset.rs | 2 + crates/skate-game/src/physics/player_state.rs | 1 + .../src/physics/player_state/post_input.rs | 4 +- .../src/physics/player_state/pre_state.rs | 2 + .../src/physics/player_state/publication.rs | 15 +- .../src/physics/player_state/selection.rs | 5 +- .../src/physics/player_state/transition.rs | 12 +- .../skate-game/src/physics/riding_outputs.rs | 14 + docs/grind-integration.md | 22 ++ 26 files changed, 688 insertions(+), 46 deletions(-) create mode 100644 crates/skate-game/src/graph_host/motion_grind.rs create mode 100644 crates/skate-game/src/physics/grind.rs create mode 100644 docs/grind-integration.md diff --git a/crates/skate-core/src/physics/grind_forces.rs b/crates/skate-core/src/physics/grind_forces.rs index c93cb5ee..0d286926 100644 --- a/crates/skate-core/src/physics/grind_forces.rs +++ b/crates/skate-core/src/physics/grind_forces.rs @@ -16,7 +16,11 @@ pub fn lateral_pin( forward_selected: bool, slope_multiplier: f32, ) -> V { - let direction = if forward_selected { board[2] } else { scale(board[2], -1.0) }; + let direction = if forward_selected { + board[2] + } else { + scale(board[2], -1.0) + }; let reference = core::array::from_fn(|i| { direction[i].mul_add(forward_offset, board[3][i]) - board[1][i] * up_offset }); @@ -26,7 +30,10 @@ pub fn lateral_pin( return [0.0; 4]; } let axis = scale(force, length.recip()); - let damping = scale(axis, strength * f32::from_bits(0x3e08_3127) * dot3(velocity, axis)); + let damping = scale( + axis, + strength * f32::from_bits(0x3e08_3127) * dot3(velocity, axis), + ); scale(sub(force, damping), slope_multiplier) } @@ -47,13 +54,70 @@ pub fn friction( if speed <= 0.001 { return [0.0; 4]; } - let strength = strengths[match engagement { 0 => 0, 1 => 1, _ => 2 }]; + let strength = strengths[match engagement { + 0 => 0, + 1 => 1, + _ => 2, + }]; let load = support[1].max(0.0); let surface_flag_multiplier = if flagged_surface { 1.9 } else { 1.0 }; - let multiplier = load / speed * time_multiplier * surface_flag_multiplier - * surface_multiplier * strength * f32::from_bits(0xbef5_c28f); + let multiplier = load / speed + * time_multiplier + * surface_flag_multiplier + * surface_multiplier + * strength + * f32::from_bits(0xbef5_c28f); scale(tangent_velocity, multiplier) } -fn sub(a: V, b: V) -> V { core::array::from_fn(|i| a[i] - b[i]) } -fn scale(a: V, scale: f32) -> V { a.map(|v| v * scale) } +fn sub(a: V, b: V) -> V { + core::array::from_fn(|i| a[i] - b[i]) +} +fn scale(a: V, scale: f32) -> V { + a.map(|v| v * scale) +} + +///82D73AB0 straight, static 50-50 branch: both support normals coincide, +///primitive motion336 is zero. The manager age controls the native exit nudge. +pub fn fifty_fifty_pop( + velocity: V, + normal: V, + direction: V, + board_position: V, + point: V, + height: f32, + nudge: f32, + manager_age: f32, +) -> V { + let across = cross(direction, normal); + let balance = if manager_age < 0.31 { + if nudge < 0.0 { + nudge.min(-0.42) + } else if nudge > 0.0 { + nudge.max(0.42) + } else if dot3(sub(board_position, point), across) > 0.0 { + 0.42 + } else { + -0.42 + } + } else { + nudge + }; + let lateral = scale(across, balance * 1.8); + let length = dot3(lateral, lateral).sqrt(); + let lateral = if length > 1.8 { + scale(lateral, 1.8 / length) + } else { + lateral + }; + let planar = sub(velocity, scale(normal, dot3(velocity, normal))); + core::array::from_fn(|i| normal[i].mul_add(height * 1.03, planar[i]) + lateral[i]) +} +fn cross(a: V, b: V) -> V { + [ + (-a[2]).mul_add(b[1], a[1] * b[2]), + (-a[0]).mul_add(b[2], a[2] * b[0]), + (-a[1]).mul_add(b[0], a[0] * b[1]), + 0.0, + ] +} diff --git a/crates/skate-core/src/player/input_phase/types.rs b/crates/skate-core/src/player/input_phase/types.rs index 550582b2..f3e60e77 100644 --- a/crates/skate-core/src/player/input_phase/types.rs +++ b/crates/skate-core/src/player/input_phase/types.rs @@ -310,6 +310,8 @@ impl Default for PlayerInputState { #[derive(Clone, Copy, Debug, PartialEq)] pub struct ProcessedPhysicsInput { + pub grind_position_1120: RawVector, + pub grind_direction_1136: RawVector, pub effective_anim_transform_192: RawMatrix, pub vectors_400_416: [RawVector; 2], pub vectors_464_480_496_512_528: [RawVector; 5], @@ -395,6 +397,8 @@ pub struct ProcessedPhysicsInput { impl Default for ProcessedPhysicsInput { fn default() -> Self { Self { + grind_position_1120: [0;4], + grind_direction_1136: [0;4], effective_anim_transform_192: [[0; 4]; 4], vectors_400_416: [[0; 4]; 2], vectors_464_480_496_512_528: [[0; 4]; 5], diff --git a/crates/skate-game/src/graph_host/mod.rs b/crates/skate-game/src/graph_host/mod.rs index 435e2e84..158d8cbf 100644 --- a/crates/skate-game/src/graph_host/mod.rs +++ b/crates/skate-game/src/graph_host/mod.rs @@ -6,6 +6,7 @@ mod condition_nodes; pub(crate) use condition_nodes::numeric as parse_numeric_condition; mod crouching_settings; pub mod motion; +pub(crate) mod motion_grind; mod motion_air_leg; mod motion_animation; #[path = "motion_offboard/cadence.rs"] diff --git a/crates/skate-game/src/graph_host/motion.rs b/crates/skate-game/src/graph_host/motion.rs index cc1bf902..1265ab90 100644 --- a/crates/skate-game/src/graph_host/motion.rs +++ b/crates/skate-game/src/graph_host/motion.rs @@ -44,6 +44,7 @@ pub struct MotionPhysical { pub foot_frame: Option, } enum Instance { + Grind(super::motion_grind::State), OffboardAir(super::motion_offboard_air::State), ToggleBoard(super::motion_toggle_board::ToggleBoard), Cadence(super::motion_cadence::MatchCadence), @@ -74,6 +75,7 @@ enum Instance { impl Instance { fn new(operation: &MotionOperation) -> Self { match operation { + MotionOperation::Grind(_) => Self::Grind(Default::default()), MotionOperation::OffboardAir(_) => Self::OffboardAir(Default::default()), MotionOperation::Cadence(_) => Self::Cadence(Default::default()), MotionOperation::AddRunoutAttribs => Self::Runout(None), @@ -127,6 +129,8 @@ impl Instance { } } pub struct MotionHost { + pub grind_physical: super::motion_grind::Physical, + grind_settings: super::motion_grind::Settings, pub offboard_output: skate_core::player::input_phase::OffBoardOutputFields, pub toggle_board_physical: super::motion_toggle_board::Physical, pub disable_dismount: bool, @@ -229,6 +233,8 @@ impl MotionHost { }); let pushing = PushingSettings::load(data, &mut metadata)?; Ok(Self { + grind_physical: Default::default(), + grind_settings: super::motion_grind::Settings::load(data)?, toggle_board_physical: Default::default(), disable_dismount: false, offboard_output: Default::default(), diff --git a/crates/skate-game/src/graph_host/motion_animation.rs b/crates/skate-game/src/graph_host/motion_animation.rs index 657544eb..b2e3a3e1 100644 --- a/crates/skate-game/src/graph_host/motion_animation.rs +++ b/crates/skate-game/src/graph_host/motion_animation.rs @@ -104,6 +104,23 @@ impl MotionAnimation { ); Ok(()) } + ///82BB0A30 samples the actual selected blend tree at normalized endpoints. + ///Work on a clone so endpoint probing cannot leak into the current pose. + pub fn attribute_endpoints(&self, name: AttributeName) -> Result<[f32;2],String> { + let source=self.current.as_ref().ok_or("Grind has no selected animation")?; + let mut values=[0.;2]; + for i in 0..2 { + let mut tree=source.clone(); + let mut attributes=self.settable.entries().to_vec(); + attributes.push(SettableAttribute {name,value:i as f32,normalized:true,sequence_id:-1}); + tree.set_attributes(&attributes)?; + // During entry the destination tree owns bounds, not the fading-out pose. + let attribute=tree.attributes(15)?.into_iter().find(|a|a.name==name) + .ok_or("Grind animation is missing its twist attribute")?; + values[i]=f32::from_bits(attribute.payload.0[0].ok_or("Grind twist is uninitialized")?); + } + Ok(values) + } pub fn tree_attributes(&self) -> &[AnimationAttribute] { &self.tree_attributes } diff --git a/crates/skate-game/src/graph_host/motion_conditions.rs b/crates/skate-game/src/graph_host/motion_conditions.rs index 92d18fe6..b32136a0 100644 --- a/crates/skate-game/src/graph_host/motion_conditions.rs +++ b/crates/skate-game/src/graph_host/motion_conditions.rs @@ -72,6 +72,8 @@ pub enum MotionCondition { Riding(super::motion_riding_conditions::MotionRidingCondition), /// Native Air condition: PhysOutAnimation collision/trajectory time. TimeToLand(NumericCondition), + ///DistToEdge82BA8238: completed OffBoard116, not a fresh nearest-rail query. + DistToEdge(NumericCondition), /// Native off-board trajectory time (PhysOutOffBoard+32). ObTimeToLand(NumericCondition), /// Native off-board trajectory time used by dismount/runout branches. @@ -177,6 +179,7 @@ impl MotionCondition { name if super::motion_riding_conditions::MotionRidingCondition::recognizes(name) => { Self::Riding(super::motion_riding_conditions::MotionRidingCondition::parse(a)?) } + "DistToEdge" => Self::DistToEdge(numeric()), "TimeToLand" => Self::TimeToLand(numeric()), "OBTimeToLand" => Self::ObTimeToLand(numeric()), "OBTrajTime" => Self::ObTrajTime(numeric()), @@ -243,6 +246,7 @@ impl MotionCondition { Self::ManualOutTimerIsActive => host.riding.manual_out_timer > 0.0, Self::Gameplay(condition) => condition.evaluate(host)?, Self::Riding(condition) => condition.evaluate(host)?, + Self::DistToEdge(n) => n.matches(host.offboard_output.distance_116), Self::TimeToLand(n) => n.matches( host.gameplay_conditions .as_ref() diff --git a/crates/skate-game/src/graph_host/motion_execute.rs b/crates/skate-game/src/graph_host/motion_execute.rs index c21ed5da..369dd11f 100644 --- a/crates/skate-game/src/graph_host/motion_execute.rs +++ b/crates/skate-game/src/graph_host/motion_execute.rs @@ -15,6 +15,12 @@ impl MotionHost { .get(behavior) .ok_or("Unbound MotionGraph behavior")?; let operation = self.operations[id].clone(); + if let MotionOperation::Grind(operation) = operation { + let Instance::Grind(state)=&mut self.instances[behavior] else {return Err("Grind instance mismatch".into())}; + let height=self.crouching_physical.map(|p|p.animation_height_72).ok_or("Grind requires physical height")?; + return super::super::motion_grind::execute(state,&operation,phase,frame.dt, + &self.grind_settings,&self.grind_physical,height,&mut self.animation); + } if let MotionOperation::OffboardAir(operation) = operation { let Instance::OffboardAir(state) = &mut self.instances[behavior] else { return Err("OffboardAir instance mismatch".into()); diff --git a/crates/skate-game/src/graph_host/motion_grind.rs b/crates/skate-game/src/graph_host/motion_grind.rs new file mode 100644 index 00000000..65d7d091 --- /dev/null +++ b/crates/skate-game/src/graph_host/motion_grind.rs @@ -0,0 +1,131 @@ +//! Stock grind graph behaviors82BAF208/82BB0A30/82BB0D40/82BB10F8. +use skate_core::animation::{ + grind_control::{Fade, FadeSettings, crouch}, + playback_parameters::{AttributeSink, SettableAttribute}, + skeleton_input::name::encode, +}; +use skate_data::{collections::Collections, state_graph::attributes::Attributes}; +#[derive(Clone, Debug, PartialEq)] +pub enum Operation { + Attributes, + Crouch, + Fade { + height: String, + twist: String, + intent: String, + }, +} +impl Operation { + pub fn parse(a: &Attributes<'_>) -> Option { + Some(match a.text("name")? { + "CreateGrindAttributes" => Self::Attributes, + "ControlGrindCrouch" => Self::Crouch, + "GrindControlFade" => Self::Fade { + height: a + .text("distBoardToCogAnimAttribute") + .unwrap_or("DistToCog") + .into(), + twist: a.text("twistAnimAttribute").unwrap_or("twist").into(), + intent: a.text("twistMGIntent").unwrap_or("GrindBalanceX").into(), + }, + _ => return None, + }) + } +} +pub struct Settings { + pub fade: FadeSettings, + pub height: [f32; 3], +} +impl Settings { + pub fn load(d: &Collections) -> Result { + let f = |n| d.float("anim_motion", "grind_twist", n); + Ok(Self { + fade: FadeSettings { + response: f("twist_smoothing")?, + input_scale: f("twist_sensitivity")?, + acceleration: f("twist_max_delta_delta")?, + maximum_step: f("twist_max_delta")?, + }, + height: [ + d.float("anim_motion", "grind_height", "min_grind_disttocog")?, + d.float("anim_motion", "grind_height", "max_grind_disttocog")?, + d.float("anim_motion", "grind_height", "grind_disttocog_speed")?, + ], + }) + } +} +#[derive(Default)] +pub struct State { + pub fade: Fade, + pub height: f32, +} +#[derive(Clone, Debug, Default)] +pub struct Physical { + pub name: String, + pub grinding: bool, + pub crouch: f32, + pub twist: f32, + pub facing_backwards: bool, +} +pub fn execute( + state: &mut State, + op: &Operation, + phase: u8, + dt: f32, + settings: &Settings, + physical: &Physical, + height: f32, + animation: &mut super::motion_animation::MotionAnimation, +) -> Result<(), String> { + let set = |animation: &mut super::motion_animation::MotionAnimation, name: &str, value: f32| { + animation.set_attribute(SettableAttribute { + name: encode(name.as_bytes()), + value, + normalized: false, + sequence_id: -1, + }); + }; + match op { + Operation::Attributes if phase == 1 && physical.grinding => { + animation.motion_intents.insert(&physical.name, 1.0); + animation.motion_intents.insert( + if physical.facing_backwards { + "GrindFacingBackwards" + } else { + "GrindFacingForwards" + }, + 1.0, + ); + } + Operation::Crouch if phase == 0 => state.height = height, + Operation::Crouch if phase == 1 => { + let intent = animation + .motion_intents + .get("Crouch") + .copied() + .unwrap_or(0.0); + let [min, max, rate] = settings.height; + state.height = crouch(state.height, intent, physical.crouch, min, max, rate, dt); + set(animation, "DistToCog", state.height); + } + Operation::Fade { + height: name, + twist, + intent, + } => { + if phase == 0 { + set(animation, name, height); + let bounds = animation.attribute_endpoints(encode(twist.as_bytes()))?; + let value = state.fade.begin(bounds[0], bounds[1], physical.twist); + set(animation, twist, value); + } else if phase == 1 { + let input = animation.motion_intents.get(intent).copied().unwrap_or(0.0); + if let Some(value) = state.fade.update(dt, input, settings.fade) { + set(animation, twist, value); + } + } + } + _ => {} + } + Ok(()) +} diff --git a/crates/skate-game/src/graph_host/motion_nodes.rs b/crates/skate-game/src/graph_host/motion_nodes.rs index e085cccd..6688ff32 100644 --- a/crates/skate-game/src/graph_host/motion_nodes.rs +++ b/crates/skate-game/src/graph_host/motion_nodes.rs @@ -14,6 +14,7 @@ use skate_data::state_graph::{ #[derive(Clone, Debug, PartialEq)] pub enum MotionOperation { + Grind(super::motion_grind::Operation), OffboardAir(super::motion_offboard_air::Operation), ToggleBoard, Cadence(super::motion_cadence::Operation), @@ -81,7 +82,9 @@ impl OperationFactory for MotionFactory { let operation = if kind == OperationKind::Condition { MotionCondition::parse(a)?.map(MotionOperation::Condition) } else if kind == OperationKind::Behavior { - if let Some(operation) = super::motion_offboard_air::Operation::parse(a) { + if let Some(operation) = super::motion_grind::Operation::parse(a) { + Some(MotionOperation::Grind(operation)) + } else if let Some(operation) = super::motion_offboard_air::Operation::parse(a) { Some(MotionOperation::OffboardAir(operation)) } else if name == "ToggleBoard" { Some(MotionOperation::ToggleBoard) diff --git a/crates/skate-game/src/grind_world.rs b/crates/skate-game/src/grind_world.rs index 3f14c89b..83d14607 100644 --- a/crates/skate-game/src/grind_world.rs +++ b/crates/skate-game/src/grind_world.rs @@ -3,8 +3,8 @@ use skate_core::math::Vector3; use bevy::prelude::*; -/// Retained geometry for the forthcoming native contact/scorer adapter. This -/// deliberately does not publish selector candidate1488 or enter state400. +/// Shared authored geometry. The physical grind owner runs native truck queries +/// against these same spline endpoints before state selection. #[derive(Resource)] pub(crate) struct GrindGeometry { pub rails: Vec, @@ -21,7 +21,7 @@ impl Plugin for GrindGeometryPlugin { GrindGeometry { rails: Vec::new(), native_blob: Vec::new() } }; if test_world { - info!("GRIND_GEOMETRY rails={} bytes={} native_acquisition=not_connected", + info!("GRIND_GEOMETRY rails={} bytes={} native_acquisition=paired_trucks_50_50", geometry.rails.len(), geometry.native_blob.len()); for rail in &geometry.rails { info!("GRIND_RAIL id={} name={} start={:?} end={:?}", diff --git a/crates/skate-game/src/physics.rs b/crates/skate-game/src/physics.rs index 2f3ddbf7..c4a40fde 100644 --- a/crates/skate-game/src/physics.rs +++ b/crates/skate-game/src/physics.rs @@ -31,6 +31,7 @@ mod animation_input; mod animation_phase; mod landing_quality; mod frame; +mod grind; mod ground_phase; mod ground_animation; mod slide_state; @@ -70,6 +71,7 @@ pub(crate) struct GamePhysics { clock: clock::SimulationClock, pub board: BoardRuntime, pub riding: RidingOutputs, + grind: grind::Runtime, world: BoardWorld, settings: PhysicsSettings, animation_profile: animation_phase::AnimationProfile, @@ -144,6 +146,7 @@ impl GamePhysics { let (query, retention) = ground::query_settings(); Ok(Self { clock: clock::SimulationClock::default(), + grind: grind::Runtime::load(&data, map.is_none())?, board, riding, world, diff --git a/crates/skate-game/src/physics/animation_phase.rs b/crates/skate-game/src/physics/animation_phase.rs index 11aa63d6..cbf34ff5 100644 --- a/crates/skate-game/src/physics/animation_phase.rs +++ b/crates/skate-game/src/physics/animation_phase.rs @@ -181,17 +181,31 @@ pub(crate) fn advance( board_on_ground: physical.off_board.flag_311 != 0, animation_height: feedback.crouching.animation_height_72, }); - // FilteredState82DE5BA0 clears grind metadata on every nongrind output. - // A future grind phase must supply its actual cached name, not an AG intent. - if physical.filtered_state_0 == 3 { - return Err("Animation phase requires the selected grind's filtered output name".into()); - } + let grinding=skater.player_state.filtered_output.as_ref().is_some_and(|f|f.grinding); + let grind_name=if grinding {physics.grind.name.clone()} else {String::new()}; + //FillSkeleton82BE1AE8 uses the vector between physical parts10/6, + //Processed352 and Reckoning1152, then wraps the signed angle to +/-pi. + let a=skater.skeleton.record.pose[10][3]; + let b=skater.skeleton.record.pose[6][3]; + let lateral=Vector3::new(a[0]-b[0],a[1]-b[1],a[2]-b[2]); + let forward=skater.player_input.toolkit.as_ref().map(|t| t.forward) + .unwrap_or([0.,0.,1.,0.]); + let mut twist=skate_core::riding::collision_response::signed_angle( + Vector3::new(forward[0],forward[1],forward[2]),lateral,physics.riding.reckoning.up); + let turns=twist* f32::from_bits(0x3e22f983); + let fraction=turns-turns.floor(); + twist=(fraction-if fraction>0.5 {1.0} else {0.0})*f32::from_bits(0x40c90fdb); + if mirrored {twist=if twist>=0.0 {std::f32::consts::PI-twist} else {-std::f32::consts::PI-twist};} + skater.animation.motion.grind_physical=crate::graph_host::motion_grind::Physical { + name:grind_name.clone(),grinding,crouch:physics.grind.output().crouch,twist, + facing_backwards: physics.grind.name.starts_with("BF_"), + }; let conditions = ConditionInputs { speeds: Some(physics.riding.graph_speeds()), physical_state: Some(PhysicalStateInputs { category: physical.filtered_state_0, - grinding: false, - grind_name: String::new(), + grinding, + grind_name, }), time_since_last_input: Some(p.time_since_last_input_2748), mirrored: Some(mirrored), diff --git a/crates/skate-game/src/physics/frame.rs b/crates/skate-game/src/physics/frame.rs index deb27f81..c3d09775 100644 --- a/crates/skate-game/src/physics/frame.rs +++ b/crates/skate-game/src/physics/frame.rs @@ -85,6 +85,8 @@ pub(super) fn advance( player_state::post_input_and_select(physics, skater)?; player_state::pre_state(physics, skater)?; match skater.player_state.current() { + skate_core::player::state::PhysicalStateId::GrindFiftyFifty + | skate_core::player::state::PhysicalStateId::Nonspecific => super::grind::update(physics, skater)?, skate_core::player::state::PhysicalStateId::BipedAir => { super::offboard::air_state::update(physics, skater)?; } diff --git a/crates/skate-game/src/physics/grind.rs b/crates/skate-game/src/physics/grind.rs new file mode 100644 index 00000000..1d6b8c94 --- /dev/null +++ b/crates/skate-game/src/physics/grind.rs @@ -0,0 +1,330 @@ +//! Live straight-spline grind owner. TU3 truck query82C1FDC0, paired scorer +//!82D89150, 50-50 update82D41D70 and exit82D3F430. +use super::{GamePhysics, SkaterRuntime}; +use skate_core::riding::ground_correction_math::dot_product as dot3; +use skate_core::{ + animation::skeleton_input::name::encode, + math::{Basis3, Vector3}, + physics::{ + drive_frames::RetailAffineTransform, + filtered_state::GrindState, + force_queue::QueuedPointForce, + grind_contact::{self, FiftyFiftyCandidate, Primitive}, + grind_forces, + }, + player::state::PhysicalStateId, + point_graph::PointGraph, +}; +use skate_data::collections::Collections; +type V = [f32; 4]; +pub(crate) struct Runtime { + primitives: Vec, + pub candidate: Option, + pub name: String, + pub active: bool, + truck_to_wheel: f32, + deck_to_truck: f32, + pin_vs_slope: PointGraph<4>, + pub distance: f32, + pop_heights: [[f32; 2]; 5], + manager_age: f32, + pub launched: bool, + pub launch_velocity: V, + cooldown: u32, + previous_position: V, +} +impl Runtime { + pub fn load(data: &Collections, test_world: bool) -> Result { + let graph = data + .words::<8>("physics_grinds", "default", "PinVsSlope")? + .map(f32::from_bits); + Ok(Self { + primitives: if test_world { + crate::grind_world::rails() + .iter() + .map(|r| Primitive { + start: lanes(r.start), + end: lanes(r.end), + owner: r.id, + }) + .collect() + } else { + Vec::new() + }, + candidate: None, + name: String::new(), + active: false, + truck_to_wheel: data.float("physics_grinds", "default", "TruckToWheel")?, + deck_to_truck: data.float("physics_grinds", "default", "DeckCenterToTruck")?, + pin_vs_slope: PointGraph { + x: graph[..4].try_into().unwrap(), + y: graph[4..8].try_into().unwrap(), + }, + pop_heights: ["easy", "normal", "hardcore", "motorized", "test"] + .map(|key| { + Ok::<_, String>([ + data.float("physics_mode", key, "Hash_3097A69281990652")?, + data.float("physics_mode", key, "Hash_FA4CDBAE0DFD1FAD")?, + ]) + }) + .into_iter() + .collect::, _>>()? + .try_into() + .unwrap(), + manager_age: 0.0, + launched: false, + launch_velocity: [0.; 4], + cooldown: 0, + distance: 0.0, + previous_position: [0.; 4], + }) + } + pub fn output(&self) -> GrindState { + if !self.active { + return GrindState::default(); + } + let owner = self + .candidate + .map(|c| self.primitives[c.primitive].owner) + .unwrap_or(u64::MAX); + GrindState { + kind: 0, + name: encode(self.name.as_bytes()), + scoring_name: encode(self.name.as_bytes()), + pathed_guid: owner, + local_guid: owner, + ..GrindState::default() + } + } +} +///Run after ProcessInput, before the native state selector. Geometry is queried +///against both current trucks; merely being near a rail cannot acquire it. +pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) { + let p = &mut skater.player_input.processed; + let runtime = &mut physics.grind; + let board = super::solve::deck_frame(&physics.board); + runtime.cooldown = runtime.cooldown.saturating_sub(1); + runtime.manager_age = (runtime.manager_age + 0.0035).min(1.0); + let old = runtime.candidate; + runtime.candidate = None; + if runtime.cooldown > 0 || skater.player_input.grind.disabled || p.flags_2476 & 0x01000000 != 0 + { + return; + } + let hits = grind_contact::truck_contacts( + board, + p.flags_2484, + runtime.truck_to_wheel, + runtime.deck_to_truck, + &runtime.primitives, + ); + let extra = &skater.animation_input.extra; + let Some(candidate) = grind_contact::fifty_fifty_candidate( + board, + [extra.grind_translation, extra.grind_stability_nudge], + hits, + ) else { + return; + }; + let velocity = p.vectors_400_416[0].map(f32::from_bits); + //82D886B8: air accepts an actual intersection; same active grind uses90deg. + //The ordinary level-ground admission uses17deg. + let angle = if p.category_2512 == 400 || p.state_2508 == 701 { + 90.0 + } else { + 17.0 + }; + if p.category_2512 != 200 + && !grind_contact::within_approach_angle(candidate.direction, velocity, angle) + { + return; + } + runtime.candidate = Some(candidate); + p.grind_position_1120 = candidate.centre.map(f32::to_bits); + p.grind_direction_1136 = candidate.direction.map(f32::to_bits); + if !runtime.active || old.is_none() { + let backwards = dot3(board[2], velocity) < 0.0; + let side = dot3(sub(candidate.centre, board[3]), board[0]) >= 0.0; + runtime.name = format!( + "{}{}_50_50", + if backwards { "BF_" } else { "" }, + if side { "FS" } else { "BS" } + ); + } +} +pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + physics.grind.active = skater.player_state.current() == PhysicalStateId::GrindFiftyFifty; + physics.grind.distance = 0.0; + physics.grind.previous_position = super::solve::deck_frame(&physics.board)[3]; + skater.ground_lifecycle.skeleton_elapsed_16505 = true; + physics + .board + .hook_mut() + .drive + .disable_animation(&mut skater.ground_lifecycle.board_animated_290); + skater.air_reckoning.state.reset_spin(); + if physics.grind.active { + bevy::log::info!( + "Grind entered: {} rail={:?}", + physics.grind.name, + physics + .grind + .candidate + .map(|c| physics.grind.primitives[c.primitive].owner) + ); + } + Ok(()) +} +pub(super) fn exit(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + physics.grind.active = false; + physics + .board + .hook_mut() + .drive + .disable_animation(&mut skater.ground_lifecycle.board_animated_290); + //82D3F7D0 restores all four wheel drags to zero. + for body in &mut physics.board.bodies_mut()[..4] { + body.inertia.linear_drag = 0.0; + } + Ok(()) +} +pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + physics.grind.launched = false; + let board = super::solve::deck_frame(&physics.board); + let p = &skater.player_input.processed; + if physics.grind.active && p.flags_2468 & 0x00400000 != 0 { + let c = physics + .grind + .candidate + .ok_or("Grind pop has no primitive")?; + let [min, max] = physics.grind.pop_heights[p.state_variant_index_2528 as usize]; + let strength = skater.animation_input.extra.jump_strength; + let velocity = grind_forces::fifty_fifty_pop( + p.vectors_400_416[0].map(f32::from_bits), + grind_contact::upright_normal(c.direction), + c.direction, + board[3], + c.centre, + (1.0 - strength).mul_add(min, max * strength), + skater.animation_input.extra.grind_stability_nudge, + physics.grind.manager_age, + ); + physics.grind.launched = true; + physics.grind.launch_velocity = velocity; + physics.grind.cooldown = 10; + physics.grind.manager_age = (physics.grind.manager_age - 0.2).max(0.0); + skater + .ground_runtime + .set_animated_velocity(&mut physics.board, velocity); + physics + .board + .hook_mut() + .drive + .disable_animation(&mut skater.ground_lifecycle.board_animated_290); + return super::input_phase::update_grind_jump(physics, skater); + } + let velocity = p.vectors_400_416[0].map(f32::from_bits); + //Both state vtables neutralize the trucks before the shared board solve. + skater.ground.steering.targets = [0.; 2]; + let normal = if physics.grind.active { + let c = physics + .grind + .candidate + .ok_or("Active grind lost its candidate before selection")?; + let normal = grind_contact::upright_normal(c.direction); + let across = cross(c.direction, normal); + let force = grind_forces::lateral_pin( + board, + c.centre, + across, + velocity, + 800., + 0., + 0.07, + true, + physics.grind.pin_vs_slope.evaluate(dot3(normal, normal)), + ); + append(physics, force)?; + //The primitive support frame is static and level for the authored rails. + //82D86318 publishes this unit support normal in candidate304. + let friction = grind_forces::friction( + velocity, + normal, + normal, + skater.player_input.grind.friction_vs_time, + false, + 1.3, + 0, + [50., 40., 40.], + ); + append(physics, friction)?; + let forward = if dot3(board[2], c.direction) > 0.0 { + c.direction + } else { + c.direction.map(|x| -x) + }; + let target = [cross(normal, forward), normal, forward, board[3]]; + let (frame, _) = skate_core::animation::foot_ik::interpolate_native(&board, &target, 0.1); + physics.board.set_hook_transform(RetailAffineTransform { + basis: Basis3 { + columns: std::array::from_fn(|i| [frame[i][0], frame[i][1], frame[i][2]]), + }, + translation: xyz(frame[3]), + }); + physics + .board + .hook_mut() + .drive + .enable_angular_only(&mut skater.ground_lifecycle.board_animated_290); + physics.grind.distance += + dot3(sub(board[3], physics.grind.previous_position), c.direction).abs(); + physics.grind.previous_position = board[3]; + normal + } else { + //Nonspecific82D42F50 applies the native -40 support force while the + //selector waits for actual wheel/collision observations after rail exit. + append( + physics, + p.vectors_544_560_592_608[0] + .map(f32::from_bits) + .map(|v| v * -40.0), + )?; + board[1] + }; + physics.riding.update_grind_reckoning( + &mut skater.air_reckoning.state, + normal, + board[2], + skater.player_input.processed.flags_2468, + if physics.grind.active { 0.9 } else { 0.98 }, + ); + //82D40D40 and the ordinary nonspecific branch both call Skeleton82BDF530. + super::input_phase::update_ground(physics, skater) +} +fn append(physics: &mut GamePhysics, force: V) -> Result<(), String> { + if !physics.board.forces_mut().append(QueuedPointForce { + tag: 0, + force_world: xyz(force), + point_body: Vector3::ZERO, + }) { + return Err("Grind force queue exhausted".into()); + } + Ok(()) +} +fn lanes(v: Vector3) -> V { + [v.x, v.y, v.z, 0.] +} +fn xyz(v: V) -> Vector3 { + Vector3::new(v[0], v[1], v[2]) +} +fn sub(a: V, b: V) -> V { + std::array::from_fn(|i| a[i] - b[i]) +} +fn cross(a: V, b: V) -> V { + [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + 0., + ] +} diff --git a/crates/skate-game/src/physics/input_phase.rs b/crates/skate-game/src/physics/input_phase.rs index ced3c7da..ac7788ee 100644 --- a/crates/skate-game/src/physics/input_phase.rs +++ b/crates/skate-game/src/physics/input_phase.rs @@ -213,3 +213,15 @@ impl PlayerInputCallbacks for Callbacks<'_, '_> { self.reset_player(board, runtime, target, player, physical, processed) } } + +///Grind pop82D40D30 calls the animated skeleton update with fast blend=false. +pub(super) fn update_grind_jump(physics:&mut GamePhysics,skater:&mut SkaterRuntime)->Result<(),String> { + let collision=collision(skater); + let mut owners=SkeletonOwners {animated:&mut skater.animated_skeleton,body:&mut skater.skeleton, + drives:&mut skater.skeleton_drives,ik:&mut skater.foot_ik,animation_input:&mut skater.animation_input, + correction:&mut skater.skeleton_output.correction,pose_errors:&mut skater.pose_errors}; + skater.skeleton_input.update_animated(&mut skater.skeleton_air,&mut physics.board, + &physics.riding.reckoning_frames.system,&mut skater.player_input.processed,&mut owners, + &skater.animation.packet.hierarchy,&collision,physics.settings.step.simulation,false)?; + Ok(()) +} diff --git a/crates/skate-game/src/physics/offboard/air_trajectory.rs b/crates/skate-game/src/physics/offboard/air_trajectory.rs index 1e185ec5..64c246c4 100644 --- a/crates/skate-game/src/physics/offboard/air_trajectory.rs +++ b/crates/skate-game/src/physics/offboard/air_trajectory.rs @@ -6,14 +6,6 @@ use skate_core::player::offboard::{ }; pub(crate) fn prepare(skater: &SkaterRuntime, current: bool) -> Result { let p = &skater.player_input.processed; - if (if current { - p.category_2512 - } else { - p.category_2516 - }) == 400 - { - return Err("Biped launch from a grind requires the grind primitive owner".into()); - } let toolkit = skater .player_input .toolkit @@ -34,9 +26,9 @@ pub(crate) fn prepare(skater: &SkaterRuntime, current: bool) -> Result Result<(), String> { + super::grind::query(physics, skater); post_input::advance(physics, skater)?; selection::advance(physics, skater) } diff --git a/crates/skate-game/src/physics/player_state/post_input.rs b/crates/skate-game/src/physics/player_state/post_input.rs index 66b5c89e..4d1ffa8b 100644 --- a/crates/skate-game/src/physics/player_state/post_input.rs +++ b/crates/skate-game/src/physics/player_state/post_input.rs @@ -112,7 +112,7 @@ struct Services<'a> { } impl PostInputServices for Services<'_> { fn update_grind_manager_82d8ab08(&mut self) { - self.grind.advance(self.processed); + if self.processed.category_2512 != 400 { self.grind.advance(self.processed); } } fn update_trajectory_selector_82d68800(&mut self) -> u8 { //82DB56B0 runs the real selector after GrindManager. That manager @@ -142,7 +142,7 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> let trajectory_input = super::super::air_phase::selector_input(physics, skater)?; let post = &mut skater.player_state.post; let p = &mut skater.player_input.processed; - if skater.ground_lifecycle.edge.is_some() || p.category_2512 == 400 || p.state_2508 == 701 { + if skater.ground_lifecycle.edge.is_some() { return Err("PostInput active grind requires its actual candidate/scorer output".into()); } let requests = &skater.ground_lifecycle.trajectory; diff --git a/crates/skate-game/src/physics/player_state/pre_state.rs b/crates/skate-game/src/physics/player_state/pre_state.rs index 2108952d..9074be0a 100644 --- a/crates/skate-game/src/physics/player_state/pre_state.rs +++ b/crates/skate-game/src/physics/player_state/pre_state.rs @@ -12,6 +12,8 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> skater.player_state.current(), PhysicalStateId::PhysicsGround | PhysicalStateId::PhysicsAir + | PhysicalStateId::GrindFiftyFifty + | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround diff --git a/crates/skate-game/src/physics/player_state/publication.rs b/crates/skate-game/src/physics/player_state/publication.rs index 28ba2ed9..9f15a73f 100644 --- a/crates/skate-game/src/physics/player_state/publication.rs +++ b/crates/skate-game/src/physics/player_state/publication.rs @@ -3,7 +3,7 @@ use super::*; use skate_core::{ physics::{ contact_feedback::choose_surface, - filtered_state::{FilteredStateInput, GrindState}, + filtered_state::{FilteredStateInput}, }, player::input_phase::CurrentStateFields, }; @@ -13,6 +13,8 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> state, PhysicalStateId::PhysicsGround | PhysicalStateId::PhysicsAir + | PhysicalStateId::GrindFiftyFifty + | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround @@ -85,6 +87,13 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> set(84, skater.slide_state.state.wall_riding); } let physical = &mut skater.player_input.physical; + if physics.grind.active { + physical.grinds.words_136_140 = [0, 1]; + if physics.grind.launched { + physical.air.launched_442=1; + physical.air.launch_velocity_128=physics.grind.launch_velocity.map(f32::to_bits); + } + } //Common Biped Fill82DB7218 precedes the selected physical state's Fill. physical.off_board.vector_64 = skater .offboard @@ -170,8 +179,8 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> offboard_has_landed: state == PhysicalStateId::BipedAir && skater.offboard.air_state.remaining <= f32::from_bits(0x3c888889), offboard_on_deck: physical.off_board.flag_315 != 0, - grind: GrindState::default(), - last_grind_distance: 0.0, + grind: physics.grind.output(), + last_grind_distance: physics.grind.distance, }); physical.filtered_state_0 = filtered.category as u32; skater.player_state.filtered_output = Some(filtered); diff --git a/crates/skate-game/src/physics/player_state/selection.rs b/crates/skate-game/src/physics/player_state/selection.rs index 2ce31ac2..66bc14c9 100644 --- a/crates/skate-game/src/physics/player_state/selection.rs +++ b/crates/skate-game/src/physics/player_state/selection.rs @@ -8,10 +8,9 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> let p = &skater.player_input.processed; let input = StateSelectionInput { processed: ProcessedStateInput { - //The authored world has no grind edges. GrindInput's real empty - //edge path clears candidate384; zero flags are its reset record. + //The live paired-truck scorer publishes native kind0 and candidate384. grind_type_1248: 0, - grind_candidate_1488: false, + grind_candidate_1488: physics.grind.candidate.is_some(), grind_investigation_flags_1516: 0, field_1776: p.external_physics_1616.flags as i32, flags_2468: p.flags_2468, diff --git a/crates/skate-game/src/physics/player_state/transition.rs b/crates/skate-game/src/physics/player_state/transition.rs index f48c15e4..e17dab88 100644 --- a/crates/skate-game/src/physics/player_state/transition.rs +++ b/crates/skate-game/src/physics/player_state/transition.rs @@ -15,6 +15,8 @@ impl PhysicalStateCalls for Calls { PhysicalStateId::Sleeping | PhysicalStateId::PhysicsGround | PhysicalStateId::PhysicsAir + | PhysicalStateId::GrindFiftyFifty + | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround @@ -29,6 +31,8 @@ impl PhysicalStateCalls for Calls { call.state.state, PhysicalStateId::PhysicsGround | PhysicalStateId::PhysicsAir + | PhysicalStateId::GrindFiftyFifty + | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround @@ -55,7 +59,9 @@ pub(super) fn set( PhysicalStateId::Sleeping => requested == PhysicalStateId::PhysicsGround, PhysicalStateId::PhysicsGround | PhysicalStateId::PhysicsAir - | PhysicalStateId::KnownAir + | PhysicalStateId::GrindFiftyFifty + | PhysicalStateId::Nonspecific + | PhysicalStateId::KnownAir | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround | PhysicalStateId::WipeoutGround @@ -65,6 +71,8 @@ pub(super) fn set( requested, PhysicalStateId::PhysicsGround | PhysicalStateId::PhysicsAir + | PhysicalStateId::GrindFiftyFifty + | PhysicalStateId::Nonspecific | PhysicalStateId::KnownAir | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround @@ -149,6 +157,7 @@ pub(super) fn set( //Native82DB8540 publishes Processed state/history BEFORE old Exit. The //same retained objects then receive Exit followed by the new Enter. match current { + PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::exit(physics, skater)?, PhysicalStateId::BipedAir => super::super::offboard::air_state::exit(physics, skater), PhysicalStateId::BipedGround => super::super::offboard::ground_state::exit(physics, skater), PhysicalStateId::PhysicsGround => super::super::ground_exit::exit(physics, skater), @@ -163,6 +172,7 @@ pub(super) fn set( _ => unreachable!("state support checked before publication"), } match requested { + PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater), PhysicalStateId::BipedAir => super::super::offboard::air_state::enter(physics, skater), PhysicalStateId::BipedGround => { super::super::offboard::ground_state::enter(physics, skater) diff --git a/crates/skate-game/src/physics/riding_outputs.rs b/crates/skate-game/src/physics/riding_outputs.rs index afb6b731..03debe27 100644 --- a/crates/skate-game/src/physics/riding_outputs.rs +++ b/crates/skate-game/src/physics/riding_outputs.rs @@ -76,6 +76,20 @@ impl RidingOutputs { }, ); } + ///Shared Reckoning82D8E3E0, argument7=false for grind/nonspecific. + pub fn update_grind_reckoning(&mut self, air: &mut skate_core::air::reckoning::AirState, + normal: [f32;4], forward: [f32;4], flags: u32, blend: f32) { + let previous_up=lanes(self.reckoning.up); + skate_core::player::offboard::ground_reckoning::update( + &mut self.reckoning,&mut self.reckoning_frames,&mut self.body_spin,air, + skate_core::player::offboard::ground_reckoning::Settings { + ground_normal_smoothing:self.orientation_settings.ground_normal_smoothing, + tilt_vs_rotation:&self.tilt_vs_rotation,tilt_vs_slope:&self.tilt_vs_slope, + },skate_core::player::offboard::ground_reckoning::Input { + previous_up,requested_up:normal,requested_forward:forward,blend, + reverse_stance:flags&0x100000!=0,enable_body_spin_input:false,physical_body_spin_2812:0., + }); + } ///Board82C0D680 resets CollisionInfo and both probes, preserving7692. pub fn reset_for_teleport(&mut self) { let elapsed = self.ground.time_without_wheel_contact; diff --git a/docs/grind-integration.md b/docs/grind-integration.md new file mode 100644 index 00000000..822f81f8 --- /dev/null +++ b/docs/grind-integration.md @@ -0,0 +1,22 @@ +# Live grind integration + +The default world now supplies seven straight grind paths shared with rail/coping collision geometry. This pass connects the paired-truck 50-50 acquisition path to the production state selector, board forces, skeleton update, stock grind animations and physical output. Imported map grind descriptors are not wired into this owner yet. + +## Source and implementation + +TU3 image: default_82000000_011B0000.bin, SHA256 f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4. Analysis used the local generated PPC and the copied IDA database; the reference game was not launched for this implementation. + +- 82C1FDC0 and 82D89150: actual truck probe rectangles, paired same-primitive contacts, depth and balance rejection. No nearest-distance snap or positional teleport. +- 82D886B8: air and ordinary level-ground admission. The host currently covers straight level rails; sloped approach classification, adjacency arbitration, tipslides, boardslides and darkslides still need their own live adapters. +- 82D41D70 / 82D3FA18 / 82D3FD88: 50-50 lateral pin and friction, using the stock PinVsSlope and FrictionVsTime curves. Authored static rail support uses the upward normal and default surface multiplier. +- 82D41E38 / 82D40890: rail-aligned target blended through the existing native matrix interpolation and angular-only board drive. User-driven grind twist forces and the remaining grind-manager transition scoring are not complete. +- 82D73AB0: straight static 50-50 pop branch, stock physics-mode launch speeds, lateral nudge and launch velocity publication. +- 82D3F430: disables the grind hook and restores wheel drag on exit. The original selector retains Nonspecific701 briefly on contact loss before choosing air/ground from actual observations; the host now supports that ordinary continuation. +- 82BAF208, 82BB0A30, 82BB0D40 and 82BB10F8: selected grind intent, stock twist endpoints and retained fade, and grind crouch. Physical twist uses the native parts10/6 direction relative to Processed352 and Reckoning up. Canonical 50-50 names select the existing B_GRIND5050/B_BF_GRIND5050 animation trees; full chromosome naming/scorable identifiers are not ported. +- 82BA8238: DistToEdge reads the existing OffBoard116 output. This resolves the mounting graph condition encountered in the launched build. + +## Visual check + +Build with Build.ps1; launch bin/skate-game.exe --assets assets (no map argument). The low straight rail is at x=-7, z=7..15, 0.45m above the lower floor; the higher rail is at x=4.5, z=8..16, 0.70m above it. Approach along the rail and land both trucks on its top. The halfpipe coping and three starting-platform edges also have authored paths. + +Build completed. No automated tests or controller-driven gameplay tests were run. Visual behavior, catch consistency, pop timing, naming and solver stability while grinding require the user's playtest; this is not a claim of complete Skate 3 grind parity.