Implement TU3 no-comply, footplant and handplant physics

This commit is contained in:
chazmm committed 2026-09-08 16:20:06 +01:00
1 parent da364c924f
commit 55cc1eff82
40 files changed
+1829 -28

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
@echo off
setlocal
cd /d "%~dp0"
set "PLANT_EXE=%~dp0bin\plants\skate3rust.exe"
set "PLANT_ASSETS=C:\Users\Daddy\AppData\Local\Skate3RustEngine\installations\957f4f78db9b4f59959f10dff8ff6a84\assets"
if not exist "%PLANT_EXE%" (
echo Missing task build: "%PLANT_EXE%"
pause
exit /b 1
)
"%PLANT_EXE%" --assets "%PLANT_ASSETS%" --test-world %*
set "PLANT_EXIT=%ERRORLEVEL%"
if not "%PLANT_EXIT%"=="0" pause
exit /b %PLANT_EXIT%
@@ -125,5 +125,25 @@ fn vector(v: [f32; 4]) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
///Handplant Reckoning82D8E1C0. Uses the ordinary persistent normal filters.
pub fn update_plant(orientation:&mut GroundOrientation,frames:&mut ReckoningFrames,
body_spin:&mut BodySpinState,state:&mut AirState,settings:&Settings,
up:[f32;4],heading:[f32;4],reverse:bool) {
frames.heading=heading;
body_spin::update(body_spin,&settings.body_spin,0.0,0.0,true,0);
let previous=lanes(orientation.up);
orientation.dynamic_up=orientation.up;
frames.target_lean_angle=0.0;state.secondary_lean_angle=0.0;
let normal=normalize(orientation.ground_filter.update(settings.ground_normal_smoothing,up));
orientation.ground_normal=vector(normal);
let up=normalize_safe(limit_angle(normal,previous,1.0),previous);
orientation.up_velocity=vector(std::array::from_fn(|i|up[i]-previous[i]));
orientation.up=vector(up);orientation.target=orientation.up;
orientation.slow_filter.filter(up);orientation.fast_filter.filter(up);
orientation.slow_filter.publish_current(up);orientation.fast_filter.publish_current(up);
frames.calculate_transform(up,normal);
frames.calculate_tilt(reverse,&settings.tilt_vs_rotation,&settings.tilt_vs_slope);
}
#[cfg(test)]
mod tests;
@@ -81,6 +81,24 @@ pub fn produce(controller: &DerivedControllerInput) -> Vec<TrickIntent> {
if right_y != 0.0 {
emit("TweakY", right_y);
}
//Fill8259A554/8259AF60: current packed bit28 (RB), without an actor gate.
//The stock ground graph attaches GrabWorld; physics selects valid coping.
if current_flags & (1 << 28) != 0 {
emit("GrabWorld", 1.0);
}
//Fill calls8259BB18 with the CURRENT RawControllerInput (words7..13).
//Descriptor constructors82F84F28..82F84F98 establish these five names.
if right_x != 0.0 {
emit("HandPlantTweakX", right_x);
}
if right_y != 0.0 {
emit("HandPlantTweakY", right_y);
}
for (bit, name) in [(20, "HandPlantDismount"), (21, "HandPlantOneFootRight"), (23, "HandPlantOneFootLeft")] {
if current_flags & (1 << bit) != 0 {
emit(name, 1.0);
}
}
output
}
@@ -103,3 +103,21 @@ pub fn finish_known_air(
roots.predicted_board_position = effective_board[3];
roots.supplied_prediction = Some(effective_board[3]);
}
///82BDEEB8/82BE0E80: anchor an animation bone or COM in world space.
///Plants retain heading_alignment and do not consume board prediction.
pub fn update_plant_roots(
roots: &mut SkeletonRootFrames,
reckoning: &Transform,
world_anchor: [f32; 4],
animation_anchor: [f32; 4],
) {
let mut world = compose_affine(reckoning, &roots.heading_alignment);
world[3] = std::array::from_fn(|i| {
let x = world[0][i] * animation_anchor[0];
let y = world[1][i].mul_add(animation_anchor[1], x);
world_anchor[i] - world[2][i].mul_add(animation_anchor[2], y)
});
roots.animation_to_world = orthonormalize(world);
roots.world_to_animation = inverse_rigid(&roots.animation_to_world);
}
@@ -24,10 +24,19 @@ pub struct AirOutputFields {
pub flag_444: u8,
/// Air324 high bits used by IsHandPlanting82BA55A8.
pub handplant_flags_324: u32,
pub handplant_position_304: RawVector,
pub handplant_time_320: f32,
pub vector_160: RawVector,
pub scalar_184: f32,
pub landing_normal_144: RawVector,
pub jump_height_200: f32,
/// FootPlantManager::Fill82D71040: predicted contact and ground-plant duration.
pub footplant_contact_time_208: f32,
pub footplant_duration_212: f32,
pub footplant_surface_height_216: f32,
pub footplant_surface_224: u32,
pub footplant_left_449: u8,
pub footplant_right_450: u8,
pub reached_apex_436: u8,
pub flag_441: u8,
pub flag_446: u8,
@@ -85,7 +85,9 @@ impl StateSelector {
PhysicalStateId::FootPlant => {
if facts.wipeout {
PhysicalStateId::WipeoutGround
} else if !p.has_2480(1) {
//82D8B970 loads the first byte of the big-endian word,
//then tests its low bit: full-word bit24 is Air448.
} else if !p.has_2480(0x0100_0000) {
p.air_variant_from_2468()
} else {
current
@@ -126,3 +126,16 @@ pub fn check_animation(state: &mut Requests, s: &Settings, mode: &Mode, f: &Fram
state.request(20, 0.0);
}
}
///82D8FDC0, shared FootPlant/HandPlant postphysics collision checks.
pub fn check_plant(state: &mut Requests, s: &Settings, f: &Frame) {
state.mode = 3;
if f.maximum_pose_error > s.ground.max_squash {
state.request(18, 0.0);
} else if super::common::length(f.pose_error) > s.air.max_displacement {
state.request(1, 0.0);
} else if super::common::force(f, s.air.max_contact, s.air.max_arm_contact) {
state.request(0, 0.0);
}
common::closing(state, f, s.air.xz_trick, s.air.y_trick);
}
+1 -1
View File
@@ -7,7 +7,7 @@ mod ground;
mod requests;
pub use air::check as check_air;
pub use data::{AirSettings, Frame, GroundSettings, Mode, RequestInput, Settings, V};
pub use ground::{check as check_ground, check_animation as check_ground_animation};
pub use ground::{check as check_ground, check_animation as check_ground_animation, check_plant};
pub use requests::Requests;
pub use common::force as regional_force;
@@ -39,7 +39,7 @@ impl ActionCondition {
target: None,
numeric: super::condition_nodes::numeric(a),
},
"IsGrabbingObject" | "IsHandPlanting" => Self::Physical(GameplayCondition::parse(a)?),
"IsGrabbingObject" | "IsHandPlanting" | "IsFootPlanting" => Self::Physical(GameplayCondition::parse(a)?),
"IsTricking" => Self::Tricking,
"IsInLocomotion" => Self::InLocomotion,
"DisableDismount" => Self::DisableDismount,
@@ -19,6 +19,12 @@ pub struct GameplayConditions {
pub can_land_on_board: bool,
pub hippy_hurdling: bool,
pub handplant_flags: u32,
pub handplant_time: f32,
/// anim_handplant/default layout0/4/8: out, into, antic.
pub handplant_thresholds: [f32; 3],
pub footplant_active: bool,
pub footplant_duration: f32,
pub footplant_contact_time: f32,
pub time_to_skitch: f32,
pub skitch_transition_time: f32,
/// TimeToLand82BA7250 reads Air184 when Air437 is valid.
@@ -50,6 +56,10 @@ pub enum GameplayCondition {
DarkCatchRequested,
OkToDoTrickOnStairs,
HandPlanting { state: u8, direction: u8 },
FootPlanting,
PrepareFootplant,
NewHandplantPosition,
PlayHandplant { phase: usize },
EnteringSkitch,
Skitching,
}
@@ -75,6 +85,10 @@ impl GameplayCondition {
| "IsDarkCatchRequested"
| "OkToDoTrickOnStairs"
| "IsHandPlanting"
| "IsFootPlanting"
| "ShouldPrepareOneFootAirForFootplant"
| "HasNewHandPlantPos"
| "ShouldPlayHandPlantAnim"
| "IsEnteringSkitch"
| "IsSkitching"
)
@@ -102,6 +116,13 @@ impl GameplayCondition {
"OkToDoTrickOnStairs" => Self::OkToDoTrickOnStairs,
"IsEnteringSkitch" => Self::EnteringSkitch,
"IsSkitching" => Self::Skitching,
"IsFootPlanting" => Self::FootPlanting,
"ShouldPrepareOneFootAirForFootplant" => Self::PrepareFootplant,
"HasNewHandPlantPos" => Self::NewHandplantPosition,
"ShouldPlayHandPlantAnim" => Self::PlayHandplant { phase: match a.text("anim") {
Some("antic") => 2, Some("into") => 1, Some("out") => 0,
value => return Err(format!("Unauthored handplant animation phase {value:?}")),
} },
"IsHandPlanting" => Self::HandPlanting {
//82BA54A0 compares these authored strings in this order.
state: match a.text("state").unwrap_or("") {
@@ -155,6 +176,17 @@ impl GameplayCondition {
_ => false,
}
}
// Factory82BCB308 -> VT82321070 slot48=82BBD888.
// Both graphs read Air448 and Air212, including the entry frame.
Self::FootPlanting => p.footplant_active && p.footplant_duration >= 0.0,
//82BBD7F0, literals820641A8 and8209975C.
Self::PrepareFootplant => p.footplant_contact_time >= 0.1 && p.footplant_contact_time <= 0.5,
Self::NewHandplantPosition => p.handplant_flags & 0x4000_0000 != 0, //82BBDB98
Self::PlayHandplant { phase } => {
//82BBDA98 subtracts one simulation tick before the comparison.
let threshold = p.handplant_thresholds[*phase];
p.handplant_time - f32::from_bits(0x3c888889) < if *phase == 0 { -threshold } else { threshold }
}
Self::DroppingBoard
| Self::Dark
| Self::UnderflipRequested
@@ -79,6 +79,7 @@ impl Operation {
/// this game.82595D08 sets a name and ORs a bit in the graph's score packet.
#[derive(Default)]
pub struct ScorePacket {
pub handplant: Option<(skate_core::animation::output::attributes::AttributeName, [f32; 2])>,
pub grab: Option<(skate_core::animation::output::attributes::AttributeName, [f32; 2])>,
pub trick: Option<skate_core::animation::output::attributes::AttributeName>,
pub name: Option<u32>,
@@ -149,6 +149,9 @@ impl OperationFactory for MotionFactory {
)),
"ClearTrickAttr" => Some(MotionOperation::ClearTrickAttr),
"SetTrickHeight"
| "FootPlantAbsorb"
| "SetHandPlantAnticLength"
| "ScoringHandPlants"
| "SetTrickAttr"
| "ScoringTrick"
| "MonitorUnderflip"
@@ -10,6 +10,9 @@ use skate_data::state_graph::attributes::Attributes;
#[derive(Clone, Debug, PartialEq)]
pub enum Operation {
FootPlantAbsorb,
HandplantAntic,
HandplantScore { base: String, directions: [Option<String>;4], intents: [String;2] },
Height {
from: AttributeName,
rename: AttributeName,
@@ -26,6 +29,13 @@ pub enum Operation {
impl Operation {
pub fn parse(a: &Attributes<'_>) -> Option<Self> {
Some(match a.text("name")? {
"FootPlantAbsorb" => Self::FootPlantAbsorb,
"SetHandPlantAnticLength" => Self::HandplantAntic,
"ScoringHandPlants" => Self::HandplantScore {
base:a.text("handplantname").unwrap_or("").to_owned(),
directions:["up","left","down","right"].map(|n|a.text(n).map(str::to_owned)),
intents:["intentX","intentY"].map(|n|a.text(n).unwrap_or("").to_owned()),
},
"SetTrickHeight" => Self::Height {
from: encode(a.text("from").unwrap_or("").as_bytes()),
rename: encode(a.text("rename").unwrap_or("").as_bytes()),
@@ -56,6 +66,32 @@ pub fn execute(
phase: u8,
) -> Result<(), String> {
match operation {
Operation::HandplantAntic if phase==0 => {
//VT82321360 Begin=82BBE5D0; shared clip lengths from82B959E8.
let duration=|name|->Result<f32,String> {
let clip=host.animation.metadata().clip(name)?;
Ok((f32::from_bits(clip.frames_bits)-1.0)/(f32::from_bits(clip.base_speed_bits)*f32::from_bits(clip.fps_bits)))
};
let normal=duration("INVERT_HANDPLANT_FS_0_ANTIC")?;
let late=duration("INVERT_HANDPLANT_FS_0_ANTIC_LATE")?;
let p=host.gameplay_conditions.as_ref().ok_or("Handplant antic requires physical output")?;
let value=((normal-(p.handplant_thresholds[2]-p.handplant_time)-late)/(normal-late)).clamp(0.0,1.0);
host.animation.set_attribute(SettableAttribute {name:encode(b"antic_length"),value,normalized:false,sequence_id:-1});
}
Operation::HandplantScore {base,directions,intents} if phase==1 => {
//82BBF758 uses the same angular windows as ScoringGrabs82BBEF60.
let xy=intents.each_ref().map(|n|host.animation.filtered_intent(n).unwrap_or(0.0));
let name=super::motion_stock_gameplay::select_grab_score(&base,directions.each_ref().map(|s|s.as_deref()),xy[0],xy[1]);
host.score_packet.handplant=Some((encode(name.as_bytes()),xy));
}
Operation::FootPlantAbsorb if phase == 1 => {
//82BBE4E0 reads the manager's curve duration, Air212.
let value = host.gameplay_conditions.as_ref()
.ok_or("FootPlantAbsorb requires physical output")?.footplant_duration;
host.animation.set_attribute(SettableAttribute {
name: encode(b"absorblength"), value, normalized: false, sequence_id: -1,
});
}
Operation::Height {
from,
rename,
+3
View File
@@ -10,6 +10,9 @@ mod colliders;
mod controls;
mod foot_ik;
mod footplant;
mod plant_skeleton;
mod boneless;
mod handplant;
mod foot_ik_queries;
mod foot_physical_output;
pub(crate) mod ground;
@@ -16,6 +16,10 @@ pub(crate) struct AirReckoning {
modes: [settings::Mode; 5],
}
impl AirReckoning {
pub fn update_plant(&mut self,riding:&mut RidingOutputs,p:&ProcessedPhysicsInput,up:[f32;4],heading:[f32;4]) {
reckoning::update_plant(&mut riding.reckoning,&mut riding.reckoning_frames,
&mut riding.body_spin,&mut self.state,&self.settings,up,heading,p.flags_2468&(1<<20)!=0);
}
pub fn load(data: &Collections) -> Result<Self, String> {
let (settings, modes) = settings::load(data)?;
Ok(Self {
@@ -99,6 +99,11 @@ pub(crate) fn advance(
can_land_on_board: physical.off_board.flag_316 != 0,
hippy_hurdling: physical.off_board.hippy_hurdling_317 != 0,
handplant_flags: physical.air.handplant_flags_324,
handplant_time: physical.air.handplant_time_320,
handplant_thresholds: skater.handplant.animation_thresholds(),
footplant_active: physical.air.flag_448 != 0,
footplant_duration: physical.air.footplant_duration_212,
footplant_contact_time: physical.air.footplant_contact_time_208,
time_to_skitch: physical.ground.scalar_276,
skitch_transition_time: profile.skitch_transition_time,
time_to_land: physical.air.scalar_184,
+103
View File
@@ -0,0 +1,103 @@
//! Physical state602, TU3 vtable82327434. Stock no-comply/boneless/fastplant
//! animations select this state with AnimSkateboard and launch with FootJump.
use super::footplant::math::*;
use super::{GamePhysics, SkaterRuntime, air_phase, plant_skeleton};
use skate_core::point_graph::PointGraph;
use skate_data::collections::Collections;
pub(crate) struct Boneless {
pub toe: usize, //52
pub anchor: [f32; 4], //64
pub right: bool, //80
curves: [PointGraph<8>; 4],
}
impl Boneless {
pub fn load(data: &Collections) -> Result<Self, String> {
let graph = |name| -> Result<_, String> {
let words = data
.words::<20>("Hash_CCB95A83C78B4FF9", "default", name)?
.map(f32::from_bits);
Ok(PointGraph {
x: words[4..12].try_into().unwrap(),
y: words[12..20].try_into().unwrap(),
})
};
Ok(Self {
toe: 15,
anchor: [0.0; 4],
right: false,
curves: [
graph("Hash_6D781EFAF01E707D")?,
graph("Hash_C9112CCD0BCB1850")?,
graph("Hash_88D0CDEFA38A36D6")?,
graph("Hash_9228B7F18C223F9C")?,
],
})
}
}
///82D4C900: anchor the last completed physical toe position.
pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
skater.ground_lifecycle.board_animated_290 = 1;
let drive = &mut physics.board.hook_mut().drive;
//Globals12/912 at822F860C/822F8990; linear HardDrive, angular SoftDrive.
drive.dynamics[..4].copy_from_slice(&[0x426f_ffff, 0, 0x4560_fffe, 2]);
drive.enable_angular_soft();
let right = skater.player_input.processed.flags_2468 & (1 << 26) != 0;
skater.boneless.toe = if right { 19 } else { 15 };
skater.boneless.anchor = skater.skeleton.record.pose[skater.boneless.toe][3];
skater.boneless.right = right;
Ok(())
}
///82D4C9B8. Authored FootJump controls every launch request; no host timer.
pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
plant_skeleton::advance(
physics,
skater,
skater.boneless.anchor,
Some(skater.boneless.toe),
)?;
plant_skeleton::hold_foot(skater, skater.boneless.right, skater.boneless.anchor, 3);
if skater.player_input.processed.flags_2480 & (1 << 13) != 0 {
launch(physics, skater)?;
}
Ok(())
}
///82D4CB18/82D4CC18, four PointNegGraphData8 values from the stock schema.
fn launch(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
let p = &skater.player_input.processed;
let up = p.vectors_544_560_592_608[0].map(f32::from_bits);
let current = p.vectors_544_560_592_608[3].map(f32::from_bits);
let prepared = p.prepared_jump_704.map(f32::from_bits);
let horizontal = sub(prepared, scale(up, dot(prepared, up)));
let speed = length(horizontal);
let g = &skater.boneless.curves;
let vertical = g[0].evaluate(up[1]) * g[1].evaluate(speed);
let scaled_speed = g[2].evaluate(speed) * speed;
let a = madd(up, vertical - dot(current, up), current);
let b = madd(normalize(horizontal), scaled_speed, scale(up, vertical));
let blend = g[3].evaluate(up[1]);
let mut velocity = madd(b, blend, scale(a, 1.0 - blend));
if (length(velocity) - length(current)).abs() > 10.0 {
velocity = current;
}
let forward = p.effective_anim_transform_192[2].map(f32::from_bits); //Processed224
let component = dot(velocity, forward).max(0.0);
if component < 1.92 {
velocity = madd(forward, 1.92 - component, velocity);
}
let com = p.vectors_544_560_592_608[2].map(f32::from_bits);
let position = madd(up, dot(sub(skater.boneless.anchor, com), up) + 0.25, com);
let mut info = air_phase::launch_info(physics, skater)?;
info.start_velocity = velocity;
info.start_position_override = position;
info.board_position_override = position;
info.use_position_override = true;
info.player_jumped = true;
let input = air_phase::selector_input(physics, skater)?;
skater.trajectory.launch(info, input, &physics.world)?;
skater.trajectory.update(input, &physics.world)?;
Ok(())
}
+19 -1
View File
@@ -2,10 +2,11 @@
//! Query results are retained separately from pending query ownership.
use skate_core::air::trajectory::{QueryResult, Trajectory};
use skate_data::collections::Collections;
mod math;
pub(super) mod math;
mod pose;
mod settings;
mod update;
pub(super) mod ground;
use settings::Settings;
pub(crate) use update::FootplantFrame;
pub(crate) type V = [f32; 4];
@@ -36,11 +37,13 @@ pub(crate) struct Footplant {
pub(super) selected_record: V, //272
pub(super) leg_direction: V, //336
pub(super) vectors_352_368: [V; 2],
pub(super) curve: [V; 4], //448/464/480/496
pub(super) physical_com: V, //384
pub(super) animation_com: V, //400
pub(super) launch_direction: V, //416
pub(super) locked_target: V, //432
pub(super) request: Trajectory, //528..591
pub(super) completed_trajectory: Trajectory, //query144, retained with result
}
impl Footplant {
pub fn load(data: &Collections) -> Result<Self, String> {
@@ -73,6 +76,7 @@ impl Footplant {
selected_record: [0.0; 4],
leg_direction: [0.0; 4],
vectors_352_368: [[0.0; 4]; 2],
curve: [[0.0; 4]; 4],
physical_com: [0.0; 4],
animation_com: [0.0; 4],
launch_direction: [0.0; 4],
@@ -83,6 +87,7 @@ impl Footplant {
acceleration: [0.0; 4],
duration: -1.0,
},
completed_trajectory: Trajectory { position: [0.0;4], velocity: [0.0;4], acceleration: [0.0;4], duration: -1.0 },
};
state.reset();
Ok(state)
@@ -123,4 +128,17 @@ impl Footplant {
self.enabled = false;
self.reset();
}
/// Common ProcessOutput82DB71B0 calls82D71040 before selected state Fill.
pub fn publish(&self, out: &mut skate_core::player::input_phase::AirOutputFields) {
if self.hit {
out.footplant_contact_time_208 = self.contact_time;
out.footplant_duration_212 = self.scalar_596;
out.flag_447 = u8::from(self.perform);
out.flag_448 = u8::from(self.perform || self.flag_627);
out.footplant_surface_224 = self.surface;
out.footplant_left_449 = u8::from(self.flag_627 && self.selected_toe == Some(15));
out.footplant_right_450 = u8::from(self.flag_627 && self.selected_toe == Some(19));
out.footplant_surface_height_216 = self.contact[1];
}
}
}
@@ -0,0 +1,180 @@
//! FootPlant601, vtable82327400 and manager82D704B0/82D70E40.
use super::{Footplant, math::*};
use crate::physics::{GamePhysics, SkaterRuntime, air_phase, plant_skeleton};
pub(in crate::physics) fn enter(
physics: &mut GamePhysics,
skater: &mut SkaterRuntime,
) -> Result<(), String> {
physics
.board
.hook_mut()
.drive
.enable_angular_only(&mut skater.ground_lifecycle.board_animated_290);
//82D91330 sets the foot/toe volume group and clears their disable counters.
for part in [15, 16, 19, 20] {
skater.skeleton_collision.parts[part].volume_group = 4;
skater.skeleton_collision.parts[part].enabled = false;
skater.skeleton_collision.disable_count[part] = 0;
}
let p = &skater.player_input.processed;
let frames = skater.footplant.start(
p.vectors_544_560_592_608[2].map(f32::from_bits),
p.vectors_544_560_592_608[3].map(f32::from_bits),
);
if frames != 0 {
//The native Start only disables collisions; the first Update sets IK.
let right = skater.footplant.selected_toe == Some(19);
for part in if right { [19, 20, 21] } else { [15, 16, 17] } {
skater.skeleton_collision.pending_reenable = true;
skater.skeleton_collision.disable_count[part] = frames;
skater.skeleton_collision.parts[part].enabled = false;
}
}
Ok(())
}
impl Footplant {
fn start(&mut self, com: [f32; 4], velocity: [f32; 4]) -> u32 {
self.perform = false;
self.scalar_600 = 0.0;
self.flag_627 = true;
let contact = self.adjusted_contact;
self.vectors_352_368[1] = contact;
let offset = sub(com, contact);
let radial = normalize(offset);
let multiplier = self
.settings
.radial_speed_scale
.evaluate(length(scale(radial, dot(velocity, radial))));
let rotation = cross(offset, velocity);
let angular_speed = length(rotation);
//830BD300 is initialized by82F82690 from821647E0.
if !(angular_speed > f32::from_bits(0x3780_0000)) {
return 0;
}
let axis = scale(rotation, reciprocal(angular_speed));
let angular_rate = angular_speed * multiplier;
//8286CD88 projects both vectors into the plane;8296EC98 signs acos.
let normal = scale(axis, -1.0);
let up = [0.0, 1.0, 0.0, 0.0];
let projected_up = normalize(sub(up, scale(normal, dot(up, normal))));
let mut angle = if length(projected_up) == 0.0 {
0.0
} else {
skate_core::trigonometry::acos(dot(radial, projected_up).clamp(-1.0, 1.0))
};
if dot(cross(radial, projected_up), normal) < 0.0 {
angle = -angle;
}
let min_end = angle + angular_rate * self.settings.min_duration;
let max_end = angle + angular_rate * self.settings.max_duration;
let end_angle = min_end
.max(self.settings.end_angle * f32::from_bits(0x3c8e_fa35))
.min(max_end);
let delta = end_angle - angle;
self.scalar_596 = delta / angular_rate;
//Quaternion(axis,delta) applied in the native two-cross-product order.
let (sin, cos) = skate_core::trigonometry::sin_cos(delta * 0.5);
let q = scale(axis, sin);
let end_radial = madd(cross(q, madd(radial, cos, cross(q, radial))), 2.0, radial);
let end = madd(end_radial, self.settings.end_leg_length, contact);
let middle = normalize(add(end_radial, radial));
let release = madd(
sub(velocity, scale(middle, dot(velocity, middle))),
multiplier,
scale(middle, self.settings.release_outward_speed),
);
self.scalar_612 = length(release);
self.vectors_352_368[0] = scale(release, reciprocal(self.scalar_612));
self.curve = [
com,
madd(
scale(velocity, multiplier),
self.scalar_596 * self.settings.start_handle,
com,
),
sub(
end,
scale(release, self.scalar_596 * self.settings.end_handle),
),
end,
];
(4 - (self.scalar_596 * f32::from_bits(0xc26f_ffff)) as i32) as u32
}
///82D70CE0: authored BodyAdjustX/Z move the release point, retaining
///the last handle length.82C1E170 removes only positive up projection.
fn adjust(&mut self, t: f32, input: [f32; 2]) {
let amount = (1.0 - t).mul_add(0.02, t * 0.01);
let mut movement = [input[0] * amount, 0.0, input[1] * amount, 0.0];
let projection = dot(movement, normalize(self.current_up));
if projection > 0.0 {
movement = sub(movement, scale(self.current_up, projection));
}
let previous = self.curve[3];
self.curve[3] = add(previous, movement);
let direction = normalize(sub(self.curve[3], self.curve[2]));
self.curve[2] = sub(
self.curve[3],
scale(direction, length(sub(self.curve[2], previous))),
);
self.vectors_352_368[0] = direction;
}
}
pub(in crate::physics) fn update(
physics: &mut GamePhysics,
skater: &mut SkaterRuntime,
) -> Result<(), String> {
let fp = &mut skater.footplant;
fp.scalar_600 += STEP;
let t = fp.scalar_600 / fp.scalar_596;
fp.adjust(t, skater.animation_input.extra.body_adjust);
let u = 1.0 - t;
let position = madd(
fp.curve[3],
t * t * t,
madd(
fp.curve[2],
3.0 * t * t * u,
madd(fp.curve[0], u * u * u, scale(fp.curve[1], 3.0 * t * u * u)),
),
);
let launched = fp.scalar_600 > fp.scalar_596;
if launched {
fp.flag_627 = false;
}
let anchor = fp.vectors_352_368[1];
let velocity = scale(fp.vectors_352_368[0], fp.scalar_612);
let right = fp.selected_toe != Some(15);
plant_skeleton::hold_foot(skater, right, anchor, 0);
plant_skeleton::advance(physics, skater, position, None)?;
if launched {
let mut info = air_phase::launch_info(physics, skater)?;
info.start_velocity = velocity;
info.start_position_override = add(anchor, [0.0, 0.2, 0.0, 0.0]);
info.board_position_override = add(info.start_position_override, [0.0, -0.1, 0.0, 0.0]);
info.use_position_override = true;
info.player_jumped = true;
let input = air_phase::selector_input(physics, skater)?;
skater.trajectory.launch(info, input, &physics.world)?;
skater.trajectory.update(input, &physics.world)?;
}
Ok(())
}
///82D4C6D8, after the shared plant collision check82D8FDC0.
pub(in crate::physics) fn post_physics(skater: &mut SkaterRuntime) {
let p = &skater.player_input.processed;
let down = scale(p.vectors_544_560_592_608[0].map(f32::from_bits), -1.0);
let velocity = p.vectors_544_560_592_608[3].map(f32::from_bits);
let descending = dot(velocity, down);
let horizontal = length(sub(velocity, scale(down, descending)));
if descending > skater.footplant.settings.max_descending_speed {
skater.wipeout.state.request(6, descending);
}
if horizontal > skater.footplant.settings.max_horizontal_speed {
skater.wipeout.state.request(23, 0.0);
}
}
+16 -13
View File
@@ -1,23 +1,26 @@
//! Original vector operation order from82D6FD60/82D6F5F0/82D70070.
//! Estimate seeds use independent PC math; Xenon bit equality is unverified.
use super::V;
pub(super) const STEP: f32 = f32::from_bits(0x3c888889);
pub(super) fn dot(a: V, b: V) -> f32 {
pub(in crate::physics) const STEP: f32 = f32::from_bits(0x3c888889);
pub(in crate::physics) fn dot(a: V, b: V) -> f32 {
(a[0] * b[0] + a[1] * b[1]) + a[2] * b[2]
}
pub(super) fn add(a: V, b: V) -> V {
pub(in crate::physics) fn cross(a: V, b: V) -> V {
[a[1].mul_add(b[2], -a[2]*b[1]), a[2].mul_add(b[0], -a[0]*b[2]), a[0].mul_add(b[1], -a[1]*b[0]), 0.0]
}
pub(in crate::physics) fn add(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] + b[i])
}
pub(super) fn sub(a: V, b: V) -> V {
pub(in crate::physics) fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
pub(super) fn scale(a: V, s: f32) -> V {
pub(in crate::physics) fn scale(a: V, s: f32) -> V {
a.map(|v| v * s)
}
pub(super) fn madd(a: V, s: f32, b: V) -> V {
pub(in crate::physics) fn madd(a: V, s: f32, b: V) -> V {
std::array::from_fn(|i| a[i].mul_add(s, b[i]))
}
pub(super) fn reciprocal(s: f32) -> f32 {
pub(in crate::physics) fn reciprocal(s: f32) -> f32 {
let mut r = s.recip();
for _ in 0..2 {
r = (-s).mul_add(r, 1.0).mul_add(r, r);
@@ -31,12 +34,12 @@ fn rsqrt(s: f32) -> f32 {
}
r
}
pub(super) fn length(a: V) -> f32 {
pub(in crate::physics) fn length(a: V) -> f32 {
let s = dot(a, a);
let l = s * rsqrt(s);
if s == 0.0 { 0.0 } else { l }
}
pub(super) fn normalize(a: V) -> V {
pub(in crate::physics) fn normalize(a: V) -> V {
let s = dot(a, a);
let r = rsqrt(s);
let l = if s == 0.0 { 0.0 } else { s * r };
@@ -46,21 +49,21 @@ pub(super) fn normalize(a: V) -> V {
[0.0; 4]
}
}
pub(super) fn point(m: &[V; 4], p: V) -> V {
pub(in crate::physics) fn point(m: &[V; 4], p: V) -> V {
std::array::from_fn(|i| {
let x = m[0][i].mul_add(p[0], m[3][i]);
let y = m[1][i].mul_add(p[1], x);
m[2][i].mul_add(p[2], y)
})
}
pub(super) fn rotate(m: &[V; 4], v: V) -> V {
pub(in crate::physics) fn rotate(m: &[V; 4], v: V) -> V {
std::array::from_fn(|i| {
let x = m[0][i] * v[0];
let y = m[1][i].mul_add(v[1], x);
m[2][i].mul_add(v[2], y)
})
}
pub(super) fn clamp_length(v: V, maximum: f32) -> V {
pub(in crate::physics) fn clamp_length(v: V, maximum: f32) -> V {
let l = length(v);
if !(l >= f32::from_bits(0x37800000)) {
return v;
@@ -68,7 +71,7 @@ pub(super) fn clamp_length(v: V, maximum: f32) -> V {
let capped = if maximum - l >= 0.0 { l } else { maximum };
scale(scale(v, capped), reciprocal(l))
}
pub(super) fn clamp01(v: f32) -> f32 {
pub(in crate::physics) fn clamp01(v: f32) -> f32 {
let a = if -v >= 0.0 { 0.0 } else { v };
if 1.0 - a >= 0.0 { a } else { 1.0 }
}
@@ -1,15 +1,27 @@
//! Cached physics_footplantmanager layout used by82D6FD60/82D6F5F0/82D70070.
use skate_data::collections::Collections;
use skate_core::point_graph::PointGraph;
pub(super) struct Settings {
pub deck_bounds: [f32; 4], //0
pub max_leg_angle_error: f32, //280 degrees
pub leg_length_on_landing: f32, //288
pub foot_volume_y_offset: f32, //296
pub deck_bounds_y_offset: f32, //300
pub radial_speed_scale: PointGraph<8>, //16
pub max_horizontal_speed: f32, //256
pub max_descending_speed: f32, //260
pub release_outward_speed: f32, //268
pub end_handle: f32, //272
pub end_angle: f32, //276 degrees
pub end_leg_length: f32, //284
pub start_handle: f32, //292
pub min_duration: f32, //304
pub max_duration: f32, //308
}
impl Settings {
pub fn load(data: &Collections) -> Result<Self, String> {
let float = |name| data.float("physics_footplantmanager", "default", name);
let graph = data.words::<20>("physics_footplantmanager", "default", "Hash_8A8887218466ED15")?.map(f32::from_bits);
Ok(Self {
deck_bounds: data
.words::<4>("physics_footplantmanager", "default", "DeckBB")?
@@ -18,6 +30,16 @@ impl Settings {
leg_length_on_landing: float("LegLengthOnLanding")?,
foot_volume_y_offset: float("FootVolumeYOffset")?,
deck_bounds_y_offset: float("DeckBBYOffset")?,
radial_speed_scale: PointGraph { x: graph[4..12].try_into().unwrap(), y: graph[12..20].try_into().unwrap() },
max_horizontal_speed: float("Hash_149A5D0D4768D2B1")?,
max_descending_speed: float("Hash_52436D9F4FE56BCB")?,
release_outward_speed: float("Hash_C5F92168DFAFD0B8")?,
end_handle: float("Hash_628C228DCC7B6ED5")?,
end_angle: float("Hash_4F45DA86685CF029")?,
end_leg_length: float("Hash_0DE0027B9913C5D5")?,
start_handle: float("Hash_5C4341EF35F4B2B0")?,
min_duration: float("Hash_D6224B710A36D643")?,
max_duration: float("Hash_68C05A7FCC6BB718")?,
})
}
}
@@ -22,6 +22,7 @@ pub(crate) struct FootplantFrame<'a> {
pub body: &'a SkeletonBody,
pub gravity: V,
pub world: &'a BoardWorld,
pub edges: &'a [skate_core::physics::grind_contact::Primitive],
}
impl Footplant {
pub fn update_candidate(&mut self, input: &KnownAirFootplantInput, frame: &FootplantFrame<'_>) {
@@ -106,7 +107,7 @@ impl Footplant {
self.contact = self.result.contact_position;
self.surface = self.result.surface;
self.hit = true;
//82D6FBB0's nearby-edge scan is empty in our explicitly edge-free BoardWorld.
self.nearby_edge(frame);
self.adjusted_contact = madd(
input.landing_normal,
self.settings.foot_volume_y_offset,
@@ -134,28 +135,61 @@ impl Footplant {
}
}
self.current_up = frame.processed.vectors_544_560_592_608[0].map(f32::from_bits);
let submitted = Trajectory {
position: self.request.position, velocity: self.request.velocity,
acceleration: frame.gravity, duration: 1.0,
};
let next = AirTrajectoryRuntime::query(
frame.world,
QueryRequest {
trajectory: Trajectory {
position: self.request.position,
velocity: self.request.velocity,
acceleration: frame.gravity,
duration: 1.0,
},
trajectory: submitted,
radius: f32::from_bits(0x3d23d70a),
start_error: 1.0,
end_error: 1.0,
},
)?;
self.result = next;
self.completed_trajectory = submitted;
self.enabled = true;
Ok(())
}
fn clear_contact(&mut self) {
self.surface = 0;
self.hit = false;
self.contact_time = -1.0;
self.contact_time = 0.0; //82D6FA80/82D6FAC4, distinct from Reset's -1.
self.contact = [0.0; 4];
}
///82D6FBB0: query the real edge geometry within the native two-metre box.
fn nearby_edge(&mut self, frame: &FootplantFrame<'_>) {
use skate_core::{math::Vector3, player::offboard::{air_ledge::visible_edges, ground_query::Edge}};
let xyz = |v: V| Vector3::new(v[0], v[1], v[2]);
let lanes = |v: Vector3| [v.x, v.y, v.z, 0.0];
let edges: Vec<_> = frame.edges.iter().filter(|e| (0..3).all(|i|
e.start[i].min(e.end[i]) <= self.contact[i] + 2.0 && e.start[i].max(e.end[i]) >= self.contact[i] - 2.0
)).map(|e| Edge { start: xyz(e.start), end: xyz(e.end) }).collect();
let original_height = self.contact[1];
let mut best = 0.25; //820C6D98
for edge in visible_edges(&edges, frame.toolkit.deck[3]) {
let start = lanes(edge.start);
let delta = sub(lanes(edge.end), start);
let raw_normal = cross(cross(delta, [0.0,1.0,0.0,0.0]), delta);
let normal = if length(raw_normal) > f32::from_bits(0x3586_37bd) { normalize(raw_normal) } else { [0.0,1.0,0.0,0.0] };
let Some(time) = skate_core::air::trajectory::grind::descending_plane_time(self.completed_trajectory, start, normal) else { continue; };
let arc = self.completed_trajectory.position_at(time);
let edge_length = length(delta);
let direction = if edge_length > f32::from_bits(0x3780_0000) { scale(delta, reciprocal(edge_length)) } else { delta };
let point = madd(direction, dot(direction, sub(arc, start)).min(edge_length).max(0.0), start);
let distance = length(sub(arc, point));
//Globals3644 at822F943C permits at most 3cm below the triangle hit.
if distance < best && point[1] - original_height > f32::from_bits(0xbcf5_c28f) {
self.contact_time = time;
self.current_up = normal;
self.contact = point;
self.surface = 0;
self.hit = true;
best = distance;
}
}
}
}
+4
View File
@@ -104,6 +104,9 @@ pub(super) fn advance(
}
player_state::pre_state(physics, skater)?;
match skater.player_state.current() {
skate_core::player::state::PhysicalStateId::HandPlant => super::handplant::update(physics,skater)?,
skate_core::player::state::PhysicalStateId::FootPlant => super::footplant::ground::update(physics, skater)?,
skate_core::player::state::PhysicalStateId::Boneless => super::boneless::update(physics, skater)?,
skate_core::player::state::PhysicalStateId::GrindBoardslide
| skate_core::player::state::PhysicalStateId::GrindFiftyFifty
| skate_core::player::state::PhysicalStateId::GrindTipslide
@@ -134,6 +137,7 @@ pub(super) fn advance(
//after Reckoning; this frame's propulsion may then set it again.
skater.ground.state.push_suppressed_2730 = false;
ground_phase::advance(physics, skater)?;
super::handplant::ground_update(physics,skater)?;
input_phase::update_ground(physics, skater)?;
}
skate_core::player::state::PhysicalStateId::PhysicsAir => {
@@ -164,6 +164,7 @@ pub(crate) fn advance(
physics: &mut GamePhysics,
skater: &mut SkaterRuntime,
) -> Result<GroundBoardOutcome, String> {
super::handplant::ground_query(physics,skater);
let p = &skater.player_input.processed;
let toolkit = skater
.player_input
+271
View File
@@ -0,0 +1,271 @@
//! TU3 HandPlantManager82D61040 and physical state600 (823273CC).
mod contact;
mod ik;
mod rotation;
mod settings;
mod trajectory;
use super::footplant::math;
use super::{GamePhysics, SkaterRuntime, plant_skeleton};
use math::*;
use settings::Settings;
use skate_core::{air::trajectory::Trajectory, physics::skeleton_animation_record::IDENTITY};
type V = [f32; 4];
const UP: V = [0.0, 1.0, 0.0, 0.0];
const GRAVITY: V = [0.0, f32::from_bits(0xc11ccccd), 0.0, 0.0];
const EMPTY: Trajectory = Trajectory {
position: [0.0; 4],
velocity: [0.0; 4],
acceleration: [0.0; 4],
duration: -1.0,
};
pub(crate) struct Handplant {
settings: Settings,
pub flags: u32,
pub phase: f32,
pub anchor: V,
previous_candidate_point: V, //2192; separate from the published anchor1856.
pending: Option<(contact::Candidate, V, V, V, V)>,
candidate: Option<contact::Candidate>,
initial: Trajectory,
entry: Trajectory,
outgoing: [Trajectory; 2],
curve: [V; 4],
rotations: [[V; 4]; 4],
direction: V,
travel_sign: f32,
elapsed: f32,
warped: f32,
apex: f32,
estimated_phase: f32, //2944; retained when the authored mean is zero.
out_duration: f32,
pub continuation: bool,
direction_hint: i32,
direction_count: i32,
ik_blend: f32,
ik_distance: f32,
ik_released: bool,
ik_latched: bool,
}
impl Handplant {
pub fn load(data: &skate_data::collections::Collections) -> Result<Self, String> {
Ok(Self {
settings: Settings::load(data)?,
flags: 0,
phase: f32::MAX,
anchor: [0.0; 4],
previous_candidate_point: [0.0; 4],
pending: None,
candidate: None,
initial: EMPTY,
entry: EMPTY,
outgoing: [EMPTY; 2],
curve: [[0.0; 4]; 4],
rotations: [IDENTITY; 4],
direction: [0.0; 4],
travel_sign: 0.0,
elapsed: -1.0,
warped: -1.0,
apex: -1.0,
estimated_phase: -1.0,
out_duration: 0.0,
continuation: true,
direction_hint: 0,
direction_count: 0,
ik_blend: 0.0,
ik_distance: -1.0,
ik_released: false,
ik_latched: false,
})
}
pub fn animation_thresholds(&self) -> [f32; 3] {
self.settings.animation
}
///82D62F20 preserves the new-position bit and the stored trajectories.
pub fn reset(&mut self) {
self.flags &= !0xb000_0000;
self.phase = f32::MAX;
self.anchor = [0.0; 4];
self.pending = None;
self.candidate = None;
self.elapsed = -1.0;
self.warped = -1.0;
self.apex = -1.0;
self.estimated_phase = -1.0;
self.out_duration = 0.0;
self.direction = [0.0; 4];
self.continuation = true;
}
pub fn full_reset(&mut self) {
self.reset();
self.reset_ik();
self.direction_hint = 0;
self.direction_count = 0;
}
fn reset_ik(&mut self) {
self.ik_blend = 0.0;
self.ik_distance = -1.0;
self.ik_released = false;
self.ik_latched = false;
}
fn estimate_apex(&mut self) {
//82D63280 averages four authored time-warp samples.
let offset = self.warped - self.apex;
let stride = (offset * f32::from_bits(0x3eaaaaab)).abs(); //Globals440
let mean = (0..4)
.map(|i| 1.0 + self.settings.time_warp.evaluate(offset + i as f32 * stride))
.sum::<f32>()
* 0.25;
if mean != 0.0 {
self.estimated_phase = (-1.0 / mean) * offset;
}
self.phase = self.estimated_phase;
}
}
pub(super) fn apex_time(t: Trajectory) -> f32 {
if t.velocity[1] < 0.0 || t.acceleration[1] >= 0.0 {
0.0
} else {
t.velocity[1] * reciprocal(-t.acceleration[1])
}
}
///Ground82D37F38 consumes the candidate query before its shared skeleton update.
pub(super) fn ground_update(
physics: &mut GamePhysics,
skater: &mut SkaterRuntime,
) -> Result<(), String> {
let p = &skater.player_input.processed;
if p.flags_2476 & (1 << 22) == 0 {
skater.handplant.full_reset();
return Ok(());
}
if let Some((candidate, com, velocity, normal, heading)) = skater.handplant.pending.take() {
let surface = skate_core::air::trajectory::grind_surface::investigate(
&physics.world,
candidate.edge.start,
candidate.edge.end,
candidate.point,
skater.handplant.settings.truck_distance,
)?;
if surface.is_some_and(|s| s.evidence.kind != 3) {
skater.handplant.launch(
candidate,
com,
velocity,
normal,
heading,
physics.riding.reckoning_frames.heading,
);
}
}
if skater.handplant.flags & 0x8000_0000 != 0 {
ik::update(skater, true);
}
Ok(())
}
///Ground82D38430 submits the candidate before82D37F38 consumes its investigation.
pub(super) fn ground_query(physics: &GamePhysics, skater: &mut SkaterRuntime) {
let p = &skater.player_input.processed;
if p.flags_2476 & (1 << 22) == 0 {
skater.handplant.full_reset();
return;
}
let h = &mut skater.handplant;
let com = p.vectors_544_560_592_608[2].map(f32::from_bits);
let velocity = p.vectors_400_416[0].map(f32::from_bits);
let normal = p.vectors_464_480_496_512_528[0].map(f32::from_bits);
let heading = p.vectors_544_560_592_608[1].map(f32::from_bits);
if p.flags_2480 & (1 << 29) != 0 || p.flags_2480 & (1 << 28) == 0 {
h.direction_hint = 0;
h.direction_count = 0;
} else if normal[1] < 0.95 {
if h.direction_hint == 0 {
h.direction_hint = h.candidate.map_or(0, |c| c.side);
}
let side = if dot(velocity, normalize(cross(UP, normal))) > 0.0 {
1
} else {
2
};
if h.direction_hint == side {
h.direction_count = 0;
} else {
h.direction_count += 1;
if h.direction_count > h.settings.direction_frames {
h.direction_hint = 0;
}
}
} else {
h.direction_hint = 0;
h.direction_count = 0;
}
let candidate = contact::select(
&h.settings,
com,
velocity,
normal,
h.direction_hint,
&physics.grind.primitives,
);
//82D61268 clears the active output when submission begins.
let old_point = h.candidate.map_or([0.0; 4], |c| c.point);
let hint = h.direction_hint;
let count = h.direction_count;
h.reset();
h.previous_candidate_point = old_point;
h.direction_hint = hint;
h.direction_count = count;
h.candidate = candidate;
h.pending = candidate.map(|c| (c, com, velocity, normal, heading));
}
pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
physics
.board
.hook_mut()
.drive
.enable_angular_only(&mut skater.ground_lifecycle.board_animated_290);
let p = &skater.player_input.processed;
let h = &mut skater.handplant;
h.entry = Trajectory {
position: p.vectors_544_560_592_608[2].map(f32::from_bits),
velocity: p.vectors_544_560_592_608[3].map(f32::from_bits),
acceleration: GRAVITY,
duration: -1.0,
};
h.warped = 0.0;
h.elapsed = 0.0;
h.continuation = true;
h.select_outgoing(&physics.world)?;
Ok(())
}
pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
//82D4C3D8 ->82D91298 disables five upper-body volumes for two frames.
for part in [8, 4, 9, 5, 2] {
skater.skeleton_collision.pending_reenable = true;
skater.skeleton_collision.disable_count[part] = 2;
skater.skeleton_collision.parts[part].enabled = false;
}
let h = &mut skater.handplant;
h.warped += (1.0 + h.settings.time_warp.evaluate(h.warped - h.apex)) * STEP;
h.estimate_apex();
h.elapsed += STEP;
let pose = skater.animated_skeleton.record.pose.map(|local| {
skate_core::physics::skeleton_animation_record::compose_affine(
&skater.animated_skeleton.roots.animation_to_world,
&local,
)
});
let (com, up, heading) = h.values(
&pose,
skater.player_input.processed.vectors_544_560_592_608[2].map(f32::from_bits),
);
skater.air_reckoning.update_plant(
&mut physics.riding,
&skater.player_input.processed,
up,
heading,
);
ik::update(skater, false);
plant_skeleton::advance(physics, skater, com, None)
}
@@ -0,0 +1,242 @@
//! PositionSelector82D63D40, velocity window82D64420 and edge clipping82D651E0.
use super::{GRAVITY, Settings, UP, V, math::*};
use skate_core::animation::foot_ik::transforms::inverse_rigid;
use skate_core::{
air::trajectory::Trajectory,
physics::{grind_contact::Primitive, skeleton_animation_record::IDENTITY},
};
#[derive(Clone, Copy)]
pub(super) struct Candidate {
pub point: V,
pub edge: Primitive,
pub side: i32,
}
pub(super) fn select(
settings: &Settings,
com: V,
velocity: V,
normal: V,
hint: i32,
edges: &[Primitive],
) -> Option<Candidate> {
//82D61D88: ascending, above minimum speed, slope over45deg and |Ny|<=.95.
if !(length(velocity) > settings.minimum_speed
&& velocity[1] > 0.0
&& normal[1].abs() <= 0.95
&& skate_core::trigonometry::acos(normal[1].clamp(-1.0, 1.0)) * f32::from_bits(0x42652ee1)
> settings.minimum_slope)
{
return None;
}
let mut frame = IDENTITY;
if normal[1].abs() <= 0.99 {
frame[0] = normalize(cross(UP, normal));
frame[1] = UP;
frame[2] = cross(frame[0], UP);
}
frame[3] = com;
let inverse = inverse_rigid(&frame);
let mut velocity = rotate(&inverse, velocity);
velocity = [
velocity[0],
(velocity[0] * velocity[0] + velocity[1] * velocity[1]).sqrt(),
0.0,
0.0,
];
if hint != 0 && hint != if velocity[0] > 0.0 { 1 } else { 2 } {
velocity[0] = if hint == 1 { 1.0 } else { -1.0 } * f32::from_bits(0x37800000);
}
let g = settings.window.each_ref().map(|g| g.evaluate(velocity[1]));
let lo_x = g[1].max(velocity[0].abs() - g[5]);
let hi_x = g[6].min(velocity[0].abs() + g[4]);
if hi_x <= lo_x {
return None;
}
let lo_y = g[0].max(velocity[1] - g[3]);
let hi_y = velocity[1] + g[2];
let sign = if velocity[0] > 0.0 { 1.0 } else { -1.0 };
let apex = |x, y| {
let trajectory = Trajectory {
position: point(&inverse, com),
velocity: [x * sign, y, 0.0, 0.0],
acceleration: GRAVITY,
duration: -1.0,
};
let mut p = trajectory.position_at(super::apex_time(trajectory));
p[1] -= settings.window_drop;
p
};
//Native order:r5 lowX/lowY,r6 highX/lowY,r7 lowX/highY,r8 highX/highY.
let [a, b, c, d] = [
apex(lo_x, lo_y),
apex(hi_x, lo_y),
apex(lo_x, hi_y),
apex(hi_x, hi_y),
];
let polygon = if d[0] > c[0] {
[c, d, b, a]
} else {
[d, c, a, b]
};
//82C1EAD8 preserves world order and caps the broadphase result at40.
let mut candidates = Vec::new();
for edge in edges
.iter()
.filter(|e| {
(0..3).all(|i| {
let extent = [2.0, 4.0, 2.0][i];
e.start[i].min(e.end[i]) <= com[i] + extent
&& e.start[i].max(e.end[i]) >= com[i] - extent
})
})
.take(40)
{
let start = point(&inverse, edge.start);
let end = point(&inverse, edge.end);
if normalize(sub(end, start))[0].abs() <= 0.71 {
continue;
} //82D64218
let mut segment = [start, end];
//82D657D0/658A0 retains -depth<localZ<0.
if !clip_depth(&mut segment, -settings.depth, true) || !clip_depth(&mut segment, 0.0, false)
{
continue;
}
let mut valid = true;
for i in 0..4 {
let delta = sub(polygon[(i + 1) % 4], polygon[i]);
let n = normalize([delta[1], -delta[0], 0.0, 0.0]);
let d0 = dot(sub(segment[0], polygon[i]), n);
let d1 = dot(sub(segment[1], polygon[i]), n);
if !clip(&mut segment, d0, d1) {
valid = false;
break;
}
}
if valid {
candidates.push((*edge, segment));
}
}
let mut best = None;
let mut height = -f32::MAX;
let mut depth = -f32::MAX;
for (i, (_, segment)) in candidates.iter().enumerate() {
let p = if segment[0][1] > segment[1][1] {
segment[0]
} else {
segment[1]
};
if p[1] > height + 0.1 || (p[1] > height - 0.1 && p[1] <= height + 0.1 && p[2] > depth) {
best = Some(i);
height = p[1];
depth = p[2];
}
}
let best = best?;
let mut connected = vec![false; candidates.len()];
connected[best] = true;
//82D65BD8: extend both ends across neighbours within .05m and30degrees.
for reverse in [false, true] {
let mut ends = candidates[best].1;
if reverse {
ends.swap(0, 1);
}
loop {
let next = candidates.iter().enumerate().find_map(|(i, (_, s))| {
if connected[i] {
return None;
}
for e in 0..2 {
if dot(sub(ends[1], s[e]), sub(ends[1], s[e])) < f32::from_bits(0x3b23d70a)
&& skate_core::trigonometry::acos(
dot(
normalize(sub(ends[1], ends[0])),
normalize(sub(s[1 - e], s[e])),
)
.clamp(-1.0, 1.0),
)
.abs()
< f32::from_bits(0x3f060a92)
{
return Some((i, [s[e], s[1 - e]]));
}
}
None
});
let Some((i, s)) = next else { break };
connected[i] = true;
ends = s;
}
}
let bottom = scale(add(polygon[2], polygon[3]), 0.5);
let top = scale(add(polygon[0], polygon[1]), 0.5);
let delta = sub(top, bottom);
let target = madd(
delta,
clamp01((height - bottom[1]) * reciprocal(delta[1])),
bottom,
);
let target = [target[0], target[1], 0.0, 0.0];
let mut nearest = [0.0; 4];
let mut distance = f32::MAX;
for (i, (_, segment)) in candidates.iter().enumerate() {
if !connected[i] {
continue;
}
let delta = sub(segment[1], segment[0]);
let square = dot(delta, delta);
let p = if square > f32::from_bits(0x37800000) {
madd(
delta,
clamp01(dot(sub(target, segment[0]), delta) * reciprocal(square)),
segment[0],
)
} else {
segment[0]
};
let square = dot(sub(p, target), sub(p, target));
if square < distance {
distance = square;
nearest = p;
}
}
Some(Candidate {
point: point(&frame, nearest),
edge: candidates[best].0,
side: if velocity[0] > 0.0 { 1 } else { 2 },
})
}
//82D658A0 excludes coplanar segments and avoids dividing near-parallel Z.
fn clip_depth(segment: &mut [V; 2], value: f32, above: bool) -> bool {
let sign = if above { 1.0 } else { -1.0 };
let a = (segment[0][2] - value) * sign;
let b = (segment[1][2] - value) * sign;
if a <= 0.0 && b <= 0.0 {
return false;
}
let delta = segment[1][2] - segment[0][2];
if delta.abs() >= f32::from_bits(0x37800000) {
let t = (value - segment[0][2]) * reciprocal(delta);
if t > 0.0 && t < 1.0 {
let p = madd(sub(segment[1], segment[0]), t, segment[0]);
segment[usize::from(b <= 0.0)] = p;
}
}
true
}
fn clip(segment: &mut [V; 2], a: f32, b: f32) -> bool {
if a < 0.0 && b < 0.0 {
return false;
}
if (a < 0.0) != (b < 0.0) {
let p = madd(
sub(segment[1], segment[0]),
a * reciprocal(a - b),
segment[0],
);
segment[usize::from(a >= 0.0)] = p;
}
true
}
@@ -0,0 +1,90 @@
//! HandPlantManager82D633B0 and Skeleton82BD9728/97D0, shared four-limb IK.
use super::*;
pub(super) fn update(skater: &mut SkaterRuntime, ground: bool) {
let p = &skater.player_input.processed;
let h = &mut skater.handplant;
if ground && (p.flags_2480 & (1 << 29) != 0 || p.flags_2480 & (1 << 28) == 0) {
h.reset_ik();
}
let right = (h.flags & 0x2000_0000 != 0) ^ (p.flags_2476 & (1 << 2) != 0);
let side = usize::from(right);
let limb = 2 + side;
let animated = &skater.animated_skeleton;
let world = |part: usize| {
point(
&animated.roots.animation_to_world,
animated.record.pose[part][3],
)
};
let mut hand = world(3 + side * 4);
let shoulder = world(6 + side * 4);
let attached = h.phase > -h.settings.hand_release;
if !attached {
h.ik_released = true;
}
let shoulder_delta = sub(shoulder, h.anchor);
if !h.ik_released && length(shoulder_delta) > f32::from_bits(0x37800000) {
let axis = normalize(shoulder_delta);
let projection = dot(sub(hand, h.anchor), axis);
if projection <= 0.0 {
h.ik_released = true;
} else {
let on_axis = madd(axis, projection, h.anchor);
if h.ik_latched {
hand = on_axis;
} else {
let radial = sub(hand, on_axis);
let radius = h.settings.hand_radius.evaluate(projection);
if length(radial) > radius {
hand = madd(normalize(radial), radius, on_axis);
}
}
}
let delta = sub(hand, h.anchor);
let distance = length(delta);
if h.ik_distance < 0.0 {
h.ik_distance = distance;
}
h.ik_distance = distance.min(h.ik_distance - h.settings.hand_approach);
if h.ik_distance < f32::from_bits(0x37800000) || distance < f32::from_bits(0x37800000) {
h.ik_released = true;
} else {
hand = madd(normalize(delta), h.ik_distance, h.anchor);
}
if !h.ik_released && !h.ik_latched {
let on_axis = madd(axis, dot(sub(hand, h.anchor), axis), h.anchor);
if length(sub(hand, on_axis)) < 0.02 {
h.ik_latched = true;
}
}
}
if h.ik_released {
hand = h.anchor;
}
let delta = sub(hand, shoulder);
if length(delta) > 0.65 {
hand = madd(normalize(delta), 0.65, shoulder);
}
h.ik_blend = clamp01(
h.ik_blend
+ if attached {
STEP / h.settings.hand_into
} else {
-STEP / h.settings.hand_out
},
);
if h.ik_blend > 0.0 {
//82BD9728 clamps against the authored reparented hand target.
let original = point(
&animated.roots.animation_to_world,
animated.targets[limb][3],
);
let delta = sub(hand, original);
if length(delta) > 0.65 {
hand = madd(normalize(delta), 0.65, original);
}
skater.foot_ik.state.external_targets[limb].world_position = hand;
skater.foot_ik.state.limbs[limb].external_target_set = true;
skater.foot_ik.state.limbs[limb].target_blend = h.ik_blend;
}
}
@@ -0,0 +1,50 @@
//! CreateMtxFromHeadingAndUp82D61B60 and axis-angle frame blend82BD3150.
use super::*;
use skate_core::animation::foot_ik::transforms::inverse_rigid;
use skate_core::math::Basis3;
use skate_core::physics::{
drive_frames::retail_quaternion_from_basis, skeleton_animation_record::compose_affine,
};
pub(super) fn frame(mut heading: V, up: V) -> [V; 4] {
if dot(heading, up).abs() > f32::from_bits(0x3f7fff58) {
heading = [1.0, 0.0, 0.0, 0.0];
if dot(heading, up).abs() > f32::from_bits(0x3f7fff58) {
heading = [0.0, 0.0, 1.0, 0.0];
}
}
let right = normalize(cross(up, heading));
let forward = normalize(cross(right, up));
[right, normalize(cross(forward, right)), forward, [0.0; 4]]
}
pub(super) fn blend(a: [V; 4], b: [V; 4], weight: f32) -> [V; 4] {
if weight >= 1.0 {
return b;
}
if weight <= 0.0 {
return a;
}
let relative = compose_affine(&b, &inverse_rigid(&a));
let q = retail_quaternion_from_basis(Basis3 {
columns: std::array::from_fn(|i| relative[i][..3].try_into().unwrap()),
});
let sign = if q.w < 0.0 { -1.0 } else { 1.0 };
let angle = 2.0 * skate_core::trigonometry::acos((q.w * sign).clamp(-1.0, 1.0));
let mut result = a;
if angle < f32::from_bits(0x3d0efa35) {
for i in 0..3 {
result[i] = normalize(madd(sub(b[i], a[i]), weight, a[i]));
}
} else {
let axis = normalize([q.x * sign, q.y * sign, q.z * sign, 0.0]);
let (sin, cos) = skate_core::trigonometry::sin_cos(angle * weight);
for i in 0..3 {
result[i] = madd(
axis,
dot(axis, a[i]) * (1.0 - cos),
madd(cross(axis, a[i]), sin, scale(a[i], cos)),
);
}
}
result[3] = madd(sub(b[3], a[3]), weight, a[3]);
result
}
@@ -0,0 +1,96 @@
//! TU3 cached layouts55B5940075F78842 and0F38084037CB9175.
use skate_core::point_graph::PointGraph;
use skate_data::collections::Collections;
pub(super) struct Settings {
pub window: [PointGraph<4>; 7],
pub depth: f32,
pub window_drop: f32,
pub time_warp: PointGraph<8>,
pub out_heading: PointGraph<8>,
pub into_rotation: PointGraph<8>,
pub out_rotation: PointGraph<8>,
pub hand_radius: PointGraph<4>,
pub curve_half_time: f32,
pub entry_blend: f32,
pub hand_out: f32,
pub hand_into: f32,
pub hand_release: f32,
pub minimum_speed: f32,
pub minimum_slope: f32,
pub rotation_time: f32,
pub committed_time: f32,
pub minimum_out_speed: f32,
pub hand_approach: f32,
pub direction_frames: i32,
pub apex_radius: f32,
pub apex_angle: f32,
pub animation: [f32; 3],
pub truck_distance: f32,
}
impl Settings {
pub fn load(data: &Collections) -> Result<Self, String> {
let root = "Hash_55B5940075F78842";
let graph4 = |class, name| -> Result<PointGraph<4>, String> {
let w = data
.words::<12>(class, "default", name)?
.map(f32::from_bits);
Ok(PointGraph {
x: w[4..8].try_into().unwrap(),
y: w[8..12].try_into().unwrap(),
})
};
let graph8 = |name| -> Result<PointGraph<8>, String> {
let w = data
.words::<20>("physics_handplantmanager", "default", name)?
.map(f32::from_bits);
Ok(PointGraph {
x: w[4..12].try_into().unwrap(),
y: w[12..20].try_into().unwrap(),
})
};
let f = |name| data.float("physics_handplantmanager", "default", name);
Ok(Self {
truck_distance: data.float("physics_grinds", "default", "DeckCenterToTruck")?,
window: [
graph4(root, "Hash_FF0DE8A42210E8C0")?,
graph4(root, "Hash_B90C78786ED04A7D")?,
graph4(root, "Hash_DC7947C6DD03CDAA")?,
graph4(root, "Hash_0ACB7AAE2662FBB2")?,
graph4(root, "Hash_CF7F668AB995380D")?,
graph4(root, "Hash_8348DA199CC87B7A")?,
graph4(root, "Hash_F27F622CAE33BD50")?,
],
depth: data.float(root, "default", "Hash_3D9138A04C6D43F6")?,
window_drop: data.float(root, "default", "Hash_A046D0516C0BD62B")?,
time_warp: graph8("Hash_F551DF3289124FB0")?,
out_heading: graph8("Hash_8FC9A4F9002CF9E0")?,
into_rotation: graph8("Hash_F61B0FB73C39311B")?,
out_rotation: graph8("Hash_D4EF764AF4D5F1CA")?,
hand_radius: graph4("physics_handplantmanager", "Hash_5494E3F9BE572D6B")?,
curve_half_time: f("Hash_2460D6544C3B64B7")?,
entry_blend: f("Hash_CCD7A82CE1BBFFF2")?,
hand_out: f("Hash_E0895C189D77CE16")?,
hand_into: f("Hash_01EB855A0ACBD84C")?,
hand_release: f("Hash_9C02B9E6998D1321")?,
minimum_speed: f("Hash_63303CA6C611DC79")?,
minimum_slope: f("Hash_D1622917F6DD215F")?,
rotation_time: f("Hash_D595A32C5F02178F")?,
committed_time: f("Hash_B5EC5A0941B407A4")?,
minimum_out_speed: f("Hash_49C91803479B9BED")?,
hand_approach: f("Hash_7C8B87F7435221C4")?,
direction_frames: data.words::<1>(
"physics_handplantmanager",
"default",
"Hash_DDE5BFBCB6A1AFD5",
)?[0] as i32,
apex_radius: f("Hash_A046D0516C0BD62B")?,
apex_angle: f("Hash_90CDD541098AB4B5")?,
animation: [
data.float("anim_handplant", "default", "Hash_B9012B86EB5258FE")?,
data.float("anim_handplant", "default", "Hash_512342FB1D4A7D57")?,
data.float("anim_handplant", "default", "Hash_673F3B1BD056C39E")?,
],
})
}
}
@@ -0,0 +1,269 @@
//! Handplant Launch82D613B0, trajectory selector82D66318/66948 and COM82D625C0.
use super::*;
use skate_core::{air::trajectory::QueryRequest, physics::board_world::BoardWorld};
impl Handplant {
pub(super) fn launch(
&mut self,
c: contact::Candidate,
com: V,
velocity: V,
normal: V,
body_heading: V,
heading: V,
) {
let mut side = normalize(cross(sub(c.edge.end, c.edge.start), UP));
if dot(side, normal) > 0.0 {
side = scale(side, -1.0);
}
self.direction = scale(side, -1.0);
let (sin, cos) = skate_core::trigonometry::sin_cos(self.settings.apex_angle);
let apex = madd(
side,
self.settings.apex_radius * cos,
madd(UP, self.settings.apex_radius * sin, c.point),
);
self.initial = to_apex(com, apex);
self.apex = apex_time(self.initial);
self.warped = 0.0;
self.outgoing = [self.initial; 2];
self.travel_sign = if dot(heading, velocity) > 0.0 {
1.0
} else {
-1.0
};
//830BD4A0=-1 and830BD320=1; signs follow native vsel order.
self.rotations[0] = rotation::frame(heading, normal);
self.rotations[0][3] = com;
let mut along = normalize(sub(c.edge.start, c.edge.end));
let direction = if dot([along[0], 0.0, along[2], 0.0], sub(apex, com)) > 0.0 {
1.0
} else {
-1.0
};
along = scale(along, direction * self.travel_sign);
self.rotations[1] = rotation::frame(along, normalize(sub(c.point, apex)));
self.rotations[1][3] = apex;
self.rotations[2] = rotation::frame(scale(UP, -self.travel_sign), normal);
self.rotations[3] = self.rotations[2];
self.estimate_apex();
let changed = length(sub(self.previous_candidate_point, c.point)) > 0.15;
let front = dot(sub(c.point, com), body_heading) < 0.0;
self.flags = (self.flags & 0x0fff_ffff)
| 0x8000_0000
| if changed { 0x4000_0000 } else { 0 }
| if front { 0x2000_0000 } else { 0 }
| if self.phase < self.settings.committed_time {
0x1000_0000
} else {
0
};
self.anchor = c.point;
self.candidate = Some(c);
}
pub(super) fn select_outgoing(&mut self, world: &BoardWorld) -> Result<(), String> {
let candidate = self
.candidate
.ok_or("Handplant entry requires the selected coping")?;
let apex = self.initial.position_at(self.apex);
let incoming = self.initial.velocity_at(self.apex);
let across = scale(self.direction, dot(incoming, self.direction));
let tangent = sub(incoming, across);
let along = normalize(tangent);
let speed = length(add(across, clamp_length(tangent, 2.0)));
if candidate.side != 0 {
let expected = scale(
cross(UP, self.direction),
if candidate.side == 1 { 1.0 } else { -1.0 },
);
self.continuation = dot(expected, along) >= 0.0;
}
let mut position = madd(self.direction, 0.1, self.anchor);
position[1] = apex[1];
let mut selected = None;
let mut best_up = 1.1;
let mut final_trajectory = EMPTY;
for angle in [
0.0,
0.24,
0.48,
f32::from_bits(0x3f3851eb),
0.96,
f32::from_bits(0x3f999999),
] {
let (sin, cos) = skate_core::trigonometry::sin_cos(angle);
let velocity = scale(madd(self.direction, sin, scale(along, cos)), speed);
let trajectory = Trajectory {
position,
velocity,
acceleration: GRAVITY,
duration: 3.0,
};
final_trajectory = trajectory;
let hit = super::super::air_trajectory::AirTrajectoryRuntime::query(
world,
QueryRequest {
trajectory,
radius: 0.2,
start_error: 1.0,
end_error: 1.0,
},
)?;
if hit.contact_time < 0.0 {
continue;
}
let point = hit.contact_position; //PredictionResults::GetPosition82D2D988.
if self.anchor[1] - point[1] <= 0.5 {
continue;
}
let normal = hit.landing_normal;
if normal[1] < 0.71 {
selected = Some((point, normal));
break;
}
if normal[1] < best_up {
best_up = normal[1];
selected = Some((point, normal));
}
}
let (landing, normal) = selected.unwrap_or((final_trajectory.position_at(3.0), UP));
let destination = madd(normal, 0.56, landing);
let mut outgoing = from_apex(apex, destination);
let fall_time = outgoing.duration;
//82D66D70 rebase the outgoing parabola to the incoming time origin.
outgoing.position = outgoing.position_at(-self.apex);
outgoing.velocity = outgoing.velocity_at(-self.apex);
outgoing.duration = -1.0;
self.outgoing = [outgoing; 2];
let speed = dot(self.direction, self.outgoing[1].velocity);
if speed < self.settings.minimum_out_speed {
self.outgoing[1].velocity = madd(
self.direction,
self.settings.minimum_out_speed - speed,
self.outgoing[1].velocity,
);
}
self.rotations[2][3] = outgoing.position_at(2.0 * self.apex);
let time = self.apex + fall_time;
self.rotations[3] = rotation::frame(
scale(normalize(outgoing.velocity_at(time)), self.travel_sign),
normal,
);
self.rotations[3][3] = outgoing.position_at(time);
self.out_duration = (time - self.phase).max(0.5);
self.build_curve();
Ok(())
}
fn build_curve(&mut self) {
//82D62408: secant handles one fixed tick apart, scaled by6.
let start = self.apex - self.settings.curve_half_time;
let end = self.apex + self.settings.curve_half_time;
self.curve[0] = self.initial.position_at(start);
self.curve[1] = madd(
sub(self.initial.position_at(start + STEP), self.curve[0]),
6.0,
self.curve[0],
);
self.curve[3] = self.outgoing[1].position_at(end);
self.curve[2] = madd(
sub(self.outgoing[1].position_at(end - STEP), self.curve[3]),
6.0,
self.curve[3],
);
}
pub(super) fn values(&mut self, pose: &[[V; 4]], physical_com: V) -> (V, V, V) {
let relative = self.warped - self.apex;
let fraction = (1.0 + relative * reciprocal(self.settings.curve_half_time)) * 0.5;
let mut com = if fraction < 0.0 {
self.initial.position_at(self.warped)
} else if fraction > 1.0 {
let blend =
clamp01((relative - self.settings.curve_half_time) * reciprocal(self.out_duration));
madd(
self.outgoing[0].position_at(self.warped),
blend,
scale(self.outgoing[1].position_at(self.warped), 1.0 - blend),
)
} else {
bezier(self.curve, fraction)
};
let foot_y = pose[15][3][1].max(pose[19][3][1]);
let (divisor, offset) = if self.flags & 0x2000_0000 != 0 {
(5.0, 0.0)
} else {
(3.2, -0.05)
};
com[1] += ((foot_y - physical_com[1]) / divisor + offset).max(0.0);
let blend = (1.0 - self.elapsed * reciprocal(self.settings.entry_blend)).max(0.0);
let blended = madd(
com,
1.0 - blend,
scale(self.entry.position_at(self.elapsed), blend),
);
if blend > 0.0 && fraction < 0.0 {
let velocity = self.initial.velocity_at(self.warped);
let correction =
dot(sub(blended, com), normalize(velocity)) * reciprocal(length(velocity));
self.warped += correction.clamp(f32::from_bits(0xbc088889), f32::from_bits(0x3c088889));
}
let frame = if self.warped < self.apex {
let fraction = (1.0
- (self.apex - self.warped)
* reciprocal(self.apex.min(self.settings.rotation_time)))
.max(0.0);
rotation::blend(
self.rotations[0],
self.rotations[1],
self.settings.into_rotation.evaluate(fraction),
)
} else {
let fraction = self.warped - self.apex;
let out = self
.settings
.out_heading
.evaluate(clamp01(fraction * reciprocal(self.out_duration)));
let into = self
.settings
.out_rotation
.evaluate(clamp01(fraction * reciprocal(self.settings.rotation_time)));
rotation::blend(
self.rotations[1],
rotation::blend(self.rotations[2], self.rotations[3], out),
into,
)
};
(blended, frame[1], frame[2])
}
}
fn to_apex(start: V, end: V) -> Trajectory {
let t = ((-2.0 * GRAVITY[1]) * (end[1] - start[1])).sqrt() * reciprocal(-GRAVITY[1]);
arc_between(start, end, t)
}
fn from_apex(start: V, end: V) -> Trajectory {
let t = ((-2.0 * GRAVITY[1]) * (start[1] - end[1])).sqrt() * reciprocal(-GRAVITY[1]);
arc_between(start, end, t)
}
fn arc_between(start: V, end: V, time: f32) -> Trajectory {
let velocity = scale(
sub(sub(end, start), scale(GRAVITY, 0.5 * time * time)),
reciprocal(time),
);
Trajectory {
position: start,
velocity,
acceleration: GRAVITY,
duration: time,
}
}
fn bezier(p: [V; 4], t: f32) -> V {
let s = 1.0 - t;
madd(
p[3],
t * t * t,
madd(
p[2],
3.0 * t * t * s,
madd(p[0], s * s * s, scale(p[1], 3.0 * t * s * s)),
),
)
}
@@ -58,6 +58,8 @@ pub(super) fn advance(
skeleton_input: &mut skater.skeleton_input,
animated: &mut skater.animated_skeleton,
skeleton_air: &mut skater.skeleton_air,
footplant: &mut skater.footplant,
handplant: &mut skater.handplant,
body: &mut skater.skeleton,
drives: &mut skater.skeleton_drives,
ik: &mut skater.foot_ik,
@@ -147,6 +149,8 @@ struct Callbacks<'a, 'p> {
skeleton_input: &'a mut SkeletonInputRuntime,
animated: &'a mut AnimatedSkeleton,
skeleton_air: &'a mut super::skeleton_air::SkeletonAir,
footplant: &'a mut super::footplant::Footplant,
handplant: &'a mut super::handplant::Handplant,
body: &'a mut SkeletonBody,
drives: &'a mut SkeletonDrives,
ik: &'a mut FootIk,
@@ -44,6 +44,8 @@ impl Callbacks<'_, '_> {
self.wiping_out,
);
self.skeleton_air.reset_board(); //Board Reset82C0606C.
self.footplant.full_reset();
self.handplant.full_reset();
self.riding.reset_for_teleport();
{
let mut owners = SkeletonOwners {
@@ -294,6 +294,7 @@ impl KnownAirRuntime for Runtime<'_> {
body: &self.skater.skeleton,
gravity: [g.x, g.y, g.z, 0.0],
world: &self.physics.world,
edges: &self.physics.grind.primitives,
};
self.skater.footplant.update_candidate(input, &f);
}
@@ -309,6 +310,7 @@ impl KnownAirRuntime for Runtime<'_> {
body: &self.skater.skeleton,
gravity: [g.x, g.y, g.z, 0.0],
world: &self.physics.world,
edges: &self.physics.grind.primitives,
};
let result = self.skater.footplant.consume_and_submit(
input,
@@ -0,0 +1,91 @@
//! Skeleton82BDEC48 (anchored foot) and82BDF090 (plant COM).
use super::{GamePhysics, SkaterRuntime, input_phase, skeleton_input_runtime::SkeletonOwners};
use skate_core::{
math::Vector3,
physics::{
board::BodyId,
board_animation::target_velocity,
skeleton_air_frames::update_plant_roots,
skeleton_animation_record::{IDENTITY, compose_affine},
},
};
pub(super) fn advance(
physics: &mut GamePhysics,
skater: &mut SkaterRuntime,
anchor: [f32; 4],
bone: Option<usize>,
) -> Result<(), String> {
let collision = input_phase::collision(skater);
let p = &mut skater.player_input.processed;
let s = &mut skater.animated_skeleton;
let local = bone.map_or(s.record.centre_of_mass, |i| {
skater.skeleton_input.drive_frames[i][3]
});
update_plant_roots(
&mut s.roots,
&physics.riding.reckoning_frames.system,
anchor,
local,
);
s.motion.trajectory = IDENTITY;
s.motion.inverse_trajectory = IDENTITY;
let local_board = if bone.is_some() {
&s.record.pose[0]
} else {
&skater.skeleton_input.drive_frames[0]
};
let target = compose_affine(&s.roots.animation_to_world, local_board);
p.flags_2468 |= 1 << 19;
s.board_frames.animation_target = target;
let effective = skater
.skeleton_air
.apply_board(&mut physics.board, &target, true);
s.board_frames.physical_board = effective;
s.board_frames.skate_root = effective;
if bone.is_none() {
let pos = physics.board.bodies()[BodyId::Deck.index()].rates.position;
let velocity = target_velocity(effective[3], [pos.x, pos.y, pos.z, 0.0], p.timestep_2604);
for body in physics.board.bodies_mut() {
body.rates.linear_velocity = Vector3::new(velocity[0], velocity[1], velocity[2]);
}
}
s.board_frames.update_com_lift(
&s.roots.animation_to_world,
s.board_frames.centre_of_mass,
0.0,
);
let mut owners = SkeletonOwners {
animated: s,
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.general_update(
p,
&mut owners,
&skater.animation.packet.hierarchy,
&collision,
physics.settings.step.simulation,
)?;
owners.animated.finish_ground();
owners.animation_input.fields.flags2468 = p.flags_2468;
Ok(())
}
pub(super) fn hold_foot(skater: &mut SkaterRuntime, right: bool, position: [f32; 4], frames: u32) {
let side = usize::from(right);
skater.foot_ik.state.external_targets[side].world_position = position;
skater.foot_ik.state.limbs[side].external_target_set = true;
skater.foot_ik.state.limbs[side].target_blend = 1.0;
if frames != 0 {
for part in if right { [19, 20, 21] } else { [15, 16, 17] } {
skater.skeleton_collision.pending_reenable = true;
skater.skeleton_collision.disable_count[part] = frames;
skater.skeleton_collision.parts[part].enabled = false;
}
}
}
@@ -15,7 +15,8 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
| PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant
| PhysicalStateId::GroundAnimation
| PhysicalStateId::SlideGround
| PhysicalStateId::WipeoutGround
| PhysicalStateId::Teleporting
@@ -16,7 +16,8 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
| PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant
| PhysicalStateId::GroundAnimation
| PhysicalStateId::SlideGround
| PhysicalStateId::WipeoutGround
| PhysicalStateId::Teleporting
@@ -61,6 +62,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
set(78, skater.wipeout.requests_runout(p));
set(71, player.flags_1296 & (1 << 24) != 0);
set(73, p.flags_2476 & (1 << 24) != 0);
set(74, state == PhysicalStateId::HandPlant && skater.handplant.continuation);
set(75, state.category() == 500);
set(76, state as u32 == 500);
//82DB78A8..78DC: the processed off-board request refreshes three
@@ -75,6 +77,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
set(77, dismount_requested);
set(79, p.state_variant_index_2528 == 3);
set(80, p.flags_2484 & (1 << 21) != 0);
set(83, skater.footplant.perform || skater.footplant.flag_627);
if let Some(output) = &output {
set(84, output.state_28.flag_84);
set(86, output.state_28.has_world_grab_intent_without_object);
@@ -87,6 +90,11 @@ 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;
physical.component_1832_word_1876=skater.handplant.flags;
physical.air.handplant_position_304=skater.handplant.anchor.map(f32::to_bits);
physical.air.handplant_time_320=skater.handplant.phase;
physical.air.flag_446=u8::from(skater.handplant.flags&0x8000_0000!=0);
skater.footplant.publish(&mut physical.air);
if physics.grind.active {
physical.grinds.words_136_140 = [physics.grind.kind, 1];
physical.grinds.flag_318 = u8::from(physics.grind.front_contact);
@@ -120,6 +128,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
physical.state = CurrentStateFields {
category_12: state.category(),
state_16: state as u32,
flag_74: u8::from(state==PhysicalStateId::HandPlant && skater.handplant.continuation),
//ProcessOutput82DB7104..7128: selector57 requests State69 here.
//The next input/selection enters702; never reset inside selection.
flag_69: u8::from(skater.player_state.selector.request_teleport),
@@ -18,6 +18,7 @@ impl PhysicalStateCalls for Calls {
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant
| PhysicalStateId::GroundAnimation
| PhysicalStateId::SlideGround
| PhysicalStateId::WipeoutGround
@@ -34,6 +35,7 @@ impl PhysicalStateCalls for Calls {
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant
| PhysicalStateId::GroundAnimation
| PhysicalStateId::SlideGround
| PhysicalStateId::WipeoutGround
@@ -62,7 +64,8 @@ pub(super) fn set(
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
| PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant
| PhysicalStateId::GroundAnimation
| PhysicalStateId::SlideGround
| PhysicalStateId::WipeoutGround
| PhysicalStateId::Teleporting
@@ -74,6 +77,7 @@ pub(super) fn set(
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant
| PhysicalStateId::GroundAnimation
| PhysicalStateId::SlideGround
| PhysicalStateId::WipeoutGround
@@ -157,6 +161,9 @@ 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::HandPlant => skater.handplant.reset(),
PhysicalStateId::FootPlant => skater.footplant.reset(), //Exit82D4C5A8
PhysicalStateId::Boneless => {}, //empty82D4C9B4
PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | 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),
@@ -172,6 +179,9 @@ pub(super) fn set(
_ => unreachable!("state support checked before publication"),
}
match requested {
PhysicalStateId::HandPlant => super::super::handplant::enter(physics, skater),
PhysicalStateId::FootPlant => super::super::footplant::ground::enter(physics, skater),
PhysicalStateId::Boneless => super::super::boneless::enter(physics, skater),
PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindTipslide | PhysicalStateId::GrindFiveO | PhysicalStateId::GrindBackslash | PhysicalStateId::GrindDarkslide | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater),
PhysicalStateId::BipedAir => super::super::offboard::air_state::enter(physics, skater),
PhysicalStateId::BipedGround => {
+4
View File
@@ -45,6 +45,8 @@ pub(crate) struct SkaterRuntime {
pub slide_state: super::slide_state::SlideState,
pub trajectory: super::air_trajectory::AirTrajectoryRuntime,
pub footplant: super::footplant::Footplant,
pub boneless: super::boneless::Boneless,
pub handplant: super::handplant::Handplant,
pub wipeout: super::wipeout::Wipeout,
pub wipeout_state: super::wipeout_states::WipeoutState,
pub teleport_state: super::teleport_state::Runtime,
@@ -206,6 +208,8 @@ impl SkaterRuntime {
slide_state: super::slide_state::SlideState::load(&data)?,
trajectory: super::air_trajectory::AirTrajectoryRuntime::load(&data)?,
footplant: super::footplant::Footplant::load(&data)?,
boneless: super::boneless::Boneless::load(&data)?,
handplant: super::handplant::Handplant::load(&data)?,
wipeout: super::wipeout::Wipeout::load(&data)?,
wipeout_state,
teleport_state: super::teleport_state::Runtime::new(super::teleport_state::Checkpoint {
+7
View File
@@ -94,6 +94,13 @@ pub(super) fn check_after_physics(
grind_normal: skater.trajectory.selector.grind_normal(),
};
match skater.player_state.current() {
PhysicalStateId::FootPlant | PhysicalStateId::HandPlant => {
wipeout::check_plant(&mut skater.wipeout.state, &skater.wipeout.settings, &observations.frame()?);
if skater.player_state.current() == PhysicalStateId::FootPlant {
super::footplant::ground::post_physics(skater);
}
Ok(())
}
PhysicalStateId::BipedAir => {
let mode = skater.wipeout.mode(&skater.player_input.processed)?;
let frame = observations.frame()?;
+110
View File
@@ -0,0 +1,110 @@
# No-complys, footplants and handplants
The stock graphs now reach physical states 602 (no-comply/boneless/fastplant),
601 (air footplant) and 600 (handplant). The implementation connects the TU3
contact selectors, authored animation events, VLT curves, board drives, skeleton
roots, limb IK, release trajectories and state publication. `IsFootPlanting` is
registered in both graphs so its formerly unsupported leaf no longer pauses the
one-foot path.
## Controller controls
These are the existing Xbox mappings and stock graph transitions. Stance and
contact determine which animation and physical state can enter.
| Action | Input |
| --- | --- |
| No-comply | Begin a front-foot push (X in regular, A in goofy), then flick the right stick for an ollie during push entry. The graph accepts the Square gesture group. |
| Boneless / fastplant | Hold one grab trigger fully, then press A or X. The chosen trigger, push foot and stance select the stock variant. |
| Air footplant | Ollie, hold LT or RT for a grab, then press and hold A or X for a one-foot air while approaching a reachable surface. Contact prediction and the extended foot determine entry. |
| Handplant | Hold RB while riding up a steep transition toward reachable coping. Move the right stick to tweak; A/X select one-foot variants and B selects the no-foot variant. |
No-comply launches come from the clip's `FootJump` event. Footplants release at
the manager's calculated curve duration. Handplant entry requires upward travel,
the authored minimum speed/slope, a selected edge and a valid surface
investigation. Holding RB on flat ground does not force a handplant.
## Local build
Run `LaunchPlants.bat` in the repository root. It uses
`bin/plants/skate3rust.exe`, the absolute installed asset directory, and
`--test-world`. Additional command-line arguments are forwarded. The default
test course is useful for no-complys and reachable footplant surfaces; handplants
need transition/coping geometry satisfying the native selector. An installed
converted map can be chosen in the map menu or supplied with `--map`.
The task executable is a separate copy, built with:
```powershell
$env:CARGO_TARGET_DIR = 'C:/Users/Daddy/Documents/skate3-imported/target'
$env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = '-C target-feature=+crt-static'
cargo build --release --locked --target x86_64-pc-windows-msvc -p skate-game --no-default-features
```
The shared target is retained. Copy its `x86_64-pc-windows-msvc/release/skate3rust.exe`
to `bin/plants/skate3rust.exe` after rebuilding this branch. No installed or
running desktop executable is replaced. The launcher contains a machine-local
asset path; other installations must update that path.
If another worktree has populated this shared target with different workspace
crates, refresh the modification times of `crates/skate-core/src/lib.rs`,
`crates/skate-data/src/lib.rs` and `crates/skate-game/src/main.rs` before building.
That forces the local crates to rebuild without deleting the dependency cache.
## Native provenance
Evidence is the owned TU3 executable image `default_82000000_011B0000.bin`, base
`82000000`, SHA256
`f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`, generated PPC
instruction listings, stock OnBoard graphs, OnBoard.abin, and skaterschema/VLT.
Registration strings and vtables identify the graph leaves. VMX128 instruction
operands take precedence over unreliable decompiler output and community names.
| Behavior | TU3 evidence |
| --- | --- |
| Input | `8259A554/8259AF60` emits GrabWorld from packed bit28; `8259BB18` emits five handplant controls, named by constructors `82F84F28..82F84F98`. Existing A/X pushes feed the authored no-comply and one-foot paths. |
| No-comply / boneless | State602 vtable `82327434`; entry `82D4C900`, update `82D4C9B8`, launch `82D4CB18/82D4CC18`. Anchors the completed physical toe, uses the stock linear/angular drive modes and four launch curves. |
| Footplant | State601 vtable `82327400`; manager Start `82D704B0`, adjustment `82D70CE0`, update `82D70E40`, publication `82D71040`, nearby-edge refinement `82D6FBB0`. Foot/toe volume setup `82D91330`; postphysics `82D4C6D8`. |
| Handplant contact | Manager `82D61040`; ground query `82D38430`, admission `82D61D88`, position selector `82D63D40`, frame/window/edge clipping `82D640F0..82D65D58`. Uses actual world grind primitives, capped at 40 broadphase candidates. |
| Handplant motion | Launch `82D613B0`; six outgoing queries `82D66318`, selection `82D66948`; arc constructors `82D60770/82D608A0`; COM `82D625C0`, Bezier `82D62408`, orientation `82D62C88`, time warp `82D631A8/82D63280`. Collision position accessor `82D2D988` and landing-normal accessor `82D2D9E8`. |
| Handplant skeleton | State600 vtable `823273CC`; entry/update `82D4C350/82D4C3D8`; reckoning `82D8E1C0`; hand IK `82D633B0`, target clamp `82BD9728/82BD97D0`, upper-body volume suppression `82D91298`. |
| Shared skeleton | Anchored-foot path `82BDEC48`, COM path `82BDF090`, root positioning `82BDEEB8/82BE0E80`. Uses the existing completed animation, physical skeleton, board and four-limb IK owners. |
| Graph timing | `IsFootPlanting` `82BBD888`; preparation `82BBD7F0`; FootPlantAbsorb Update `82BBE4E0`; handplant phase/new-position `82BBDA98/82BBDB98`; antic Begin `82BBE5D0`; handplant scoring Update `82BBF758`. |
| Lifecycle | Footplant release `82D8B970` reads big-endian byte2480 bit0, hence full-word bit24. Common handplant publication `82DB7130..82DB7190`; shared plant wipeout check `82D8FDC0`. Teleport/reset paths clear active plant state. |
Settings are loaded from the installation, using the schema's cached layout:
| Collection class | Used data |
| --- | --- |
| `CCB95A83C78B4FF9/default` | Four PointNegGraphData8 launch curves for state602. |
| `physics_footplantmanager/default` (`172D553937D8E07C`) | Radial speed curve; contact/deck dimensions; leg lengths; Bezier handles; minimum/maximum plant duration; release and wipeout limits. |
| `55B5940075F78842/default` | Seven PointGraph4 query-window curves; depth and vertical drop. |
| `physics_handplantmanager/default` (`0F38084037CB9175`) | Time warp; orientation/heading curves; hand-radius curve; entry/release/IK timings; admission thresholds; apex and outgoing-velocity parameters. |
| `anim_handplant/default` (`F190522E738EC5CB`) | Out/into/antic thresholds at offsets0/4/8. |
The antic blend reads actual clip lengths: `INVERT_HANDPLANT_FS_0_ANTIC` is
0.2 seconds and `INVERT_HANDPLANT_FS_0_ANTIC_LATE` is 1/30 second. FootPlantAbsorb
publishes `absorblength` from the physical manager; no host animation timer or
extra jump impulse selects these launches.
## Validation and boundaries
Release compilation succeeded. Static inspection checked all 32 operation names
in the seven plant templates against source registrations, the five stock
collection classes and point-graph layouts, both antic clip lengths, and the
`FootJump` attributes in the no-comply/boneless/fastplant clips. This is not a
runtime graph-coverage or gameplay-parity result.
The game, launcher, recomp and gameplay harnesses were not executed. In-game
entry/release, contacts, stance combinations, wipeouts, visuals and preservation
of skating/off-board/grind behavior remain for the user's validation.
Queries use the engine's existing synchronous static-world collision and grind
geometry adapters. The outgoing batch is consumed on handplant entry rather
than using TU3's asynchronous job-completion gate and redundant ground-probe
completion dependency. Moving-object coping is not supplied by this adapter.
Scalar vector/trigonometric helpers and quaternion-derived axis-angle rotation
reuse existing engine math; bit-exact Xenon/VMX execution is not claimed.
Handplant scoring updates the existing internal score packet, whose presentation
remains outside this change. Stock assets and private research/build files are
excluded from the commit.