Connect centre-deck boardslide contacts and physical state

This commit is contained in:
chazmm committed 2026-09-07 20:15:01 +01:00
1 parent d60a8d7cd3
commit 685d55601e
13 files changed
+824 -187

No files matched your search

+253 -111
View File
@@ -1,82 +1,195 @@
//! TU3 rail-trajectory candidate leaves82D60C80/82D60B98,
//! 82D6A398 and82D6A168. Admission and physical adjustment are separate owners.
use super::{math::*, Prediction, Trajectory};
use super::{Prediction, Trajectory, math::*};
use crate::{physics::grind_contact::Primitive, point_graph::PointGraph};
#[derive(Clone, Copy, Debug)]
pub struct GrindTrajectoryCandidate {
pub point: Vector, //0 projected point on rail
pub trajectory_point: Vector, //16 centre with radius/offset removed
pub direction: Vector, //32 normalized rail direction
pub approach: Vector, //48 velocity rejected from rail normal
pub distance: f32, //64 perpendicular miss distance
pub time: f32, //68 crossing time
pub angle: f32, //72 folded approach angle
pub frame: i32, //76 fixed60Hz time
pub primitive: usize, //80 query index
pub point: Vector, //0 projected point on rail
pub trajectory_point: Vector, //16 centre with radius/offset removed
pub direction: Vector, //32 normalized rail direction
pub approach: Vector, //48 velocity rejected from rail normal
pub distance: f32, //64 perpendicular miss distance
pub time: f32, //68 crossing time
pub angle: f32, //72 folded approach angle
pub frame: i32, //76 fixed60Hz time
pub primitive: usize, //80 query index
}
///The82C21930 result must come from the world geometry investigator. A
///spline's authored name or a nearest-distance guess cannot supply these lanes.
#[derive(Clone, Copy, Debug)]
pub struct GrindSurfaceEvidence {
pub kind: u32, //0 rail,1 edge,2 ledge,3 rejected
pub side: Vector, //classification64
pub kind: u32, //0 rail,1 edge,2 ledge,3 rejected
pub side: Vector, //classification64
}
#[derive(Clone, Debug)]
pub struct GrindAssistLimits {
pub lock_distance: f32, //physics_mode80
pub lock_distance: f32, //physics_mode80
pub max_speed_squared_ledge: f32, //physics_trajectory520
pub max_speed_squared_rail: f32, //524
pub max_downward_speed: f32, //528
pub ledge_scalars: [f32;4], //532,536,540,544
pub tip_scalar: f32, //512
pub maximum_adjust_angle: f32, //560 degrees
pub ledge_scalars: [f32; 4], //532,536,540,544
pub tip_scalar: f32, //512
pub maximum_adjust_angle: f32, //560 degrees
///Actual deck dimensions from82762E50 (DeckMidLength) and82C00868.
pub deck_dimensions: [f32;2],
pub deck_dimensions: [f32; 2],
}
///Accepted-target publication82D6AEA4..82D6AF38. The original contact
///position/material stay intact; time, frame, trajectory and normal change.
pub fn apply_admitted_target(
prediction: &mut Prediction,
candidate: GrindTrajectoryCandidate,
correction: Vector,
support_normal: Vector,
reference_velocity: Vector,
maximum_adjust: f32,
landing_velocity_scalar: f32,
landing_max_angle: f32,
) {
super::launch::adjust_trajectory(
&mut prediction.request.trajectory,
candidate.frame,
correction,
maximum_adjust,
);
prediction.result.contact_frame = candidate.frame;
prediction.result.contact_time = candidate.frame as f32 * STEP;
prediction.result.contact_normal = landing_normal(
candidate.direction,
reference_velocity,
support_normal,
landing_velocity_scalar,
landing_max_angle,
);
}
///82D6AF58: bias by cross-rail velocity, bound against the investigated
///support normal, reject the rail axis, then normalize.
pub fn landing_normal(
direction: Vector,
velocity: Vector,
support: Vector,
velocity_scalar: f32,
maximum_angle_degrees: f32,
) -> Vector {
let across = sub(velocity, scale(direction, dot(direction, velocity)));
let target = madd(across, -velocity_scalar, UP);
let limited = clamp_angle(
target,
support,
maximum_angle_degrees * f32::from_bits(0x3c8e_fa35),
);
normalize(sub(limited, scale(direction, dot(limited, direction))))
}
///82BD3E78, shared with the offboard surface-frame port.
fn clamp_angle(target: Vector, reference: Vector, limit: f32) -> Vector {
let a = normalize(target);
let b = normalize(reference);
let axis = cross(a, b);
let angle = crate::trigonometry::acos(dot(a, b).clamp(-1.0, 1.0));
let turns = angle.mul_add(f32::from_bits(0x3e22_f983), 0.5).floor();
let wrapped = (-turns).mul_add(f32::from_bits(0x40c9_0fdb), angle);
if wrapped.abs() < limit || f32::from_bits(0x3780_0000) > dot(axis, axis) {
return target;
}
let unit = normalize(axis);
let (sin, cos) = crate::trigonometry::sin_cos(-limit * 0.5);
let q = scale(unit, sin);
scale(
madd(
cross(q, madd(reference, cos, cross(q, reference))),
2.0,
reference,
),
length(target),
)
}
///Admission and displacement portion82D6A840. The caller retries the next
///ranked candidate on None, then applies82D609E0 and82D6AF58 on success.
///This deliberately requires native surface evidence before any correction.
pub fn admitted_displacement(
prediction: Prediction, candidate: GrindTrajectoryCandidate, edge: Primitive,
reference_velocity: Vector, processed_592: Vector, surface: GrindSurfaceEvidence,
prediction: Prediction,
candidate: GrindTrajectoryCandidate,
edge: Primitive,
reference_velocity: Vector,
processed_592: Vector,
surface: GrindSurfaceEvidence,
limits: &GrindAssistLimits,
) -> Option<Vector> {
let natural_landing=prediction.request.trajectory.position_at(prediction.result.contact_time);
if candidate.point[1]-natural_landing[1]<f32::from_bits(0xbecc_cccd)
|| candidate.direction[1]>0.9 { return None; }
let direction=normalize(sub(edge.end,edge.start));
let perpendicular=sub(reference_velocity,scale(direction,dot(direction,reference_velocity)));
if (perpendicular[1]*(1.0-direction[1].abs())).abs()>limits.max_downward_speed {
let natural_landing = prediction
.request
.trajectory
.position_at(prediction.result.contact_time);
if candidate.point[1] - natural_landing[1] < f32::from_bits(0xbecc_cccd)
|| candidate.direction[1] > 0.9
{
return None;
}
if surface.kind==3 { return None; }
let mut horizontal=perpendicular; horizontal[1]=0.0;
let max_speed=if surface.kind==2 {limits.max_speed_squared_ledge} else {limits.max_speed_squared_rail};
if dot(horizontal,horizontal)>max_speed { return None; }
let delta=sub(candidate.trajectory_point,candidate.point);
let lock_distance=if surface.kind==2 {
let incoming=dot(delta,surface.side)>0.0;
let body=dot(sub(processed_592,edge.start),surface.side)>0.0;
limits.lock_distance*limits.ledge_scalars[if incoming {0} else {2}+if body {0} else {1}]
} else {limits.lock_distance};
if candidate.distance>=lock_distance.max(0.1) { return None; }
let miss=sub(candidate.trajectory_point,edge.start);
let mut correction=scale(sub(miss,scale(candidate.direction,dot(candidate.direction,miss))),-1.0);
let distance=length(correction);
if distance>=limits.lock_distance {
let half_dimension=limits.deck_dimensions[1].mul_add(0.5,limits.deck_dimensions[0]*0.5);
correction=scale(correction,(-limits.tip_scalar).mul_add(half_dimension,distance)/distance);
let direction = normalize(sub(edge.end, edge.start));
let perpendicular = sub(
reference_velocity,
scale(direction, dot(direction, reference_velocity)),
);
if (perpendicular[1] * (1.0 - direction[1].abs())).abs() > limits.max_downward_speed {
return None;
}
if surface.kind == 3 {
return None;
}
let mut horizontal = perpendicular;
horizontal[1] = 0.0;
let max_speed = if surface.kind == 2 {
limits.max_speed_squared_ledge
} else {
limits.max_speed_squared_rail
};
if dot(horizontal, horizontal) > max_speed {
return None;
}
let delta = sub(candidate.trajectory_point, candidate.point);
let lock_distance = if surface.kind == 2 {
let incoming = dot(delta, surface.side) > 0.0;
let body = dot(sub(processed_592, edge.start), surface.side) > 0.0;
limits.lock_distance
* limits.ledge_scalars[if incoming { 0 } else { 2 } + if body { 0 } else { 1 }]
} else {
limits.lock_distance
};
if candidate.distance >= lock_distance.max(0.1) {
return None;
}
let miss = sub(candidate.trajectory_point, edge.start);
let mut correction = scale(
sub(
miss,
scale(candidate.direction, dot(candidate.direction, miss)),
),
-1.0,
);
let distance = length(correction);
if distance >= limits.lock_distance {
let half_dimension =
limits.deck_dimensions[1].mul_add(0.5, limits.deck_dimensions[0] * 0.5);
correction = scale(
correction,
(-limits.tip_scalar).mul_add(half_dimension, distance) / distance,
);
}
let original = prediction.request.trajectory.velocity;
let adjusted = madd(correction, reciprocal(candidate.time), original);
let angle = if dot(original, original) * dot(adjusted, adjusted) > f32::from_bits(0x38d1_b717) {
angle_between(original, adjusted)
} else {
0.0
};
if angle > limits.maximum_adjust_angle * f32::from_bits(0x3c8e_fa35) {
return None;
}
let original=prediction.request.trajectory.velocity;
let adjusted=madd(correction,reciprocal(candidate.time),original);
let angle=if dot(original,original)*dot(adjusted,adjusted)>f32::from_bits(0x38d1_b717) {
angle_between(original,adjusted)
} else {0.0};
if angle>limits.maximum_adjust_angle*f32::from_bits(0x3c8e_fa35) { return None; }
Some(correction)
}
@@ -84,103 +197,132 @@ pub fn admitted_displacement(
/// A double root must be strictly positive. The two-root branch takes max;
/// it does not add a positive-time restriction absent from the source.
pub fn descending_plane_time(t: Trajectory, point: Vector, normal: Vector) -> Option<f32> {
let c = dot(normal,t.position)-dot(point,normal);
let b = dot(normal,t.velocity);
let a = dot(normal,scale(t.acceleration,reciprocal(2.0)));
let discriminant = b*b - (4.0*a)*c;
if discriminant < 0.0 { return None; }
let inverse = reciprocal(2.0*a);
let c = dot(normal, t.position) - dot(point, normal);
let b = dot(normal, t.velocity);
let a = dot(normal, scale(t.acceleration, reciprocal(2.0)));
let discriminant = b * b - (4.0 * a) * c;
if discriminant < 0.0 {
return None;
}
let inverse = reciprocal(2.0 * a);
if discriminant > 0.0 {
let root = discriminant*inverse_length(discriminant);
Some((inverse*(-b+root)).max(inverse*(-b-root)))
let root = discriminant * inverse_length(discriminant);
Some((inverse * (-b + root)).max(inverse * (-b - root)))
} else {
let time = inverse * -b;
(time>0.0).then_some(time)
(time > 0.0).then_some(time)
}
}
///82D6A398. The padding is physics_trajectory+516, not a host snap radius.
pub fn consider_grind_primitive(
prediction: Prediction, edge: Primitive, primitive: usize, padding: f32,
prediction: Prediction,
edge: Primitive,
primitive: usize,
padding: f32,
) -> Option<GrindTrajectoryCandidate> {
let mut delta = sub(edge.end,edge.start);
let normal_unscaled = cross(delta,cross(UP,delta));
let normal = scale(normal_unscaled,inverse_length(dot(normal_unscaled,normal_unscaled)));
let mut delta = sub(edge.end, edge.start);
let normal_unscaled = cross(delta, cross(UP, delta));
let normal = scale(
normal_unscaled,
inverse_length(dot(normal_unscaled, normal_unscaled)),
);
let rail_length = length(delta);
//830BD320 is initialized to1 by82F825D0. The original endpoints remain
//unchanged; only the direction and endpoint overrun vector are capped.
if rail_length>1.0 { delta=scale(delta,reciprocal(rail_length)); }
let offset = madd(normal,prediction.request.radius,scale(normal,padding));
let time = descending_plane_time(prediction.request.trajectory,add(edge.start,offset),normal)?;
let trajectory_point = sub(prediction.request.trajectory.position_at(time),offset);
if rail_length > 1.0 {
delta = scale(delta, reciprocal(rail_length));
}
let offset = madd(normal, prediction.request.radius, scale(normal, padding));
let time = descending_plane_time(
prediction.request.trajectory,
add(edge.start, offset),
normal,
)?;
let trajectory_point = sub(prediction.request.trajectory.position_at(time), offset);
let direction = normalize(delta);
let difference = sub(trajectory_point,edge.start);
let distance = length(cross(difference,direction));
let point = madd(direction,dot(direction,difference),edge.start);
let extension = scale(delta,0.1);
let difference = sub(trajectory_point, edge.start);
let distance = length(cross(difference, direction));
let point = madd(direction, dot(direction, difference), edge.start);
let extension = scale(delta, 0.1);
//Strict endpoint test82D6A708. No closest-point clamping.
if !(dot(sub(point,sub(edge.start,extension)),sub(point,add(edge.end,extension)))<0.0) {
if !(dot(
sub(point, sub(edge.start, extension)),
sub(point, add(edge.end, extension)),
) < 0.0)
{
return None;
}
let velocity = prediction.request.trajectory.velocity;
let approach = normalize(sub(velocity,scale(normal,dot(normal,velocity))));
let mut horizontal = sub(point,prediction.request.trajectory.position);
horizontal[1]=0.0;
let angle = folded_approach_angle(horizontal,direction,normal);
let approach = normalize(sub(velocity, scale(normal, dot(normal, velocity))));
let mut horizontal = sub(point, prediction.request.trajectory.position);
horizontal[1] = 0.0;
let angle = folded_approach_angle(horizontal, direction, normal);
Some(GrindTrajectoryCandidate {
point,trajectory_point,direction,approach,distance,time,angle,
frame:(time*60.0) as i32,primitive,
point,
trajectory_point,
direction,
approach,
distance,
time,
angle,
frame: (time * 60.0) as i32,
primitive,
})
}
///8296EC98's oriented angle then82E09C80's actual fractional-turn fold.
///Keep the wrap arithmetic: acos(abs(dot)) is not binary32 equivalent.
fn folded_approach_angle(a: Vector,b: Vector,normal: Vector) -> f32 {
let mut angle=angle_between(a,b);
let aa=dot(a,a); let bb=dot(b,b);
if aa>f32::from_bits(0x38d1_b717) && bb>f32::from_bits(0x38d1_b717) {
let unit=|v,square| {
let r=crate::physics::native_arithmetic::reciprocal_square_root_estimate(square);
scale(v,(r*0.5).mul_add((-square).mul_add(r*r,1.0),r))
fn folded_approach_angle(a: Vector, b: Vector, normal: Vector) -> f32 {
let mut angle = angle_between(a, b);
let aa = dot(a, a);
let bb = dot(b, b);
if aa > f32::from_bits(0x38d1_b717) && bb > f32::from_bits(0x38d1_b717) {
let unit = |v, square| {
let r = crate::physics::native_arithmetic::reciprocal_square_root_estimate(square);
scale(v, (r * 0.5).mul_add((-square).mul_add(r * r, 1.0), r))
};
if dot(cross(unit(a,aa),unit(b,bb)),normal)<0.0 {
angle=f32::from_bits(0x40c9_0fdb)-angle;
if dot(cross(unit(a, aa), unit(b, bb)), normal) < 0.0 {
angle = f32::from_bits(0x40c9_0fdb) - angle;
}
}
let turns=angle*f32::from_bits(0x3e22_f983);
let fraction=turns-turns.floor();
let wrapped=(fraction-if fraction>0.5 {1.0} else {0.0})*f32::from_bits(0x40c9_0fdb);
let sign=if wrapped>0.0 {1.0} else {-1.0};
let magnitude=wrapped*sign;
let folded=if magnitude>f32::from_bits(0x3fc9_0fdb) {
magnitude-std::f32::consts::PI
} else {magnitude};
(sign*folded).abs()
let turns = angle * f32::from_bits(0x3e22_f983);
let fraction = turns - turns.floor();
let wrapped = (fraction - if fraction > 0.5 { 1.0 } else { 0.0 }) * f32::from_bits(0x40c9_0fdb);
let sign = if wrapped > 0.0 { 1.0 } else { -1.0 };
let magnitude = wrapped * sign;
let folded = if magnitude > f32::from_bits(0x3fc9_0fdb) {
magnitude - std::f32::consts::PI
} else {
magnitude
};
(sign * folded).abs()
}
///82D6A168 consumes one winner per call. Retrying after a rejected target
///must retain the remaining insertion order, not sort the list by distance.
pub fn take_best_grind(
candidates: &mut Vec<GrindTrajectoryCandidate>,
difficulty_distance: f32, height_penalty: &PointGraph<8>,
difficulty_distance: f32,
height_penalty: &PointGraph<8>,
) -> Option<GrindTrajectoryCandidate> {
let mut chosen=None;
let mut fallback=1000.0;
let mut best_angle=std::f32::consts::PI;
let mut height=-10000.0;
for (i,candidate) in candidates.iter().enumerate() {
if candidate.distance<difficulty_distance {
let benefit=best_angle-candidate.angle;
if benefit>height_penalty.evaluate(candidate.point[1]-height) {
height=candidate.point[1];
fallback=-1.0;
best_angle=candidate.angle;
chosen=Some(i);
let mut chosen = None;
let mut fallback = 1000.0;
let mut best_angle = std::f32::consts::PI;
let mut height = -10000.0;
for (i, candidate) in candidates.iter().enumerate() {
if candidate.distance < difficulty_distance {
let benefit = best_angle - candidate.angle;
if benefit > height_penalty.evaluate(candidate.point[1] - height) {
height = candidate.point[1];
fallback = -1.0;
best_angle = candidate.angle;
chosen = Some(i);
}
} else if candidate.distance<fallback {
fallback=candidate.distance;
chosen=Some(i);
} else if candidate.distance < fallback {
fallback = candidate.distance;
chosen = Some(i);
}
}
chosen.map(|i|candidates.remove(i))
chosen.map(|i| candidates.remove(i))
}
@@ -0,0 +1,112 @@
//! Geometry fields consumed by trajectory assist:82C20728,82C20C08,
//!82C21828 and82C21930. Material/foot-placement flags have other consumers.
use super::{grind::GrindSurfaceEvidence, math::*};
use crate::math::Vector3;
use crate::physics::{
board_world::BoardWorld,
ground_hang_geometry::{HangGeometryInput, hang_lines},
};
#[derive(Clone, Copy, Debug)]
pub struct GrindSurface {
pub evidence: GrindSurfaceEvidence,
pub normal: Vector,
}
///The query consists of six consecutive48-byte descriptors; descriptor4 is
///the vertical centre line and descriptor5 the raised cross line. The optional
///seventh descriptor is a separate caller probe. Result indexing stays intact.
pub fn investigate(
world: &BoardWorld,
start: Vector,
end: Vector,
reference: Vector,
deck_center_to_truck: f32,
) -> Result<Option<GrindSurface>, &'static str> {
let Some(lines) = hang_lines(
HangGeometryInput {
edge_start: xyz(start),
edge_end: xyz(end),
reference_point: xyz(reference),
},
deck_center_to_truck,
) else {
return Ok(None);
};
let mut hits = [None; 6];
for (i, line) in lines.iter().enumerate() {
hits[i] = world.query_swept_line(line.start, line.end, line.radius)?;
}
let direction = normalize(sub(end, start));
let up = normalize(cross(direction, cross(UP, direction)));
let side = normalize(cross(UP, direction));
//82C20ED8 reads result5 (stack448), not the centre probe atstack384.
let kind = if (hits[0].is_some() && hits[1].is_some()) || hits[5].is_some() {
3
} else if hits[0].is_some() {
if hits[2].is_some_and(|h| h.geometry.fraction < 0.65) {
2
} else {
1
}
} else if hits[1].is_some() {
if hits[3].is_some_and(|h| h.geometry.fraction < 0.65) {
2
} else {
1
}
} else {
0
};
let side = if hits[0].is_none() && hits[1].is_some() {
scale(side, -1.0)
} else {
side
};
let centre = madd(direction, dot(sub(reference, start), direction), start);
let far: [Vector; 2] = std::array::from_fn(|i| {
let p = hits[i + 2].map_or(lines[i + 2].end, |h| h.geometry.position);
[p.x, p.y, p.z, 0.0]
});
//82C212CC..82C213F4: clearance angle16degrees expands both sides.
let clearance = f32::from_bits(0x3e8e_fa35);
let angle0 = crate::trigonometry::acos(
dot(scale(up, -1.0), normalize(sub(far[0], centre))).clamp(-1.0, 1.0),
) + std::f32::consts::FRAC_PI_2
+ clearance;
let angle1 = std::f32::consts::TAU
- crate::trigonometry::acos(
dot(scale(up, -1.0), normalize(sub(far[1], centre))).clamp(-1.0, 1.0),
)
- std::f32::consts::FRAC_PI_2
- clearance;
let pi = std::f32::consts::PI;
let normal = if angle0 < angle1 && angle0 < pi && angle1 > pi {
up
} else {
let angle = if angle0 < angle1 {
if (pi - angle0).abs() < (pi - angle1).abs() {
angle0
} else {
angle1
}
} else {
(angle0 + angle1) * 0.5
};
rotate(direction, scale(up, -1.0), angle)
};
Ok(Some(GrindSurface {
evidence: GrindSurfaceEvidence { kind, side },
normal,
}))
}
///82C1E220 constructs a half-angle quaternion and rotates the supplied vector.
fn rotate(axis: Vector, value: Vector, angle: f32) -> Vector {
let (sin, cos) = crate::trigonometry::sin_cos(angle * 0.5);
let q = scale(axis, sin);
madd(cross(q, madd(value, cos, cross(q, value))), 2.0, value)
}
fn xyz(v: Vector) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
@@ -2,6 +2,7 @@
//! Host world storage and batch ownership are independent of console layout.
mod launch;
pub mod grind;
pub mod grind_surface;
mod math;
mod prediction;
mod query;
+145 -18
View File
@@ -26,6 +26,101 @@ pub struct FiftyFiftyCandidate {
pub primitive: usize,
}
///82C1FB98: the longitudinal deck rectangle. Unlike the two truck rectangles,
///this catches a rail crossing between the trucks (boardslides/lipslides).
pub fn deck_contact(
board: [V; 4],
flags_2484: u32,
above: f32,
below: f32,
deck_center_to_truck: f32,
primitives: &[Primitive],
) -> Option<TruckContact> {
if flags_2484 & 0x0020_0000 != 0 {
return None;
}
let centre = add(board[3], scale(board[1], above));
let along = scale(board[2], deck_center_to_truck);
let down = scale(board[1], -(above + below));
let a = add(centre, along);
let b = sub(centre, along);
let c = add(a, down);
let d = add(b, down);
let mut best = None;
let mut distance = 1_000_000.0;
for (primitive, edge) in primitives.iter().enumerate() {
let first = segment_triangle(edge.start, edge.end, [a, b, c]);
let second = segment_triangle(edge.start, edge.end, [d, b, c]);
if let Some(position) = first.or(second) {
let delta = sub(position, board[3]);
let square = dot3(delta, delta);
if square < distance {
distance = square;
best = Some(TruckContact {
position,
primitive,
});
}
}
}
best
}
///82D889A8 geometry admission. The state manager owns approach-angle and
///candidate arbitration separately. No nearest-rail substitute is used.
pub fn boardslide_candidate(
board: [V; 4],
contact: TruckContact,
edge: Primitive,
velocity: V,
category: u32,
manager_frames: i32,
flags_2476: u32,
deck_center_to_truck: f32,
truck_to_wheel: f32,
) -> Option<FiftyFiftyCandidate> {
if flags_2476 & 0x4000_0000 != 0 {
return None;
}
let delta = sub(edge.end, edge.start);
let direction = scale(delta, dot3(delta, delta).sqrt().recip());
if dot3(upright_normal(direction), board[1]) <= 0.65 {
return None;
}
let depth = dot3(sub(board[3], contact.position), board[1]);
if depth * depth >= f32::from_bits(0x3b6b_edfa) {
return None;
}
if category == 100 && dot3(velocity, direction).abs() <= 0.75 && manager_frames <= 10 {
return None;
}
let horizontal = sub(delta, scale(board[1], dot3(delta, board[1])));
let length = dot3(horizontal, horizontal).sqrt();
if !(length > 0.0) {
return None;
}
let horizontal = scale(horizontal, length.recip());
let alignment = dot3(horizontal, board[2]).abs();
let from = sub(contact.position, board[3]);
let across = sub(from, scale(board[1], dot3(from, board[1])));
let remaining = deck_center_to_truck - dot3(across, across).sqrt();
let clearance = if alignment > 0.0 {
remaining / alignment * (1.0 - alignment * alignment).sqrt()
} else {
1000000.0
};
if !(clearance > truck_to_wheel) {
return None;
}
Some(FiftyFiftyCandidate {
direction,
centre: contact.position,
front: edge.end,
rear: edge.start,
primitive: contact.primitive,
})
}
///82D89150's same-primitive contact branch, before shared admission82D886B8.
/// Linked/adjacent primitive arbitration belongs to the caller; this boundary
/// explicitly returns none for that case instead of guessing an owner.
@@ -40,10 +135,14 @@ pub fn fifty_fifty_candidate(
///82D89150 also accepts two segment indices when their hits pass its signed
/// cross-rail separation check. Splines with multiple segments need this path.
pub fn fifty_fifty_candidate_on_splines(
board: [V; 4], balance: [f32; 2],
contacts: [Option<TruckContact>; 2], primitives: &[Primitive],
board: [V; 4],
balance: [f32; 2],
contacts: [Option<TruckContact>; 2],
primitives: &[Primitive],
) -> Option<FiftyFiftyCandidate> {
let [Some(front), Some(rear)] = contacts else { return None };
let [Some(front), Some(rear)] = contacts else {
return None;
};
if balance[0].abs().max(balance[1].abs()) >= 0.9 {
return None;
}
@@ -53,7 +152,9 @@ pub fn fifty_fifty_candidate_on_splines(
let direction = scale(delta, dot3(delta, delta).sqrt().recip());
let across = cross(upright_normal(direction), direction);
//820641A8. This comparison is signed in the original routine.
if dot3(sub(front.position, rear.position), across) > 0.1 { return None; }
if dot3(sub(front.position, rear.position), across) > 0.1 {
return None;
}
}
let front_depth = dot3(sub(board[3], front.position), board[1]);
let rear_depth = dot3(sub(board[3], rear.position), board[1]);
@@ -63,7 +164,9 @@ pub fn fifty_fifty_candidate_on_splines(
let difference = sub(front.position, rear.position);
let length = dot3(difference, difference).sqrt();
// A degenerate pair cannot define a finite native grind frame.
if !length.is_finite() || length == 0.0 { return None; }
if !length.is_finite() || length == 0.0 {
return None;
}
Some(FiftyFiftyCandidate {
direction: scale(difference, length.recip()),
centre: scale(add(front.position, rear.position), 0.5),
@@ -78,8 +181,12 @@ pub fn upright_normal(direction: V) -> V {
let cross_up = cross([0.0, 1.0, 0.0, 0.0], direction);
let mut normal = cross(cross_up, direction);
let length = dot3(normal, normal).sqrt();
if length <= 0.0 { return [1.0, 0.0, 0.0, 0.0]; }
if normal[1] < 0.0 { normal = scale(normal, -1.0); }
if length <= 0.0 {
return [1.0, 0.0, 0.0, 0.0];
}
if normal[1] < 0.0 {
normal = scale(normal, -1.0);
}
scale(normal, length.recip())
}
@@ -89,7 +196,9 @@ pub fn within_approach_angle(direction: V, velocity: V, degrees: f32) -> bool {
let normal = upright_normal(direction);
let projected = sub(velocity, scale(normal, dot3(velocity, normal)));
let length = dot3(projected, projected).sqrt();
if length <= 0.001 { return true; }
if length <= 0.001 {
return true;
}
let alignment = dot3(scale(projected, length.recip()), direction).abs();
alignment > (degrees * f32::from_bits(0x3c8e_fa35)).cos()
}
@@ -133,7 +242,10 @@ pub fn truck_contacts(
let squared = dot3(delta, delta);
if distance[truck] > squared {
distance[truck] = squared;
result[truck] = Some(TruckContact { position, primitive });
result[truck] = Some(TruckContact {
position,
primitive,
});
}
}
}
@@ -151,23 +263,38 @@ pub fn segment_triangle(start: V, end: V, triangle: [V; 3]) -> Option<V> {
let direction = sub(end, start);
let normal = cross(ab, ac);
let denominator = -dot3(direction, normal);
if denominator == 0.0 { return None; }
if denominator == 0.0 {
return None;
}
let from = sub(start, a);
let inverse = 1.0 / denominator;
let t = inverse * dot3(from, normal);
if t < 0.0 || t > 1.0 { return None; }
if t < 0.0 || t > 1.0 {
return None;
}
let side = cross(from, direction);
let u = inverse * dot3(ac, side);
let v = (1.0 / -denominator) * dot3(ab, side);
if u + v > 1.0 || u < 0.0 || v < 0.0 { return None; }
if u + v > 1.0 || u < 0.0 || v < 0.0 {
return None;
}
Some(core::array::from_fn(|i| direction[i].mul_add(t, start[i])))
}
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(a: V, s: f32) -> V { a.map(|v| v * s) }
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(a: V, s: f32) -> V {
a.map(|v| v * s)
}
fn cross(a: V, b: V) -> V {
[(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]), 0.0]
[
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
0.0,
]
}
@@ -77,6 +77,49 @@ fn scale(a: V, scale: f32) -> V {
a.map(|v| v * scale)
}
///Boardslide82D419A0, static rail branch (investigation kind!=2).
///Returns the native point forces in order. Translation comes from the stock
///PhysGrindTranslation animation attribute, not controller magnitude.
pub fn boardslide_control(
position: V,
point: V,
direction: V,
normal: V,
velocity: V,
translation: f32,
total_mass: f32,
exiting: bool,
ledge: bool,
) -> Vec<V> {
let across = cross(direction, normal);
let offset = dot3(sub(position, point), across);
let inward = scale(across, if offset > 0.0 { -1.0 } else { 1.0 });
if exiting {
return if offset.abs() > 0.12 {
vec![scale(inward, 201.0)]
} else {
Vec::new()
};
}
let perpendicular = sub(velocity, scale(direction, dot3(velocity, direction)));
if ledge {
return vec![scale(across, translation * 25.0)];
}
if offset.abs() > 0.16 {
let mut forces = vec![scale(inward, 10.0)];
if dot3(inward, perpendicular) < 0.0 {
let mut stop = scale(perpendicular, -(total_mass * 60.0));
stop[1] = 0.0;
forces.push(stop);
}
forces
} else {
let mut damping = scale(perpendicular, -20.0);
damping[1] = 0.0;
vec![scale(across, translation * 25.0), damping]
}
}
///82D73AB0 straight, static 50-50 branch: both support normals coincide,
///primitive motion336 is zero. The manager age controls the native exit nudge.
pub fn fifty_fifty_pop(
+2 -1
View File
@@ -85,7 +85,8 @@ pub(super) fn advance(
player_state::post_input_and_select(physics, skater)?;
player_state::pre_state(physics, skater)?;
match skater.player_state.current() {
skate_core::player::state::PhysicalStateId::GrindFiftyFifty
skate_core::player::state::PhysicalStateId::GrindBoardslide
| skate_core::player::state::PhysicalStateId::GrindFiftyFifty
| skate_core::player::state::PhysicalStateId::Nonspecific => super::grind::update(physics, skater)?,
skate_core::player::state::PhysicalStateId::BipedAir => {
super::offboard::air_state::update(physics, skater)?;
+213 -42
View File
@@ -22,11 +22,18 @@ pub(crate) struct Runtime {
pub candidate: Option<FiftyFiftyCandidate>,
pub name: String,
pub active: bool,
pub kind: u32,
test_depth_epsilon: f32,
test_depth: f32,
exiting: bool,
surface_kind: u32,
surface_normal: V,
truck_to_wheel: f32,
deck_to_truck: f32,
pin_vs_slope: PointGraph<4>,
pub distance: f32,
pop_heights: [[f32; 2]; 5],
boardslide_pop_heights: [[f32; 2]; 5],
manager_age: f32,
pub launched: bool,
pub launch_velocity: V,
@@ -35,7 +42,10 @@ pub(crate) struct Runtime {
diagnostic_tick: u32,
}
impl Runtime {
pub fn load(data: &Collections, map: Option<&skate_data::skate_map::SkateMap>) -> Result<Self, String> {
pub fn load(
data: &Collections,
map: Option<&skate_data::skate_map::SkateMap>,
) -> Result<Self, String> {
let graph = data
.words::<8>("physics_grinds", "default", "PinVsSlope")?
.map(f32::from_bits);
@@ -44,6 +54,12 @@ impl Runtime {
candidate: None,
name: String::new(),
active: false,
kind: 0,
test_depth_epsilon: data.float("physics_grinds", "default", "TestDepthEpsilon")?,
test_depth: data.float("physics_grinds", "default", "TestDepth")?,
exiting: false,
surface_kind: 0,
surface_normal: [0.0, 1.0, 0.0, 0.0],
truck_to_wheel: data.float("physics_grinds", "default", "TruckToWheel")?,
deck_to_truck: data.float("physics_grinds", "default", "DeckCenterToTruck")?,
pin_vs_slope: PointGraph {
@@ -61,6 +77,18 @@ impl Runtime {
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap(),
//82D41CE8 consumes mode100/104;50-50's82D40D60 uses92/96.
boardslide_pop_heights: ["easy", "normal", "hardcore", "motorized", "test"]
.map(|key| {
Ok::<_, String>([
data.float("physics_mode", key, "Hash_B2B1170AFFC8AC69")?,
data.float("physics_mode", key, "Hash_1B3E9F9C836D287D")?,
])
})
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.try_into()
.unwrap(),
manager_age: 0.0,
launched: false,
launch_velocity: [0.; 4],
@@ -79,7 +107,7 @@ impl Runtime {
.map(|c| self.primitives[c.primitive].owner)
.unwrap_or(u64::MAX);
GrindState {
kind: 0,
kind: self.kind as i32,
name: encode(self.name.as_bytes()),
scoring_name: encode(self.name.as_bytes()),
pathed_guid: owner,
@@ -97,37 +125,106 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
runtime.cooldown = runtime.cooldown.saturating_sub(1);
runtime.manager_age = (runtime.manager_age + 0.0035).min(1.0);
let old = runtime.candidate;
let old_kind = runtime.kind;
runtime.candidate = None;
runtime.diagnostic_tick = runtime.diagnostic_tick.wrapping_add(1);
let gated = runtime.cooldown > 0 || skater.player_input.grind.disabled
let gated = runtime.cooldown > 0
|| skater.player_input.grind.disabled
|| p.flags_2476 & 0x01000000 != 0;
// Native GrindData query82D8A828: a 1.2m half-extent, at most40 chords.
let nearby: Vec<usize> = runtime.primitives.iter().enumerate().filter(|(_,edge)|
(0..3).all(|i| edge.start[i].min(edge.end[i]) <= board[3][i]+1.2
&& edge.start[i].max(edge.end[i]) >= board[3][i]-1.2))
.map(|(i,_)|i).take(40).collect();
if nearby.is_empty() { return; }
let edges: Vec<Primitive> = nearby.iter().map(|i|runtime.primitives[*i]).collect();
let nearby: Vec<usize> = runtime
.primitives
.iter()
.enumerate()
.filter(|(_, edge)| {
(0..3).all(|i| {
edge.start[i].min(edge.end[i]) <= board[3][i] + 1.2
&& edge.start[i].max(edge.end[i]) >= board[3][i] - 1.2
})
})
.map(|(i, _)| i)
.take(40)
.collect();
if nearby.is_empty() {
return;
}
let edges: Vec<Primitive> = nearby.iter().map(|i| runtime.primitives[*i]).collect();
let mut hits = grind_contact::truck_contacts(
board, p.flags_2484, runtime.truck_to_wheel, runtime.deck_to_truck, &edges,
board,
p.flags_2484,
runtime.truck_to_wheel,
runtime.deck_to_truck,
&edges,
);
for hit in hits.iter_mut().flatten() { hit.primitive = nearby[hit.primitive]; }
for hit in hits.iter_mut().flatten() {
hit.primitive = nearby[hit.primitive];
}
let extra = &skater.animation_input.extra;
let candidate = grind_contact::fifty_fifty_candidate_on_splines(
board, [extra.grind_translation, extra.grind_stability_nudge], hits,
let fifty = grind_contact::fifty_fifty_candidate_on_splines(
board,
[extra.grind_translation, extra.grind_stability_nudge],
hits,
&runtime.primitives,
);
let velocity = p.vectors_400_416[0].map(f32::from_bits);
let slide = grind_contact::deck_contact(
board,
p.flags_2484,
runtime.test_depth_epsilon,
runtime.test_depth,
runtime.deck_to_truck,
&edges,
)
.and_then(|mut hit| {
hit.primitive = nearby[hit.primitive];
grind_contact::boardslide_candidate(
board,
hit,
runtime.primitives[hit.primitive],
velocity,
p.category_2512,
skater.player_input.grind.low_wheel_frames as i32,
p.flags_2476,
runtime.deck_to_truck,
runtime.truck_to_wheel,
)
});
//82D875A8 tries the current concrete state before the ordinary truck,
//tip and centre-deck fallback sequence.
let (candidate, kind) = if runtime.active && runtime.kind == 1 && slide.is_some() {
(slide, 1)
} else if fifty.is_some() {
(fifty, 0)
} else {
(slide, 1)
};
// Passive diagnostics for the user's visual run; no input simulation.
if runtime.diagnostic_tick % 30 == 0 || (!gated && candidate.is_some()) != old.is_some() {
bevy::log::info!("GRIND_QUERY state={} gated={} nearby={} hits={:?} depth={:?} candidate={} board={:?} up={:?} forward={:?} probe={:?} flags={:08x}/{:08x}/{:08x}/{:08x}/{:08x}",
p.state_2508, gated, nearby.len(), hits.map(|h|h.map(|h|h.primitive)),
hits.map(|h|h.map(|h|dot3(sub(board[3],h.position),board[1]))),
candidate.is_some(),board[3],board[1],board[2],
[runtime.truck_to_wheel,runtime.deck_to_truck],
p.flags_2468,p.flags_2472,p.flags_2476,p.flags_2480,p.flags_2484);
bevy::log::info!(
"GRIND_QUERY state={} gated={} nearby={} hits={:?} depth={:?} candidate={} board={:?} up={:?} forward={:?} probe={:?} flags={:08x}/{:08x}/{:08x}/{:08x}/{:08x}",
p.state_2508,
gated,
nearby.len(),
hits.map(|h| h.map(|h| h.primitive)),
hits.map(|h| h.map(|h| dot3(sub(board[3], h.position), board[1]))),
candidate.is_some(),
board[3],
board[1],
board[2],
[runtime.truck_to_wheel, runtime.deck_to_truck],
p.flags_2468,
p.flags_2472,
p.flags_2476,
p.flags_2480,
p.flags_2484
);
}
if gated { return; }
let Some(candidate) = candidate else { return; };
if gated {
return;
}
let Some(candidate) = candidate else {
return;
};
let velocity = p.vectors_400_416[0].map(f32::from_bits);
//82D886B8: air accepts an actual intersection; same active grind uses90deg.
//The ordinary level-ground admission uses17deg.
@@ -142,20 +239,45 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
return;
}
runtime.candidate = Some(candidate);
runtime.kind = kind;
if kind == 1 {
let edge = runtime.primitives[candidate.primitive];
match skate_core::air::trajectory::grind_surface::investigate(
&physics.world,
edge.start,
edge.end,
candidate.centre,
runtime.deck_to_truck,
) {
Ok(Some(surface)) => {
runtime.surface_kind = surface.evidence.kind;
runtime.surface_normal = surface.normal;
}
Ok(None) | Err(_) => {
runtime.candidate = None;
return;
}
}
}
p.grind_position_1120 = candidate.centre.map(f32::to_bits);
p.grind_direction_1136 = candidate.direction.map(f32::to_bits);
if !runtime.active || old.is_none() {
if !runtime.active || old.is_none() || old_kind != kind {
let backwards = dot3(board[2], velocity) < 0.0;
let side = dot3(sub(candidate.centre, board[3]), board[0]) >= 0.0;
runtime.name = format!(
"{}{}_50_50",
if backwards { "BF_" } else { "" },
if side { "FS" } else { "BS" }
"{}{}_{}",
if backwards && kind == 0 { "BF_" } else { "" },
if side { "FS" } else { "BS" },
if kind == 0 { "50_50" } else { "BOARD" }
);
}
}
pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
physics.grind.active = skater.player_state.current() == PhysicalStateId::GrindFiftyFifty;
physics.grind.active = matches!(
skater.player_state.current(),
PhysicalStateId::GrindFiftyFifty | PhysicalStateId::GrindBoardslide
);
physics.grind.exiting = false;
physics.grind.distance = 0.0;
physics.grind.previous_position = super::solve::deck_frame(&physics.board)[3];
skater.ground_lifecycle.skeleton_elapsed_16505 = true;
@@ -199,7 +321,12 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
.grind
.candidate
.ok_or("Grind pop has no primitive")?;
let [min, max] = physics.grind.pop_heights[p.state_variant_index_2528 as usize];
let heights = if physics.grind.kind == 1 {
&physics.grind.boardslide_pop_heights
} else {
&physics.grind.pop_heights
};
let [min, max] = heights[p.state_variant_index_2528 as usize];
let strength = skater.animation_input.extra.jump_strength;
let velocity = grind_forces::fifty_fifty_pop(
p.vectors_400_416[0].map(f32::from_bits),
@@ -233,20 +360,47 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
.grind
.candidate
.ok_or("Active grind lost its candidate before selection")?;
let normal = grind_contact::upright_normal(c.direction);
let normal = if physics.grind.kind == 1 {
physics.grind.surface_normal
} else {
grind_contact::upright_normal(c.direction)
};
let across = cross(c.direction, normal);
let force = grind_forces::lateral_pin(
board,
c.centre,
across,
velocity,
800.,
0.,
0.07,
true,
physics.grind.pin_vs_slope.evaluate(dot3(normal, normal)),
);
append(physics, force)?;
if physics.grind.kind == 1 {
physics.grind.exiting |= p.flags_2468 & 0x0020_0000 != 0;
let mass = skater
.player_input
.toolkit
.as_ref()
.ok_or("Boardslide needs board mass")?
.total_mass;
for force in grind_forces::boardslide_control(
board[3],
c.centre,
c.direction,
normal,
velocity,
skater.animation_input.extra.grind_translation,
mass,
physics.grind.exiting,
physics.grind.surface_kind == 2,
) {
append(physics, force)?;
}
} else {
let force = grind_forces::lateral_pin(
board,
c.centre,
across,
velocity,
800.,
0.,
0.07,
true,
physics.grind.pin_vs_slope.evaluate(dot3(normal, normal)),
);
append(physics, force)?;
}
//The primitive support frame is static and level for the authored rails.
//82D86318 publishes this unit support normal in candidate304.
let friction = grind_forces::friction(
@@ -257,7 +411,11 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
false,
1.3,
0,
[50., 40., 40.],
if physics.grind.kind == 1 {
[55., 55., 60.]
} else {
[50., 40., 40.]
},
);
append(physics, friction)?;
let forward = if dot3(board[2], c.direction) > 0.0 {
@@ -265,7 +423,20 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
} else {
c.direction.map(|x| -x)
};
let target = [cross(normal, forward), normal, forward, board[3]];
let target = if physics.grind.kind == 1 {
//82D418B8 aligns the board's RIGHT axis with the rail;50-50
//82D41E38 aligns its FORWARD axis instead.
let right = if dot3(board[0], c.direction) > 0.0 {
c.direction
} else {
c.direction.map(|x| -x)
};
[right, normal, cross(right, normal), board[3]]
} else {
[cross(normal, forward), normal, forward, board[3]]
};
//82D40890 uses constant0.1 for interpolation; its f1 parameter is
//separate random perturbation amplitude, not the interpolation weight.
let (frame, _) = skate_core::animation::foot_ik::interpolate_native(&board, &target, 0.1);
physics.board.set_hook_transform(RetailAffineTransform {
basis: Basis3 {
@@ -12,7 +12,7 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
skater.player_state.current(),
PhysicalStateId::PhysicsGround
| PhysicalStateId::PhysicsAir
| PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
@@ -13,7 +13,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
state,
PhysicalStateId::PhysicsGround
| PhysicalStateId::PhysicsAir
| PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
@@ -88,7 +88,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
}
let physical = &mut skater.player_input.physical;
if physics.grind.active {
physical.grinds.words_136_140 = [0, 1];
physical.grinds.words_136_140 = [physics.grind.kind, 1];
if physics.grind.launched {
physical.air.launched_442=1;
physical.air.launch_velocity_128=physics.grind.launch_velocity.map(f32::to_bits);
@@ -8,8 +8,8 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
let p = &skater.player_input.processed;
let input = StateSelectionInput {
processed: ProcessedStateInput {
//The live paired-truck scorer publishes native kind0 and candidate384.
grind_type_1248: 0,
//The retained contact owner publishes native kind and candidate384.
grind_type_1248: physics.grind.kind,
grind_candidate_1488: physics.grind.candidate.is_some(),
grind_investigation_flags_1516: 0,
field_1776: p.external_physics_1616.flags as i32,
@@ -15,7 +15,7 @@ impl PhysicalStateCalls for Calls {
PhysicalStateId::Sleeping
| PhysicalStateId::PhysicsGround
| PhysicalStateId::PhysicsAir
| PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
@@ -31,7 +31,7 @@ impl PhysicalStateCalls for Calls {
call.state.state,
PhysicalStateId::PhysicsGround
| PhysicalStateId::PhysicsAir
| PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
@@ -59,7 +59,7 @@ pub(super) fn set(
PhysicalStateId::Sleeping => requested == PhysicalStateId::PhysicsGround,
PhysicalStateId::PhysicsGround
| PhysicalStateId::PhysicsAir
| PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
@@ -71,7 +71,7 @@ pub(super) fn set(
requested,
PhysicalStateId::PhysicsGround
| PhysicalStateId::PhysicsAir
| PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty
| PhysicalStateId::Nonspecific
| PhysicalStateId::KnownAir
| PhysicalStateId::GroundAnimation
@@ -157,7 +157,7 @@ pub(super) fn set(
//Native82DB8540 publishes Processed state/history BEFORE old Exit. The
//same retained objects then receive Exit followed by the new Enter.
match current {
PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::exit(physics, skater)?,
PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::exit(physics, skater)?,
PhysicalStateId::BipedAir => super::super::offboard::air_state::exit(physics, skater),
PhysicalStateId::BipedGround => super::super::offboard::ground_state::exit(physics, skater),
PhysicalStateId::PhysicsGround => super::super::ground_exit::exit(physics, skater),
@@ -172,7 +172,7 @@ pub(super) fn set(
_ => unreachable!("state support checked before publication"),
}
match requested {
PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater),
PhysicalStateId::GrindBoardslide | PhysicalStateId::GrindFiftyFifty | PhysicalStateId::Nonspecific => super::super::grind::enter(physics, skater),
PhysicalStateId::BipedAir => super::super::offboard::air_state::enter(physics, skater),
PhysicalStateId::BipedGround => {
super::super::offboard::ground_state::enter(physics, skater)
+38
View File
@@ -0,0 +1,38 @@
# Boardslide contact integration
The live grind owner now supports paired-truck 50-50 and centre-deck boardslide
contacts. The ordinary authored default-world splines and imported splines use
the same primitive collection. This is an incremental playable port, not all
retail grind families or established 1:1 parity.
Native TU3 source boundaries:
- 82C1FB98: longitudinal deck rectangle, TestDepthEpsilon592, TestDepth596,
DeckCenterToTruck636, first-triangle precedence and closest valid hit.
- 82D889A8: upright dot>.65, squared deck depth<.0036, low-speed ground gate,
projected rail/deck clearance and truck-width exclusion.
- 82D875A8: current-state preference followed by fallback contact paths.
Only the implemented truck and centre-deck paths are currently connected.
- 82D418B8/82D40890: right-axis alignment for boardslides, compared with
forward-axis alignment for50-50. The native matrix interpolation weight is
.1; the separate random orientation perturbation is not implemented here.
- 82D419A0/82D3FD08: translation25, lateral damping20, off-centre force10,
mass-scaled60Hz opposing-velocity correction and exit force201.
- 82D41848: friction strengths55/55/60.
- 82D41CE8: interpolated mode100/104 pop values, distinct from50-50 mode92/96.
- 82D41D18: Grinds136 publishes kind1. Selector maps kind1 to state400.
Geometry investigation now retains six native probe indices. Native82C20C08
reads the raised cross line at result5 when deciding the blocked geometry kind;
the older ground-only helper incorrectly inspected result4 there. The new
boardslide investigation does not reuse that incorrect classification shortcut.
The existing ground-only caller has not been changed in this increment.
Still incomplete: other concrete grind state owners, full chromosome/name
production (the existing approach-name convention remains), all material and
engagement branches, and airborne GrindAirAdjust. The new trajectory admission,
publication and landing-normal leaves remain disconnected from live airborne
selection until that owner is complete. No unconditional snapping was added.
Validation: compilation only. The user has reserved all gameplay and stability
playtesting for themselves; no automated gameplay or test suite was run.
+6 -4
View File
@@ -1,6 +1,7 @@
# Native grind assist port status
The live game still supports the paired-truck50-50 path only. The new
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.
@@ -45,9 +46,10 @@ The core crate builds. The user has reserved gameplay testing for themselves.
## Proposed read-only reference trace
Awaiting user authorization because earlier instructions prohibit launching
the recomp. No synthetic gameplay input is proposed; the user would approach
and land on a rail in the reference game.
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