Connect difficulty-dependent native spline acquisition and airborne grind adjustment

This commit is contained in:
chazmm committed 2026-09-07 21:35:54 +01:00
1 parent 40eb5b5005
commit 9575567a1f
19 files changed
+846 -110

No files matched your search

+16 -1
View File
@@ -1,6 +1,6 @@
//! TU3 rail-trajectory candidate leaves82D60C80/82D60B98,
//! 82D6A398 and82D6A168. Admission and physical adjustment are separate owners.
use super::{Prediction, Trajectory, math::*};
use super::{math::*, Prediction, Trajectory};
use crate::{physics::grind_contact::Primitive, point_graph::PointGraph};
#[derive(Clone, Copy, Debug)]
@@ -326,3 +326,18 @@ pub fn take_best_grind(
}
chosen.map(|i| candidates.remove(i))
}
/// A real world query and its accepted target, retained through selector scoring.
#[derive(Clone, Copy, Debug)]
pub struct GrindTarget {
pub air_limits: [Vector; 3],
pub edge: Primitive,
pub point: Vector,
pub normal: Vector,
}
#[derive(Clone, Copy, Debug)]
pub struct GrindEvaluation {
pub target: Option<GrindTarget>,
pub score: f32,
pub distance: f32,
}
@@ -4,7 +4,7 @@ use super::{grind::GrindSurfaceEvidence, math::*};
use crate::math::Vector3;
use crate::physics::{
board_world::BoardWorld,
ground_hang_geometry::{HangGeometryInput, hang_lines},
ground_hang_geometry::{hang_lines, HangGeometryInput},
};
#[derive(Clone, Copy, Debug)]
@@ -113,3 +113,56 @@ fn rotate(axis: Vector, value: Vector, angle: f32) -> Vector {
fn xyz(v: Vector) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
impl GrindSurface {
///82C21930's three angular reference directions for GrindAirAdjust.
pub fn air_limits(
&self,
start: Vector,
end: Vector,
point: Vector,
board_position: Vector,
) -> [Vector; 3] {
let rail = normalize(sub(end, start));
let up = normalize(cross(rail, cross(UP, rail)));
let side = cross(up, rail);
if self.evidence.kind == 0 {
let sign = if dot(side, sub(board_position, point)) > 0.0 {
1.0
} else {
-1.0
};
let oriented = scale(side, sign);
let axis = cross(oriented, up);
[
side,
rotate(axis, oriented, f32::from_bits(0x3eb2b8c3)),
rotate(axis, oriented, f32::from_bits(0x401a25c2)),
]
} else {
let sign = if dot(side, self.evidence.side) > 0.0 {
1.0
} else {
-1.0
};
let axis = cross(side, up);
[
if self.evidence.kind == 1 {
side
} else {
cross(self.normal, rail)
},
rotate(
scale(axis, -sign),
scale(side, -sign),
f32::from_bits(0x3eb2b8c3),
),
rotate(
scale(axis, sign),
scale(side, sign),
f32::from_bits(0x3f3ba866),
),
]
}
}
}
+24 -11
View File
@@ -1,8 +1,6 @@
//! TU3 scoring82D68FE8/82D68AF0/82D69790 for a world with no grind edges.
//! That topology is supplied by the authored BoardWorld, not inferred from a
//! failed collision. Every wall probe still queries real world geometry.
//! TU3 scoring82D68FE8/82D68AF0/82D69790 with world-supplied grind admission.
use super::{
Prediction, SelectorInput, SelectorSettings, SurfaceHit, launch::adjust_trajectory, math::*,
launch::adjust_trajectory, math::*, Prediction, SelectorInput, SelectorSettings, SurfaceHit,
};
#[derive(Clone, Copy, Debug)]
@@ -15,6 +13,7 @@ pub(super) struct Candidate {
pub score: f32,
pub wall_score: f32,
pub wall_ride: bool,
pub grind: Option<super::grind::GrindTarget>,
}
pub(super) fn score(
candidates: &mut [Candidate],
@@ -22,6 +21,7 @@ pub(super) fn score(
adjusted_on_vert: bool,
input: SelectorInput,
s: &SelectorSettings,
mut grind: impl FnMut(&mut Prediction, bool) -> Result<super::grind::GrindEvaluation, String>,
mut line: impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, String>,
) -> Result<bool, String> {
let mut all_miss = true; //selector9662, consumed by PhysicsAir COM latch
@@ -30,6 +30,7 @@ pub(super) fn score(
c.score = 0.0;
c.wall_score = 0.0;
c.wall_ride = false;
c.grind = None;
if !c.prediction.result.valid() {
continue;
}
@@ -53,13 +54,25 @@ pub(super) fn score(
let force_score = (force_dot * s.score_landing_force) * scalar;
let direction_score = (sideways * s.score_landing_direction) * scalar;
//82D69C00 returns0 immediately for the actual empty edge query. Its
//9552 flag remains clear.82D69D68 then evaluates the stock graph at
//its native invalid-query distance1000; do not substitute a score.
let grind_score = if input.flags_2476 & 0x0200_0000 == 0
&& (input.grind_lock_distance > 0.5 || !middle)
{
s.grind_penalty_vs_distance.evaluate(1000.0)
//82D69C00 attempts acquisition only on the first-pass centre trajectory.
let mut evaluation = grind(
&mut c.prediction,
middle
&& !(input.flags_2472 & 0x2000_0000 != 0
&& input.offboard_flags_1776 & 0x0400_0000 != 0
&& input.offboard_flags_1776 & 0x0800_0000 == 0),
)?;
if adjusted_on_vert && input.directional_input < 0.5 && evaluation.score > 0.0 {
evaluation.target = None;
evaluation.score = s.grind_penalty_vs_distance.evaluate(0.0);
}
c.grind = evaluation.target;
let grind_score = if input.flags_2476 & 0x0200_0000 != 0 {
0.0
} else if evaluation.score != 0.0 {
evaluation.score
} else if input.grind_lock_distance > 0.5 || !middle {
s.grind_penalty_vs_distance.evaluate(evaluation.distance)
} else {
0.0
};
@@ -1,9 +1,10 @@
//! Original selector Launch82D67848 and Update82D68800 with typed batch ownership.
use super::{
LaunchInfo, Prediction, QueryRequest, QueryResult, SelectorInput, SelectorSettings, SurfaceHit,
Trajectory, launch,
launch,
math::*,
scoring::{self, Candidate},
LaunchInfo, Prediction, QueryRequest, QueryResult, SelectorInput, SelectorSettings, SurfaceHit,
Trajectory,
};
///The current authored world contains collision triangles and no grind edges.
@@ -22,7 +23,8 @@ pub struct Selection {
pub collision_position: Vector, //2688
pub com_trajectory: Trajectory, //2704
pub surface_category: u32, //9640
pub wall_ride: bool, //9661
pub wall_ride: bool,
pub grind: Option<super::grind::GrindTarget>, //9661
}
#[derive(Clone, Debug, Default)]
pub struct TrajectorySelector {
@@ -172,6 +174,7 @@ impl TrajectorySelector {
score: 0.0,
wall_score: 0.0,
wall_ride: false,
grind: None,
}
});
for (index, c) in self
@@ -199,6 +202,28 @@ impl TrajectorySelector {
s: &SelectorSettings,
_topology: WorldWithoutGrindEdges,
line: impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, String>,
) -> Result<bool, String> {
self.complete_batch_with_grinds(
results,
input,
s,
|_, _| {
Ok(super::grind::GrindEvaluation {
target: None,
score: 0.0,
distance: 1000.0,
})
},
line,
)
}
pub fn complete_batch_with_grinds(
&mut self,
results: &[QueryResult],
input: SelectorInput,
s: &SelectorSettings,
grind: impl FnMut(&mut Prediction, bool) -> Result<super::grind::GrindEvaluation, String>,
line: impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, String>,
) -> Result<bool, String> {
self.just_changed = false;
if !self.pending {
@@ -220,6 +245,7 @@ impl TrajectorySelector {
self.adjusted_on_vert,
input,
s,
grind,
line,
)?;
let mut index = 0;
@@ -250,6 +276,7 @@ impl TrajectorySelector {
}),
surface_category: self.selection.map_or(0, |v| v.surface_category),
wall_ride: c.wall_ride,
grind: c.grind,
};
if c.prediction.result.contact_time < s.minimum_valid_time {
self.suggested_normal = Some(c.prediction.result.contact_normal);
@@ -260,10 +287,17 @@ impl TrajectorySelector {
}
selection.surface_category = (c.prediction.result.surface >> 7) & 31;
selection.com_trajectory = self.com_trajectory(selection, input, s);
//No grind edges means native9652/9653 are false. AIPath is absent from
//this player-owned runtime, so original9659 is false as well.
self.grind_locked_to_middle = false;
let second_pass = self.pass == 1
//82D68C80: an accepted middle trajectory owns9653 and skips the second pass.
self.grind_locked_to_middle = index == 0 && c.grind.is_some();
if let Some(target) = self.candidates.iter().take(count).find_map(|c| c.grind) {
//82D6AF30/82E0A120 publish the rail-derived upright plane normal.
self.grind_normal = Some(crate::physics::grind_contact::upright_normal(sub(
target.edge.end,
target.edge.start,
)));
}
let second_pass = !self.grind_locked_to_middle
&& self.pass == 1
&& count >= 2
&& !(angle_between(selection.landing_normal, input.ground_normal)
< s.minimum_normal_delta_second_pass);
+338
View File
@@ -0,0 +1,338 @@
//! TU3 GrindAirAdjust82D712E0: predicted board/contact selection and bounded pose correction.
use super::grind_contact::{control::rotate, Primitive};
use super::skeleton_animation_record::{AnimationPartTransform as Frame, IDENTITY};
use crate::riding::ground_correction_math::dot_product as dot;
type V = [f32; 4];
const UP: V = [0., 1., 0., 0.];
const DEG: f32 = core::f32::consts::PI / 180.0;
#[derive(Clone, Debug)]
pub struct Settings {
pub frames: usize,
pub stomp: f32,
pub points: [V; 5],
pub ranges: [f32; 7],
pub distances: [f32; 7],
pub yaw_assist: [f32; 7],
pub max_offset: f32,
pub max_delta: f32,
pub max_angle: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct Target {
pub edge: Primitive,
pub limits: [V; 3],
}
#[derive(Clone, Copy, Debug)]
pub struct Adjustment {
pub axis: V,
pub offset: V,
pub angles: V,
}
#[derive(Default)]
pub struct GrindAir {
pub target: Option<Target>,
offset_delta: V,
offset: V,
angle_delta: V,
angles: V,
previous_kind: Option<usize>,
}
impl GrindAir {
///Start82D34FE8 clears retained displacement and angle integrators.
pub fn start(&mut self, target: Target) {
*self = Self {
target: Some(target),
..Self::default()
};
}
pub fn update(
&mut self,
board: Frame,
velocity: V,
angular_velocity: V,
up: V,
dt: f32,
flags: [u32; 3],
inverted: bool,
s: &Settings,
) -> Option<Adjustment> {
let target = self.target?;
//82D712E0 suppression: Processed2480 bit26,2472 bit15,2468 bit3.
if flags[0] & 0x0400_0000 != 0 || flags[1] & 0x8000 != 0 || flags[2] & 8 != 0 {
self.offset = [0.; 4];
self.angles = [0.; 4];
return None;
}
let rail = unit(sub(target.edge.end, target.edge.start));
let normal = unit(cross(rail, cross(up, rail)));
let count = s.frames;
if count < 2 {
return None;
}
//82D71430 integrates physical angular velocity and native gravity/stomp.
let mut samples = Vec::with_capacity(count);
let mut headings = Vec::with_capacity(count);
let mut current = board;
let mut vel = velocity;
let speed = length(angular_velocity);
let axis = unit(angular_velocity);
let acceleration = add([0., -9.8, 0., 0.], scale(up, s.stomp));
for _ in 0..count {
let points = s.points.map(|p| point(current, p));
let forward = sub(points[1], points[4]);
let flat = unit(sub(forward, scale(normal, dot(forward, normal))));
let aligned = if dot(flat, rail) < 0.0 {
scale(flat, -1.0)
} else {
flat
};
let mut angle = crate::trigonometry::acos(dot(aligned, rail).clamp(-1.0, 1.0));
if angle.abs() > core::f32::consts::FRAC_PI_2 {
angle = core::f32::consts::PI - angle;
}
let sign = if dot(normal, cross(aligned, rail)) < 0.0 {
-1.0
} else {
1.0
};
headings.push(-sign * angle / DEG);
samples.push(points);
if speed >= f32::from_bits(0x37800000) {
for column in &mut current[..3] {
*column = rotate(axis, *column, speed * dt);
}
}
vel = add(vel, scale(acceleration, dt));
current[3] = add(current[3], scale(vel, dt));
}
//82D721A0: first strict plane crossing for each of the five deck probes.
let mut times = [-1.0; 5];
let mut distances = [0.0; 5];
let mut corrections = [[0.; 4]; 5];
let mut earliest = count as f32;
for probe in 0..5 {
for i in 1..count {
let a = dot(normal, sub(samples[i - 1][probe], target.edge.start));
let b = dot(normal, sub(samples[i][probe], target.edge.start));
if a * b < 0.0 {
let fraction = (a / (a - b)).abs();
let time = (i - 1) as f32 + fraction;
let at = add(
scale(samples[i - 1][probe], 1.0 - fraction),
scale(samples[i][probe], fraction),
);
let on = add(
target.edge.start,
scale(rail, dot(sub(at, target.edge.start), rail)),
);
let correction = sub(on, at);
times[probe] = time;
distances[probe] = length(correction);
corrections[probe] = correction;
earliest = earliest.min(time);
break;
}
}
}
//82D723D0/82D724E8. Entries0..6: inverted, tips, deck, both trucks, each truck.
let probes = [0, 1, 4, 0, 2, 2, 3];
let desired = [90., 90., 90., 90., 0., 0., 0.];
let valid = |kind: usize| {
let p = probes[kind];
let t = times[p];
if kind == 4 {
if times[2].min(times[3]) < 4.0 && (times[2] - times[3]).abs() > 2.0 {
return false;
}
} else if earliest < 5.0 && (earliest - t).abs() > 3.0 {
return false;
}
t > 0.0
&& distances[p] < s.distances[kind]
&& (desired[kind] - headings[(t + 0.5) as usize].abs()).abs() < s.ranges[kind]
};
let mut selected = None;
for mut kind in 0..7 {
if inverted != (kind == 0) || !valid(kind) {
continue;
}
if kind == 3 {
if let Some(previous @ (1 | 2)) = self.previous_kind {
if valid(previous) {
kind = previous;
}
}
}
if selected.is_none_or(|old: usize| distances[probes[kind]] < distances[probes[old]]) {
selected = Some(kind);
}
if selected.is_some_and(|k| k >= 5) {
break;
}
}
let kind = selected?;
self.previous_kind = Some(kind);
let p = probes[kind];
let time = times[p];
let sample = (time + 0.5) as usize;
//82D72610: integrate correction/time; bound each step and total offset.
self.offset_delta = limited(
add(self.offset_delta, scale(corrections[p], 1.0 / time)),
s.max_delta,
);
self.offset = limited(add(self.offset, self.offset_delta), s.max_offset);
//82D72810: native angle integrators use degrees for the yaw error only.
let heading = headings[sample];
let desired = if heading > 0.0 {
desired[kind]
} else {
-desired[kind]
};
let speed =
((desired - heading) / time * s.yaw_assist[kind]).clamp(-s.max_angle, s.max_angle);
self.angle_delta[1] =
(self.angle_delta[1] / DEG + speed).clamp(-s.max_angle, s.max_angle) * DEG;
let forward = sub(samples[sample][1], samples[sample][4]);
let across_board = unit(sub(forward, scale(rail, dot(forward, rail))));
let roll_error = match kind {
0 => {
let v = signed_angle(across_board, target.limits[0], rail);
if v > core::f32::consts::FRAC_PI_2 {
v - core::f32::consts::PI
} else if v < -core::f32::consts::FRAC_PI_2 {
v + core::f32::consts::PI
} else {
v
}
}
1 | 2 | 3 => {
let from = if kind == 1 {
scale(across_board, -1.0)
} else {
across_board
};
let a = signed_angle(from, target.limits[1], rail);
let b = signed_angle(from, target.limits[2], rail);
if a.abs() < b.abs() {
a
} else {
b
}
}
_ => 0.0,
};
self.angle_delta[0] =
(self.angle_delta[0] + roll_error / time).clamp(-s.max_angle * DEG, s.max_angle * DEG);
//82D71BB0: projection of effective deck-right relative to support/up.
let projected = unit(board[0].map(|v| v - dot(board[0], normal)));
let pitch_error = if dot(projected, projected) > 0.9 {
signed_angle(projected, normal, board[2]) * 0.9
} else {
0.0
};
self.angle_delta[2] = if matches!(kind, 0 | 1 | 2 | 3) {
pitch_error / time
} else {
0.0
};
self.angles = add(self.angles, self.angle_delta);
Some(Adjustment {
axis: rail,
offset: self.offset,
angles: self.angles,
})
}
}
impl Adjustment {
///82BDCFB0 writes the existing15-frame board/IK offset owner15696.
pub fn local_transform(
self,
animation_to_world: Frame,
board_position: V,
physical_forward: V,
) -> Frame {
let across = unit(cross(UP, self.axis));
let up = unit(cross(self.axis, across));
let rotation = |v| {
rotate(
up,
rotate(
self.axis,
rotate(physical_forward, v, self.angles[2]),
self.angles[0],
),
self.angles[1],
)
};
let mut world = IDENTITY;
for i in 0..3 {
world[i] = rotation(IDENTITY[i]);
}
world[3] = sub(add(board_position, self.offset), rotation(board_position));
let inverse = inverse(animation_to_world);
super::skeleton_animation_record::compose_affine(
&inverse,
&super::skeleton_animation_record::compose_affine(&world, &animation_to_world),
)
}
}
fn inverse(f: Frame) -> Frame {
let mut r = IDENTITY;
for i in 0..3 {
for j in 0..3 {
r[i][j] = f[j][i];
}
}
r[3] = [-dot(f[0], f[3]), -dot(f[1], f[3]), -dot(f[2], f[3]), 0.];
r
}
fn point(f: Frame, v: V) -> V {
super::skeleton_animation_record::transform_point(&f, v)
}
fn signed_angle(a: V, b: V, axis: V) -> f32 {
let a = unit(a);
let b = unit(b);
let angle = crate::trigonometry::acos(dot(a, b).clamp(-1.0, 1.0));
if dot(cross(a, b), axis) < 0.0 {
-angle
} else {
angle
}
}
fn sub(a: V, b: V) -> V {
core::array::from_fn(|i| a[i] - b[i])
}
fn add(a: V, b: V) -> V {
core::array::from_fn(|i| a[i] + b[i])
}
fn scale(v: V, s: f32) -> V {
v.map(|v| v * s)
}
fn length(v: V) -> f32 {
dot(v, v).sqrt()
}
fn unit(v: V) -> V {
let l = length(v);
if l > 0.000001 {
scale(v, 1.0 / l)
} else {
[0.; 4]
}
}
fn limited(v: V, max: f32) -> V {
let l = length(v);
if l > max {
scale(v, max / l)
} else {
v
}
}
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.,
]
}
+2
View File
@@ -71,3 +71,5 @@ pub mod skeleton_landing;
pub mod skeleton_motion;
pub mod skeleton_output;
pub mod skeleton_root;
pub mod grind_air;
@@ -0,0 +1,165 @@
//! Live TU3 trajectory-to-spline query82D69C00 and admission82D6A840.
use skate_core::{
air::trajectory::{grind::*, Prediction},
physics::{board_world::BoardWorld, grind_contact::Primitive},
point_graph::PointGraph,
};
use skate_data::collections::Collections;
type V = [f32; 4];
pub(super) struct Settings {
pub limits: GrindAssistLimits,
height: PointGraph<8>,
padding: f32,
maximum_adjust: f32,
velocity_scalar: f32,
max_angle: f32,
score: f32,
truck_distance: f32,
}
impl Settings {
pub fn load(data: &Collections) -> Result<Self, String> {
let t = |key| data.float("physics_trajectory", "default", key);
Ok(Self {
limits: GrindAssistLimits {
lock_distance: 0.0,
max_speed_squared_ledge: t("GrindMaxSpeedSqrIntoLedge")?,
max_speed_squared_rail: t("GrindMaxSpeedSqrIntoGrind")?,
max_downward_speed: t("GrindMaxSpeedDownOntoGrind")?,
ledge_scalars: [
t("GrindLockLedgeScalar2")?,
t("GrindLockLedgeScalar")?,
t("GrindLockLedgeLowSideScalar2")?,
t("GrindLockLedgeLowSideScalar")?,
],
tip_scalar: t("GrindTipScalar")?,
maximum_adjust_angle: t("GrindAdjustMaxAngle")?,
deck_dimensions: [
data.float("physicsdeck", "default", "DeckMidLength")?,
data.float("physicsdeck", "default", "DeckFrontEndSize")?,
],
},
height: super::settings::graph8(
data,
"physics_trajectory",
"RequiredAngleVsHeight",
true,
)?,
padding: t("GrindOffset")?,
maximum_adjust: t("MaxTrajectoryAdjust")?,
velocity_scalar: t("GrindLandingVelScalar")?,
max_angle: t("GrindLandingMaxAngle")?,
score: t("ScoreGrind")?,
truck_distance: data.float("physics_grinds", "default", "DeckCenterToTruck")?,
})
}
pub fn evaluate(
&self,
prediction: &mut Prediction,
acquire: bool,
world: &BoardWorld,
edges: &[Primitive],
nearby: &mut Vec<Primitive>,
lock_distance: f32,
com: V,
board_position: V,
) -> Result<GrindEvaluation, String> {
let velocity = prediction.collision_velocity();
let landing = prediction
.request
.trajectory
.position_at(prediction.result.contact_time);
if acquire {
*nearby = edges
.iter()
.copied()
.filter(|e| {
(0..3).all(|i| {
e.start[i].min(e.end[i]) <= landing[i] + if i == 1 { 4.0 } else { 2.0 }
&& e.start[i].max(e.end[i])
>= landing[i] - if i == 1 { 0.5 } else { 2.0 }
})
})
.take(40)
.collect();
}
let landing = prediction.result.contact_position;
let mut distance: f32 = 1000.0;
for e in nearby.iter() {
let delta = sub(e.end, e.start);
let len = dot(delta, delta);
let from = sub(landing, e.start);
let projected = sub(from, delta.map(|v| v * dot(from, delta) / len));
distance = distance.min(
dot(projected, projected)
.sqrt()
.min(dot(from, from).sqrt())
.min(dot(sub(landing, e.end), sub(landing, e.end)).sqrt()),
);
}
if acquire {
let mut candidates = nearby
.iter()
.enumerate()
.filter_map(|(i, e)| consider_grind_primitive(*prediction, *e, i, self.padding))
.collect();
let mut limits = self.limits.clone();
limits.lock_distance = lock_distance;
while let Some(c) = take_best_grind(&mut candidates, lock_distance, &self.height) {
let e = nearby[c.primitive];
let Some(surface) = skate_core::air::trajectory::grind_surface::investigate(
world,
e.start,
e.end,
c.point,
self.truck_distance,
)?
else {
continue;
};
let Some(delta) = admitted_displacement(
*prediction,
c,
e,
velocity,
com,
surface.evidence,
&limits,
) else {
continue;
};
apply_admitted_target(
prediction,
c,
delta,
surface.normal,
velocity,
self.maximum_adjust,
self.velocity_scalar,
self.max_angle,
);
bevy::log::info!("GRIND_ASSIST lock_distance={} miss_distance={} time={} rail={} correction={:?}",lock_distance,c.distance,c.time,e.owner,delta);
return Ok(GrindEvaluation {
target: Some(GrindTarget {
edge: e,
point: c.point,
normal: surface.normal,
air_limits: surface.air_limits(e.start, e.end, c.point, board_position),
}),
score: self.score,
distance,
});
}
}
Ok(GrindEvaluation {
target: None,
score: 0.0,
distance,
})
}
}
fn sub(a: V, b: V) -> V {
core::array::from_fn(|i| a[i] - b[i])
}
fn dot(a: V, b: V) -> f32 {
skate_core::riding::ground_correction_math::dot_product(a, b)
}
@@ -1,10 +1,11 @@
//! Real stock-settings and BoardWorld adapter for the player trajectory selector.
mod grind;
mod settings;
mod world;
use skate_core::{
air::trajectory::{
LaunchInfo, QueryRequest, QueryResult, SelectorInput, SelectorSettings, TrajectorySelector,
query_trajectory,
query_trajectory, LaunchInfo, QueryRequest, QueryResult, SelectorInput, SelectorSettings,
TrajectorySelector,
},
physics::board_world::BoardWorld,
};
@@ -13,6 +14,11 @@ use skate_data::collections::Collections;
pub struct AirTrajectoryRuntime {
pub selector: TrajectorySelector,
pub settings: SelectorSettings,
pub grind_board_position: [f32; 4],
pub grind_com_position: [f32; 4],
pub edges: Vec<skate_core::physics::grind_contact::Primitive>,
grind_candidates: Vec<skate_core::physics::grind_contact::Primitive>,
grind_settings: grind::Settings,
pending_results: Option<Vec<QueryResult>>,
}
impl AirTrajectoryRuntime {
@@ -29,6 +35,11 @@ impl AirTrajectoryRuntime {
selector: TrajectorySelector::new(),
settings: settings::load(collections)?,
pending_results: None,
edges: Vec::new(),
grind_board_position: [0.; 4],
grind_com_position: [0.; 4],
grind_settings: grind::Settings::load(collections)?,
grind_candidates: Vec::new(),
})
}
pub fn launch(
@@ -47,11 +58,22 @@ impl AirTrajectoryRuntime {
let Some(results) = self.pending_results.take() else {
return Ok(self.selector.update_without_completion());
};
let valid = self.selector.complete_batch(
let valid = self.selector.complete_batch_with_grinds(
&results,
input,
&self.settings,
world::topology(world),
|prediction, acquire| {
self.grind_settings.evaluate(
prediction,
acquire,
world,
&self.edges,
&mut self.grind_candidates,
input.grind_lock_distance,
self.grind_com_position,
self.grind_board_position,
)
},
|start, end, radius| world::line(world, start, end, radius),
)?;
if self.selector.pending() {
@@ -80,7 +80,7 @@ pub(super) fn load(data: &Collections) -> Result<SelectorSettings, String> {
vert_jump_align_max_angle: r("VertJumpAlignMaxAngle")?, //723FF102068622CF
})
}
fn graph8(
pub(super) fn graph8(
data: &Collections,
class: &str,
name: &str,
@@ -2,11 +2,11 @@
//!82772028, across the host's world-format boundary. BoardWorld stores static
//!world-space triangles, so their source geometry transform is identity.
use skate_core::{
air::trajectory::{SurfaceHit, WorldWithoutGrindEdges},
air::trajectory::SurfaceHit,
math::Vector3,
physics::{
board_world::BoardWorld,
triangle_query::{TriangleLineHit, triangle_segment},
triangle_query::{triangle_segment, TriangleLineHit},
},
};
type Vector = [f32; 4];
@@ -17,13 +17,6 @@ const IDENTITY: [[f32; 4]; 4] = [
[0.0; 4],
];
///BoardWorld's authored format contains triangles only; no grind primitive
///collection is present. Update this adapter together with any world format
///extension that introduces actual grind edges.
pub(super) fn topology(_world: &BoardWorld) -> WorldWithoutGrindEdges {
WorldWithoutGrindEdges
}
pub(super) fn line(
world: &BoardWorld,
start: Vector,
+3
View File
@@ -28,6 +28,7 @@ pub(super) fn advance(
//Publishing Ground here runs its ForcePhysics Begin before the initial
//input reset82DB8998, which would immediately clear that mode again.
}
if skater.trajectory.edges.is_empty() { skater.trajectory.edges.clone_from(&physics.grind.primitives); }
physics.board.clear_forces();
let query_timer = crate::performance::Scope::new("wheel_and_foot_queries");
//World8275EA20 starts both batches before actor SetUpPhysics. Foot
@@ -63,6 +64,8 @@ pub(super) fn advance(
actions,
input_available,
)?;
skater.trajectory.grind_board_position = solve::deck_frame(&physics.board)[3];
skater.trajectory.grind_com_position = skater.player_input.processed.vectors_544_560_592_608[2].map(f32::from_bits);
skater.ground_settings = skater.ground_profiles.select(
skater.player_input.processed.state_variant_index_2528,
skater.player_input.processed.surface_mode_2540,
+1 -1
View File
@@ -23,7 +23,7 @@ mod camera;
mod chromosome;
pub(crate) struct Runtime {
camera: camera::GrindCamera,
primitives: Vec<Primitive>,
pub(super) primitives: Vec<Primitive>,
pub candidate: Option<FiftyFiftyCandidate>,
pub name: String,
pub active: bool,
@@ -0,0 +1,67 @@
//! Stock GrindAirAdjust settings82D72B40; lengths from the actual deck/truck collections.
use skate_core::physics::grind_air::Settings;
use skate_data::collections::Collections;
pub(super) fn load(data: &Collections) -> Result<Settings, String> {
let air = |key| data.float("physics_grinds_air", "default", key);
let deck = |key| data.float("physicsdeck", "default", key);
let half = deck("DeckMidLength")? * 0.5;
let tip = deck("DeckFrontEndSize")? * air("TipOffsetFraction")?;
let angle = deck("DeckFrontEndAngle")? * core::f32::consts::PI / 180.0;
let z = half + data.float("physicstrucks", "default", "TruckZPosFront")?;
let y = data.float("physicstrucks", "default", "TruckYPos")?;
let frames = data.integer("physics_grinds_air", "default", "NumFramesToTest")? as usize;
if !(2..=12).contains(&frames) {
return Err("GrindAir NumFramesToTest exceeds native12-frame storage".into());
}
Ok(Settings {
frames,
stomp: air("StompAccelerationAdjust")?,
points: [
[0.; 4],
[
0.,
tip * skate_core::trigonometry::sin_cos(angle).0,
half + tip,
0.,
],
[0., y, z, 0.],
[0., y, -z, 0.],
[
0.,
tip * skate_core::trigonometry::sin_cos(angle).0,
-half - tip,
0.,
],
],
ranges: [
90.,
air("AngleRangeTipSlide")?,
air("AngleRangeTipSlide")?,
air("AngleRangeBoardSlide")?,
air("AngleRange50_50")?,
air("AngleRange5_0")?,
air("AngleRange5_0")?,
],
distances: [
z,
z * air("MaxDistTipSlide")?,
z * air("MaxDistTipSlide")?,
z * air("MaxDistBoardSlide")?,
z * air("MaxDist50_50")?,
z * air("MaxDist5_0")?,
z * air("MaxDist5_0")?,
],
yaw_assist: [
air("AngleYAssistBoardSlide")?,
air("AngleYAssistTipSlide")?,
air("AngleYAssistTipSlide")?,
air("AngleYAssistBoardSlide")?,
air("AngleYAssist50_50")?,
air("AngleYAssist5_0")?,
air("AngleYAssist5_0")?,
],
max_offset: air("MaxOffsetDist")?,
max_delta: air("MaxOffsetDeltaPerFrame")?,
max_angle: air("MaxAngleDeltaPerFrame")?,
})
}
@@ -127,11 +127,14 @@ impl KnownAirRuntime for Runtime<'_> {
self.skater.trajectory.selector.grind_locked_to_middle()
}
fn start_grind_air_adjust_from_selector(&mut self) {
//The host BoardWorld has an explicitly empty edge collection. Its selector
//cannot produce9653; do not manufacture the required2944/2960/edge9568 data.
self.record(Err(
"KnownAir received a grind selection from an edge-free world".into(),
));
if let Some(target)=self.selection.grind {
self.skater.skeleton_input.grind_air.start(skate_core::physics::grind_air::Target {
edge:target.edge,limits:target.air_limits,
});
self.skater.skeleton_input.grind_air_started=true;
} else {
self.record(Err("KnownAir grind target lost its native spline record".into()));
}
}
fn selector_closest_trajectory_point(&mut self, com: V, zero: V) -> (i32, V) {
let t = self.selection.com_trajectory;
+1
View File
@@ -231,6 +231,7 @@ impl SkaterRuntime {
player_input: PlayerInputRuntime::load(&data, NoGrindEdges)?,
player_state: super::player_state::PlayerState::load(&data, mode)?,
skeleton_input: SkeletonInputRuntime {
grind_air_settings: Some(SkeletonInputRuntime::load_grind_settings(&data)?),
drive_frames: initial_parts,
extra_target_positions: [
initial_targets.com,
@@ -3,6 +3,8 @@
//! GeneralUpdate belongs to the later movement-state phase, not PlayerInput.
#[path = "skeleton_input_general.rs"]
mod general;
#[path = "grind_air_settings.rs"]
mod grind_settings;
#[path = "skeleton_input_teleport.rs"]
mod teleport;
@@ -10,7 +12,7 @@ use super::{
animated_skeleton::AnimatedSkeleton, animation_input::AnimationInput, foot_ik::FootIk,
};
use skate_core::{
animation::output::{NativeMatrix, attributes::AnimationAttribute},
animation::output::{attributes::AnimationAttribute, NativeMatrix},
input::controller::ActionMap,
physics::{
board::BodyId,
@@ -77,6 +79,8 @@ pub(crate) struct SkeletonInputRuntime {
///GrindAirAdjust ctor82D7116C initializes activated289=false.
///Only KnownAir Update82D35D30/Exit82D3597C subsequently write this flag.
///288, ctor82D71160; activated289 is a separate retained flag.
pub grind_air: skate_core::physics::grind_air::GrindAir,
pub grind_air_settings: Option<skate_core::physics::grind_air::Settings>,
pub grind_air_started: bool,
pub grind_air_active: bool,
pub grind_air_adjusting: bool,
@@ -100,6 +104,8 @@ impl Default for SkeletonInputRuntime {
force_mode: 0,
head_tracking_history: [[0.0; 4]; 8],
head_tracking_active: false,
grind_air: Default::default(),
grind_air_settings: None,
grind_air_started: false,
grind_air_active: false,
grind_air_adjusting: false,
@@ -108,6 +114,11 @@ impl Default for SkeletonInputRuntime {
}
impl SkeletonInputRuntime {
pub fn load_grind_settings(
data: &skate_data::collections::Collections,
) -> Result<skate_core::physics::grind_air::Settings, String> {
grind_settings::load(data)
}
///Called by PlayerInputCallbacks::process_skeleton, after the actual
///BoardToolkit and line tests have been published. It does not advance
///Ground, rebuild a toolkit or submit a second physical simulation.
@@ -154,9 +165,7 @@ impl SkeletonInputRuntime {
//These authored offset producers are separate from Ground riding.
//Do not silently reuse a riding pose if a future state enables them.
let reparented_hands = owners.animated.reparented_hand_indices();
super::offboard::pose_adjust::update(
owners.animated, pose.globals, reparented_hands, p,
)?;
super::offboard::pose_adjust::update(owners.animated, pose.globals, reparented_hands, p)?;
if p.flags_2476 & 0x100 != 0 {
if p.flags_2476 & 0x10000 == 0 {
return Err(
@@ -168,7 +177,46 @@ impl SkeletonInputRuntime {
//Inactive Ground leaves retained total offset/angle untouched.
self.grind_air_adjusting = false;
if self.grind_air_active {
return Err("Active air grind adjustment requires82D712E0 geometric stages".into());
let settings = self
.grind_air_settings
.as_ref()
.ok_or("Missing stock GrindAir settings")?;
let mut frame = super::solve::deck_frame(board);
if p.flags_2468 & 0x0010_0000 != 0 {
frame[0] = frame[0].map(|v| -v);
frame[2] = frame[2].map(|v| -v);
}
let mapped = skate_core::physics::skeleton_animation_record::map_animation_parts(
pose.globals,
&owners.animated.bone_indices,
&owners.animated.physics_frames,
)?;
skate_core::physics::skeleton_motion::SkeletonMotion::publish_unadjusted_board(
&mapped[0],
&mut p.flags_2472,
);
if let Some(adjustment) = self.grind_air.update(
frame,
p.vectors_400_416[0].map(f32::from_bits),
p.vectors_720_784_800_816_832_864[0].map(f32::from_bits),
p.vectors_544_560_592_608[0].map(f32::from_bits),
p.timestep_2604,
[p.flags_2480, p.flags_2472, p.flags_2468],
p.flags_2484 & 0x0020_0000 != 0,
settings,
) {
let pivot = skate_core::physics::skeleton_animation_record::transform_point(
&owners.animated.roots.animation_to_world,
mapped[0][3],
);
let offset = adjustment.local_transform(
owners.animated.roots.animation_to_world,
pivot,
owners.animated.board_frames.physical_board[2],
);
owners.animated.board_offset.refresh_transform(offset);
self.grind_air_adjusting = true;
}
}
}
let up = p.vectors_544_560_592_608[0].map(f32::from_bits);
+4 -2
View File
@@ -45,11 +45,13 @@ extracts are kept locally under logs/biped-*.txt.
All six contact families and their named variations are connected for a manual
playability pass. This is not verified retail parity. Existing host limitations
remain: trajectory-driven magnetic acquisition, specialized tipslide drop-in
exit handling, full native material/engagement feedback, and the air439 history
remain: specialized tipslide drop-in exit handling, full native
material/engagement feedback, and the air439 history
snapshot are not completely connected. Common pop/exit behavior still supplies
those families in this adapter. The ground history supplies approach naming;
short approaches and rapid re-grinds particularly need visual comparison.
Only compilation and source review were performed, as requested. Gameplay and
animation behavior are for the user's visual test on the default map.
Trajectory-driven magnetic acquisition is now connected; see [magnetism](grind-magnetism-difficulty.md).
+29 -8
View File
@@ -9,9 +9,9 @@ Observed in assets/private/stock/skater-collections.json, physics_mode:
| hardcore | 3E19999A | 0.15 |
The field is native physics_mode+80. The live SelectorInput already receives
this field when the active difficulty changes. It does not currently result in
magnetic acquisition because AirTrajectoryRuntime passes WorldWithoutGrindEdges,
and KnownAir's StartGrindAirAdjust callback reports an unsupported owner.
this field when the active difficulty changes. AirTrajectoryRuntime now queries
the authored spline primitives and the selected result retains the accepted
primitive and angular reference directions through KnownAir entry.
Observed TU3 path: ScoreTrajectories82D68FE8 attempts82D69C00 on the first-pass
middle trajectory. Its actual spline query uses bounds around the natural landing
@@ -32,9 +32,30 @@ from the difficulty-dependent initial capture distance.
Native source identity: TU3 default_82000000_011B0000.bin SHA256
f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4.
Instruction extracts are in local logs/biped-*.txt. No gameplay tests or reference
process were run. Experimental selector plumbing is saved in
logs/magnetism-in-progress, outside the production source: the incomplete pose
owner must be connected before exposing9653 in the playable game.
Instruction extracts are in local logs/biped-*.txt.
Status: findings confirmed statically; magnetism is NOT fixed in the staged game.
## Live integration
The centre-trajectory query supplies ranking, admission, trajectory displacement
and the landing normal. Alternative candidates reuse its actual edge collection
for the distance penalty; they do not issue independent replacement queries.
The selector retains the accepted middle target and skips its normal-delta
second pass exactly at that handoff. The current processed COM and board position,
predicted collision velocity and active difficulty feed the admission checks.
KnownAir starts the retained GrindAir owner and activates it at the original
apex/remaining-frame boundary. It predicts the physical deck's rotation and
translation, intersects five deck/truck points with the rail plane, chooses from
seven native contact choices, and integrates bounded translation and angular
corrections. ProcessData publishes the unadjusted board orientation flag before
this decision. The result refreshes the existing board/IK offset owner; ordinary
exit deactivates the owner and preserves the native offset decay.
`GRIND_ASSIST` log entries record accepted capture distance, miss distance,
predicted contact time, primitive owner and correction. These are passive output
from the user's play session; no scripted inputs are sent.
Status: implemented and compiled, ready for the user's gameplay test. No tests
or reference process were run, as requested. The equations and constants were
translated from static TU3 source; matched retail replay/bitwise parity has not
been established. The current world adapter supplies static spline primitives.
+11 -55
View File
@@ -1,59 +1,15 @@
# Native grind assist port status
# Native grind assist
The live game now supports paired-truck50-50 and centre-deck boardslide contacts
(see boardslide-port.md). The new
`skate-core/src/air/trajectory/grind.rs` leaves are not connected to gameplay.
They must not be presented as working magnetic assistance or all-grind support.
The live acquisition and airborne board-adjustment path is now connected.
See [difficulty and integration details](grind-magnetism-difficulty.md) and
[grind families](grind-families.md).
Core trajectory leaves82D6A398/82D6A168/82D6A840 are supplied with the real spline
collection and indexed collision investigation. Accepted targets pass through
selector scoring and KnownAir into GrindAirAdjust82D712E0. Difficulty uses the
stock physics_mode80 field, not a new snapping radius.
Reference: TU3 image SHA256
f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4,
generated PPC in the local Skate3Research/Skate3CustomEngineLayer checkout.
No reference process or gameplay tests were run for this port.
## Ported leaves
- 82D60C80/82D60B98: quadratic trajectory-plane intersection and later-root selection.
- 82D6A398: projected rail trajectory candidate, signed endpoint interval,
radius plus GrindOffset plane, miss distance, crossing time, fixed60Hz frame,
rail direction and approach direction.
- 8296EC98/82E09C80: oriented approach angle and fractional-turn fold.
- 82D6A168: height/angle ranking inside the difficulty lock distance and
distance fallback outside it; removing one winner preserves retry order.
- Admission/displacement portion of82D6A840: relative height, vertical rail,
perpendicular speed, ledge-side lock scalars, deck-tip allowance and maximum
trajectory angle. It requires a supplied native surface result; no dummy
classification or unconditional snap is provided.
These are static source translations, not a claim of verified bitwise parity.
The core crate builds. The user has reserved gameplay testing for themselves.
## Missing live dependencies
1. Preserve the complete indexed world-query result set for82C20728/82C20C08,
then implement the rail/edge/ledge classification82C21930. The existing
ground hang-up adapter only produces the fields that its ground caller reads.
2. Complete82D6A840's accepted-target publication and82D6AF58 landing-normal
construction. Feed the real result through trajectory scoring/second-pass
selection instead of the current WorldWithoutGrindEdges marker.
3. Connect GrindAirAdjust82D712E0 and its projection, target, and correction
owners to the physical skeleton/board. The displacement leaf by itself is
not the complete airborne assist.
4. Port the other contact probes/scorers and state owners. Native manager
82D875A8 uses separate centre-deck, inverted-deck, truck and tip probes.
State400 boardslide,40150-50,402 tipslide,4035-0,404 backslash and405
darkslide are distinct physics states. Naming variants requires the actual
chromosome producer as well as those state IDs.
## Proposed read-only reference trace
A reference run was authorized and launched, but its controller backend did
not expose the physical controller. The user requested continuation from code.
No successful reference grind capture was obtained and no synthetic gameplay
input was sent.
Capture query inputs and indexed hit validity/fraction/position/normal/surface
at82C20728 and82C20C08, the classification at82C21930, ranked candidates and
accepted/rejected attempts at82D6A168/82D6A840, and GrindAirAdjust output at
82D712E0. Include state/category, mode, simulation tick, spline ID, board frame
and controller inputs so these observations can be matched to their callers.
Keep the trace local and do not modify the reference binaries or database.
matching local Skate3CustomEngineLayer generated PPC. Static source translation
and compilation only; gameplay validation is reserved for the user.