Connect live fifty-fifty grinds, stock animation and rail exits

This commit is contained in:
chazmm committed 2026-09-07 18:38:01 +01:00
1 parent 88b51467e9
commit 4ddc99959b
26 files changed
+688 -46

No files matched your search

+71 -7
View File
@@ -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,
]
}
@@ -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],
+1
View File
@@ -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"]
@@ -44,6 +44,7 @@ pub struct MotionPhysical {
pub foot_frame: Option<PushFootFrame>,
}
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(),
@@ -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
}
@@ -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()
@@ -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());
@@ -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<Self> {
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<Self, String> {
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(())
}
@@ -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)
+3 -3
View File
@@ -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<Rail>,
@@ -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={:?}",
+3
View File
@@ -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,
@@ -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),
+2
View File
@@ -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)?;
}
+330
View File
@@ -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<Primitive>,
pub candidate: Option<FiftyFiftyCandidate>,
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<Self, String> {
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::<Result<Vec<_>, _>>()?
.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.,
]
}
@@ -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(())
}
@@ -6,14 +6,6 @@ use skate_core::player::offboard::{
};
pub(crate) fn prepare(skater: &SkaterRuntime, current: bool) -> Result<air_launch::Launch, String> {
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<air_launc
position_592: p.vectors_544_560_592_608[2].map(f32::from_bits),
target_112: toolkit.effective[3],
velocity_912: p.vectors_880_896_912_928_944[2].map(f32::from_bits),
//The current authored world has no grind primitives; category400 is rejected above.
grind_position_1120: [0.; 4],
grind_axis_1136: [0.; 4],
//Retained live grind primitive, published before native state entry.
grind_position_1120: p.grind_position_1120.map(f32::from_bits),
grind_axis_1136: p.grind_direction_1136.map(f32::from_bits),
stick: [
skater.animation_input.extra.biped_world_x,
skater.animation_input.extra.biped_world_z,
@@ -1,6 +1,5 @@
//! Original82D8A828/82D8ABD8/82D875A8 ordinary empty-edge path.
//! The authored host floor explicitly contains no grind edges. This is an
//! asset declaration, not a missing-loader fallback or an inferred no-hit.
//! Input timing/gates from82D8A828/82D8ABD8. The geometry query/scorer runs
//! in physics::grind after input completion, before the state selector.
use skate_core::{player::input_phase::ProcessedPhysicsInput, point_graph::PointGraph};
use skate_data::collections::Collections;
#[derive(Clone, Copy, Debug)]
@@ -88,11 +87,6 @@ impl GrindInputState {
} else {
0.
};
//Empty real edge query selects standard board collision in82D89DC0
//for ordinary ground. An active grind state needs its geometry owner.
if p.category_2512 == 400 || state == 701 {
return Err("Active grind state requires authored grind geometry".into());
}
self.low_wheel_frames = if self.previous_candidate
&& state == 100
&& p.wheel_count_2556 < 2
@@ -12,6 +12,8 @@ pub(crate) fn reset_processed(input: &mut ProcessedPhysicsInput) {
[0, 0, 1.0f32.to_bits(), 0],
[0; 4],
],
grind_position_1120: input.grind_position_1120,
grind_direction_1136: input.grind_direction_1136,
vector_1520: input.vector_1520,
matrix_1536: input.matrix_1536,
byte_1600: input.byte_1600,
@@ -108,6 +108,7 @@ pub(crate) fn post_input_and_select(
physics: &mut GamePhysics,
skater: &mut SkaterRuntime,
) -> Result<(), String> {
super::grind::query(physics, skater);
post_input::advance(physics, skater)?;
selection::advance(physics, skater)
}
@@ -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;
@@ -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
@@ -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);
@@ -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,
@@ -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)
@@ -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;
+22
View File
@@ -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.