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);