Reconcile archive offboard and grind owners with retained game features

This commit is contained in:
chazmm committed 2026-09-08 20:54:58 +01:00
1 parent ae452ba3fe
commit edce9c8e7d
434 files changed
+35475 -6522

No files matched your search

+7 -3
View File
@@ -14,7 +14,8 @@ pub fn calculate(
mode: GroundJumpMode,
s: &GroundJumpSettings,
) -> GroundJump {
if input.flags_2468 & 0x0040_0000 == 0 {
let hippy = input.flags_2480 & 0x0000_1000 != 0;
if input.flags_2468 & 0x0040_0000 == 0 && !hippy {
return GroundJump::default();
}
let normal = input.filtered_ground_normal;
@@ -32,7 +33,9 @@ pub fn calculate(
value[1] += 1.0;
value
} else {
let minimum = if input.flags_2484 & 0x800 != 0 {
let minimum = if hippy {
s.hippy_minimum_height
} else if input.flags_2484 & 0x800 != 0 {
mode.minimum_height_64
} else {
mode.minimum_height_68
@@ -43,7 +46,8 @@ pub fn calculate(
s.absolute_minimum_height,
);
let high = s.maximum_height_vs_speed.evaluate(speed_fraction).mul_add(
mode.maximum_height - s.absolute_minimum_height,
if hippy { s.hippy_maximum_height } else { mode.maximum_height }
- s.absolute_minimum_height,
s.absolute_minimum_height,
);
let height = low.mul_add(1.0 - input.jump_strength, high * input.jump_strength);
@@ -23,10 +23,15 @@ pub struct GroundJumpSettings {
pub adjust_z_factor: f32, //432
pub adjust_x_factor: f32, //436
pub absolute_minimum_height: f32, //456, metres
/// physics_jump.HippyJumpMinHeight/MaxHeight. These are separate from
/// the ordinary jump-mode bounds in the stock data.
pub hippy_minimum_height: f32,
pub hippy_maximum_height: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct GroundJumpInput {
pub flags_2468: u32,
pub flags_2480: u32,
pub flags_2484: u32,
pub flags_2488: u32,
pub effective_forward: [f32; 4], //Processed160
+53 -320
View File
@@ -1,343 +1,76 @@
//! TU3 rail-trajectory candidate leaves82D60C80/82D60B98,
//! 82D6A398 and82D6A168. Admission and physical adjustment are separate owners.
use super::{math::*, Prediction, Trajectory};
//! Original S3 trajectory grind acquisition and typed retained selection.
mod admission;
mod candidate;
use super::grind_surface::LandingOrientation;
use super::{Prediction, Trajectory, math::*};
use crate::{physics::grind_contact::Primitive, point_graph::PointGraph};
pub use admission::{admitted_displacement, apply_admitted_target};
pub use candidate::{descending_plane_time, consider_grind_primitive, take_best_grind, trajectory_box_filter};
#[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,
pub trajectory_point: Vector,
pub direction: Vector,
pub approach: Vector,
pub distance: f32,
pub time: f32,
pub angle: f32,
pub frame: i32,
pub primitive: usize,
}
///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,
pub side: Vector,
}
#[derive(Clone, Debug)]
pub struct GrindAssistLimits {
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
///Actual deck dimensions from82762E50 (DeckMidLength) and82C00868.
pub lock_distance: f32,
pub max_speed_squared_ledge: f32,
pub max_speed_squared_rail: f32,
pub max_downward_speed: f32,
pub ledge_scalars: [f32; 4],
pub tip_scalar: f32,
pub maximum_adjust_angle: f32,
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,
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 {
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;
}
Some(correction)
}
///82D60C80's quadratic roots followed by82D60B98's later-root selection.
/// 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);
if discriminant > 0.0 {
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)
}
}
///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,
) -> 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 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);
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);
//Strict endpoint test82D6A708. No closest-point clamping.
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);
Some(GrindTrajectoryCandidate {
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))
};
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()
}
///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>,
) -> 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);
}
} else if candidate.distance < fallback {
fallback = candidate.distance;
chosen = Some(i);
}
}
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 provider_index: usize,
pub primitive_flags: u32,
pub orientation: LandingOrientation,
pub point: Vector,
pub normal: Vector,
///Selector2896, distinct from landing contact normal.
pub vertical_normal: Vector,
}
impl GrindTarget {
pub fn air_target(self) -> crate::physics::grind_air::Target {
crate::physics::grind_air::Target {
edge: self.edge,
primitive_flags: self.primitive_flags,
orientation: self.orientation,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct GrindEvaluation {
pub target: Option<GrindTarget>,
pub score: f32,
pub distance: f32,
///9663/9664 can be written even when candidate admission fails.
pub effective_lock_distance: Option<f32>,
pub penalty_domain: f32,
}
impl GrindEvaluation {
pub fn penalty_input(self) -> f32 {
if let Some(distance) = self.effective_lock_distance {
let denominator = distance + f32::from_bits(0x3e19_999a);
if denominator < self.penalty_domain {
return (self.penalty_domain / denominator) * self.distance;
}
}
self.distance
}
}
@@ -0,0 +1,166 @@
//! Original S3 82D6A840 admission and82D6AF58 landing publication.
use super::*;
///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::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),
);
//82C1E170 removes only the positive normalized-direction component,
//multiplying by the ORIGINAL supplied direction.
let component = dot(limited, normalize(direction));
normalize(if component > 0.0 {
sub(limited, scale(direction, component))
} else {
limited
})
}
///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,
limits: &GrindAssistLimits,
effective_lock_distance: &mut Option<f32>,
) -> 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 {
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
};
let lock_distance = lock_distance.max(0.1);
*effective_lock_distance = Some(lock_distance);
if candidate.distance >= lock_distance {
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;
}
Some(correction)
}
@@ -0,0 +1,192 @@
//! Original S3 82D60C80/82D60B98/82D6A398/82D6A168.
use super::*;
///82C1F310 after provider traversal; pairwise rejection preserves global order.
pub fn trajectory_box_filter(
indices: &[usize],
primitives: &[Primitive],
takeoff: Vector,
) -> Vec<usize> {
let directions: Vec<_> = indices
.iter()
.map(|&i| {
let d = sub(primitives[i].end, primitives[i].start);
scale(d, inverse_length(dot(d, d)))
})
.collect();
let mut accepted = Vec::with_capacity(indices.len());
for (i, &index) in indices.iter().enumerate() {
let a = primitives[index];
let mut keep = true;
for (j, &other) in indices.iter().enumerate() {
if i == j {
continue;
}
let crossed = cross(directions[i], directions[j]);
if !(dot(crossed, crossed) < f32::from_bits(0x38d1_b717)) {
continue;
}
let b = primitives[other];
let delta = sub(b.start, a.start);
let perpendicular = sub(delta, scale(directions[j], dot(directions[j], delta)));
let distance = dot(perpendicular, perpendicular);
if !(distance > f32::from_bits(0x38d1_b717) && distance < f32::from_bits(0x3c23_d70b)) {
continue;
}
let middle = scale(add(a.start, a.end), 0.5);
let from = sub(b.start, middle);
if !(dot(from, sub(b.end, middle)) < 0.) {
continue;
}
let up = normalize(cross(cross(directions[i], UP), directions[i]));
let height = dot(delta, up);
let alternative = add(
middle,
sub(from, scale(directions[j], dot(directions[j], from))),
);
let first = sub(middle, takeoff);
let second = sub(alternative, takeoff);
if (height > -0.02 && dot(first, first) > dot(second, second)) || height > 0.02 {
keep = false;
}
}
if keep {
accepted.push(index);
}
}
accepted
}
///82D60C80's quadratic roots followed by82D60B98's later-root selection.
/// 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);
if discriminant > 0.0 {
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)
}
}
///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,
) -> 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 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);
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);
//Strict endpoint test82D6A708. No closest-point clamping.
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);
Some(GrindTrajectoryCandidate {
point,
trajectory_point,
direction,
approach,
distance,
time,
angle,
frame: (time * f32::from_bits(0x426f_ffff)) 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))
};
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()
}
///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>,
) -> 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);
}
} else if candidate.distance < fallback {
fallback = candidate.distance;
chosen = Some(i);
}
}
chosen.map(|i| candidates.remove(i))
}
@@ -1,168 +1,162 @@
//! 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::{hang_lines, HangGeometryInput},
};
//! Static geometry investigation from original S3 TU3 82C20728/82C20C08.
//! Query execution is supplied by the world owner; a miss is not missing data.
//! No moving-object observations or authored material codes are synthesized.
use super::math::*;
mod classify;
mod orientation;
mod landing;
pub use landing::{LandingOrientation, update_landing_orientation};
#[cfg(test)]
mod tests;
pub type V = [f32; 4];
#[derive(Clone, Copy, Debug)]
pub struct InvestigationInput {
pub start: V,
pub end: V,
pub reference: V,
/// Input +48, enabled by input byte +64: a six-unit world-down probe.
pub optional_probe: Option<V>,
/// physics_grinds layout +636, supplied from the stock collection.
pub deck_center_to_truck: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Probe {
pub start: V,
pub end: V,
pub radius: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct ProbeHit {
pub fraction: f32,
pub position: V,
pub normal: V,
/// Original query result +100, NOT a rendering tag or triangle index.
pub packed_surface: u32,
}
#[derive(Clone, Debug)]
pub struct Investigation {
pub center: V,
pub upmost_normal: V,
pub direction: V,
pub probes: Vec<Probe>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum GeometryType {
ThinRail = 0,
FatRail = 1,
Ledge = 2,
Impossible = 3,
}
#[derive(Clone, Copy, Debug)]
pub struct GrindSurface {
pub evidence: GrindSurfaceEvidence,
pub normal: Vector,
///Investigation16/32: far-side query endpoints, consumed by82D883E0.
pub far_points: [Vector; 2],
}
///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,
far_points: far,
}))
}
///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])
pub center: V,
pub far_points: [V; 2],
pub upmost_normal: V,
pub direction: V,
pub high_side: V,
pub normal_limits: [f32; 2],
pub kind: GeometryType,
pub audio_surface: u32,
pub physics_surface: u32,
/// Native output +116. Preserve unnamed bits, rather than invent meaning.
pub flags: u32,
/// 82C21828 evaluated from the geometric limits; distinct from +48 upmost.
pub tilted_upmost_normal: V,
}
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),
),
]
pub const INVALID: u32 = 0x8000_0000;
pub const CURB: u32 = 0x4000_0000;
pub const BLOCKED_CROSS_SECTION: u32 = 0x2000_0000;
pub const STAIR: u32 = 0x1000_0000;
pub const OPTIONAL_NORMAL_TEST: u32 = 0x0800_0000;
pub const OPTIONAL_DROP_TEST: u32 = 0x0400_0000;
/// 82C20C08's no-submission output; not an artificial startup blocker.
fn not_submitted(input: InvestigationInput) -> Self {
let distance = input.deck_center_to_truck;
let x = [1., 0., 0., 0.];
let z = [0., 0., 1., 0.];
Self {
center: input.reference,
far_points: [madd(x, distance, input.reference), sub(input.reference, scale(x, distance))],
upmost_normal: z,
direction: z,
high_side: z,
normal_limits: [0.; 2],
kind: GeometryType::Impossible,
audio_surface: 3,
physics_surface: 1,
flags: Self::INVALID,
tilted_upmost_normal: orientation::tilted_normal(z, z, [0.; 2]),
}
}
}
/// 82C20728: six fixed descriptors and the optional seventh descriptor.
/// Returns None exactly for the native inadequate-up-vector gate.
pub fn prepare(input: InvestigationInput) -> Option<Investigation> {
let delta = sub(input.end, input.start);
let raw_up = cross(delta, cross(UP, delta));
let up_length = length(raw_up);
let retain_normalized = |v| {
let square = dot(v, v);
let inverse = inverse_length(square);
let magnitude = if square == 0. { 0. } else { square * inverse };
if magnitude > f32::from_bits(0x3586_37bd) { scale(v, inverse) } else { v }
};
let up = retain_normalized(raw_up);
let direction = retain_normalized(delta);
if up_length < f32::from_bits(0x3727_c5ac) { return None; }
let projected = madd(direction, dot(sub(input.reference, input.start), direction), input.start);
let center = sub(input.reference, sub(input.reference, projected));
let side = cross(up, direction);
let short_up = scale(up, f32::from_bits(0x3d23_d70a));
let near_side = scale(side, f32::from_bits(0x3db8_51ec));
let far_side = scale(side, input.deck_center_to_truck);
let far_up = scale(up, input.deck_center_to_truck * f32::from_bits(0x3f87_ae14));
let raised = scale(up, f32::from_bits(0x3db8_51ec));
let line = |at, half, radius| Probe { start: add(at, half), end: sub(at, half), radius };
let mut probes = vec![
line(add(center, near_side), short_up, 0.),
line(sub(center, near_side), short_up, 0.),
line(add(center, far_side), far_up, 0.),
line(sub(center, far_side), far_up, 0.),
line(center, short_up, 0.),
line(add(center, raised), near_side, f32::from_bits(0x3a83_126f)),
];
if let Some(start) = input.optional_probe {
probes.push(Probe { start, end: add(start, [0., -6., 0., 0.]), radius: 0. });
}
Some(Investigation { center, upmost_normal: up, direction, probes })
}
/// Full synchronous host equivalent of prepare/submit/wait/read/classify.
/// The callback must provide the original nearest-hit contract and packed
/// surface code for each probe, honoring its world-query filtering context.
pub fn investigate<E>(
input: InvestigationInput,
mut query: impl FnMut(usize, Probe) -> Result<Option<ProbeHit>, E>,
) -> Result<GrindSurface, E> {
let Some(plan) = prepare(input) else { return Ok(GrindSurface::not_submitted(input)); };
let mut hits = [None; 7];
for (index, &probe) in plan.probes.iter().enumerate() {
hits[index] = query(index, probe)?;
}
Ok(classify::resolve(&plan, &hits, 0))
}
/// Explicit result processing also permits a native-style reused output flag
/// word. Bit29 is set on obstruction, not cleared on the other native branch.
pub fn resolve(plan: &Investigation, hits: &[Option<ProbeHit>; 7], previous_flags: u32) -> GrindSurface {
classify::resolve(plan, hits, previous_flags)
}
@@ -0,0 +1,85 @@
use super::*;
/// 82C20ED4..82C213F4, including the helpers 82C21410/21558/216A0.
pub(super) fn resolve(plan: &Investigation, hits: &[Option<ProbeHit>; 7], previous_flags: u32) -> GrindSurface {
let [h0, h1, h2, h3, h4, h5, supplied_h6] = *hits;
let h6 = if plan.probes.len() == 7 { supplied_h6 } else { None };
let blocked = (h0.is_some() && h1.is_some()) || h5.is_some();
let kind = if blocked { GeometryType::Impossible }
else if h0.is_some() {
if h2.is_some_and(|h| h.fraction < f32::from_bits(0x3f26_6666)) { GeometryType::Ledge } else { GeometryType::FatRail }
} else if h1.is_some() {
if h3.is_some_and(|h| h.fraction < f32::from_bits(0x3f26_6666)) { GeometryType::Ledge } else { GeometryType::FatRail }
} else { GeometryType::ThinRail };
// Native result high side uses unguarded normalization, not NormalizeSafe.
let side = cross(UP, plan.direction);
let side = scale(side, inverse_length(dot(side, side)));
let high_side = if !blocked && h0.is_none() && h1.is_some() { scale(side, -1.) } else { side };
let curb = kind == GeometryType::Ledge && is_curb(h2, h3, plan.upmost_normal, plan.center);
let stair = [h0, h1].into_iter().flatten().any(|h| h.packed_surface & 0xf80 == 0x400);
let optional_normal = h6.is_some_and(|h| optional_normal_test(h, plan.center, plan.direction));
let optional_drop = h6.is_none_or(|h| !(h.position[1] - plan.center[1] > -1.));
let mut flags = previous_flags & !(GrindSurface::INVALID | GrindSurface::CURB | GrindSurface::STAIR
| GrindSurface::OPTIONAL_NORMAL_TEST | GrindSurface::OPTIONAL_DROP_TEST);
if blocked { flags |= GrindSurface::BLOCKED_CROSS_SECTION; }
if curb { flags |= GrindSurface::CURB; }
if stair { flags |= GrindSurface::STAIR; }
if optional_normal { flags |= GrindSurface::OPTIONAL_NORMAL_TEST; }
if optional_drop { flags |= GrindSurface::OPTIONAL_DROP_TEST; }
let (audio_surface, physics_surface) = surface_info(kind, curb, [h0, h1, h2, h3, h4]);
let far_points = [h2.map_or(plan.probes[2].end, |h| h.position), h3.map_or(plan.probes[3].end, |h| h.position)];
let negative_up = scale(plan.upmost_normal, -1.);
// Native first computes lengths, then separately refines their reciprocals.
// No safe-normalize epsilon is introduced for the far-point directions.
let angles = far_points.map(|point| {
let delta = sub(point, plan.center);
let unit = scale(delta, reciprocal(length(delta)));
crate::trigonometry::acos(dot(negative_up, unit).max(-1.).min(1.))
});
let half_pi = f32::from_bits(0x3fc9_0fdb);
let clearance = f32::from_bits(0x3e8e_fa35);
let normal_limits = [(angles[0] + half_pi) + clearance,
((f32::from_bits(0x40c9_0fdb) - angles[1]) - half_pi) - clearance];
let tilted_upmost_normal = orientation::tilted_normal(plan.direction, plan.upmost_normal, normal_limits);
GrindSurface { center: plan.center, far_points, upmost_normal: plan.upmost_normal,
direction: plan.direction, high_side, normal_limits, kind, audio_surface,
physics_surface, flags, tilted_upmost_normal }
}
fn is_curb(a: Option<ProbeHit>, b: Option<ProbeHit>, up: V, center: V) -> bool {
let (Some(a), Some(b)) = (a, b) else { return false; };
if !(dot(up, a.normal) > 0.99 && dot(up, b.normal) > 0.99) { return false; }
let ah = dot(sub(a.position, center), up);
let bh = dot(sub(b.position, center), up);
let (high, low) = if ah >= bh { (ah, bh) } else { (bh, ah) };
high > -0.035 && high < 0.035 && low > -0.24 && low < -0.2
}
fn optional_normal_test(hit: ProbeHit, center: V, direction: V) -> bool {
// 82C1E170 subtracts ORIGINAL direction times a positive normalized dot.
let component = dot(hit.normal, normalize(direction));
let projected = if component > 0. { sub(hit.normal, scale(direction, component)) } else { hit.normal };
let normal = normalize(projected);
if !(f32::from_bits(0x3f66_6666) > normal[1]) { return false; }
let mut offset = sub(hit.position, center);
offset[1] = 0.;
dot(offset, normal) > 0.
}
fn surface_info(kind: GeometryType, curb: bool, hits: [Option<ProbeHit>; 5]) -> (u32, u32) {
if curb { return (4, 2); }
if kind == GeometryType::ThinRail {
return (hits[4].map_or(11, |h| h.packed_surface & 0x7f), 4);
}
// Pair priority 0/1, then 2/3, then 4. Equal heights select the second.
for pair in [0, 2] {
let selected = match (hits[pair], hits[pair + 1]) {
(Some(a), Some(b)) => Some(if a.position[1] > b.position[1] { a } else { b }),
(a, b) => a.or(b),
};
if let Some(hit) = selected { return unpack(hit.packed_surface); }
}
hits[4].map_or((3, 1), |h| unpack(h.packed_surface))
}
fn unpack(surface: u32) -> (u32, u32) { (surface & 0x7f, (surface >> 7) & 0x1f) }
@@ -0,0 +1,82 @@
//! Original S3 TU3 82C21930 after six-query completion, not the S2 type3 gate.
//! Query ownership stays with the caller; None means actual failed completion.
use super::{GeometryType, GrindSurface, V, cross, dot, orientation, sub};
/// Native PossibleGrindOrientations output offsets. The returned up-vector is
/// separate: 82C21930 can leave it and these vectors untouched on failure.
#[derive(Clone, Copy, Debug)]
pub struct LandingOrientation {
pub kind: GeometryType, // +0
pub garbage: bool, // +4
pub boardslide_dir: V, // +16
pub tipslide_dir: V, // +32
pub backslash_dir: V, // +48
pub high_side: V, // +64
}
/// 82C21930: primitive endpoints and grind_position form a six-probe geometry
/// investigation (optional_probe=None). Pass its completed surface as Some;
/// when prepare returns None, pass None and retain the caller's cached vectors.
/// takeoff_position is original r5, grind_position r6; neither is a velocity.
/// Completed kind3 is deliberately NOT rejected here; the analyzer owns its
/// later type3 rejection. Do not replace it with S2 82CFCA88's early type gate.
pub fn update_landing_orientation(
surface: Option<&GrindSurface>,
takeoff_position: V,
grind_position: V,
up: &mut V,
out: &mut LandingOrientation,
) {
let Some(surface) = surface else {
out.kind = GeometryType::Impossible;
out.garbage = true;
return;
};
if surface.flags & GrindSurface::INVALID != 0 {
// 82C21A10..82C21A5C: completed but invalid geometry has its own
// fully written fallback, distinct from the no-completion branch.
let x = [1., 0., 0., 0.];
*up = [0., 1., 0., 0.];
out.kind = GeometryType::ThinRail;
out.garbage = true;
out.boardslide_dir = x;
out.tipslide_dir = x;
out.backslash_dir = negate(x);
out.high_side = x;
return;
}
*up = orientation::tilted_normal(surface.direction, surface.upmost_normal, surface.normal_limits);
out.kind = surface.kind;
out.garbage = false;
out.high_side = surface.high_side;
// Original cross products are not followed by normalization.
let side = cross(surface.upmost_normal, surface.direction);
let tipslide_angle = f32::from_bits(0x3eb2_b8c3); // 822F8618
if surface.kind == GeometryType::ThinRail {
out.boardslide_dir = side;
let oriented = if dot(side, sub(takeoff_position, grind_position)) > 0. {
side
} else { negate(side) };
let axis = cross(oriented, surface.upmost_normal);
out.tipslide_dir = orientation::rotate(axis, oriented, tipslide_angle);
out.backslash_dir = orientation::rotate(axis, oriented,
f32::from_bits(0x401a_25c2)); // 822F93B8, stored pi-minus-backslash
} else {
let axis = cross(side, surface.upmost_normal);
out.boardslide_dir = if surface.kind == GeometryType::FatRail {
side
} else { cross(*up, surface.direction) };
let sign = if dot(side, surface.high_side) > 0. { 1. } else { -1. };
// Original sign vectors broadcast across all four lanes. These are
// multiplies, unlike the XOR sign inversion in the thin-rail branch.
out.backslash_dir = orientation::rotate(
axis.map(|v| v * sign), side.map(|v| v * sign),
f32::from_bits(0x3f3b_a866)); // 822F8614
out.tipslide_dir = orientation::rotate(
axis.map(|v| v * -sign), side.map(|v| v * -sign), tipslide_angle);
}
}
/// Native XOR toggles every sign bit, including W and signed zero.
fn negate(v: V) -> V { v.map(|v| f32::from_bits(v.to_bits() ^ 0x8000_0000)) }
@@ -0,0 +1,20 @@
use super::*;
/// 82C21828. Strict straddling test and nearest-limit tie-to-high branch.
pub(super) fn tilted_normal(direction: V, up: V, limits: [f32; 2]) -> V {
let [lo, hi] = limits;
let pi = f32::from_bits(0x4049_0fdb);
let angle = if lo < hi {
if lo < pi && hi > pi { return up; }
if (pi - lo).abs() < (pi - hi).abs() { lo } else { hi }
} else { (hi + lo) * 0.5 };
rotate(direction, scale(up, -1.), angle)
}
/// 82C1E220: half-angle sine/cosine quaternion; existing independent PC
/// trigonometry supplies its numerical leaves, not a generated SIMD helper.
pub(super) fn rotate(axis: V, value: V, angle: f32) -> V {
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., value)
}
@@ -0,0 +1,117 @@
use super::*;
fn input() -> InvestigationInput {
InvestigationInput { start: [0.; 4], end: [0., 0., 2., 0.],
reference: [4., 5., 1., 0.], optional_probe: None, deck_center_to_truck: 0.243 }
}
fn hit(y: f32, fraction: f32, surface: u32) -> ProbeHit {
ProbeHit { position: [0.243, y, 1., 0.], normal: UP, fraction, packed_surface: surface }
}
#[test]
fn seven_descriptors_preserve_native_index_and_world_down_probe() {
let mut value = input();
value.optional_probe = Some([2., 3., 4., 0.]);
let plan = prepare(value).unwrap();
assert_eq!(plan.probes.len(), 7);
assert_eq!(plan.center, [0., 0., 1., 0.]);
assert_eq!(plan.probes[0].start, [0.09, 0.04, 1., 0.]);
assert_eq!(plan.probes[4].start, [0., 0.04, 1., 0.]);
assert_eq!(plan.probes[5].start, [0.09, 0.09, 1., 0.]);
assert_eq!(plan.probes[5].radius.to_bits(), 0x3a83_126f);
assert_eq!(plan.probes[6], Probe { start: [2., 3., 4., 0.], end: [2., -3., 4., 0.], radius: 0. });
let mut called = Vec::new();
investigate(value, |i, _| { called.push(i); Ok::<_, ()>(None) }).unwrap();
assert_eq!(called, [0, 1, 2, 3, 4, 5, 6]);
}
#[test]
fn raised_cross_probe_not_center_probe_blocks_geometry() {
let plan = prepare(input()).unwrap();
let mut hits = [None; 7];
hits[4] = Some(hit(0., 0.5, 17));
let result = resolve(&plan, &hits, 0);
assert_eq!(result.kind, GeometryType::ThinRail);
assert_eq!((result.audio_surface, result.physics_surface), (17, 4));
hits[5] = Some(hit(0., 0.5, 17));
let result = resolve(&plan, &hits, 0);
assert_eq!(result.kind, GeometryType::Impossible);
assert_ne!(result.flags & GrindSurface::BLOCKED_CROSS_SECTION, 0);
assert_eq!(result.flags & GrindSurface::INVALID, 0);
}
#[test]
fn strict_coarse_boundary_high_side_and_surface_codes() {
let plan = prepare(input()).unwrap();
let mut hits = [None; 7];
hits[1] = Some(hit(0., 0.5, 23 | (8 << 7)));
hits[3] = Some(hit(-0.1, 0.65, 9));
let result = resolve(&plan, &hits, 0);
assert_eq!(result.kind, GeometryType::FatRail);
assert_eq!(result.high_side[0], -1.);
assert_eq!((result.audio_surface, result.physics_surface), (23, 8));
assert_ne!(result.flags & GrindSurface::STAIR, 0);
hits[3].as_mut().unwrap().fraction = f32::from_bits(0.65f32.to_bits() - 1);
assert_eq!(resolve(&plan, &hits, 0).kind, GeometryType::Ledge);
}
#[test]
fn curb_requires_two_coarse_normals_and_strict_height_ranges() {
let plan = prepare(input()).unwrap();
let mut hits = [None; 7];
hits[0] = Some(hit(0., 0.5, 17));
hits[2] = Some(hit(0., 0.5, 17));
hits[3] = Some(hit(-0.22, 0.9, 17));
let result = resolve(&plan, &hits, 0);
assert_ne!(result.flags & GrindSurface::CURB, 0);
assert_eq!((result.audio_surface, result.physics_surface), (4, 2));
hits[3].as_mut().unwrap().position[1] = -0.2;
assert_eq!(resolve(&plan, &hits, 0).flags & GrindSurface::CURB, 0);
hits[3].as_mut().unwrap().position[1] = -0.22;
hits[3].as_mut().unwrap().normal = [0., 0.99, 0., 0.];
assert_eq!(resolve(&plan, &hits, 0).flags & GrindSurface::CURB, 0);
}
#[test]
fn optional_query_flags_use_seventh_result_and_strict_drop_test() {
let mut value = input();
value.optional_probe = Some([0., 2., 1., 0.]);
let plan = prepare(value).unwrap();
let mut hits = [None; 7];
hits[6] = Some(ProbeHit { position: [1., -1., 1., 0.], normal: [1., 0., 0., 0.], fraction: 0.5, packed_surface: 0 });
let result = resolve(&plan, &hits, 0);
assert_ne!(result.flags & GrindSurface::OPTIONAL_NORMAL_TEST, 0);
assert_ne!(result.flags & GrindSurface::OPTIONAL_DROP_TEST, 0);
hits[6].as_mut().unwrap().position[1] = -0.99;
assert_eq!(resolve(&plan, &hits, 0).flags & GrindSurface::OPTIONAL_DROP_TEST, 0);
}
#[test]
fn output_reuse_preserves_unassigned_bits_and_native_blocked_latch() {
let plan = prepare(input()).unwrap();
let old = GrindSurface::INVALID | GrindSurface::CURB | GrindSurface::BLOCKED_CROSS_SECTION | 7;
let result = resolve(&plan, &[None; 7], old);
assert_eq!(result.flags & 7, 7);
assert_ne!(result.flags & GrindSurface::BLOCKED_CROSS_SECTION, 0);
assert_eq!(result.flags & (GrindSurface::INVALID | GrindSurface::CURB), 0);
}
#[test]
fn vertical_query_uses_native_no_submission_result() {
let mut value = input();
value.end = [0., 2., 0., 0.];
let result = investigate(value, |_, _| -> Result<Option<ProbeHit>, ()> { panic!("native gate should not submit") }).unwrap();
assert_ne!(result.flags & GrindSurface::INVALID, 0);
assert_eq!((result.audio_surface, result.physics_surface), (3, 1));
assert_eq!(result.kind, GeometryType::Impossible);
assert_eq!(result.tilted_upmost_normal[2], -1.);
}
#[test]
fn tilted_normal_is_distinct_from_unrotated_upmost() {
let rail = [0., 0., 1., 0.];
assert_eq!(orientation::tilted_normal(rail, UP, [2., 4.]), UP);
let tilted = orientation::tilted_normal(rail, UP, [0., 0.]);
assert_eq!(tilted[1], -1.);
assert_eq!(tilted[0], 0.);
}
+1 -1
View File
@@ -1,8 +1,8 @@
//! TU3 trajectory selection82D67848/82D682E8/82D68800.
//! Host world storage and batch ownership are independent of console layout.
mod launch;
pub mod grind;
pub mod grind_surface;
pub mod grind;
mod math;
mod prediction;
mod query;
+11 -10
View File
@@ -1,6 +1,8 @@
//! TU3 scoring82D68FE8/82D68AF0/82D69790 with world-supplied grind admission.
//! 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.
use super::{
launch::adjust_trajectory, math::*, Prediction, SelectorInput, SelectorSettings, SurfaceHit,
Prediction, SelectorInput, SelectorSettings, SurfaceHit, launch::adjust_trajectory, math::*,
};
#[derive(Clone, Copy, Debug)]
@@ -54,25 +56,20 @@ pub(super) fn score(
let force_score = (force_dot * s.score_landing_force) * scalar;
let direction_score = (sideways * s.score_landing_direction) * scalar;
//82D69C00 attempts acquisition only on the first-pass centre trajectory.
let mut evaluation = grind(
let 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 {
let mut 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)
s.grind_penalty_vs_distance.evaluate(evaluation.penalty_input())
} else {
0.0
};
@@ -109,6 +106,10 @@ pub(super) fn score(
c.normal = c.prediction.result.contact_normal;
let mut transition = 0.0;
if adjusted_on_vert {
if input.directional_input < 0.5 && grind_score > 0.0 {
c.grind = None;
grind_score = s.grind_penalty_vs_distance.evaluate(0.0);
}
let sign = if input.directional_input >= 0.5 {
1.0
} else {
@@ -1,10 +1,9 @@
//! Original selector Launch82D67848 and Update82D68800 with typed batch ownership.
use super::{
launch,
LaunchInfo, Prediction, QueryRequest, QueryResult, SelectorInput, SelectorSettings, SurfaceHit,
Trajectory, launch,
math::*,
scoring::{self, Candidate},
LaunchInfo, Prediction, QueryRequest, QueryResult, SelectorInput, SelectorSettings, SurfaceHit,
Trajectory,
};
///The current authored world contains collision triangles and no grind edges.
@@ -23,8 +22,8 @@ pub struct Selection {
pub collision_position: Vector, //2688
pub com_trajectory: Trajectory, //2704
pub surface_category: u32, //9640
pub wall_ride: bool,
pub grind: Option<super::grind::GrindTarget>, //9661
pub wall_ride: bool, //9661
pub grind: Option<super::grind::GrindTarget>,
}
#[derive(Clone, Debug, Default)]
pub struct TrajectorySelector {
@@ -196,28 +195,6 @@ impl TrajectorySelector {
///Complete a real world query batch in order. A second pass is exposed by
///pending()/requests(); host scheduling preserves its next Update boundary.
pub fn complete_batch(
&mut self,
results: &[QueryResult],
input: SelectorInput,
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,
@@ -257,6 +234,9 @@ impl TrajectorySelector {
}
}
let c = self.candidates[index];
if let Some(target) = self.candidates.iter().take(count).find_map(|c| c.grind) {
self.grind_normal = Some(target.vertical_normal);
}
self.selected_index = Some(index);
//The copied COM trajectory is only recalculated on a valid-time result.
let previous_com = self.selection.map(|v| v.com_trajectory);
@@ -287,18 +267,11 @@ impl TrajectorySelector {
}
selection.surface_category = (c.prediction.result.surface >> 7) & 31;
selection.com_trajectory = self.com_trajectory(selection, input, s);
//82D68C80: an accepted middle trajectory owns9653 and skips the second pass.
//82D68C80: only a winning centre acquisition locks the grind.
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
let second_pass = self.pass == 1
&& count >= 2
&& !self.grind_locked_to_middle
&& !(angle_between(selection.landing_normal, input.ground_normal)
< s.minimum_normal_delta_second_pass);
self.selection = Some(selection);
@@ -3,17 +3,16 @@ pub mod blend;
pub mod contact;
pub mod drive;
pub mod external;
mod math;
mod physical_solve;
pub mod post_contact;
pub mod post_physics;
mod math;
pub use math::{interpolate_affine, inverse_affine};
pub use math::interpolate as interpolate_native;
pub use math::interpolate_affine;
pub use math::inverse_affine;
pub mod settings;
pub mod state;
pub mod status;
#[cfg(test)]
mod tests;
pub mod transforms;
pub mod two_bone;
#[cfg(test)]
mod tests;
@@ -67,7 +67,7 @@ pub(super) fn target(
///Scalar constructor path through original8252D980. Its argument is already
///clamped to [.01,1.5607964]; coefficients come from822FE3C0..E0.
pub(crate) fn tangent(angle: f32) -> f32 {
fn tangent(angle: f32) -> f32 {
let quadrant = (angle * f32::from_bits(0x3F22_F983)).round_ties_even();
let reduced = (-quadrant).mul_add(f32::from_bits(0x3FC9_0FDB), angle);
let reduced = (-quadrant).mul_add(f32::from_bits(0x2E85_A309), reduced);
@@ -1,10 +1,13 @@
//! TU3 GrindControlFade82BB0D40 and ControlGrindCrouch82BB10F8.
//! Animation attributes, physical observations and collection values remain
//! explicit inputs; these routines do not choose the grind or animation.
//! S3 TU3 GrindControlFade82BB0D40 and ControlGrindCrouch82BB10F8.
//! Adapted from the authorized ZIP after independent original-image comparison.
//! Animation selection, physical observations and tuning remain external inputs.
pub mod facing;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug)]
pub struct FadeSettings {
///Collection owner312: offsets1640,1644,1648,1652.
/// anim_motion/grind_twist: offsets1640/1644/1648/1652.
pub response: f32,
pub input_scale: f32,
pub acceleration: f32,
@@ -21,49 +24,88 @@ pub struct Fade {
pub minimum: f32,
pub maximum: f32,
}
impl Fade {
///Begin82BB0A30 obtains bounds by evaluating the animation attribute at
///both endpoints. The caller supplies those evaluations and mirrored twist.
/// Begin82BB0A30 supplies the actual queried endpoints and mirrored twist.
pub fn begin(&mut self, minimum: f32, maximum: f32, twist: f32) -> f32 {
let value = bound(twist, minimum, maximum);
*self = Self {
just_began: true,
minimum,
maximum,
value: bound(twist, minimum, maximum),
target: bound(twist, minimum, maximum),
value,
target: value,
..Self::default()
};
self.value
value
}
///The first Update after Begin only clears byte8; it emits no attribute.
/// First Update only clears the latch. dt affects elapsed, NOT twist speed.
pub fn update(&mut self, dt: f32, intent: f32, settings: FadeSettings) -> Option<f32> {
if self.just_began {
self.just_began = false;
return None;
}
self.elapsed += dt;
self.target = bound(settings.input_scale.mul_add(intent, self.target), self.minimum, self.maximum);
let desired = (1.0 - settings.response).mul_add(self.value, self.target * settings.response);
let step = bound(desired - self.value, self.step - settings.acceleration, self.step + settings.acceleration);
self.target = bound(
settings.input_scale.mul_add(intent, self.target),
self.minimum,
self.maximum,
);
let desired =
(1.0 - settings.response).mul_add(self.value, self.target * settings.response);
let step = bound(
desired - self.value,
self.step - settings.acceleration,
self.step + settings.acceleration,
);
self.step = bound(step, -settings.maximum_step, settings.maximum_step);
self.value = bound(self.value + self.step, self.minimum, self.maximum);
Some(self.value)
}
}
///Collection owner340: min1760,max1792,rate1816. Begin82BB1078 seeds
///previous with physical crouching output56+72; it does not seed from intent.
pub fn crouch(previous: f32, intent: f32, grind_crouch: f32, minimum: f32, maximum: f32, rate: f32, dt: f32) -> f32 {
let desired = bound(1.0 - intent, minimum, maximum)
.min(bound(1.0 - grind_crouch, minimum, maximum));
/// S3 always includes physical crouch; S2's optional enable switch is absent.
/// anim_motion/grind_height: min1760/max1792/rate1816; previous starts at Anim+72.
pub fn crouch(
previous: f32,
intent: f32,
grind_crouch: f32,
minimum: f32,
maximum: f32,
rate: f32,
dt: f32,
) -> f32 {
let input = bound(1.0 - intent, minimum, maximum);
let physical = bound(1.0 - grind_crouch, minimum, maximum);
let desired = if input - physical >= 0.0 {
physical
} else {
input
};
let step = dt * rate;
bound(desired, previous - step, previous + step)
}
/// Begin82BB0CBC..CE4: mirror the Skeleton+504 angle exactly once.
pub fn mirrored_twist(twist: f32, mirrored: bool) -> f32 {
if !mirrored {
return twist;
}
if twist >= 0.0 {
std::f32::consts::PI - twist
} else {
-std::f32::consts::PI - twist
}
}
fn bound(value: f32, minimum: f32, maximum: f32) -> f32 {
//The native lower comparison precedes its upper comparison.
let value = if minimum - value >= 0.0 { minimum } else { value };
if maximum - value >= 0.0 { value } else { maximum }
let value = if minimum - value >= 0.0 {
minimum
} else {
value
};
if maximum - value >= 0.0 {
value
} else {
maximum
}
}
@@ -0,0 +1,15 @@
//! CreateGrindAttributes82BAF3C8..444: original VMX reduction.
//! Let P(x,y,z,w)=(y,z,x,w). The instructions compute
//! P(n*P(b)-P(n)*b), then read lane1: n.z*b.x-n.x*b.z.
//! Raw vnmsubfp word1180636F has VA=0,VB=12,VC=13: VB-VA*VC.
//! No normalization, epsilon or grind-name classification occurs here.
/// n is Motion+80 deck velocity, b is Basic+128 effective board forward.
/// ground_flag is the historical API name for Motion273 (processed bit20);
/// mirrored is LIVE ISkaterAnim virtual28. Ground80 is a different vector.
pub fn backwards(n: [f32; 3], b: [f32; 3], ground_flag: bool, mirrored: bool) -> bool {
let side = (-n[0]).mul_add(b[2], n[2] * b[0]);
// Native fcmpu/ble falls through on unordered as well as strictly positive.
let positive_or_unordered = !(side <= 0.0);
positive_or_unordered ^ ground_flag ^ mirrored
}
@@ -0,0 +1,82 @@
use super::*;
fn settings() -> FadeSettings {
FadeSettings {
response: 1.0,
input_scale: 1.0,
acceleration: 0.25,
maximum_step: 0.5,
}
}
#[test]
fn fade_skips_first_update_then_accelerates_and_reverses_without_dt_scaling() {
let mut a = Fade::default();
assert_eq!(a.begin(-1.0, 1.0, 0.0), 0.0);
assert_eq!(a.update(10.0, 1.0, settings()), None);
assert_eq!(a.elapsed, 0.0);
let mut b = a;
assert_eq!(a.update(0.01, 1.0, settings()), Some(0.25));
assert_eq!(b.update(0.5, 1.0, settings()), Some(0.25));
assert_ne!(a.elapsed, b.elapsed);
assert_eq!(a.update(0.01, 1.0, settings()), Some(0.75));
assert_eq!(a.update(0.01, -1.0, settings()), Some(1.0));
assert_eq!(a.step, 0.25, "reversal must decelerate the retained step");
assert_eq!(a.update(0.01, -1.0, settings()), Some(1.0));
assert_eq!(a.update(0.01, -1.0, settings()), Some(0.75));
}
#[test]
fn fade_target_accumulates_and_begin_discards_previous_run() {
let mut a = Fade::default();
a.begin(-2.0, 2.0, 9.0);
assert_eq!((a.value, a.target), (2.0, 2.0));
a.update(1.0, 0.0, settings());
a.update(1.0, -0.25, settings());
assert_eq!(a.target, 1.75);
a.update(1.0, -0.25, settings());
assert_eq!(a.target, 1.5);
a.begin(-1.0, 1.0, -9.0);
assert_eq!(
(a.value, a.target, a.step, a.elapsed),
(-1.0, -1.0, 0.0, 0.0)
);
assert!(a.just_began);
}
#[test]
fn crouch_obeys_physical_minimum_and_two_sided_rate_limit() {
assert_eq!(crouch(1.0, 0.0, 0.75, 0.25, 1.0, 2.0, 0.125), 0.75);
assert_eq!(crouch(0.75, 0.0, 0.75, 0.25, 1.0, 2.0, 0.125), 0.5);
assert_eq!(crouch(0.25, 0.0, 0.0, 0.25, 1.0, 2.0, 0.125), 0.5);
assert_eq!(crouch(1.0, 0.5, 0.0, 0.25, 1.0, 10.0, 1.0), 0.5);
}
#[test]
fn physical_twist_is_mirrored_exactly_once() {
assert_eq!(mirrored_twist(0.25, false), 0.25);
assert_eq!(mirrored_twist(0.25, true), std::f32::consts::PI - 0.25);
assert_eq!(mirrored_twist(-0.25, true), -std::f32::consts::PI + 0.25);
}
#[test]
fn facing_uses_cross_y_and_both_flags_including_zero_boundary() {
for ground in [false, true] {
for mirror in [false, true] {
assert_eq!(
facing::backwards([1.0, 8.0, 0.0], [0.0, 3.0, -1.0], ground, mirror),
true ^ ground ^ mirror
);
assert_eq!(
facing::backwards([1.0, 8.0, 0.0], [0.0, 3.0, 1.0], ground, mirror),
ground ^ mirror
);
assert_eq!(
facing::backwards([0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ground, mirror),
ground ^ mirror
);
}
}
// The other cross-product term is not interchangeable with a board Z test.
assert!(facing::backwards(
[0.0, 0.0, 1.0],
[1.0, 0.0, 0.0],
false,
false
));
}
+3 -2
View File
@@ -1,6 +1,4 @@
//! Animation timing and event semantics recovered from Skate 3 TU3.
pub mod bump;
pub mod manual;
pub mod clip_clock;
pub mod commands;
pub mod output;
@@ -30,7 +28,10 @@ pub mod ground_acceleration;
pub mod foot_ik;
pub mod vbr;
pub mod kickturn;
pub mod manual;
pub mod intent_filter;
pub mod posture;
pub mod bump;
+1 -1
View File
@@ -152,7 +152,7 @@ mod startup_tests;
/// Independent reciprocal-square-root estimate followed by the original two
/// refinement steps. Host arithmetic is not claimed to be bit-exact Xenon math.
pub fn magnitude(squared: f32) -> f32 {
pub(super) fn magnitude(squared: f32) -> f32 {
let mut inverse = estimate(squared);
for _ in 0..2 {
let correction = (-squared).mul_add(inverse * inverse, 1.0);
@@ -1,21 +1,25 @@
//! TU3 Listener::Fill825999F0, grind emissions8259ACF0..8259AD94.
//! Publish controller-space values; the stock graphs apply stance/mirror filters.
//! Original S3 Fill825999F0; emissions8259ACF0..8259AD94.
//! Derived controller-space values: graph filters own stance/mirror transforms.
use super::{controller::DerivedControllerInput, riding_intentions::RidingIntent};
#[cfg(test)]
mod tests;
pub fn produce(controller: &DerivedControllerInput) -> Vec<RidingIntent> {
let words = controller.words();
let left_x = f32::from_bits(words[7]);
let right_x = f32::from_bits(words[9]);
let right_y = f32::from_bits(words[10]);
// S3 adds both X axes. S2 Fill825FB4C8 uses right X alone: do not copy it.
let sum = left_x + right_x;
let lower = if -1.0 - sum >= 0.0 { -1.0 } else { sum };
let translation = if 1.0 - lower >= 0.0 { lower } else { 1.0 };
[
("GrindBalanceX", -left_x),
("PhysGrindTranslation", (left_x + right_x).clamp(-1.0, 1.0)),
("PhysGrindTranslation", translation),
("PhysGrindStabilityNudge", left_x),
("PhysGrindUpDown", right_y),
]
.into_iter()
// Native Fill omits neutral records. Rebuilding the AG map removes the
// previous tick's input, letting the graph end the corresponding MG intent.
.filter(|(_, value)| *value != 0.0)
.map(|(name, value)| RidingIntent { name, value })
.collect()
@@ -0,0 +1,38 @@
use super::*;
fn values(x: f32, rx: f32, ry: f32) -> Vec<(&'static str, f32)> {
let mut words = [0; 26];
words[7] = x.to_bits();
words[9] = rx.to_bits();
words[10] = ry.to_bits();
produce(&DerivedControllerInput::from_words(words))
.into_iter()
.map(|i| (i.name, i.value))
.collect()
}
#[test]
fn all_four_s3_emissions_preserve_order_and_sign() {
assert_eq!(
values(0.25, 0.5, -0.75),
vec![
("GrindBalanceX", -0.25),
("PhysGrindTranslation", 0.75),
("PhysGrindStabilityNudge", 0.25),
("PhysGrindUpDown", -0.75),
]
);
}
#[test]
fn translation_saturates_both_directions_and_cancellation_omits_only_it() {
assert_eq!(values(0.75, 0.75, 0.0)[1], ("PhysGrindTranslation", 1.0));
assert_eq!(values(-0.75, -0.75, 0.0)[1], ("PhysGrindTranslation", -1.0));
assert_eq!(
values(0.5, -0.5, 0.0),
vec![("GrindBalanceX", -0.5), ("PhysGrindStabilityNudge", 0.5),]
);
assert!(values(0.0, -0.0, 0.0).is_empty());
}
#[test]
fn left_only_translation_distinguishes_s3_from_s2() {
assert_eq!(values(0.5, 0.0, 0.0)[1], ("PhysGrindTranslation", 0.5));
}
+1 -1
View File
@@ -4,7 +4,6 @@ pub mod gesture;
pub mod anticipation_intentions;
pub mod animation_packet;
pub mod controller;
pub mod manual_intentions;
pub mod gameplay_map;
pub mod tick;
pub mod graph_intents;
@@ -13,6 +12,7 @@ pub mod history;
pub mod pad;
pub mod power_sliding;
pub mod riding_intentions;
pub mod manual_intentions;
pub mod trick_intentions;
pub mod set_turning;
pub mod steering_intentions;
@@ -55,6 +55,8 @@ pub fn produce_discrete(
emit("OB_AirBodyTweakY", right_y);
//8259A604..A6C0 computes both;8259AF80 gates only NewToggle by actor bit10.
//The listener has no biped/category gate. Stock OffBoard AG maps these
//same toggle intentions to OB_Mount/OB_MountRaw; trigger edges recall.
if !air_reckoning_active && !held(29) && held(22) {
//Native bge does not take the branch for unordered timer values.
if (rising(22) || !(axis(23) >= f32::from_bits(0x3cf5_c28f)))
@@ -87,14 +89,6 @@ pub fn produce_analog(
) -> [OffboardIntent; 4] {
let words = controller.words();
let stick = [f32::from_bits(words[7]), 0.0, f32::from_bits(words[8]), 0.0];
produce_analog_world(stick, observation)
}
/// Host camera conversion precedes the native world-space Biped attributes.
pub fn produce_analog_world(
stick: [f32; 4],
observation: AnalogObservation,
) -> [OffboardIntent; 4] {
let mut horizontal = observation.effective_skeleton_z;
horizontal[1] = 0.0;
let forward = safe_unit(horizontal);
@@ -143,4 +137,3 @@ fn safe_unit(vector: [f32; 4]) -> [f32; 4] {
#[cfg(test)]
#[path = "offboard_intentions/tests.rs"]
mod tests;
@@ -46,6 +46,16 @@ fn physical_and_modifier_gates_block_both_but_actor_gate_only_blocks_new() {
}
}
#[test]
fn y_remains_toggle_for_stock_offboard_mount_not_board_recall() {
//S3 listener8259A604..A6C0 has no physical-state input. The stock
//OffBoard/OBGround/Default graph consumes both of these for remount.
let output = values(toggle_words(false, 0.0), 0, false);
assert!(has(&output, "NewToggleOffBoardState"));
assert!(has(&output, "ToggleOffBoardState"));
assert!(!has(&output, "OB_RetrieveBoard"));
}
#[test]
fn jump_requires_new_button_and_does_not_inherit_toggle_gates() {
let mut words = [0; 26];
@@ -1,4 +1,4 @@
//! Push, brake, body-spin, steering, kick-turn, crouch and powerslide intents
//! Push, brake, body-spin, steering, kick-turn, crouch, powerslide and world-grab intents
//! from the TU3 ActionGraph input listener (`825999F0`).
use super::{
angle::left_stick_angle,
@@ -151,6 +151,12 @@ pub fn produce(
emit("RightSlide", (2.0 - angle) * SLIDE_SCALE);
}
}
//8259A54C/554 reads current raw flags bit28;8259AF68..7C emits
//GrabWorld (830BE780, named by initializer82F84EB0) while held.
//No actor inhibition or rising-edge gate applies to this intention.
if current & (1 << 28) != 0 {
emit("GrabWorld", 1.0);
}
output
}
@@ -1,4 +1,22 @@
use super::*;
#[test]
fn right_bumper_publishes_held_grab_world_without_actor_gate() {
for (previous, current, expected) in [
(0, 1 << 28, true),
(1 << 28, 1 << 28, true),
(1 << 28, 0, false),
(0, 1 << 29, false),
] {
for actor_flags in [0, u32::MAX] {
let output = produce(&snapshot(previous, current, 0.5), actor_flags,
PushPreferences::default());
let values: Vec<_> = output.iter().filter(|i| i.name == "GrabWorld").collect();
assert_eq!(values.len(), usize::from(expected));
if expected { assert_eq!(values[0].value, 1.0); }
}
}
}
#[test]
fn crouch_preserves_zero_presence_and_uses_maximum_trigger() {
let mut words = [0u32; 26];
@@ -120,7 +120,8 @@ pub struct BoardGroundState {
pub wheel_contact_count: u8,
///Body7692, accumulated only while all four physical wheels lack contact.
pub time_without_wheel_contact: f32,
/// Native wheel inertia+36 writes made after normal selection.
/// Angular drag: S3 postphysics82C08634 writes wheel inertia+36;
/// paired S2 postphysics82B372E0 names this mAngularDrag.
pub wheel_angular_drag: [f32; 4],
}
impl Default for BoardGroundState {
+32 -32
View File
@@ -55,6 +55,7 @@ impl BoardRuntime {
self.step.diagnostic_capture = enabled;
self.step.diagnostic_snapshot.take()
}
/// `authored` contains part poses in board space. `spawn` places the deck
/// part in world space, and moves the other parts by the same rigid delta.
/// All mass properties come from the caller's physical data producer.
@@ -102,13 +103,9 @@ impl BoardRuntime {
}
}
pub fn collision_group(&self) -> u32 {
self.collision_group
}
pub fn collision_group(&self) -> u32 { self.collision_group }
///All seven native board parts share the current assembly group.
pub fn set_collision_group(&mut self, group: u32) {
self.collision_group = group
}
pub fn set_collision_group(&mut self, group: u32) { self.collision_group = group }
pub fn bodies(&self) -> &[BodySnapshot; BODY_COUNT] {
&self.bodies
@@ -151,8 +148,9 @@ impl BoardRuntime {
body.rates.angular_velocity = Vector3::ZERO;
body.rates.force_acceleration = gravity;
body.rates.torque_acceleration = Vector3::ZERO;
body.rates.world_inverse_inertia =
world_inverse_inertia(body.rates.basis, body.inertia.inverse_tensor);
body.rates.world_inverse_inertia = world_inverse_inertia(
body.rates.basis, body.inertia.inverse_tensor,
);
}
copy_pose(&hook, &mut self.hook.body.rates);
self.forces.clear();
@@ -161,6 +159,32 @@ impl BoardRuntime {
self.step = BoardStep::default();
}
/// Move the complete board assembly while preserving live rates, forces,
/// contacts, and solver ownership. This is the C055F0/C0B2C8 possession
/// position operation, distinct from the full physical reset above.
pub fn set_transform(&mut self, target: RetailAffineTransform) {
let authored = self.part_transforms();
let mut parts = core::array::from_fn(|i| PartPose {
transform: pose_words(authored[i]),
local_mass_frame: Some(mass_frame_words(self.mass_frames[i])),
body: Some(body_pose_words(self.bodies[i].rates)),
inertia: Some(inertia_words(self.bodies[i].inertia)),
});
let mut hook = PartPose {
transform: pose_words(self.hook_transform()),
local_mass_frame: None,
body: Some(body_pose_words(self.hook.body.rates)),
inertia: None,
};
set_board_transform(&mut parts, &mut hook, pose_words(target));
for (body, part) in self.bodies.iter_mut().zip(&parts) {
copy_pose(part, &mut body.rates);
body.rates.world_inverse_inertia =
world_inverse_inertia(body.rates.basis, body.inertia.inverse_tensor);
}
copy_pose(&hook, &mut self.hook.body.rates);
}
/// Gameplay may publish actual velocities/accumulators or physical mode
/// changes here. Rendering should use the immutable view or part poses.
pub fn bodies_mut(&mut self) -> &mut [BodySnapshot; BODY_COUNT] {
@@ -271,30 +295,6 @@ impl BoardRuntime {
set_part_transform(&mut part, pose_words(requested));
copy_pose(&part, &mut self.hook.body.rates);
}
///82C0B2C8, used by board retrieval and hiding. Changes live part poses
///without Reset's rate, contact-history, or force-queue clearing.
pub fn set_transform(&mut self, requested: RetailAffineTransform) {
let poses = self.part_transforms();
let mut parts = core::array::from_fn(|i| PartPose {
transform: pose_words(poses[i]),
local_mass_frame: Some(mass_frame_words(self.mass_frames[i])),
body: Some(body_pose_words(self.bodies[i].rates)),
inertia: Some(inertia_words(self.bodies[i].inertia)),
});
let mut hook = PartPose {
transform: pose_words(self.hook_transform()),
local_mass_frame: None,
body: Some(body_pose_words(self.hook.body.rates)),
inertia: None,
};
set_board_transform(&mut parts, &mut hook, pose_words(requested));
for (body, part) in self.bodies.iter_mut().zip(&parts) {
copy_pose(part, &mut body.rates);
body.rates.world_inverse_inertia =
world_inverse_inertia(body.rates.basis, body.inertia.inverse_tensor);
}
copy_pose(&hook, &mut self.hook.body.rates);
}
}
const ZERO_INERTIA: RetailInertiaDynamics = RetailInertiaDynamics {
+51 -94
View File
@@ -2,9 +2,11 @@
//! Rust owns geometry storage and traversal. Recovered query8277B720,
//! triangle fixup82AD3130, material combine82763078 and contact retention
//! determine the physical result, in world-triangle then moving-volume order.
pub mod query_metadata;
mod broadphase;
mod query_index;
pub mod query_metadata;
use crate::math::Vector3;
use broadphase::{conservative_bounds, primitive_bounds};
use query_metadata::{Bounds, QueryMetadata};
use super::{
@@ -106,26 +108,51 @@ pub struct BoardWorld {
triangle_bounds: Vec<Bounds>,
query_metadata: Option<QueryMetadata>,
query_index: query_index::QueryIndex,
maximum_fatness: f32,
maximum_triangle_margin: f32,
contacts: Vec<BoardCollision>,
buffer: ContactBuffer,
maximum_fatness: f32,
}
impl BoardWorld {
pub fn new(triangles: Vec<WorldTriangle>) -> Self {
let triangle_bounds = triangles.iter().map(|t| Bounds::from_points(t.triangle.vertices)
.unwrap_or(Bounds { min: Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
max: Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY) })).collect();
let triangle_bounds: Vec<_> = triangles
.iter()
.map(|t| {
Bounds::from_points(t.triangle.vertices).unwrap_or(Bounds {
min: Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
max: Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY),
})
})
.collect();
let maximum_fatness = triangles
.iter()
.map(|t| t.triangle.fatness)
.map(|t| {
if t.triangle.fatness.is_finite() && t.triangle.fatness >= 0. {
t.triangle.fatness
} else {
f32::INFINITY
}
})
.fold(0., f32::max);
// The current thin-triangle leaf accepts barycentric weights down to
// -epsilon. Two negative weights can extend an axis by 2*epsilon*span.
let maximum_triangle_margin = triangle_bounds
.iter()
.map(|b| {
let span = (b.max.x - b.min.x)
.max(b.max.y - b.min.y)
.max(b.max.z - b.min.z);
span * (2. * broadphase::THIN_MARGIN)
})
.fold(0., f32::max);
Self {
maximum_fatness,
triangles,
triangle_bounds,
query_metadata: None,
query_index: query_index::QueryIndex::default(),
maximum_fatness,
maximum_triangle_margin,
contacts: Vec::new(),
buffer: ContactBuffer {
count: 0,
@@ -162,44 +189,6 @@ impl BoardWorld {
&self.triangles
}
/// Conservative cluster culling, retaining canonical traversal order and
/// every triangle in intersecting clusters. Narrow-phase remains unchanged.
pub fn candidate_ranges(&self, bounds: Option<Bounds>) -> Vec<std::ops::Range<usize>> {
let (Some(metadata), Some(bounds)) = (&self.query_metadata, bounds) else {
return vec![0..self.triangles.len()];
};
let scale = [
bounds.min.x,
bounds.min.y,
bounds.min.z,
bounds.max.x,
bounds.max.y,
bounds.max.z,
]
.into_iter()
.map(f32::abs)
.fold(1., f32::max);
let bounds = bounds.expanded(self.maximum_fatness + scale * (8. * f32::EPSILON));
self.query_index.query(bounds, &metadata.meshes)
.into_iter()
.map(|i| metadata.meshes[i].triangle_range.clone())
.collect()
}
pub fn line_candidates(
&self,
start: Vector3,
end: Vector3,
radius: f32,
) -> impl Iterator<Item = (usize, &WorldTriangle)> {
let bounds = Bounds::from_points([start, end]).map(|b| conservative_bounds(b, radius + self.maximum_fatness));
self.candidate_ranges(bounds)
.into_iter()
.flatten()
.filter(move |&i| self.query_metadata.is_none() || bounds.is_none_or(|b| self.triangle_bounds[i].overlaps(b)))
.map(|i| (i, &self.triangles[i]))
}
/// Board wheel segments use zero query radius; rounded world triangles
/// still select the native swept branch through their own fatness.
pub fn query_thin_line(
@@ -280,7 +269,7 @@ impl BoardWorld {
/// World-space shapes come from the live part poses and authored children.
/// Cluster bounds include shape radii and the maximum predictive padding.
/// Candidate pairs retain source query order.
/// Candidate pairs retain world-triangle then moving-volume order.
pub fn query_primitives(
&mut self,
volumes: &[BoardWorldVolume],
@@ -295,10 +284,18 @@ impl BoardWorld {
self.buffer.capacity = retention.capacity;
self.buffer.distance_squared_threshold = retention.duplicate_distance_squared;
self.buffer.deferred_reduction = u8::from(retention.deferred_reduction);
let padding = query.volume_padding.max(0.) + query.maximum_separating_distance.max(0.);
let padding =
if query.volume_padding.is_finite() && query.maximum_separating_distance.is_finite() {
query.volume_padding.max(0.) + query.maximum_separating_distance.max(0.)
} else {
f32::INFINITY
};
let volume_bounds: Vec<_> = volumes
.iter()
.map(|v| primitive_bounds(v.primitive).map(|b| conservative_bounds(b, padding + self.maximum_fatness)))
.map(|v| {
primitive_bounds(v.primitive)
.map(|b| conservative_bounds(b, padding + self.maximum_fatness))
})
.collect();
let bounds: Option<Vec<_>> = volume_bounds.iter().copied().collect();
let bounds = bounds
@@ -313,7 +310,8 @@ impl BoardWorld {
let entry = &self.triangles[index];
for (volume, volume_bounds) in volumes.iter().zip(&volume_bounds) {
if self.query_metadata.is_some()
&& volume_bounds.is_some_and(|b| !self.triangle_bounds[index].overlaps(b)) {
&& volume_bounds.is_some_and(|b| !self.triangle_bounds[index].overlaps(b))
{
continue;
}
let Some(manifold) = primitive_triangle_world_contacts(
@@ -360,51 +358,6 @@ impl BoardWorld {
}
}
fn primitive_bounds(primitive: ContactPrimitive) -> Option<Bounds> {
let (center, radius) = match primitive {
ContactPrimitive::Sphere(s) => (s.center, s.radius),
ContactPrimitive::Capsule {
center,
axis,
half_length,
radius,
} => {
let offset = Vector3::new(axis.x * half_length, axis.y * half_length, axis.z * half_length);
return Bounds::from_points([
Vector3::new(center.x - offset.x, center.y - offset.y, center.z - offset.z),
Vector3::new(center.x + offset.x, center.y + offset.y, center.z + offset.z),
]).map(|b| b.expanded(radius.abs()));
},
ContactPrimitive::RoundedBox {
center,
basis,
half_extents,
radius,
} => {
let half = [half_extents.x, half_extents.y, half_extents.z];
let extent: [f32; 3] = std::array::from_fn(|axis| radius.abs() + basis.columns.iter().zip(half)
.map(|(column, h)| h.abs() * column[axis].abs()).sum::<f32>());
return Bounds::from_points([
Vector3::new(center.x - extent[0], center.y - extent[1], center.z - extent[2]),
Vector3::new(center.x + extent[0], center.y + extent[1], center.z + extent[2]),
]);
}
ContactPrimitive::Triangle(t) => {
return Bounds::from_points(t.vertices).map(|b| b.expanded(t.fatness));
}
};
if !radius.is_finite() {
return None;
}
Bounds::from_points([center]).map(|b| b.expanded(radius.abs()))
}
fn conservative_bounds(bounds: Bounds, padding: f32) -> Bounds {
let scale = [bounds.min.x, bounds.min.y, bounds.min.z, bounds.max.x, bounds.max.y, bounds.max.z]
.into_iter().map(f32::abs).fold(1., f32::max);
bounds.expanded(padding + scale * (8. * f32::EPSILON))
}
/// Retention reads only contact header geometry, body IDs and material/tag.
/// BoardStep builds the actual body workspaces after this frame's force queue
/// has been applied, preventing stale acceleration copies in solver contacts.
@@ -447,3 +400,7 @@ fn collision_from_record(record: &ContactRecord) -> BoardCollision {
#[cfg(test)]
#[path = "tests/board_world.rs"]
mod tests;
#[cfg(test)]
#[path = "board_world/tests.rs"]
mod broadphase_tests;
@@ -0,0 +1,182 @@
//! ZIP world broadphase helpers; recovered narrow-phase stays in its existing modules.
use super::{BoardWorld, ContactPrimitive, Vector3, WorldTriangle, query_metadata::Bounds};
// Positive magnitude of thin_triangle's existing 0xb727_c5ac tolerance.
pub(super) const THIN_MARGIN: f32 = f32::from_bits(0x3727_c5ac);
impl BoardWorld {
/// Conservative cluster culling, retaining canonical traversal order and
/// every triangle in intersecting clusters. Narrow-phase remains unchanged.
pub fn candidate_ranges(&self, bounds: Option<Bounds>) -> Vec<std::ops::Range<usize>> {
let (Some(metadata), Some(bounds)) = (&self.query_metadata, bounds.filter(|b| b.valid()))
else {
return vec![0..self.triangles.len()];
};
let scale = [
bounds.min.x,
bounds.min.y,
bounds.min.z,
bounds.max.x,
bounds.max.y,
bounds.max.z,
]
.into_iter()
.map(f32::abs)
.fold(1., f32::max);
let bounds = bounds.expanded(
self.maximum_fatness + self.maximum_triangle_margin + scale * (8. * f32::EPSILON),
);
self.query_index
.query(bounds, &metadata.meshes)
.into_iter()
.map(|i| metadata.meshes[i].triangle_range.clone())
.collect()
}
pub fn line_candidates(
&self,
start: Vector3,
end: Vector3,
radius: f32,
) -> impl Iterator<Item = (usize, &WorldTriangle)> {
let bounds = self.line_candidate_bounds(start, end, radius);
self.candidate_ranges(bounds)
.into_iter()
.flatten()
.filter(move |&i| {
self.query_metadata.is_none()
|| bounds.is_none_or(|b| self.triangle_bounds[i].overlaps(b))
})
.map(|i| (i, &self.triangles[i]))
}
/// Bounds shared with adapters that retain mesh pool/group/identity filtering.
/// Includes the current thin-leaf endpoint and barycentric margins.
pub fn line_candidate_bounds(
&self,
start: Vector3,
end: Vector3,
radius: f32,
) -> Option<Bounds> {
if !radius.is_finite() || radius < 0. {
return None;
}
let span = (end.x - start.x)
.abs()
.max((end.y - start.y).abs())
.max((end.z - start.z).abs());
Bounds::from_points([start, end]).map(|b| {
conservative_bounds(
b,
radius + self.maximum_fatness + self.maximum_triangle_margin + span * THIN_MARGIN,
)
})
}
/// ZIP hierarchy results in canonical mesh order, for current metadata consumers.
/// Pool, matching group, rejection mask and identity stay caller-owned.
pub fn candidate_mesh_indices(
&self,
bounds: Option<Bounds>,
) -> Result<Vec<usize>, &'static str> {
let metadata = self.query_metadata()?;
let Some(bounds) = bounds.filter(|b| b.valid()) else {
return Ok((0..metadata.meshes.len()).collect());
};
Ok(self.query_index.query(
conservative_bounds(bounds, self.maximum_fatness + self.maximum_triangle_margin),
&metadata.meshes,
))
}
}
pub(super) fn primitive_bounds(primitive: ContactPrimitive) -> Option<Bounds> {
let (center, radius) = match primitive {
ContactPrimitive::Sphere(s) => (s.center, s.radius),
ContactPrimitive::Capsule {
center,
axis,
half_length,
radius,
} => {
if !radius.is_finite() {
return None;
}
let offset = Vector3::new(
axis.x * half_length,
axis.y * half_length,
axis.z * half_length,
);
return Bounds::from_points([
Vector3::new(
center.x - offset.x,
center.y - offset.y,
center.z - offset.z,
),
Vector3::new(
center.x + offset.x,
center.y + offset.y,
center.z + offset.z,
),
])
.map(|b| b.expanded(radius.abs()));
}
ContactPrimitive::RoundedBox {
center,
basis,
half_extents,
radius,
} => {
if !radius.is_finite() {
return None;
}
let half = [half_extents.x, half_extents.y, half_extents.z];
let extent: [f32; 3] = std::array::from_fn(|axis| {
radius.abs()
+ basis
.columns
.iter()
.zip(half)
.map(|(column, h)| h.abs() * column[axis].abs())
.sum::<f32>()
});
return Bounds::from_points([
Vector3::new(
center.x - extent[0],
center.y - extent[1],
center.z - extent[2],
),
Vector3::new(
center.x + extent[0],
center.y + extent[1],
center.z + extent[2],
),
]);
}
ContactPrimitive::Triangle(t) => {
if !t.fatness.is_finite() {
return None;
}
return Bounds::from_points(t.vertices).map(|b| b.expanded(t.fatness.abs()));
}
};
if !radius.is_finite() {
return None;
}
Bounds::from_points([center]).map(|b| b.expanded(radius.abs()))
}
pub(super) fn conservative_bounds(bounds: Bounds, padding: f32) -> Bounds {
let scale = [
bounds.min.x,
bounds.min.y,
bounds.min.z,
bounds.max.x,
bounds.max.y,
bounds.max.z,
]
.into_iter()
.map(f32::abs)
.fold(1., f32::max);
bounds.expanded(padding + scale * (8. * f32::EPSILON))
}
@@ -15,18 +15,37 @@ struct Node {
}
impl QueryIndex {
pub(super) fn new(meshes: &[QueryMesh]) -> Self {
let mut index = Self { nodes: Vec::new(), order: (0..meshes.len()).collect() };
if !meshes.is_empty() { index.build(meshes, 0..meshes.len()); }
let mut index = Self {
nodes: Vec::new(),
order: (0..meshes.len()).collect(),
};
if !meshes.is_empty() {
index.build(meshes, 0..meshes.len());
}
index
}
fn build(&mut self, meshes: &[QueryMesh], range: Range<usize>) -> usize {
let bounds = Bounds::from_points(self.order[range.clone()].iter()
.flat_map(|&i| [meshes[i].local_bounds.min, meshes[i].local_bounds.max])).unwrap();
let bounds = Bounds::from_points(
self.order[range.clone()]
.iter()
.flat_map(|&i| [meshes[i].local_bounds.min, meshes[i].local_bounds.max]),
)
.unwrap();
let id = self.nodes.len();
self.nodes.push(Node { bounds, children: None, range: range.clone() });
self.nodes.push(Node {
bounds,
children: None,
range: range.clone(),
});
if range.len() > 8 {
let extents = [bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y, bounds.max.z - bounds.min.z];
let axis = (0..3).max_by(|&a, &b| extents[a].total_cmp(&extents[b])).unwrap();
let extents = [
bounds.max.x - bounds.min.x,
bounds.max.y - bounds.min.y,
bounds.max.z - bounds.min.z,
];
let axis = (0..3)
.max_by(|&a, &b| extents[a].total_cmp(&extents[b]))
.unwrap();
let center = |i: usize| {
let b = meshes[i].local_bounds;
let low = [b.min.x, b.min.y, b.min.z][axis];
@@ -34,8 +53,9 @@ impl QueryIndex {
low * 0.5 + high * 0.5
};
let mid = range.start + range.len() / 2;
self.order[range.clone()].select_nth_unstable_by(range.len() / 2,
|&a, &b| center(a).total_cmp(&center(b)).then(a.cmp(&b)));
self.order[range.clone()].select_nth_unstable_by(range.len() / 2, |&a, &b| {
center(a).total_cmp(&center(b)).then(a.cmp(&b))
});
let left = self.build(meshes, range.start..mid);
let right = self.build(meshes, mid..range.end);
self.nodes[id].children = Some((left, right));
@@ -44,44 +64,28 @@ impl QueryIndex {
}
pub(super) fn query(&self, bounds: Bounds, meshes: &[QueryMesh]) -> Vec<usize> {
let mut result = Vec::new();
if self.nodes.is_empty() { return result; }
if self.nodes.is_empty() {
return result;
}
let mut stack = vec![0];
while let Some(id) = stack.pop() {
let node = &self.nodes[id];
if !node.bounds.overlaps(bounds) { continue; }
if !node.bounds.overlaps(bounds) {
continue;
}
if let Some((left, right)) = node.children {
stack.push(right); stack.push(left);
stack.push(right);
stack.push(left);
} else {
result.extend(self.order[node.range.clone()].iter().copied()
.filter(|&i| meshes[i].local_bounds.overlaps(bounds)));
result.extend(
self.order[node.range.clone()]
.iter()
.copied()
.filter(|&i| meshes[i].local_bounds.overlaps(bounds)),
);
}
}
result.sort_unstable();
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{math::Vector3, physics::{drive_frames::RetailAffineTransform, board_world::query_metadata::QueryPool}};
#[test]
fn hierarchy_matches_linear_scan_including_boundaries_and_source_order() {
let meshes: Vec<_> = (0..1024).map(|i| {
let x = ((i * 37) % 32) as f32 * 10.;
let z = (i / 32) as f32 * 10.;
QueryMesh { triangle_range: i..i+1,
local_to_world: RetailAffineTransform::IDENTITY, world_to_local: RetailAffineTransform::IDENTITY,
local_bounds: Bounds { min: Vector3::new(x, 0., z), max: Vector3::new(x+5., 5., z+5.) },
matching_group: -1, pool: QueryPool::Ground }
}).collect();
let index = QueryIndex::new(&meshes);
for x in [-100., 0., 5., 10., 47., 155., 310., 320.] {
for radius in [0., 5., 31., 1000.] {
let bounds = Bounds::from_points([Vector3::new(x, 0., x)]).unwrap().expanded(radius);
let expected: Vec<_> = meshes.iter().enumerate().filter(|(_, m)| m.local_bounds.overlaps(bounds)).map(|(i, _)| i).collect();
assert_eq!(index.query(bounds, &meshes), expected);
}
}
}
}
@@ -74,7 +74,7 @@ impl Bounds {
&& p.z >= self.min.z
&& p.z <= self.max.z
}
fn valid(self) -> bool {
pub(super) fn valid(self) -> bool {
finite(self.min)
&& finite(self.max)
&& self.min.x <= self.max.x
@@ -97,6 +97,11 @@ pub struct QueryMesh {
pub world_to_local: RetailAffineTransform,
pub local_bounds: Bounds,
pub matching_group: i32,
/// S3 mesh184: trajectory rejection mask, separate from surface/material.
pub rejection_flags: u32,
/// S3 mesh176 support/body identity; static registration uses0. This is
/// separate from the host metadata index and authored mesh/unit names.
pub geometry: u32,
pub pool: QueryPool,
}
@@ -0,0 +1,242 @@
use super::*;
use crate::math::Basis3;
use crate::physics::drive_frames::RetailAffineTransform;
use query_metadata::{QueryMesh, QueryPool};
fn material() -> RetailContactMaterial {
RetailContactMaterial {
static_friction: 0.7,
dynamic_friction: 0.4,
restitution: 0.2,
}
}
fn face(x: f32, tag: u32, fatness: f32) -> WorldTriangle {
WorldTriangle::from_vertices(
[
Vector3::new(x - 2., 0., -2.),
Vector3::new(x - 2., 0., 2.),
Vector3::new(x + 2., 0., -2.),
],
material(),
tag,
0xe0,
[1.; 3],
fatness,
)
.unwrap()
}
fn annotated(triangles: Vec<WorldTriangle>) -> BoardWorld {
let meshes = triangles
.iter()
.enumerate()
.map(|(i, t)| QueryMesh {
triangle_range: i..i + 1,
local_to_world: RetailAffineTransform::IDENTITY,
world_to_local: RetailAffineTransform::IDENTITY,
local_bounds: Bounds::from_points(t.triangle.vertices).unwrap(),
matching_group: -1,
rejection_flags: 0x2000,
geometry: 5000 + i as u32,
pool: QueryPool::Ground,
})
.collect();
let packed_surfaces = vec![17; triangles.len()];
BoardWorld::with_query_metadata(
triangles,
QueryMetadata {
packed_surfaces,
meshes,
static_edges: vec![],
island_flags: 0,
},
)
.unwrap()
}
fn tiled() -> Vec<WorldTriangle> {
(0..1024)
.map(|i| face(((i * 37) % 1024) as f32 * 10., i, 0.))
.collect()
}
#[test]
fn zip_hierarchy_matches_inclusive_linear_mesh_scan_and_source_order() {
let world = annotated(tiled());
let meshes = &world.query_metadata().unwrap().meshes;
let index = query_index::QueryIndex::new(meshes);
for x in [-100., -2., 0., 2., 8., 10., 155., 10230., 10300.] {
for radius in [0., 2., 31., 20000.] {
let bounds = Bounds::from_points([Vector3::new(x, 0., 0.)])
.unwrap()
.expanded(radius);
let expected: Vec<_> = meshes
.iter()
.enumerate()
.filter(|(_, m)| m.local_bounds.overlaps(bounds))
.map(|(i, _)| i)
.collect();
assert_eq!(index.query(bounds, meshes), expected);
}
}
}
#[test]
fn line_candidates_prune_distant_clusters_and_keep_canonical_indices() {
let world = annotated(tiled());
let start = Vector3::new(-1., 1., -1.);
let end = Vector3::new(-1., -1., -1.);
let indices: Vec<_> = world
.line_candidates(start, end, 0.)
.map(|(i, _)| i)
.collect();
assert_eq!(indices, vec![0]);
assert_eq!(world.query_metadata().unwrap().meshes[0].geometry, 5000);
assert_eq!(
world.query_metadata().unwrap().meshes[0].rejection_flags,
0x2000
);
assert_eq!(world.query_metadata().unwrap().packed_surfaces[0], 17);
assert_eq!(world.candidate_ranges(None), vec![0..1024]);
let invalid = Bounds {
min: Vector3::new(f32::NAN, 0., 0.),
max: Vector3::ZERO,
};
assert_eq!(world.candidate_ranges(Some(invalid)), vec![0..1024]);
let empty = annotated(vec![]);
assert_eq!(empty.line_candidates(start, end, 0.).count(), 0);
}
#[test]
fn accelerated_lines_match_full_scan_with_fatness_and_equal_hits() {
let mut triangles = tiled();
triangles.insert(0, face(0., 9000, 0.1));
triangles.insert(0, face(0., 9001, 0.1));
let linear = BoardWorld::new(triangles.clone());
let world = annotated(triangles);
for x in [-100., -2.1, -1., 0., 2., 9., 509., 10229.] {
for radius in [0., 0.03, 0.5] {
let start = Vector3::new(x, 2., -1.);
let end = Vector3::new(x, -2., -1.);
assert_eq!(
world.query_swept_line(start, end, radius),
linear.query_swept_line(start, end, radius)
);
}
}
assert_eq!(
world
.query_thin_line(Vector3::new(-1., 2., -1.), Vector3::new(-1., -2., -1.))
.unwrap()
.unwrap()
.tag,
9001
);
}
#[test]
fn thin_endpoint_and_barycentric_tolerances_survive_broadphase() {
let triangles = vec![face(0., 7, 0.)];
let linear = BoardWorld::new(triangles.clone());
let world = annotated(triangles);
for (start, end) in [
(Vector3::new(-1., 1., -1.), Vector3::new(-1., 0.000005, -1.)),
(
Vector3::new(-1., -0.000005, -1.),
Vector3::new(-1., -1., -1.),
),
(
Vector3::new(-2.00002, 1., -1.),
Vector3::new(-2.00002, -1., -1.),
),
] {
let expected = linear.query_thin_line(start, end).unwrap();
assert!(
expected.is_some(),
"fixture must exercise the thin-leaf tolerance"
);
assert_eq!(world.query_thin_line(start, end).unwrap(), expected);
}
}
#[test]
fn predictive_contacts_and_retention_match_full_scan_for_every_primitive() {
let mut triangles = tiled();
triangles.insert(0, face(0., 9000, 0.02));
triangles.insert(0, face(0., 9001, 0.02));
let mut linear = BoardWorld::new(triangles.clone());
let mut world = annotated(triangles);
let center = Vector3::new(-0.7, 0.25, -0.7);
let primitives = [
ContactPrimitive::Sphere(Sphere {
center,
radius: 0.2,
}),
ContactPrimitive::Capsule {
center,
axis: Vector3::new(1., 0., 0.),
half_length: 0.3,
radius: 0.2,
},
ContactPrimitive::RoundedBox {
center,
basis: Basis3 {
columns: [[0.6, 0., 0.8], [0., 1., 0.], [-0.8, 0., 0.6]],
},
half_extents: Vector3::new(0.3, 0.2, 0.1),
radius: 0.02,
},
ContactPrimitive::Triangle(triangle_from_volume(
[
Vector3::new(-1., 0.1, -1.),
Vector3::new(-1., 0.1, 0.),
Vector3::new(0., 0.1, -1.),
],
0.03,
[1.; 3],
0xe0,
)),
];
let mut observed = false;
for velocity in [-60., -1., 0., 1.] {
let volumes: Vec<_> = primitives
.iter()
.enumerate()
.map(|(i, &primitive)| BoardWorldVolume {
body: CollisionBody::Attached(i),
primitive,
linear_velocity: Vector3::new(0., velocity, 0.),
material: material(),
})
.collect();
for (padding, maximum) in [(0., 0.), (0., 0.5), (0.2, 0.), (0.02, 0.5)] {
let query = WorldContactSettings {
volume_padding: padding,
maximum_separating_distance: maximum,
edge_cos_bend_normal_threshold: -1.,
convexity_epsilon: 0.,
is_object: false,
};
for capacity in [1, 3, 100] {
for deferred_reduction in [false, true] {
let retention = ContactRetentionSettings {
capacity,
duplicate_distance_squared: 0.000001,
deferred_reduction,
};
let snapshot = |hits: &[BoardCollision]| {
hits.iter()
.map(|h| retention_record(h.body_a, h.contact))
.collect::<Vec<_>>()
};
let expected = snapshot(linear.query_primitives(&volumes, query, retention));
observed |= !expected.is_empty();
assert_eq!(
snapshot(world.query_primitives(&volumes, query, retention)),
expected
);
assert_eq!(world.dropped_contacts(), linear.dropped_contacts());
}
}
}
}
assert!(observed);
}
+166 -285
View File
@@ -1,26 +1,41 @@
//! 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];
//! Original S3 82D71138/82D34FE8/82D712E0, with persistent native histories.
//! Independent PC arithmetic; not a bit-exact Xenon emulation claim.
mod angles;
mod pose;
mod prediction;
mod selection;
mod targets;
use super::skeleton_animation_record::AnimationPartTransform as Frame;
use super::{grind_contact::Primitive, native_arithmetic as arithmetic};
use crate::air::trajectory::grind_surface::LandingOrientation;
pub use pose::PoseInput;
pub use targets::{DeckDimensions, Settings};
pub type V = [f32; 4];
const ZERO: V = [0.; 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,
}
const DEG: f32 = f32::from_bits(0x3c8e_fa35);
const RAD: f32 = f32::from_bits(0x4265_2ee1);
const PI: f32 = f32::from_bits(0x4049_0fdb);
const TAU: f32 = f32::from_bits(0x40c9_0fdb);
#[derive(Clone, Copy, Debug)]
pub struct Target {
pub edge: Primitive,
pub limits: [V; 3],
pub primitive_flags: u32,
pub orientation: LandingOrientation,
}
#[derive(Clone, Copy, Debug)]
pub struct Input {
pub active: bool,
pub board: Frame,
pub velocity: V,
pub angular_velocity: V,
pub up: V,
pub timestep: f32,
pub flags_2468: u32,
pub flags_2472: u32,
pub flags_2480: u32,
pub flags_2484: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct Adjustment {
@@ -28,277 +43,98 @@ pub struct Adjustment {
pub offset: V,
pub angles: V,
}
#[derive(Default)]
#[derive(Clone, Debug)]
pub struct GrindAir {
pub target: Option<Target>,
offset_delta: V,
offset: V,
angle_delta: V,
angles: V,
previous_kind: Option<usize>,
selected_kind: Option<usize>,
// Native array is initialized once, not cleared on each prediction.
headings: [f32; 12],
}
impl Default for GrindAir {
fn default() -> Self {
Self {
target: None,
offset_delta: ZERO,
offset: ZERO,
angle_delta: ZERO,
angles: ZERO,
selected_kind: None,
headings: [0.; 12],
}
}
}
impl GrindAir {
///Start82D34FE8 clears retained displacement and angle integrators.
///82D34FE8. Started/activated latches are owned by the existing KnownAir caller.
pub fn start(&mut self, target: Target) {
*self = Self {
target: Some(target),
..Self::default()
};
self.target = Some(target);
self.offset_delta = ZERO;
self.offset = ZERO;
self.angle_delta = ZERO;
self.angles = ZERO;
self.selected_kind = None;
}
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;
input: Input,
settings: &Settings,
) -> Result<Option<Adjustment>, String> {
if !input.active {
return Ok(None);
}
if input.flags_2480 & 0x0400_0000 != 0
|| input.flags_2472 & 0x8000 != 0
|| input.flags_2468 & 8 != 0
{
self.offset = ZERO;
self.angles = ZERO;
return Ok(None);
}
let target = self
.target
.ok_or("Active GrindAirAdjust has no selected primitive")?;
settings.validate()?;
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 normal = unit(cross(rail, cross(input.up, rail)));
let samples = prediction::project(input, settings, rail, normal, &mut self.headings);
let contacts =
selection::contacts(&samples, settings.frames, target.edge.start, rail, normal);
let Some(kind) = selection::choose(
&contacts,
&self.headings,
settings,
input.flags_2484 & 0x0020_0000 != 0,
self.selected_kind,
) else {
return Ok(None);
};
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.selected_kind = Some(kind);
let probe = targets::CONTACTS[kind];
let time = contacts.times[probe];
//82D72610, per-frame and total displacement are separately bounded.
self.offset_delta = limited(
add(self.offset_delta, scale(corrections[p], 1.0 / time)),
s.max_delta,
add(
self.offset_delta,
scale(contacts.corrections[probe], reciprocal(time)),
),
settings.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 {
self.offset = limited(add(self.offset, self.offset_delta), settings.max_offset);
angles::update(
self, input, settings, target, &samples, rail, normal, kind, time,
);
Ok(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 dot(a: V, b: V) -> f32 {
arithmetic::dot3(a, b)
}
fn sub(a: V, b: V) -> V {
core::array::from_fn(|i| a[i] - b[i])
@@ -307,32 +143,77 @@ 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)
v.map(|x| x * s)
}
fn madd(v: V, s: f32, a: V) -> V {
core::array::from_fn(|i| v[i].mul_add(s, a[i]))
}
fn cross(a: V, b: V) -> V {
[
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
0.,
]
}
fn inverse_length(square: f32) -> f32 {
let mut r = arithmetic::reciprocal_square_root_estimate(square);
for _ in 0..2 {
r = (r * 0.5).mul_add((-square).mul_add(r * r, 1.), r);
}
r
}
fn reciprocal(value: f32) -> f32 {
let mut r = arithmetic::reciprocal_estimate(value);
for _ in 0..2 {
r = r.mul_add((-r).mul_add(value, 1.), r);
}
r
}
fn length(v: V) -> f32 {
dot(v, v).sqrt()
let s = dot(v, v);
if s == 0. { 0. } else { s * inverse_length(s) }
}
fn unit(v: V) -> V {
let l = length(v);
if l > 0.000001 {
scale(v, 1.0 / l)
let s = dot(v, v);
let r = inverse_length(s);
let magnitude = if s == 0. { 0. } else { s * r };
if magnitude > f32::from_bits(0x3586_37bd) {
scale(v, r)
} else {
[0.; 4]
ZERO
}
}
fn limited(v: V, max: f32) -> V {
let l = length(v);
if l > max {
scale(v, max / l)
fn limited(v: V, maximum: f32) -> V {
let magnitude = length(v);
if magnitude > maximum {
scale(v, maximum * reciprocal(magnitude))
} 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.,
]
fn wrap(angle: f32) -> f32 {
let turns = angle * f32::from_bits(0x3e22_f983);
let fraction = turns - turns.floor();
(fraction - if fraction > 0.5 { 1. } else { 0. }) * TAU
}
///8296EC98: one refinement, positive full-turn signed angle then8258DB98 wrap.
fn signed_angle(a: V, b: V, axis: V) -> f32 {
let aa = dot(a, a);
let bb = dot(b, b);
if !(aa > f32::from_bits(0x38d1_b717) && bb > f32::from_bits(0x38d1_b717)) {
return 0.;
}
let normalize = |v, square| {
let r = arithmetic::reciprocal_square_root_estimate(square);
scale(v, (r * 0.5).mul_add((-square).mul_add(r * r, 1.), r))
};
let a = normalize(a, aa);
let b = normalize(b, bb);
let angle = crate::trigonometry::acos(dot(a, b).max(-1.).min(1.));
wrap(if dot(cross(a, b), axis) < 0. {
TAU - angle
} else {
angle
})
}
@@ -0,0 +1,67 @@
use super::*;
pub(super) fn update(
state: &mut GrindAir,
input: Input,
s: &Settings,
target: Target,
samples: &prediction::Samples,
rail: V,
normal: V,
kind: usize,
time: f32,
) {
//82D723D0 stores angleArray[type], NOT the range validator's frame index.
let heading = state.headings[kind];
let desired = if heading > 0. {
targets::DESIRED[kind]
} else {
-targets::DESIRED[kind]
};
let speed = ((desired - heading) / time * s.yaw_assist[kind])
.max(-s.max_angle)
.min(s.max_angle);
state.angle_delta[1] = (state.angle_delta[1] * RAD + speed)
.max(-s.max_angle)
.min(s.max_angle)
* DEG;
let points = samples[(time + 0.5) as usize];
let forward = sub(points[1], points[4]);
let projected = unit(sub(forward, scale(rail, dot(rail, forward))));
let o = target.orientation;
let error = match kind {
0 | 3 => {
let angle = signed_angle(projected, o.boardslide_dir, rail);
if angle > PI * 0.5 {
angle - PI
} else if angle < -PI * 0.5 {
angle + PI
} else {
angle
}
}
1 | 2 => {
let from = if kind == 1 {
scale(projected, -1.)
} else {
projected
};
let a = signed_angle(from, o.tipslide_dir, rail);
let b = signed_angle(from, o.backslash_dir, rail);
if a.abs() < b.abs() { a } else { b }
}
_ => 0.,
};
let maximum = s.max_angle * DEG;
state.angle_delta[0] = (state.angle_delta[0] + error / time)
.max(-maximum)
.min(maximum);
//82D71BB0 native broadcast subtraction is intentional, not vector rejection.
let projected_right = unit(input.board[0].map(|v| v - dot(input.board[0], normal)));
let correction = if dot(projected_right, projected_right) > 0.9 {
signed_angle(input.board[0], projected_right, input.board[2]) * 0.9
} else {
0.
};
state.angle_delta[2] = if kind < 4 { correction / time } else { 0. };
state.angles = add(state.angles, state.angle_delta);
}
@@ -0,0 +1,76 @@
use super::*;
#[derive(Clone, Copy, Debug)]
pub struct PoseInput {
///Skeleton11920, not the processed physical board frame.
pub animation_to_world: Frame,
///Skeleton11984, already produced by UpdateRootTransforms.
pub world_to_animation: Frame,
///Skeleton12528: retained physical-board Z axis.
pub physical_forward: V,
///Skeleton12672: this frame's mapped animation board translation.
pub animation_board_position: V,
}
///Full-angle axis matrix used by82D71430 and82BDCFB0 (not half-angle quaternion).
pub(super) fn axis_rotation(axis: V, angle: f32) -> Frame {
let (sin, cos) = crate::trigonometry::sin_cos(angle);
let [x, y, z, _] = axis;
let k = 1. - cos;
[
[
(k * x).mul_add(x, cos),
(k * x).mul_add(y, sin * z),
(k * x).mul_add(z, -sin * y),
0.,
],
[
(k * y).mul_add(x, -sin * z),
(k * y).mul_add(y, cos),
(k * y).mul_add(z, sin * x),
0.,
],
[
(k * z).mul_add(x, sin * y),
(k * z).mul_add(y, -sin * x),
(k * z).mul_add(z, cos),
0.,
],
ZERO,
]
}
pub(super) fn direction(frame: Frame, vector: V) -> V {
madd(
frame[2],
vector[2],
madd(frame[1], vector[1], scale(frame[0], vector[0])),
)
}
impl Adjustment {
///82BDCFB0: R(up,Y)*R(rail,X)*R(physicalZ,Z), converted to animation space,
///then pivot around the current mapped board and add the local displacement.
pub fn local_transform(self, input: PoseInput) -> Frame {
//Unlike NormalizeSafe, this native wrapper has no zero-vector fallback.
let across = cross(UP, self.axis);
let across = scale(across, inverse_length(dot(across, across)));
let up = cross(self.axis, across);
let up = scale(up, inverse_length(dot(up, up)));
let yaw = axis_rotation(up, self.angles[1]);
let lean = axis_rotation(self.axis, self.angles[0]);
let pitch = axis_rotation(input.physical_forward, self.angles[2]);
let mut local = super::super::skeleton_animation_record::IDENTITY;
for i in 0..3 {
local[i] = direction(
input.world_to_animation,
direction(
yaw,
direction(lean, direction(pitch, input.animation_to_world[i])),
),
);
}
let pivot = input.animation_board_position;
local[3] = sub(
add(direction(input.world_to_animation, self.offset), pivot),
direction(local, pivot),
);
local
}
}
@@ -0,0 +1,61 @@
use super::*;
pub(super) type Samples = [[V; 5]; 12];
///82D71430: frame zero is the actual processed deck; advance velocity before position.
pub(super) fn project(
input: Input,
s: &Settings,
rail: V,
normal: V,
headings: &mut [f32; 12],
) -> Samples {
let mut samples = [[ZERO; 5]; 12];
let mut board = input.board;
let mut velocity = input.velocity;
let speed = length(input.angular_velocity);
let rotation = if speed < f32::from_bits(0x3780_0000) {
super::super::skeleton_animation_record::IDENTITY
} else {
pose::axis_rotation(
scale(input.angular_velocity, 1. / speed),
speed * input.timestep,
)
};
let acceleration_step = madd(
input.up,
s.stomp * input.timestep,
[0., input.timestep * f32::from_bits(0xc11c_cccd), 0., 0.],
);
for frame in 0..s.frames {
samples[frame] = s.points.map(|p| {
madd(
board[2],
p[2],
madd(board[1], p[1], madd(board[0], p[0], board[3])),
)
});
//82D71F40, preserve the target's literal post-ACos 90/180 fold.
let forward = sub(samples[frame][1], samples[frame][4]);
let projected = unit(sub(forward, scale(normal, dot(normal, forward))));
let aligned = if dot(projected, rail) < 0. {
scale(projected, -1.)
} else {
projected
};
let mut angle = crate::trigonometry::acos(dot(aligned, rail).max(-1.).min(1.)).abs();
if angle > 90. {
angle = 180. - angle;
}
let sign = if dot(normal, cross(aligned, rail)) < 0. {
-1.
} else {
1.
};
headings[frame] = -(sign * angle * RAD);
for axis in &mut board[..3] {
*axis = pose::direction(rotation, *axis);
}
velocity = add(velocity, acceleration_step);
board[3] = madd(velocity, input.timestep, board[3]);
}
samples
}
@@ -0,0 +1,82 @@
use super::*;
pub(super) struct Contacts {
pub times: [f32; 5],
pub distances: [f32; 5],
pub corrections: [V; 5],
earliest: f32,
}
pub(super) fn contacts(
samples: &prediction::Samples,
count: usize,
origin: V,
rail: V,
normal: V,
) -> Contacts {
let mut out = Contacts {
times: [-1.; 5],
distances: [0.; 5],
corrections: [ZERO; 5],
earliest: count as f32,
};
for p in 0..5 {
for i in 1..count {
let a = dot(normal, sub(samples[i - 1][p], origin));
let b = dot(normal, sub(samples[i][p], origin));
if a * b < 0. {
let f = (a / (a - b)).abs();
let at = madd(samples[i - 1][p], 1. - f, scale(samples[i][p], f));
let on = madd(rail, dot(sub(at, origin), rail), origin);
out.times[p] = (i - 1) as f32 + f;
out.corrections[p] = sub(on, at);
out.distances[p] = length(out.corrections[p]);
out.earliest = out.earliest.min(out.times[p]);
break;
}
}
}
out
}
pub(super) fn choose(
c: &Contacts,
headings: &[f32; 12],
s: &Settings,
inverted: bool,
previous: Option<usize>,
) -> Option<usize> {
let valid = |kind: usize| {
let p = targets::CONTACTS[kind];
let t = c.times[p];
if kind == 4 {
if c.times[2].min(c.times[3]) < 4. && (c.times[2] - c.times[3]).abs() > 2. {
return false;
}
} else if c.earliest < 5. && (c.earliest - t).abs() > 3. {
return false;
}
t > 0.
&& c.distances[p] < s.distances[kind]
&& (targets::DESIRED[kind] - headings[(t + 0.5) as usize].abs()).abs() < s.ranges[kind]
};
let mut selected: Option<usize> = None;
for mut kind in 0..7 {
if inverted != (kind == 0) || !valid(kind) {
continue;
}
if kind == 3 {
if let Some(old @ (1 | 2)) = previous {
if valid(old) {
kind = old;
}
}
}
if selected.is_none_or(|old| {
c.distances[targets::CONTACTS[kind]] < c.distances[targets::CONTACTS[old]]
}) {
selected = Some(kind);
}
if selected.is_some_and(|kind| kind < 5) {
break;
}
}
selected
}
@@ -0,0 +1,48 @@
use super::*;
pub(super) const CONTACTS: [usize; 7] = [0, 1, 4, 0, 2, 2, 3];
pub(super) const DESIRED: [f32; 7] = [90., 90., 90., 90., 0., 0., 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 DeckDimensions {
pub middle_length: f32,
pub front_end_size: f32,
pub front_end_angle_degrees: f32,
pub truck_z_front: f32,
pub truck_y: f32,
}
impl DeckDimensions {
///82D72B40 uses sine for tip height but adds the unprojected tip length to Z.
pub fn contact_points(self, tip_fraction: f32) -> [V; 5] {
let half = self.middle_length * 0.5;
let tip = self.front_end_size * tip_fraction;
let y = crate::trigonometry::sin_cos(self.front_end_angle_degrees * DEG).0 * tip;
let z = half + self.truck_z_front;
[
ZERO,
[0., y, tip + half, 0.],
[0., self.truck_y, z, 0.],
[0., self.truck_y, -z, 0.],
[0., y, -(tip + half), 0.],
]
}
}
impl Settings {
pub fn validate(&self) -> Result<(), String> {
// Host data validation, not a gameplay clamp or a fabricated setting.
if !(2..=12).contains(&self.frames) {
return Err("Stock GrindAir NumFramesToTest exceeds native 2..12 storage".into());
}
Ok(())
}
}
+36 -21
View File
@@ -2,9 +2,22 @@
//! triangle query: exact parallel rejection and closed segment/barycentric bounds.
use super::native_arithmetic::dot3;
type V = [f32; 4];
#[path = "grind_contact/families.rs"]
pub mod families;
#[path = "grind_contact/entry.rs"]
pub mod entry;
#[path = "grind_contact/control.rs"]
pub mod control;
#[path = "grind_contact/admission.rs"]
pub mod admission;
#[path = "grind_contact/arithmetic.rs"]
mod arithmetic;
#[path = "grind_contact/investigator.rs"]
pub mod investigator;
#[path = "grind_contact/manager.rs"]
pub mod manager;
#[path = "grind_contact/balance.rs"]
pub mod balance;
/// Native query entry: endpoints plus a spline owner, not three vectors.
#[derive(Clone, Copy, Debug)]
@@ -81,36 +94,39 @@ pub fn boardslide_candidate(
flags_2476: u32,
deck_center_to_truck: f32,
truck_to_wheel: f32,
admission: &admission::Admission<'_>,
) -> 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());
let direction = scale(delta, arithmetic::reciprocal(arithmetic::square_root(dot3(delta, delta))));
if dot3(upright_normal(direction), board[1]) <= 0.65 {
return None;
}
if !admission.test(400, direction, false).allowed { return None; }
let depth = dot3(sub(board[3], contact.position), board[1]);
if depth * depth >= f32::from_bits(0x3b6b_edfa) {
let projected = scale(board[1], depth);
if dot3(projected, projected) >= 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();
let length = arithmetic::square_root(dot3(horizontal, horizontal));
if !(length > 0.0) {
return None;
}
let horizontal = scale(horizontal, length.recip());
let horizontal = scale(horizontal, arithmetic::reciprocal(length));
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 remaining = deck_center_to_truck - arithmetic::square_root(dot3(across, across));
let clearance = if alignment > 0.0 {
remaining / alignment * (1.0 - alignment * alignment).sqrt()
remaining / alignment * arithmetic::square_root(1.0 - alignment * alignment)
} else {
1000000.0
999.0
};
if !(clearance > truck_to_wheel) {
return None;
@@ -152,7 +168,7 @@ pub fn fifty_fifty_candidate_on_splines(
if front.primitive != rear.primitive {
let edge = primitives.get(front.primitive)?;
let delta = sub(edge.end, edge.start);
let direction = scale(delta, dot3(delta, delta).sqrt().recip());
let direction = scale(delta, arithmetic::inverse_square_root(dot3(delta, delta)));
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 {
@@ -165,13 +181,8 @@ pub fn fifty_fifty_candidate_on_splines(
return None;
}
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;
}
Some(FiftyFiftyCandidate {
direction: scale(difference, length.recip()),
direction: scale(difference, arithmetic::inverse_square_root(dot3(difference, difference))),
centre: scale(add(front.position, rear.position), 0.5),
front: front.position,
rear: rear.position,
@@ -182,15 +193,15 @@ pub fn fifty_fifty_candidate_on_splines(
///82D370F8: upward-facing normal perpendicular to the spline direction.
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();
let normal = cross(cross_up, direction);
let mut length = arithmetic::square_root(dot3(normal, normal));
if length <= 0.0 {
return [1.0, 0.0, 0.0, 0.0];
}
if normal[1] < 0.0 {
normal = scale(normal, -1.0);
length = -length;
}
scale(normal, length.recip())
scale(normal, arithmetic::reciprocal(length))
}
///82D88518, common active-grind angular admission. Plane projection occurs
@@ -198,12 +209,12 @@ pub fn upright_normal(direction: V) -> V {
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();
let length = arithmetic::square_root(dot3(projected, projected));
if length <= 0.001 {
return true;
}
let alignment = dot3(scale(projected, length.recip()), direction).abs();
alignment > (degrees * f32::from_bits(0x3c8e_fa35)).cos()
let alignment = dot3(scale(projected, arithmetic::reciprocal(length)), direction).abs();
alignment > crate::trigonometry::cos(degrees * f32::from_bits(0x3c8e_fa35))
}
/// Truck rectangles from82C1FDC0. The geometry dimensions come from the
@@ -301,3 +312,7 @@ fn cross(a: V, b: V) -> V {
0.0,
]
}
#[cfg(test)]
#[path = "grind_contact/tests.rs"]
mod tests;
@@ -0,0 +1,107 @@
//! S3 82D886B8, paired with S2 IsEntryAngleAcuteEnough 82DDA748.
//! Test each family/contact before arbitration, retaining its entry kind.
use super::{arithmetic, dot3, scale, sub, within_approach_angle, V};
use crate::point_graph::PointGraph;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum EntryKind {
RideFromAbove = 0,
RideFromBelow = 1,
RideIntoCoping = 2,
StayInGrind = 3,
ChangeGrind = 4,
AirToGrind = 5,
DropIn = 6,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Decision {
pub kind: EntryKind,
pub allowed: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct Admission<'a> {
pub category: u32,
/// The current processed state2508, not the requested/previous state.
pub state: u32,
pub speed: f32,
pub velocity: V,
/// physics_grinds/DVEntryThreshScalarVsSinSlope, X480/Y496.
pub threshold_vs_slope: &'a PointGraph<4>,
}
impl Admission<'_> {
pub fn test(&self, requested_state: u32, tangent: V, allows_drop_in: bool) -> Decision {
let kind = if self.category == 400 || self.state == 701 {
if self.state == requested_state || requested_state == 402 && self.state == 404 {
EntryKind::StayInGrind
} else {
EntryKind::ChangeGrind
}
} else if self.category == 100 {
if allows_drop_in && self.speed < 1.45 {
EntryKind::DropIn
} else {
let slope = approach_slope_sine(tangent, self.velocity);
if slope > 0.96 {
EntryKind::RideIntoCoping
} else if slope > 0.25 {
EntryKind::RideFromBelow
} else {
EntryKind::RideFromAbove
}
}
} else {
EntryKind::AirToGrind
};
let allowed = match kind {
EntryKind::RideFromAbove => within_approach_angle(tangent, self.velocity, 17.0),
EntryKind::StayInGrind => within_approach_angle(tangent, self.velocity, 90.0),
EntryKind::ChangeGrind => within_approach_angle(tangent, self.velocity, 24.0),
EntryKind::AirToGrind | EntryKind::DropIn => true,
EntryKind::RideFromBelow | EntryKind::RideIntoCoping => {
let square_speed = dot3(self.velocity, self.velocity);
let transverse = sub(scale(tangent, dot3(self.velocity, tangent)), self.velocity);
let square_transverse = dot3(transverse, transverse);
let ratio = if square_speed <= 0.002 {
1.0
} else {
arithmetic::square_root(square_transverse / square_speed)
};
let slope = approach_slope_sine(tangent, self.velocity);
let multiplier = self.threshold_vs_slope.evaluate(slope);
let limit = (1.0 - ratio).mul_add(2.4, 2.8) * multiplier;
square_transverse < limit * limit
}
};
Decision { kind, allowed }
}
}
/// S3 82D861D8 / S2 CalcApproachSlopeSine 82DDA1A8.
/// This is the vertical component of normalized cross-rail velocity, not
/// rail incline or the board's forward/rail angle.
pub fn approach_slope_sine(tangent: V, velocity: V) -> f32 {
slope_sine(tangent, velocity, 0.01)
}
/// S3 82D860F8 uses a smaller dead zone for coping and engagement.
pub fn engagement_slope_sine(tangent: V, velocity: V) -> f32 {
slope_sine(tangent, velocity, 0.001)
}
fn slope_sine(tangent: V, velocity: V, dead_zone: f32) -> f32 {
let transverse = sub(velocity, scale(tangent, dot3(velocity, tangent)));
let length = arithmetic::square_root(dot3(transverse, transverse));
if length <= dead_zone {
0.0
} else {
arithmetic::reciprocal(length) * transverse[1]
}
}
#[cfg(test)]
#[path = "admission_tests.rs"]
mod tests;
@@ -0,0 +1,50 @@
use super::*;
const GRAPH: PointGraph<4> = PointGraph { x: [0.0, 0.3, 0.7, 1.0], y: [1.0; 4] };
const RAIL: V = [1.0, 0.0, 0.0, 0.0];
fn context(category: u32, state: u32, velocity: V) -> Admission<'static> {
Admission { category, state, speed: 2.0, velocity, threshold_vs_slope: &GRAPH }
}
#[test]
fn current_grind_and_family_change_have_different_angle_gates() {
let input = context(400, 401, [0.8660254, 0.0, 0.5, 0.0]);
assert_eq!(input.test(401, RAIL, false), Decision { kind: EntryKind::StayInGrind, allowed: true });
assert_eq!(input.test(403, RAIL, true), Decision { kind: EntryKind::ChangeGrind, allowed: false });
let backslash = Admission { state: 404, ..input };
assert_eq!(backslash.test(402, RAIL, true).kind, EntryKind::StayInGrind);
}
#[test]
fn ground_entries_distinguish_above_below_coping_and_drop_in() {
let above = context(100, 100, [2.0, -1.0, 0.0, 0.0]);
assert_eq!(above.test(401, RAIL, false).kind, EntryKind::RideFromAbove);
let below = context(100, 100, [2.0, 1.0, 1.0, 0.0]);
assert_eq!(below.test(401, RAIL, false).kind, EntryKind::RideFromBelow);
let coping = context(100, 100, [2.0, 1.0, 0.0, 0.0]);
assert_eq!(coping.test(401, RAIL, false).kind, EntryKind::RideIntoCoping);
let drop_in = Admission { speed: 1.0, ..above };
assert_eq!(drop_in.test(403, RAIL, true), Decision { kind: EntryKind::DropIn, allowed: true });
assert_eq!(drop_in.test(401, RAIL, false).kind, EntryKind::RideFromAbove);
let boundary = Admission { speed: 1.45, ..drop_in };
assert_ne!(boundary.test(403, RAIL, true).kind, EntryKind::DropIn);
}
#[test]
fn coping_admission_uses_cross_rail_speed_and_the_authored_graph() {
let slow = context(100, 100, [1.0, 1.0, 0.0, 0.0]);
assert!(slow.test(401, RAIL, false).allowed);
let fast = context(100, 100, [1.0, 10.0, 0.0, 0.0]);
assert!(!fast.test(401, RAIL, false).allowed);
let zero = PointGraph { x: GRAPH.x, y: [0.0; 4] };
assert!(!Admission { threshold_vs_slope: &zero, ..slow }.test(401, RAIL, false).allowed);
}
#[test]
fn airborne_admission_and_projection_keep_the_original_leaf_boundary() {
assert_eq!(context(200, 201, [0.0, 0.0, 8.0, 0.0]).test(401, RAIL, false),
Decision { kind: EntryKind::AirToGrind, allowed: true });
assert!(context(100, 100, [0.0, -8.0, 0.0, 0.0]).test(401, RAIL, false).allowed);
assert_eq!(approach_slope_sine(RAIL, [3.0, 0.0, 0.0, 0.0]), 0.0);
assert_eq!(approach_slope_sine(RAIL, [3.0, 0.01, 0.0, 0.0]), 0.0);
}
@@ -0,0 +1,29 @@
//! Refinement sequence used by S3 82D861D8/82D88518/82D886B8.
//! Seeds use existing independent PC math, not a Xenon bit-exact claim.
use crate::physics::native_arithmetic;
pub(super) fn reciprocal(value: f32) -> f32 {
let mut result = native_arithmetic::reciprocal_estimate(value);
for _ in 0..2 {
let error = (-result).mul_add(value, 1.0);
result = result.mul_add(error, result);
}
result
}
pub(super) fn inverse_square_root(value: f32) -> f32 {
let mut result = native_arithmetic::reciprocal_square_root_estimate(value);
for _ in 0..2 {
let square = result * result;
let half = result * 0.5;
let error = (-value).mul_add(square, 1.0);
result = half.mul_add(error, result);
}
result
}
pub(super) fn square_root(value: f32) -> f32 {
let result = value * inverse_square_root(value);
// The original vector select explicitly chooses zero for a zero radicand.
if value == 0.0 { 0.0 } else { result }
}
@@ -0,0 +1,287 @@
//! S3 TU3 GrindBalance: 82D86318, 82D86808, 82D86C88; constructor82D8A318.
//! S2 counterparts82DD2168/82DD64D8/82DD26C0 corroborate field identities.
//! Keep these three stages in that order, after geometry correction and before
//! engagement. Independent PC arithmetic is not bit-exact Xenon emulation.
use super::{V, admission::EntryKind, arithmetic, cross, dot3, scale, sub};
use crate::air::trajectory::grind_surface::{GeometryType, GrindSurface};
use crate::{point_graph::PointGraph, trigonometry};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BalanceState {
pub elapsed: f32,
pub exit_angle_degrees: f32,
pub entry_delay: f32,
pub frames_away: i32,
/// Unrotated normal retained by82D86318, not the exit-lean output normal.
pub previous_normal: V,
pub exit_direction: V,
}
impl Default for BalanceState {
fn default() -> Self {
//82D8A4BC..82D8A4EC; original vectors82139A20 and82139A10.
Self {
elapsed: 0.0,
exit_angle_degrees: 0.0,
entry_delay: 2.0,
frames_away: 21,
previous_normal: [0.0, 1.0, 0.0, 0.0],
exit_direction: [1.0, 0.0, 0.0, 0.0],
}
}
}
/// A valid, geometry-corrected investigation, never a synthesized surface.
#[derive(Clone, Copy, Debug)]
pub struct BalanceContact<'a> {
pub surface: &'a GrindSurface,
/// Investigation+0; distinct from travel-directed investigation+32.
pub primitive_direction: V,
pub directed_grind_direction: V,
pub kind: u32,
pub entry_kind: EntryKind,
}
#[derive(Clone, Copy, Debug)]
pub struct TargetUpInput {
pub category: u32,
/// Processed+2504, NOT current state+2508.
pub previous_state: u32,
pub board_up: V,
}
#[derive(Clone, Copy, Debug)]
pub struct ExitLeanInput {
pub category: u32,
/// Processed+2508, unlike TargetUpInput::previous_state.
pub current_state: u32,
/// Processed+2536; value2 freezes the elapsed-time increment.
pub grind_substate: u32,
pub timestep: f32,
}
/// Investigation+48/+64, deliberately without a fabricated default.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BalanceVectors {
pub grind_normal: V,
pub target_up: V,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ForceExitProbe {
pub start: V,
pub end: V,
}
#[derive(Clone, Copy, Debug)]
pub struct ForceExitHit {
pub normal: V,
}
impl BalanceState {
///82D86318. None is the native invalid-candidate no-write branch.
pub fn update_target_up(
&mut self,
input: TargetUpInput,
contact: Option<&BalanceContact<'_>>,
vectors: &mut BalanceVectors,
) {
let Some(contact) = contact else { return };
let surface = contact.surface;
let normal = if input.category != 400 {
input.board_up
} else if matches!(input.previous_state, 402 | 404) {
blend(
self.previous_normal,
0.85,
surface.upmost_normal,
f32::from_bits(0x3e19_9998),
)
} else {
blend(
self.previous_normal,
0.925,
surface.tilted_upmost_normal,
f32::from_bits(0x3d99_9998),
)
};
let direction = contact.directed_grind_direction;
let projected = cross(cross(direction, normal), direction);
let length = magnitude(projected);
vectors.grind_normal = if length <= 0.001 {
surface.upmost_normal
} else {
scale(projected, arithmetic::reciprocal(length))
};
self.previous_normal = vectors.grind_normal;
let target = blend(
input.board_up,
0.8,
surface.upmost_normal,
f32::from_bits(0x3e4c_cccc),
);
// Unlike the projected-normal and rotation-axis branches, the original
// has NO zero-length fallback here. Do not silently invent one.
vectors.target_up = scale(target, arithmetic::reciprocal(magnitude(target)));
let alignment = dot3(vectors.grind_normal, vectors.target_up)
.max(-1.0)
.min(1.0);
let max_angle = f32::from_bits(0x3e7a_35dd);
if trigonometry::acos(alignment) > max_angle {
let axis = cross(vectors.grind_normal, vectors.target_up);
let length = magnitude(axis);
vectors.target_up = if length <= 0.001 {
vectors.grind_normal
} else {
rotate(
scale(axis, arithmetic::reciprocal(length)),
vectors.grind_normal,
max_angle,
)
};
}
}
///82D86808. Returns investigation+396 even without a valid candidate.
/// Graph is physics_grinds.ExitLeanAngleVsTime, eight X at+16/Y at+48.
pub fn update_exit_lean(
&mut self,
input: ExitLeanInput,
contact: Option<&BalanceContact<'_>>,
exit_lean_angle_vs_time: &PointGraph<8>,
vectors: &mut BalanceVectors,
) -> f32 {
let active = input.category == 400 || input.current_state == 701 || contact.is_some();
self.frames_away = if active {
0
} else {
self.frames_away.wrapping_add(1)
};
let droppable = contact.is_some_and(|contact| {
if contact.kind == 2 {
active && contact.surface.kind != GeometryType::ThinRail
} else {
contact.surface.kind == GeometryType::Ledge
}
});
let mut started = false;
//82D86880..94's carry/sign sequence implements a signed >20 test.
if self.frames_away > 20 {
self.exit_angle_degrees = 0.0;
self.elapsed = 0.0;
self.entry_delay = 2.0;
} else if self.elapsed > 0.0 || (active && droppable && contact.is_some()) {
if self.elapsed == 0.0 {
// Only a real contact can start the timer in this branch.
if let Some(contact) = contact {
self.entry_delay = match contact.entry_kind {
EntryKind::RideFromBelow => 0.8,
EntryKind::RideIntoCoping => 0.1,
_ => 2.0,
};
}
started = true;
}
if input.grind_substate != 2 {
self.elapsed = input.timestep + self.elapsed;
}
}
if self.elapsed <= 0.0 {
return self.exit_angle_degrees;
}
let time = self.elapsed - self.entry_delay;
self.exit_angle_degrees = if time <= 0.0 {
0.0
} else {
exit_lean_angle_vs_time.evaluate(time)
};
if let Some(contact) = contact {
let surface = contact.surface;
self.exit_direction = if surface.kind == GeometryType::ThinRail {
retain_side(
self.exit_direction,
cross(surface.upmost_normal, contact.primitive_direction),
)
} else if started {
surface.high_side
} else {
retain_side(self.exit_direction, surface.high_side)
};
if self.exit_angle_degrees > 0.0 {
let direction = contact.directed_grind_direction;
let axis = if cross(direction, self.exit_direction)[1] > 0.0 {
direction
} else {
negate(direction)
};
let radians = self.exit_angle_degrees * f32::from_bits(0x3c8e_fa35);
// Both outputs rotate; previous_normal deliberately does not.
vectors.target_up = rotate(axis, vectors.target_up, radians);
vectors.grind_normal = rotate(axis, vectors.grind_normal, radians);
}
}
self.exit_angle_degrees
}
///82D86C88. The callback must execute the real thin nearest world-line
/// query82E0AAD0, retaining its filtering and endpoint/edge tolerances.
/// No query is issued outside (15,28]. Errors are not collision misses.
/// Updates only investigation+412 bit31, and leaves flags intact on error.
pub fn update_force_exit<E>(
&self,
primitive_location: V,
investigation_flags: &mut u32,
mut world_line: impl FnMut(ForceExitProbe) -> Result<Option<ForceExitHit>, E>,
) -> Result<(), E> {
let force_exit = if self.exit_angle_degrees > 28.0 {
true
} else if self.exit_angle_degrees > 15.0 {
let start = sub(primitive_location, scale(self.exit_direction, 0.1));
let end = super::add(start, [0.0, -5.0, 0.0, 0.0]);
match world_line(ForceExitProbe { start, end })? {
Some(hit) => dot3(self.exit_direction, hit.normal) > -0.1,
None => true,
}
} else {
false
};
*investigation_flags = (*investigation_flags & 0x7fff_ffff) | ((force_exit as u32) << 31);
Ok(())
}
}
fn magnitude(value: V) -> f32 {
arithmetic::square_root(dot3(value, value))
}
fn blend(a: V, a_weight: f32, b: V, b_weight: f32) -> V {
core::array::from_fn(|i| a[i].mul_add(a_weight, b[i] * b_weight))
}
fn negate(value: V) -> V {
value.map(|lane| -lane)
}
fn retain_side(previous: V, side: V) -> V {
if dot3(previous, side) > 0.0 {
side
} else {
negate(side)
}
}
///82C1E220 and82D86BC0..C70: quaternion half-angle rotation, not a
/// Rodrigues replacement with a different operation order.
fn rotate(axis: V, value: V, angle: f32) -> V {
let (sin, cos) = trigonometry::sin_cos(angle * 0.5);
let q = scale(axis, sin);
let first_cross = cross(q, value);
let intermediate = core::array::from_fn(|i| value[i].mul_add(cos, first_cross[i]));
let second_cross = cross(q, intermediate);
core::array::from_fn(|i| second_cross[i].mul_add(2.0, value[i]))
}
#[cfg(test)]
#[path = "balance_tests.rs"]
mod tests;
@@ -0,0 +1,421 @@
use super::*;
const X: V = [1.0, 0.0, 0.0, 0.0];
const Y: V = [0.0, 1.0, 0.0, 0.0];
const Z: V = [0.0, 0.0, 1.0, 0.0];
// Explicit synthetic geometry tests branches, not a substitute runtime map.
fn surface(kind: GeometryType) -> GrindSurface {
GrindSurface {
center: [0.0; 4],
far_points: [negate(Z), Z],
upmost_normal: Y,
direction: Z,
high_side: X,
normal_limits: [0.0, 6.0],
kind,
audio_surface: 0,
physics_surface: 0,
flags: 0,
tilted_upmost_normal: Y,
}
}
fn contact(surface: &GrindSurface, kind: u32, entry_kind: EntryKind) -> BalanceContact<'_> {
BalanceContact {
surface,
primitive_direction: Z,
directed_grind_direction: Z,
kind,
entry_kind,
}
}
fn vectors() -> BalanceVectors {
BalanceVectors {
grind_normal: Y,
target_up: Y,
}
}
fn exit_input(dt: f32) -> ExitLeanInput {
ExitLeanInput {
category: 100,
current_state: 100,
grind_substate: 0,
timestep: dt,
}
}
fn graph() -> PointGraph<8> {
PointGraph {
x: [0., 1., 2., 3., 4., 5., 6., 7.],
y: [0., 10., 20., 30., 40., 50., 60., 70.],
}
}
fn close(a: V, b: V) {
for i in 0..4 {
assert!((a[i] - b[i]).abs() < 0.00001, "lane{i}: {a:?} != {b:?}");
}
}
#[test]
fn constructor_and_invalid_target_preserve_the_source_history() {
let mut state = BalanceState::default();
assert_eq!(
state,
BalanceState {
elapsed: 0.,
exit_angle_degrees: 0.,
entry_delay: 2.,
frames_away: 21,
previous_normal: Y,
exit_direction: X
}
);
let original = state;
let mut output = BalanceVectors {
grind_normal: X,
target_up: Z,
};
state.update_target_up(
TargetUpInput {
category: 400,
previous_state: 402,
board_up: Y,
},
None,
&mut output,
);
assert_eq!(state, original);
assert_eq!(
output,
BalanceVectors {
grind_normal: X,
target_up: Z
}
);
}
#[test]
fn target_selects_previous_tip_state_and_separate_tilted_normal() {
let mut geometry = surface(GeometryType::Ledge);
geometry.tilted_upmost_normal = X;
let candidate = contact(&geometry, 1, EntryKind::StayInGrind);
for previous_state in [400, 402, 404] {
let mut state = BalanceState::default();
let mut output = vectors();
state.update_target_up(
TargetUpInput {
category: 400,
previous_state,
board_up: Y,
},
Some(&candidate),
&mut output,
);
if previous_state == 400 {
let raw = [f32::from_bits(0x3d99_9998), 0.925, 0., 0.];
let expected = scale(raw, 1.0 / (raw[0] * raw[0] + raw[1] * raw[1]).sqrt());
close(output.grind_normal, expected);
} else {
close(output.grind_normal, Y);
}
assert_eq!(state.previous_normal, output.grind_normal);
}
}
#[test]
fn target_non_grind_uses_current_up_and_projection_fallback() {
let geometry = surface(GeometryType::ThinRail);
let candidate = contact(&geometry, 0, EntryKind::AirToGrind);
let mut state = BalanceState::default();
let mut output = vectors();
state.update_target_up(
TargetUpInput {
category: 200,
previous_state: 402,
board_up: X,
},
Some(&candidate),
&mut output,
);
close(output.grind_normal, X);
state.update_target_up(
TargetUpInput {
category: 200,
previous_state: 402,
board_up: Z,
},
Some(&candidate),
&mut output,
);
close(output.grind_normal, Y); // projected vector is exactly zero
}
#[test]
fn target_clamps_to_fourteen_degrees_with_the_native_rotation() {
let geometry = surface(GeometryType::Ledge);
let candidate = contact(&geometry, 1, EntryKind::StayInGrind);
let mut state = BalanceState::default();
let mut output = vectors();
state.update_target_up(
TargetUpInput {
category: 400,
previous_state: 400,
board_up: X,
},
Some(&candidate),
&mut output,
);
let angle = 14.0_f32.to_radians();
close(output.target_up, [angle.sin(), angle.cos(), 0., 0.]);
close(state.previous_normal, Y);
// Anti-parallel target: its zero cross axis selects grind_normal.
state.update_target_up(
TargetUpInput {
category: 400,
previous_state: 400,
board_up: negate(Y),
},
Some(&candidate),
&mut output,
);
close(output.target_up, Y);
}
#[test]
fn quaternion_rotation_preserves_axis_component_and_direction() {
let half_pi = core::f32::consts::FRAC_PI_2;
close(rotate(Z, X, half_pi), Y);
close(rotate(Z, Y, half_pi), negate(X));
close(rotate(Y, Z, half_pi), X);
close(rotate(Z, [1., 0., 2., 0.], half_pi), [0., 1., 2., 0.]);
close(rotate(Z, X, -half_pi), negate(Y));
}
#[test]
fn exit_entry_delays_and_tipslide_geometry_eligibility() {
for (entry, delay) in [
(EntryKind::RideFromBelow, 0.8),
(EntryKind::RideIntoCoping, 0.1),
(EntryKind::RideFromAbove, 2.),
(EntryKind::StayInGrind, 2.),
(EntryKind::ChangeGrind, 2.),
(EntryKind::AirToGrind, 2.),
(EntryKind::DropIn, 2.),
] {
let geometry = surface(GeometryType::Ledge);
let mut state = BalanceState::default();
state.update_exit_lean(
exit_input(0.01),
Some(&contact(&geometry, 1, entry)),
&graph(),
&mut vectors(),
);
assert_eq!(state.entry_delay, delay);
assert_eq!(state.elapsed, 0.01);
assert_eq!(state.frames_away, 0);
assert_eq!(state.exit_angle_degrees, 0.);
}
for geometry_type in [
GeometryType::ThinRail,
GeometryType::FatRail,
GeometryType::Ledge,
] {
for kind in [0, 1, 2, 3, 4, 5] {
let geometry = surface(geometry_type);
let mut state = BalanceState::default();
state.update_exit_lean(
exit_input(0.01),
Some(&contact(&geometry, kind, EntryKind::RideFromAbove)),
&graph(),
&mut vectors(),
);
let starts = geometry_type == GeometryType::Ledge
|| (kind == 2 && geometry_type != GeometryType::ThinRail);
assert_eq!(state.elapsed > 0., starts, "kind{kind} {geometry_type:?}");
}
}
}
#[test]
fn exit_substate_two_freezes_time_and_zero_dt_does_not_update_side() {
let mut geometry = surface(GeometryType::Ledge);
geometry.high_side = negate(X);
let candidate = contact(&geometry, 1, EntryKind::RideIntoCoping);
let mut state = BalanceState::default();
let mut input = exit_input(0.2);
input.grind_substate = 2;
state.update_exit_lean(input, Some(&candidate), &graph(), &mut vectors());
assert_eq!(state.elapsed, 0.);
assert_eq!(state.entry_delay, 0.1);
assert_eq!(state.exit_direction, X);
input.grind_substate = 0;
state.update_exit_lean(input, Some(&candidate), &graph(), &mut vectors());
assert_eq!(state.exit_direction, negate(X));
let old_time = state.elapsed;
input.grind_substate = 2;
state.update_exit_lean(input, Some(&candidate), &graph(), &mut vectors());
assert_eq!(state.elapsed, old_time);
let mut state = BalanceState::default();
state.update_exit_lean(exit_input(0.), Some(&candidate), &graph(), &mut vectors());
assert_eq!(state.exit_direction, X);
}
#[test]
fn exit_history_retains_twenty_away_frames_then_resets_only_timers() {
let geometry = surface(GeometryType::Ledge);
let mut state = BalanceState::default();
state.update_exit_lean(
exit_input(0.2),
Some(&contact(&geometry, 1, EntryKind::RideIntoCoping)),
&graph(),
&mut vectors(),
);
let mut output = BalanceVectors {
grind_normal: X,
target_up: Z,
};
for count in 1..=20 {
state.update_exit_lean(exit_input(0.01), None, &graph(), &mut output);
assert_eq!(state.frames_away, count);
assert!(state.elapsed > 0.);
}
assert_eq!(
output,
BalanceVectors {
grind_normal: X,
target_up: Z
}
);
state.previous_normal = Z;
state.exit_direction = negate(X);
state.update_exit_lean(exit_input(0.01), None, &graph(), &mut output);
assert_eq!(
(state.elapsed, state.exit_angle_degrees, state.entry_delay),
(0., 0., 2.)
);
assert_eq!(state.previous_normal, Z);
assert_eq!(state.exit_direction, negate(X));
state.frames_away = i32::MAX;
state.elapsed = 1.;
state.update_exit_lean(exit_input(0.01), None, &graph(), &mut output);
assert_eq!(state.frames_away, i32::MIN);
assert!(state.elapsed > 1.); // native signed comparison, not unsigned/clamped
}
#[test]
fn grind_category_and_current_701_keep_existing_timer_alive_without_contact() {
for (category, current_state) in [(400, 401), (700, 701)] {
let mut state = BalanceState {
elapsed: 0.5,
frames_away: 20,
..BalanceState::default()
};
let input = ExitLeanInput {
category,
current_state,
..exit_input(0.1)
};
state.update_exit_lean(input, None, &graph(), &mut vectors());
assert_eq!(state.frames_away, 0);
assert_eq!(state.elapsed, 0.6);
}
}
#[test]
fn exit_side_continuity_and_both_output_rotations_leave_history_unrotated() {
let mut geometry = surface(GeometryType::Ledge);
let mut state = BalanceState::default();
let mut output = vectors();
let angle = state.update_exit_lean(
exit_input(0.6),
Some(&contact(&geometry, 1, EntryKind::RideIntoCoping)),
&graph(),
&mut output,
);
assert!((angle - 5.).abs() < 0.00001);
let radians = angle.to_radians();
let expected = [-radians.sin(), radians.cos(), 0., 0.];
close(output.grind_normal, expected);
close(output.target_up, expected);
assert_eq!(state.previous_normal, Y);
geometry.high_side = negate(X);
state.update_exit_lean(
exit_input(0.),
Some(&contact(&geometry, 1, EntryKind::StayInGrind)),
&graph(),
&mut vectors(),
);
close(state.exit_direction, X);
// Thin rail chooses upmost cross primitive, not travel-directed direction.
geometry.kind = GeometryType::ThinRail;
let mut candidate = contact(&geometry, 0, EntryKind::StayInGrind);
candidate.directed_grind_direction = negate(Z);
state.update_exit_lean(exit_input(0.), Some(&candidate), &graph(), &mut vectors());
close(state.exit_direction, X);
state.exit_direction = Z; // exact zero alignment takes the negated side
state.update_exit_lean(exit_input(0.), Some(&candidate), &graph(), &mut vectors());
close(state.exit_direction, negate(X));
}
#[test]
fn force_exit_thresholds_query_geometry_and_other_bits() {
let location = [4., 3., 2., 0.];
for (angle, expected, queries) in [
(15., false, 0),
(15.001, true, 1),
(28., true, 1),
(28.001, true, 0),
] {
let state = BalanceState {
exit_angle_degrees: angle,
..BalanceState::default()
};
let mut flags = 0x8123_4567;
let mut calls = 0;
state
.update_force_exit(location, &mut flags, |probe| {
calls += 1;
assert_eq!(
probe,
ForceExitProbe {
start: [3.9, 3., 2., 0.],
end: [3.9, -2., 2., 0.]
}
);
Ok::<_, ()>(None)
})
.unwrap();
assert_eq!(calls, queries);
assert_eq!(flags & 0x7fff_ffff, 0x0123_4567);
assert_eq!(flags >> 31 != 0, expected);
}
}
#[test]
fn force_exit_normal_comparison_is_strict_and_query_errors_preserve_flags() {
let state = BalanceState {
exit_angle_degrees: 20.,
..BalanceState::default()
};
for (normal_x, force) in [(-0.10001, false), (-0.1, false), (-0.09999, true)] {
let mut flags = 0x8123_4567;
state
.update_force_exit([0.; 4], &mut flags, |_| {
Ok::<_, ()>(Some(ForceExitHit {
normal: [normal_x, 1., 0., 0.],
}))
})
.unwrap();
assert_eq!(flags >> 31 != 0, force);
}
let mut flags = 0x8123_4567;
assert_eq!(
state.update_force_exit([0.; 4], &mut flags, |_| Err("world unavailable")),
Err("world unavailable")
);
assert_eq!(flags, 0x8123_4567);
}
@@ -34,16 +34,16 @@ impl Control {
let mut target = input;
if tilt.abs() > 0.26 {
target = if selected {
input.max(0.0)
} else {
input.min(0.0)
} else {
input.max(0.0)
};
}
if height < -0.25 {
target = if selected {
input.min(0.0)
} else {
input.max(0.0)
} else {
input.min(0.0)
};
}
let sign = (if front { 1.0 } else { -1.0 }) * if switched { -1.0 } else { 1.0 };
@@ -58,7 +58,10 @@ impl Control {
if hanging_back {
target = (target + sign * 0.5).clamp(-1.0, 1.0);
}
let yaw = (deadzone(translation) * 0.85 + deadzone(nudge) * 0.79).clamp(-0.85, 0.85);
//82D8A238/240: round the nudge product, then fused translation.
let yaw = deadzone(translation)
.mul_add(0.85, deadzone(nudge) * 0.79)
.clamp(-0.85, 0.85);
(yaw * yaw * yaw, target * target * target, target != input)
} else {
(0.0, 0.0, false)
@@ -1,6 +1,103 @@
//! TU3 82D86F00 airborne entry and 82D872B8 impact admission.
//! The caller retains this result until Grind::PreUpdate82D40AF8 consumes it.
use super::{V, dot3};
use super::{V, dot3, arithmetic, admission::engagement_slope_sine};
use crate::point_graph::PointGraph;
#[derive(Clone, Copy, Debug)]
pub struct Input<'a> {
pub valid: bool,
pub kind: u32,
pub category: u32,
pub previous_state_2504: u32,
pub speed: f32,
pub balance_2720: f32,
pub direction: V,
pub normal: V,
pub up: V,
pub board_velocity: V,
pub air_velocity: V,
pub surface_kind: u32,
pub high_side: V,
/// physics_grinds/VertEngagementHelpVsVelY, X144/Y160.
pub vertical_help: &'a PointGraph<4>,
/// Stock wipeout collection layout256, checked by82D872B8.
pub max_delta: f32,
pub flags: u32,
pub previous_entry_velocity: V,
}
#[derive(Clone, Debug)]
pub struct Output {
pub valid: bool,
pub flags: u32,
pub entry_velocity: V,
pub impact_speed: f32,
/// Wipeout reason offsets minus20. Each request also sets input2468 bit18.
pub wipeout_reasons: Vec<usize>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Engagement { pub tipslide_frames: u32 }
impl Engagement {
///82D86DE8: retain flags/velocity when invalid; reset impact every update.
pub fn update(&mut self, input: Input<'_>) -> Output {
self.tipslide_frames = if input.valid && input.kind == 2
&& matches!(input.previous_state_2504, 701 | 403) && input.speed > 1.8 {
8
} else {
let next = self.tipslide_frames.wrapping_sub(1);
if (next as i32) < 0 { 0 } else { next }
};
let mut out = Output { valid: input.valid,
flags: (input.flags & !0x0200_0000) | if self.tipslide_frames > 0 { 0x0200_0000 } else { 0 },
entry_velocity: input.previous_entry_velocity, impact_speed: 0., wipeout_reasons: Vec::new() };
if !out.valid { return out; }
out.flags &= !0x4000_0000;
if !matches!(input.category, 100 | 200) { return out; }
if dot3(input.up, input.direction).abs() > 0.5 {
out.valid = false;
if input.category == 200 { out.wipeout_reasons.push(13); }
return out;
}
out.flags |= 0x4000_0000;
let initial = if input.category == 100 { input.board_velocity } else { input.air_velocity };
out.entry_velocity = if input.category == 100 {
grounded_velocity(input.kind, input.direction, input.board_velocity,
input.balance_2720, input.speed, input.vertical_help)
} else { airborne_velocity(input.kind, input.direction, input.normal, input.air_velocity) };
let delta = core::array::from_fn(|i| out.entry_velocity[i] - initial[i]);
out.impact_speed = arithmetic::square_root(dot3(delta, delta));
// MeasureEngagement runs for BOTH ground and air accepted entries.
if out.impact_speed > input.max_delta { out.wipeout_reasons.push(9); }
let along = dot3(input.board_velocity, input.direction);
let mut transverse = core::array::from_fn(|i| input.direction[i] * along - input.board_velocity[i]);
transverse[1] = 0.;
if dot3(transverse, transverse) > 49.
&& (input.surface_kind == 0 || dot3(input.board_velocity, input.high_side) > 0.) {
out.wipeout_reasons.push(14);
}
if !out.wipeout_reasons.is_empty() { out.valid = false; }
out
}
}
pub fn grounded_velocity(kind: u32, direction: V, velocity: V,
balance: f32, speed: f32, vertical_help: &PointGraph<4>) -> V {
let base = match kind {
1 => 0.1,
2 | 4 if balance != 0. && speed < 1.45 => 1.,
2 => 0.4,
0 | 3 | 4 | 5 => 0.2,
_ => 0.,
};
let help = vertical_help.evaluate(engagement_slope_sine(direction, velocity));
let amount = help.mul_add(1. - base, base);
// Native fsel on (1-amount), including unordered selecting1.
let amount = if 1. - amount >= 0. { amount } else { 1. };
let along = dot3(velocity, direction);
core::array::from_fn(|i| (direction[i] * along).mul_add(amount, velocity[i] * (1. - amount)))
}
pub fn airborne_velocity(kind: u32, direction: V, normal: V, velocity: V) -> V {
let across = cross(normal, direction);
@@ -25,7 +122,7 @@ pub fn airborne_rejections(
if dot3(up, direction).abs() > 0.5 { return vec![13]; }
let delta = core::array::from_fn(|i| corrected[i] - air_velocity[i]);
let mut reasons = Vec::new();
if dot3(delta, delta).sqrt() > max_delta { reasons.push(9); }
if arithmetic::square_root(dot3(delta, delta)) > max_delta { reasons.push(9); }
let along = dot3(board_velocity, direction);
let mut transverse = core::array::from_fn(|i| direction[i] * along - board_velocity[i]);
transverse[1] = 0.0;
@@ -38,3 +135,7 @@ 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]
}
#[cfg(test)]
#[path = "entry_tests.rs"]
mod tests;
@@ -0,0 +1,68 @@
use super::*;
const HELP: PointGraph<4> = PointGraph { x: [-1., 0., 0.5, 1.], y: [0.; 4] };
fn input() -> Input<'static> {
Input { valid: true, kind: 0, category: 200, previous_state_2504: 200,
speed: 4., balance_2720: 0., direction: [1., 0., 0., 0.],
normal: [0., 1., 0., 0.], up: [0., 1., 0., 0.],
board_velocity: [4., -2., 3., 0.], air_velocity: [4., -2., 3., 0.],
surface_kind: 0, high_side: [0., 0., 1., 0.], vertical_help: &HELP,
max_delta: 100., flags: 0, previous_entry_velocity: [9.; 4] }
}
#[test]
fn ground_and_air_use_different_velocity_projections() {
let mut state = Engagement::default();
let air = state.update(input());
let ground = state.update(Input { category: 100, ..input() });
assert!((air.entry_velocity[1] + 2.).abs() < 0.00001);
assert!((air.entry_velocity[2] - 1.8).abs() < 0.00001);
assert!((ground.entry_velocity[1] + 1.6).abs() < 0.00001);
assert!((ground.entry_velocity[2] - 2.4).abs() < 0.00001);
}
#[test]
fn steep_ground_entry_rejects_without_air_wipeout() {
let steep = Input { up: [1., 0., 0., 0.], ..input() };
let mut state = Engagement::default();
let air = state.update(steep);
assert!(!air.valid);
assert_eq!(air.wipeout_reasons, [13]);
let ground = state.update(Input { category: 100, ..steep });
assert!(!ground.valid);
assert!(ground.wipeout_reasons.is_empty());
assert_eq!(ground.flags & 0x4000_0000, 0);
}
#[test]
fn both_impact_requests_survive_first_rejection_including_on_ground() {
let mut state = Engagement::default();
let result = state.update(Input { category: 100, max_delta: 0.,
board_velocity: [4., 0., 8., 0.], ..input() });
assert!(!result.valid);
assert_eq!(result.wipeout_reasons, [9, 14]);
}
#[test]
fn tipslide_latch_counts_down_during_invalid_investigations() {
let mut state = Engagement::default();
let result = state.update(Input { kind: 2, previous_state_2504: 403, ..input() });
assert_ne!(result.flags & 0x0200_0000, 0);
assert_eq!(state.tipslide_frames, 8);
for remaining in (0..8).rev() {
let result = state.update(Input { valid: false, ..input() });
assert_eq!(state.tipslide_frames, remaining);
assert_eq!(result.entry_velocity, [9.; 4]);
assert_eq!(result.impact_speed, 0.);
}
state.update(Input { valid: false, ..input() });
assert_eq!(state.tipslide_frames, 0);
}
#[test]
fn coping_and_entry_slope_dead_zones_are_distinct() {
let velocity = [1., 0.005, 0., 0.];
let tangent = [1., 0., 0., 0.];
assert_eq!(super::super::admission::approach_slope_sine(tangent, velocity), 0.);
assert!((engagement_slope_sine(tangent, velocity) - 1.).abs() < 0.00001);
}
@@ -1,11 +1,13 @@
//! Additional TU3 deck/truck contact queries and family admission leaves.
use super::*;
use super::admission::{Admission, EntryKind};
#[derive(Clone, Copy, Debug)]
pub struct Contact {
pub geometry: FiftyFiftyCandidate,
pub front: bool,
pub kind: u32,
pub entry_kind: EntryKind,
}
fn rectangle(a: V, b: V, c: V, d: V, reference: V, edges: &[Primitive]) -> Option<TruckContact> {
@@ -79,7 +81,7 @@ pub fn tip_contacts(
fn geometry(hit: TruckContact, edge: Primitive) -> FiftyFiftyCandidate {
let delta = sub(edge.end, edge.start);
FiftyFiftyCandidate {
direction: scale(delta, dot3(delta, delta).sqrt().recip()),
direction: scale(delta, arithmetic::inverse_square_root(dot3(delta, delta))),
centre: hit.position,
front: edge.end,
rear: edge.start,
@@ -121,13 +123,19 @@ pub fn darkslide(
velocity: V,
category: u32,
low_wheel_frames: u32,
admission: &Admission<'_>,
) -> Option<Contact> {
let g = geometry(hit, edge);
if dot3(upright_normal(g.direction), scale(board[1], -1.0)) <= 0.65 {
let mut g = geometry(hit, edge);
let delta = sub(edge.end, edge.start);
g.direction = scale(delta, arithmetic::reciprocal(arithmetic::square_root(dot3(delta, delta))));
if dot3(upright_normal(g.direction), scale(board[1], -1.0)) <= 0.45 {
return None;
}
//82D8907C deliberately tests state400, even for darkslide kind5.
let decision = admission.test(400, g.direction, false);
if !decision.allowed { return None; }
let depth = dot3(sub(board[3], hit.position), board[1]);
if depth * depth >= f32::from_bits(0x3b6bedfa) {
if dot3(scale(board[1], depth), scale(board[1], depth)) >= 0.005625 {
return None;
}
if category == 100 && dot3(velocity, g.direction).abs() <= 0.75 && low_wheel_frames <= 10 {
@@ -137,6 +145,7 @@ pub fn darkslide(
geometry: g,
front: false,
kind: 5,
entry_kind: decision.kind,
})
}
@@ -151,6 +160,7 @@ pub fn five_o(
flags_2472: u32,
ground_frames: u32,
translation: f32,
admission: &Admission<'_>,
) -> Option<Contact> {
if flags_2476 & 0x4000_0000 != 0 {
return None;
@@ -159,14 +169,13 @@ pub fn five_o(
hit.and_then(|hit| {
let g = geometry(hit, edges[hit.primitive]);
let depth = dot3(sub(board[3], hit.position), board[1]);
if depth * depth >= f32::from_bits(0x3c8a71de)
let projected = scale(board[1], depth);
if dot3(projected, projected) >= f32::from_bits(0x3c8a71de) { return None; }
let decision = admission.test(403, g.direction, true);
if !decision.allowed
|| dot3(board[0], g.direction).abs() >= 0.906
|| dot3(board[1], g.direction).abs() >= 0.46
{
None
} else {
Some(g)
}
|| dot3(board[1], g.direction).abs() >= 0.46 { return None; }
Some((g, decision.kind))
})
});
let forwards = dot3(board[2], velocity) > 0.0;
@@ -194,9 +203,10 @@ pub fn five_o(
return None;
};
Some(Contact {
geometry: valid[index]?,
geometry: valid[index]?.0,
front: index == 0,
kind: 3,
entry_kind: valid[index]?.1,
})
}
@@ -213,6 +223,7 @@ pub fn tipslide(
ground_frames: u32,
balance: f32,
reference_right: V,
admission: &Admission<'_>,
) -> Option<Contact> {
if flags_2476 & 0x4000_0000 != 0 {
return None;
@@ -220,12 +231,16 @@ pub fn tipslide(
let front = hits[0].is_some();
let hit = if front { hits[0]? } else { hits[1]? };
let g = geometry(hit, edges[hit.primitive]);
if velocity[1] < 0.0 && category != 400 && board[3][1] <= hit.position[1] {
let decision = admission.test(402, g.direction, true);
if !decision.allowed { return None; }
if (decision.kind == EntryKind::DropIn || velocity[1] < 0.0 && category != 400)
&& board[3][1] <= hit.position[1] {
return None;
}
let delta = sub(board[3], hit.position);
let depth = dot3(delta, board[1]);
if depth * depth >= f32::from_bits(0x3b6bedfa) {
let projected = scale(board[1], depth);
if dot3(projected, projected) >= f32::from_bits(0x3b6bedfa) {
return None;
}
let perpendicular = sub(velocity, scale(g.direction, dot3(g.direction, velocity)));
@@ -245,5 +260,6 @@ pub fn tipslide(
geometry: g,
front,
kind: 2,
entry_kind: decision.kind,
})
}
@@ -0,0 +1,104 @@
//! Family/proximity arbitration from S3 82D875A8.
//! Geometry refinement and balance/engagement run afterward in the manager.
use super::{admission::Admission, families::{self, Contact}, *};
#[derive(Clone, Copy, Debug)]
pub struct Query<'a> {
pub board: [V; 4],
pub admission: Admission<'a>,
pub flags_2468: u32,
pub flags_2472: u32,
pub flags_2476: u32,
pub flags_2484: u32,
pub tip_state: u32,
pub truck_to_wheel: f32,
pub deck_to_truck: f32,
pub test_above: f32,
pub test_below: f32,
pub translation: f32,
pub stability_nudge: f32,
pub balance: f32,
pub reference_right: V,
pub ground_frames: u32,
pub low_wheel_frames: u32,
/// Grind permission OR retained70-frame exclusion while published kind!=2.
pub forbidden: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct Proximity {
pub contact: TruckContact,
/// Investigator412 bit26: deck/inverted rather than truck contact.
pub deck_contact: bool,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Investigation {
pub candidate: Option<Contact>,
/// Only the fallback path sets412 bit27. It does not authorize a grind.
pub proximity: Option<Proximity>,
}
/// `edges` is already the native bounded query result, in its selection order.
pub fn investigate(query: &Query<'_>, edges: &[Primitive]) -> Investigation {
if edges.is_empty() { return Investigation::default(); }
let q = query;
let a = &q.admission;
let trucks = truck_contacts(q.board, q.flags_2484, q.truck_to_wheel, q.deck_to_truck, edges);
let tips = families::tip_contacts(q.board, q.flags_2484, q.tip_state, q.flags_2468, q.deck_to_truck, edges);
let deck = deck_contact(q.board, q.flags_2484, q.test_above, q.test_below, q.deck_to_truck, edges);
let inverted = families::inverted_contact(q.board, q.flags_2484, q.test_above, q.test_below, edges);
if !q.forbidden {
let fifty = || {
let geometry = fifty_fifty_candidate_on_splines(q.board,
[q.translation, q.stability_nudge], trucks, edges)?;
let decision = a.test(401, geometry.direction, false);
decision.allowed.then_some(Contact { geometry, front: false, kind: 0, entry_kind: decision.kind })
};
let five = || families::five_o(q.board, trucks, edges, a.velocity,
q.flags_2476, q.flags_2472, q.ground_frames, q.translation, a);
let tip = || families::tipslide(q.board, tips, edges, a.velocity, a.category,
q.flags_2476, q.flags_2472, q.ground_frames, q.balance, q.reference_right, a);
let slide = || {
let hit = deck?;
let geometry = boardslide_candidate(q.board, hit, edges[hit.primitive], a.velocity,
a.category, q.low_wheel_frames as i32, q.flags_2476, q.deck_to_truck, q.truck_to_wheel, a)?;
Some(Contact { geometry, front: false, kind: 1,
entry_kind: a.test(400, geometry.direction, false).kind })
};
let dark = || {
let hit = inverted?;
families::darkslide(q.board, hit, edges[hit.primitive], a.velocity,
a.category, q.low_wheel_frames, a)
};
// The original retries the truck family as 50-50 first, even in403.
let retained = match a.state {
401 | 403 => fifty().or_else(five),
402 | 404 => tip(),
400 => slide(),
405 => dark(),
_ => None,
};
if let Some(candidate) = retained.or_else(fifty).or_else(five)
.or_else(tip).or_else(slide).or_else(dark) {
return Investigation { candidate: Some(candidate), proximity: None };
}
}
//82D87C5C..82D87E60: keep contact-only metadata even when entry is denied.
let proximity = fallback(trucks, deck, inverted, tips);
Investigation { candidate: None, proximity }
}
fn fallback(trucks: [Option<TruckContact>; 2], deck: Option<TruckContact>,
inverted: Option<TruckContact>, tips: [Option<TruckContact>; 2]) -> Option<Proximity> {
//82D87D24 short-circuits: v82 becomes1 only on an actual deck hit.
trucks[0].or(trucks[1]).map(|contact| Proximity { contact, deck_contact: false })
.or_else(|| deck.map(|contact| Proximity { contact, deck_contact: true }))
.or_else(|| inverted.or(tips[0]).or(tips[1])
.map(|contact| Proximity { contact, deck_contact: false }))
}
#[cfg(test)]
#[path = "investigator_tests.rs"]
mod tests;
@@ -0,0 +1,20 @@
use super::*;
fn hit(primitive: usize) -> Option<TruckContact> {
Some(TruckContact { position: [0.; 4], primitive })
}
#[test]
fn fallback_keeps_native_contact_order_and_classifies_only_ordinary_deck() {
let result = fallback([hit(1), hit(2)], hit(3), hit(4), [hit(5), hit(6)]).unwrap();
assert_eq!(result.contact.primitive, 1);
assert!(!result.deck_contact);
let result = fallback([None; 2], hit(3), hit(4), [hit(5), hit(6)]).unwrap();
assert_eq!(result.contact.primitive, 3);
assert!(result.deck_contact);
let result = fallback([None; 2], None, hit(4), [hit(5), hit(6)]).unwrap();
assert_eq!(result.contact.primitive, 4);
assert!(!result.deck_contact);
let result = fallback([None; 2], None, None, [None, hit(6)]).unwrap();
assert_eq!(result.contact.primitive, 6);
assert!(!result.deck_contact);
assert!(fallback([None; 2], None, None, [None; 2]).is_none());
}
@@ -0,0 +1,140 @@
//! Manager leaves recovered from original TU3, independent of host storage.
use super::{arithmetic, dot3, scale, sub, V};
use crate::point_graph::PointGraph;
///82D87460, after CalcNormalizedGrindDir82D37048.
pub fn directed_tangent(start: V, end: V, velocity: V, previous: V) -> V {
let delta = sub(end, start);
let mut direction = scale(delta, arithmetic::reciprocal(arithmetic::square_root(dot3(delta, delta))));
let speed = dot3(direction, velocity);
if speed < 0. { direction = scale(direction, -1.); }
if speed.abs() < 0.1 && dot3(previous, direction) < -0.9 {
direction = scale(direction, -1.);
}
direction
}
#[derive(Clone, Copy, Debug)]
pub struct GeometryInput {
pub valid: bool,
pub family: u32,
pub current_state: u32,
pub category: u32,
pub air_frames: u32,
pub geometry_kind: u32,
pub geometry_flags: u32,
pub far_points: [V; 2],
pub upmost: V,
pub high_side: V,
pub point: V,
pub direction: V,
pub board_position: V,
pub board_forward: V,
pub velocity: V,
pub deck_to_truck: f32,
pub previous_exit_angle: f32,
pub previous_exit_direction: V,
pub flags: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct GeometryOutput { pub valid: bool, pub family: u32, pub flags: u32 }
///82D880D0, with the retained one-truck exclusion owned by the investigator.
pub fn tweak_geometry(i: GeometryInput, avoid_five_o_frames: &mut u32) -> GeometryOutput {
let mut o = GeometryOutput { valid: i.valid, family: i.family, flags: i.flags };
if i.geometry_kind == 3
|| i.family == 2 && i.geometry_flags & 0x1000_0000 != 0 && i.category != 400 && i.air_frames < 20
|| i.family == 1 && i.geometry_flags & 0x1000_0000 != 0 { o.valid = false; }
if o.valid && is_backslash(&i) { o.family = 4; }
if o.valid && i.previous_exit_angle > 0. && i.geometry_kind != 2
&& dot3(i.previous_exit_direction, i.high_side) < 0. { o.valid = false; }
if o.valid && i.geometry_kind != 0 {
let mut transverse = sub(scale(i.direction, dot3(i.direction, i.velocity)), i.velocity);
transverse[1] = 0.;
if dot3(transverse, transverse) > 12.25 && dot3(i.velocity, i.high_side) < 0. { o.valid = false; }
}
// Source executes this branch even if an earlier check rejected validity.
if o.family == 3 && i.geometry_kind == 2 {
if (*avoid_five_o_frames as i32) > 0 { o.valid = false; }
else {
let hanging = if o.flags & 0x2000_0000 != 0 { scale(i.board_forward, -1.) } else { i.board_forward };
if dot3(hanging, i.high_side) > 0. {
let vertical = dot3(hanging, i.upmost);
o.flags = (o.flags & !0x1000_0000) | if vertical < 0.19 { 0x1000_0000 } else { 0 };
if vertical < 0. { o.valid = false; *avoid_five_o_frames = 30; }
}
}
}
o
}
///82D883E0: height DIFFERENCE chooses the high-side sample, not two positive heights.
fn is_backslash(i: &GeometryInput) -> bool {
if i.family != 2 || i.current_state == 402 || i.geometry_flags & 0x8000_0000 != 0 { return false; }
let offsets = i.far_points.map(|p| sub(p, i.point));
let heights = offsets.map(|v| dot3(v, i.upmost));
if (heights[0] - heights[1]).abs() <= i.deck_to_truck * 0.1 { return false; }
let high = if heights[0] > heights[1] { offsets[0] } else { offsets[1] };
dot3(sub(i.board_position, i.point), high) > 0.
}
///82D8ACF0 / S2 ManageCopingAssistance82DDBC48. Update, then decrement, even
///on the activation frame. FrictionVsTime is a different pre-update producer.
pub fn gravity_relief(timer: &mut f32, valid: bool, category: u32,
tangent: V, velocity: V, dt: f32, vertical: &PointGraph<4>, linear: &PointGraph<4>) -> f32 {
if valid && category == 100
&& super::admission::engagement_slope_sine(tangent, velocity) > 0.85 {
*timer = vertical.evaluate(velocity[1]) * linear.evaluate(dot3(tangent, velocity).abs());
}
let next = *timer - dt;
*timer = if -next >= 0. { 0. } else { next };
*timer
}
#[derive(Clone, Copy, Debug)]
pub struct JumpGeometry {
pub geometry_kind: u32,
pub high_side: V,
pub normal: V,
pub direction: V,
pub upmost: V,
pub point: V,
}
#[derive(Clone, Copy, Debug)]
pub struct Jumper {
pub launched: bool,
pub cooldown: u32,
pub family: u32,
pub energy: f32,
pub geometry: JumpGeometry,
}
impl Default for Jumper {
///82D8A318; original vector constants82139A10/82139A20 checked in memory.
fn default() -> Self {
Self { launched: false, cooldown: 0, family: 3, energy: 1., geometry: JumpGeometry {
geometry_kind: 0, high_side: [1., 0., 0., 0.], normal: [0., 1., 0., 0.],
direction: [1., 0., 0., 0.], upmost: [0., 1., 0., 0.], point: [0.; 4] } }
}
}
impl Jumper {
///82D739D8: invalid investigation retains geometry. Return a flag to OR,
///not a replacement for the complete processed flags2476 word.
pub fn update(&mut self, geometry: Option<JumpGeometry>, published_family: u32) -> u32 {
self.launched = false;
let next = self.cooldown.wrapping_sub(1);
self.cooldown = if (next as i32) < 0 { 0 } else { next };
if let Some(geometry) = geometry { self.geometry = geometry; }
if published_family != u32::MAX { self.family = published_family; }
let energy = self.energy + 0.0035;
self.energy = if 1. - energy >= 0. { energy } else { 1. };
if self.cooldown > 0 { 0x0200_0000 } else { 0 }
}
}
#[cfg(test)]
#[path = "manager_tests.rs"]
mod tests;
@@ -0,0 +1,62 @@
use super::*;
fn geometry() -> GeometryInput {
GeometryInput { valid: true, family: 2, current_state: 100, category: 100,
air_frames: 0, geometry_kind: 2, geometry_flags: 0,
far_points: [[0., 0.1, 0.2, 0.], [0., -0.1, -0.2, 0.]],
upmost: [0., 1., 0., 0.], high_side: [0., 0., 1., 0.],
point: [0.; 4], direction: [1., 0., 0., 0.],
board_position: [0., 0.1, 0.2, 0.], board_forward: [1., 0., 0., 0.],
velocity: [1., 0., 0., 0.], deck_to_truck: 0.243,
previous_exit_angle: 0., previous_exit_direction: [1., 0., 0., 0.], flags: 0 }
}
#[test]
fn backslash_uses_height_difference_and_preserves_existing_tipslide() {
let mut counter = 0;
assert_eq!(tweak_geometry(geometry(), &mut counter).family, 4);
assert_eq!(tweak_geometry(GeometryInput { current_state: 402, ..geometry() }, &mut counter).family, 2);
assert_eq!(tweak_geometry(GeometryInput { board_position: [0., 0., -1., 0.], ..geometry() }, &mut counter).family, 2);
}
#[test]
fn five_o_hanging_deck_sets_pry_then_excludes_thirty_frames() {
let mut counter = 0;
let input = GeometryInput { family: 3, board_forward: [0., -0.1, 0.9, 0.], ..geometry() };
let result = tweak_geometry(input, &mut counter);
assert!(!result.valid);
assert_ne!(result.flags & 0x1000_0000, 0);
assert_eq!(counter, 30);
assert!(!tweak_geometry(GeometryInput { board_forward: [1., 0., 0., 0.], ..input }, &mut counter).valid);
}
#[test]
fn coping_timer_is_reduced_on_activation_and_expires_without_contact() {
let vertical = PointGraph { x: [-1., 0., 1., 2.], y: [2.; 4] };
let linear = PointGraph { x: [0., 1., 2., 3.], y: [0.5; 4] };
let mut timer = 0.;
let tangent = [1., 0., 0., 0.];
let velocity = [3., 1., 0., 0.];
assert_eq!(gravity_relief(&mut timer, true, 100, tangent, velocity, 0.25, &vertical, &linear), 0.75);
assert_eq!(gravity_relief(&mut timer, false, 200, tangent, velocity, 1., &vertical, &linear), 0.);
}
#[test]
fn jumper_retains_geometry_and_regenerates_per_update_not_per_second() {
let mut jumper = Jumper { cooldown: 2, energy: 0.4, ..Jumper::default() };
assert_eq!(jumper.update(None, u32::MAX), 0x0200_0000);
assert_eq!(jumper.family, 3);
assert_eq!(jumper.geometry.normal, [0., 1., 0., 0.]);
assert_eq!(jumper.energy, 0.4 + 0.0035);
assert_eq!(jumper.update(None, 5), 0);
assert_eq!(jumper.family, 5);
}
#[test]
fn directed_rail_retains_previous_sign_only_at_low_speed() {
let start = [0.; 4];
let end = [1., 0., 0., 0.];
let previous = [-1., 0., 0., 0.];
assert!(directed_tangent(start, end, [0.05, 0., 0., 0.], previous)[0] < 0.);
assert!(directed_tangent(start, end, [0.1, 0., 0., 0.], previous)[0] > 0.);
}
@@ -0,0 +1,54 @@
use super::*;
use crate::point_graph::PointGraph;
const BOARD: [V; 4] = [[1.,0.,0.,0.],[0.,1.,0.,0.],[0.,0.,1.,0.],[0.;4]];
const GRAPH: PointGraph<4> = PointGraph { x: [0.,0.3,0.7,1.], y: [1.;4] };
fn admission(category: u32, state: u32) -> admission::Admission<'static> {
admission::Admission { category, state, speed: 2., velocity: [0.,0.,2.,0.], threshold_vs_slope: &GRAPH }
}
fn rail(start: V, end: V) -> Primitive { Primitive { start, end, owner: 1 } }
#[test]
fn one_truck_admission_happens_before_front_rear_selection() {
let edges = [rail([0.;4],[0.5,0.,0.8660254,0.]), rail([0.;4],[0.,0.,1.,0.])];
let hits = [Some(TruckContact { position: [0.,0.,0.2,0.], primitive: 0 }),
Some(TruckContact { position: [0.,0.,-0.2,0.], primitive: 1 })];
let result = families::five_o(BOARD, hits, &edges, [0.,0.,2.,0.], 0,0,0,1., &admission(400,400)).unwrap();
assert!(!result.front);
assert_eq!(result.geometry.primitive, 1);
assert_eq!(result.entry_kind, admission::EntryKind::ChangeGrind);
}
#[test]
fn tipslide_drop_in_rejects_contact_above_deck_without_descent() {
let edges = [rail([0.;4],[0.,0.,1.,0.])];
let hit = Some(TruckContact { position: [0.,0.01,0.2,0.], primitive: 0 });
let input = admission::Admission { speed: 1., velocity: [0.,0.1,0.,0.], ..admission(100,100) };
assert!(families::tipslide(BOARD,[hit,None],&edges,input.velocity,100,0,0,0,0.,BOARD[0],&input).is_none());
}
#[test]
fn darkslide_uses_its_own_tilt_and_depth_thresholds() {
let edges = [rail([0.;4],[0.,0.,1.,0.])];
let board = [[-0.6,-0.8,0.,0.],[0.8,-0.6,0.,0.],[0.,0.,1.,0.],[0.056,-0.042,0.,0.]];
let hit = TruckContact { position: [0.;4], primitive: 0 };
let result = families::darkslide(board,hit,edges[0],[0.,0.,2.,0.],200,0,&admission(200,201));
assert!(result.is_some());
let mut deeper = board;
deeper[3] = scale(board[1], 0.08);
assert!(families::darkslide(deeper,hit,edges[0],[0.,0.,2.,0.],200,0,&admission(200,201)).is_none());
}
#[test]
fn one_truck_clamps_match_both_originals_not_zip_reversed_signs() {
for (front, tilt, input, expected) in [
(true,-0.3,0.5,0.), (true,-0.3,-0.5,-0.011249996),
(false,0.3,-0.5,0.), (false,0.3,0.5,0.011249996),
(true,0.3,-0.5,0.), (true,0.3,0.5,0.011249996),
(false,-0.3,0.5,0.), (false,-0.3,-0.5,-0.011249996),
] {
let mut control = control::Control::default();
control.update(3,[0.,0.,1.,0.],[0.,1.,tilt,0.],front,false,false,0.,0.,input,0.);
assert!((control.pitch-expected).abs()<1e-7, "front={front} tilt={tilt} input={input}: {}",control.pitch);
}
}
+62 -127
View File
@@ -1,10 +1,29 @@
//! TU3 grind force leaves. Their inputs belong to the retained grind manager;
//! contact admission and lifecycle are separate from these force calculations.
use super::native_arithmetic::dot3;
type V = [f32; 4];
pub type V = [f32; 4];
#[path = "grind_forces/slide.rs"]
pub mod slide;
#[path = "grind_forces/release.rs"]
pub mod release;
#[path = "grind_forces/noise.rs"]
pub mod noise;
#[path = "grind_forces/post.rs"]
pub mod post;
#[path = "grind_forces/launch.rs"]
pub mod launch;
#[path = "grind_forces/orientation.rs"]
pub mod orientation;
#[path = "grind_forces/support.rs"]
pub mod support;
#[path = "grind_forces/reckoning.rs"]
pub mod reckoning;
#[cfg(test)]
#[path = "grind_forces/tests.rs"]
mod tests;
///82D3FA18, called by the 50-50 update82D41D70 with800,0,.07.
/// Returns a force at the deck origin; never moves the body to a spline.
/// Returns a force at the deck origin; None means no accumulator call.
pub fn lateral_pin(
board: [V; 4],
point: V,
@@ -15,7 +34,7 @@ pub fn lateral_pin(
up_offset: f32,
forward_selected: bool,
slope_multiplier: f32,
) -> V {
) -> Option<V> {
let direction = if forward_selected {
board[2]
} else {
@@ -25,16 +44,16 @@ pub fn lateral_pin(
direction[i].mul_add(forward_offset, board[3][i]) - board[1][i] * up_offset
});
let force = scale(across, dot3(sub(point, reference), across) * strength);
let length = dot3(force, force).sqrt();
if length <= 0.0 {
return [0.0; 4];
let length = length(force);
if !(length > 0.0) {
return None;
}
let axis = scale(force, length.recip());
let axis = scale(force, reciprocal(length));
let damping = scale(
axis,
strength * f32::from_bits(0x3e08_3127) * dot3(velocity, axis),
);
scale(sub(force, damping), slope_multiplier)
Some(scale(sub(force, damping), slope_multiplier))
}
///82D3FD88 after its native surface/material selection. It damps velocity in
@@ -46,15 +65,15 @@ pub fn friction(
time_multiplier: f32,
flagged_surface: bool,
surface_multiplier: f32,
engagement: u32,
geometry_kind: u32,
strengths: [f32; 3],
) -> V {
let tangent_velocity = sub(velocity, scale(grind_normal, dot3(velocity, grind_normal)));
let speed = dot3(tangent_velocity, tangent_velocity).sqrt();
let speed = length(tangent_velocity);
if speed <= 0.001 {
return [0.0; 4];
}
let strength = strengths[match engagement {
let strength = strengths[match geometry_kind {
0 => 0,
1 => 1,
_ => 2,
@@ -77,128 +96,44 @@ 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(
velocity: V,
normal: V,
direction: V,
board_position: V,
point: V,
height: f32,
nudge: f32,
manager_age: f32,
) -> V {
let across = cross(direction, normal);
let balance = if manager_age < 0.31 {
if nudge < 0.0 {
nudge.min(-0.42)
} else if nudge > 0.0 {
nudge.max(0.42)
} else if dot3(sub(board_position, point), across) > 0.0 {
0.42
} else {
-0.42
}
} else {
nudge
};
let lateral = scale(across, balance * 1.8);
let length = dot3(lateral, lateral).sqrt();
let lateral = if length > 1.8 {
scale(lateral, 1.8 / length)
} else {
lateral
};
///82D3FD88 skips the accumulator call at or below its planar-speed threshold.
pub fn friction_applies(velocity: V, normal: V) -> bool {
let planar = sub(velocity, scale(normal, dot3(velocity, normal)));
core::array::from_fn(|i| normal[i].mul_add(height * 1.03, planar[i]) + lateral[i])
}
fn cross(a: V, b: V) -> V {
[
(-a[2]).mul_add(b[1], a[1] * b[2]),
(-a[0]).mul_add(b[2], a[2] * b[0]),
(-a[1]).mul_add(b[0], a[0] * b[1]),
0.0,
]
length(planar) > 0.001
}
/// Grind PreUpdate82D3F4E8 / exit decision82D40DA0. Counts are native
/// simulation updates, not render frames. Once leaving, the phase is latched.
#[derive(Default)]
pub struct Release {
updates: u32,
leaving_updates: u32,
pub leaving: bool,
fn length(v: V) -> f32 {
let square = dot3(v, v);
if square == 0.0 { return 0.0; }
square * super::board_motion_output::inverse_length_squared(square, 2)
}
impl Release {
pub fn begin_update(&mut self, speed: f32, explicit_exit: bool) {
self.leaving |= explicit_exit || (self.updates > 60 && speed < 0.15);
fn reciprocal(value: f32) -> f32 {
let mut inverse = value.recip();
for _ in 0..2 {
let correction = (-inverse).mul_add(value, 1.0);
inverse = inverse.mul_add(correction, inverse);
}
pub fn finish_update(&mut self) -> bool {
self.leaving_updates = if self.leaving { self.leaving_updates.wrapping_add(1) } else { 0 };
self.updates = self.updates.wrapping_add(1);
self.leaving_updates > 35
inverse
}
///82D3FD88's material switch. S3 adds material13 to S2's material2 case.
pub fn material_multiplier(material: u32) -> f32 {
match material {
2 | 13 => 1.7,
3 => 2.6,
4 => 0.6,
5 => 4.0,
6 => 5.0,
_ => 1.3,
}
}
///82D3F850: push away from the contacted edge until the native lateral
///speed limit is reached. Ledge engagement may supply its outward direction.
pub fn release_force(
position: V, point: V, direction: V, normal: V, velocity: V,
surface_kind: u32, surface_side: V, force_across: bool,
strength: f32, speed_limit: f32, lift: f32,
) -> V {
let outward = if !force_across && surface_kind != 0 {
scale(surface_side, -1.0)
} else {
let across = cross(direction, normal);
scale(across, if dot3(across, sub(position, point)) > 0.0 { 1.0 } else { -1.0 })
};
if dot3(velocity, outward) >= speed_limit { return [0.0; 4]; }
let lateral = scale(outward, strength);
let upward = normal[1].max(0.0) * lift;
core::array::from_fn(|i| normal[i].mul_add(upward, lateral[i]))
///82D3FA18: gravity relief bypasses the authored PinVsSlope four-point graph.
pub fn pin_slope(
normal: V, upmost_normal: V, gravity_relief: f32,
pin_vs_slope: &crate::point_graph::PointGraph<4>,
) -> f32 {
if gravity_relief > 0.0 { 1.0 } else { pin_vs_slope.evaluate(dot3(normal, upmost_normal)) }
}
@@ -0,0 +1,74 @@
//! LaunchSkateboard82D73AB0; S2 named counterpart82DCB3A8.
//! S3 replaces tunables with literals; energy cost is0.6, not ZIP's0.2.
use super::{length, reciprocal, scale, sub, V};
use crate::physics::{grind_contact::manager::Jumper, native_arithmetic::dot3};
#[derive(Clone, Copy, Debug)]
pub struct Input {
pub velocity_400: V,
pub position_112: V,
/// Low-energy zero-balance side selection uses CURRENT1120, not cache96.
pub current_point_1120: V,
pub balance_2800: f32,
pub geometry_side_jump: f32,
pub vertical_jump: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct Output {
pub velocity: V,
/// Complete retained cache, to replace the SAME owner updated by82D739D8.
pub jumper: Jumper,
pub flags_2476_to_or: u32,
}
pub fn launch(mut jumper: Jumper, input: Input) -> Output {
let geometry = jumper.geometry;
let mut normal = geometry.normal;
let candidate = if matches!(jumper.family, 2 | 4) {
let across = cross(normal, geometry.direction);
let outward = if dot3(across, sub(input.position_112, geometry.point)) > 0. {
across
} else { scale(across, -1.) };
let amount = if jumper.family == 4 { -0.1093 } else { 0.291 };
core::array::from_fn(|i| normal[i] + outward[i] * amount)
} else {
core::array::from_fn(|i| geometry.upmost[i].mul_add(0.5, normal[i] * 0.5))
};
let magnitude = length(candidate);
if magnitude > 0.1 { normal = scale(candidate, reciprocal(magnitude)); }
let planar = sub(input.velocity_400, scale(normal, dot3(input.velocity_400, normal)));
let vertical = input.vertical_jump * 1.03;
let base: V = core::array::from_fn(|i| normal[i].mul_add(vertical, planar[i]));
let across = cross(geometry.direction, normal);
let mut balance = input.balance_2800;
let low_energy = jumper.energy < 0.31;
if low_energy {
if balance < 0. {
if balance - -0.42 >= 0. { balance = -0.42; }
} else if balance > 0. {
if !(balance - 0.42 >= 0.) { balance = 0.42; }
} else {
balance = if dot3(sub(input.position_112, input.current_point_1120), across) > 0. {
0.42
} else { -0.42 };
}
jumper.cooldown = 10;
}
let side = if geometry.geometry_kind != 0 { input.geometry_side_jump } else { 0. };
let mut lateral = sub(scale(across, balance * 1.8), scale(geometry.high_side, side));
let magnitude = length(lateral);
if magnitude > 1.8 { lateral = scale(lateral, 1.8 / magnitude); }
let velocity = core::array::from_fn(|i| base[i] + lateral[i]);
jumper.launched = true;
let energy = jumper.energy - 0.6;
jumper.energy = if -energy >= 0. { 0. } else { energy };
Output { velocity, jumper, flags_2476_to_or: if low_energy { 0x0200_0000 } else { 0 } }
}
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]),
(-a[3]).mul_add(b[3], a[3] * b[3])]
}
@@ -0,0 +1,43 @@
//! Original orientation noise82D40580 / S2 AddNoiseToOrientation82D8FE78.
//! No replacement RNG: caller supplies the three consecutive board-stream u32
//! draws, even for zero amount/speed. S3 hardcodes S2's full-speed/scalar tunables.
use super::V;
pub fn angles(speed: f32, amount: f32, samples: [u32; 3]) -> [f32; 3] {
let speed_factor = (speed * 0.5).min(1.0);
let amplitude = speed_factor * amount * 0.06;
let width = amplitude * 2.0;
samples.map(|sample| {
let unit = (sample % 100_000) as f32 * 0.00001;
unit.mul_add(width, -amplitude)
})
}
pub fn apply(frame: [V; 4], speed: f32, amount: f32, samples: [u32; 3]) -> [V; 4] {
let [x, y, z] = angles(speed, amount, samples);
let (sx, cx) = crate::trigonometry::sin_cos(x);
let (sy, cy) = crate::trigonometry::sin_cos(y);
let (sz, cz) = crate::trigonometry::sin_cos(z);
// Raw standard-VMX words82D407F0=10CC526E and82D407F4=118C4AAE
// disambiguate IDA's misleading printed multiply/add operand order.
let sx_cz = sx * cz;
let cx_sz = cx * sz;
let cx_cz = cx * cz;
let sx_sz = sx * sz;
let columns = [
[cy * cz, cy * sz, -sy],
[sy * sx_cz - cx_sz, sy.mul_add(sx_sz, cx_cz), cy * sx],
[sy.mul_add(cx_cz, sx_sz), sy * cx_sz - sx_cz, cy * cx],
];
let mut result = frame;
for column in 0..3 {
let v = columns[column];
result[column] = core::array::from_fn(|lane| {
v[2].mul_add(frame[2][lane],
v[1].mul_add(frame[1][lane], v[0] * frame[0][lane]))
});
}
//82D407EC/40810/40828 multiply the three basis vectors by zero
//and add the original translation. No positional jitter is introduced.
result
}
@@ -0,0 +1,104 @@
//! ZIP orientation formulas checked against original S3 family bodies.
//! S2 equivalents82D9B818/918/BAC0/BCF8/BD88 retain tunable angles/blends;
//! S3 uses the literals below and adds darkslide's inverted normal.
use super::{length, reciprocal, scale, sub, V};
use crate::physics::{native_arithmetic::dot3, board_motion_output::inverse_length_squared};
pub type Frame = [V; 4];
#[derive(Clone, Copy, Debug)]
pub struct Input {
/// Exact Processed64..127, not an effective/fakie-adjusted board frame.
pub board: Frame,
pub direction: V,
pub normal: V,
pub point: V,
pub yaw_1504: f32,
pub pitch_1508: f32,
pub switched: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct Target { pub frame: Frame, pub noise_amount: f32 }
pub fn target(family: u32, i: Input) -> Result<Target, &'static str> {
let frame = match family {
0 if i.pitch_1508.abs() <= 0.12 => {
let forward = signed(i.direction, dot3(i.direction, i.board[2]) > 0.);
[cross(i.normal, forward), i.normal, forward, i.board[3]]
}
0 | 3 => truck(i),
1 | 5 => {
let right = signed(i.direction, dot3(i.direction, i.board[0]) > 0.);
let up = if family == 5 { scale(i.normal, -1.) } else { i.normal };
[right, up, cross(right, up), i.board[3]]
}
2 | 4 => {
let across = cross(i.direction, i.normal);
let axis = signed(i.direction, !(dot3(across, sub(i.board[3], i.point)) > 0.));
// Original822F8618/822F8614,20/42degrees expressed in radians.
let angle = f32::from_bits(if family == 4 { 0x3f3b_a866 } else { 0x3eb2_b8c3 });
let up = rotate(axis, i.normal, angle);
let right = signed(i.direction, dot3(i.direction, i.board[0]) > 0.);
[right, up, cross(right, up), i.board[3]]
}
_ => return Err("Unknown physical grind family"),
};
Ok(Target { frame, noise_amount: if family == 0 { 0. } else { 0.09 } })
}
///82D40290: zero angles skip rotation; near-parallel support returns the entire
///input frame. Neither branch introduces a replacement normal or heading.
fn truck(i: Input) -> Frame {
let yaw = i.yaw_1504 * -0.68;
let mut forward = i.board[2];
if yaw.abs() > 0. { forward = rotate(i.normal, forward, yaw); }
let right = cross(i.normal, forward);
let magnitude = length(right);
if magnitude <= 0.001 { return i.board; }
let right = scale(right, reciprocal(magnitude));
let mut up = cross(forward, right);
let pitch = (if i.switched { -i.pitch_1508 } else { i.pitch_1508 }) * 0.68;
if pitch.abs() > 0. {
forward = rotate(right, forward, pitch);
up = cross(forward, right);
}
[right, up, forward, i.board[3]]
}
///82D40890: reverse-order classical Gram-Schmidt on BOTH frames, then
///CURRENT's original82BD3150 helper. The temporary orthonormal frames' fourth
///vectors are explicitly zeroed (82D40930/82D409C8), not a position teleport.
pub fn blend(current: Frame, target: Frame) -> Frame {
crate::animation::foot_ik::interpolate_native(
&orthonormal(current), &orthonormal(target), 0.1,
).0
}
fn orthonormal(input: Frame) -> Frame {
let mut output = [[0.; 4]; 4];
for axis in (0..3).rev() {
let mut value = input[axis];
for previous in ((axis + 1)..3).rev() {
value = sub(value, scale(output[previous], dot3(output[previous], input[axis])));
}
output[axis] = scale(value, inverse_length_squared(dot3(value, value), 2));
}
output
}
fn rotate(axis: V, value: V, angle: f32) -> V {
let (sine, cosine) = crate::trigonometry::sin_cos(angle * 0.5);
let q = scale(axis, sine);
let first = cross(q, value);
let inner = core::array::from_fn(|i| cosine.mul_add(value[i], first[i]));
let second = cross(q, inner);
core::array::from_fn(|i| 2.0f32.mul_add(second[i], value[i]))
}
fn signed(v: V, positive: bool) -> V { if positive { v } else { scale(v, -1.) } }
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]),
(-a[3]).mul_add(b[3], a[3] * b[3])]
}
@@ -0,0 +1,51 @@
//! Grind Post82D43098 -> Wipeout82D90898; S2 82D85E78 ->82DDF248.
//! Unlike Ground, no cooldown/vehicle/squash/balance checks are added here.
use crate::{
physics::{board_motion_output::inverse_length_squared, native_arithmetic::dot3},
player::wipeout::{Frame, Requests, regional_force},
trigonometry,
};
/// Required physics_wipeout/default layout values. No borrowed Ground/Air
/// thresholds and no default-value fallback. Shared stock wiring supplies these.
#[derive(Clone, Copy, Debug)]
pub struct Settings {
pub max_arm_contact_164: f32,
pub max_body_contact_168: f32,
pub xz_acceleration_204: f32,
pub max_displacement_208: f32,
pub max_angular_deck_error_212: f32,
}
pub fn check(state: &mut Requests, settings: Settings, frame: &Frame) {
state.mode = 5; // Preserve balance, timers and existing requests.
if dot3(frame.pose_error, frame.pose_error)
> settings.max_displacement_208 * settings.max_displacement_208
{
state.request(1, 0.0);
} else if regional_force(frame, settings.max_body_contact_168, settings.max_arm_contact_164) {
state.request(0, 0.0);
}
//82D90AB8. Closing-velocity checks still run after displacement/contact.
if frame.board_contact {
let v = frame.closing_velocity;
let m = frame.world_to_animation;
let local: [f32; 4] = core::array::from_fn(|i| {
m[2][i].mul_add(v[2], m[1][i].mul_add(v[1], m[0][i] * v[0]))
});
let square = dot3([local[0], 0.0, local[2], local[3]],
[local[0], 0.0, local[2], local[3]]);
let length = if square == 0.0 { 0.0 } else { square * inverse_length_squared(square, 2) };
if length > settings.xz_acceleration_204 || local[1].abs() > 100.0 {
state.request(2, 0.0);
if regional_force(frame, 1.0, 20.0) { state.request(0, 0.0); }
}
}
//82D909B0 always checks all three corresponding deck/Input axes.
let cosine = dot3(frame.deck[0], frame.input_board[0])
.min(dot3(frame.deck[1], frame.input_board[1]))
.min(dot3(frame.deck[2], frame.input_board[2]));
if trigonometry::acos(cosine.max(-1.0).min(1.0)) > settings.max_angular_deck_error_212 {
state.request(3, 0.0);
}
}
@@ -0,0 +1,52 @@
//! UpdateGrindStates82D8E3E0, compared with S2 named82DDD2A8.
//! Maintains CURRENT's shared filters/frames and BodySpin history; no second
//! reckoning owner and no ground/air-state substitute.
use super::{V, length};
use crate::{air::{body_spin::{self, BodySpinState}, reckoning::AirState}, math::Vector3,
physics::{board_motion_output::inverse_length_squared, native_arithmetic::dot3},
point_graph::PointGraph,
riding::{ground_orientation::GroundOrientation, reckoning_frames::ReckoningFrames}};
pub struct Settings {
pub ground_normal_smoothing: V,
/// Stock schema: layout800 TiltVsRotGround,672 TiltVsSlopeGround.
pub tilt_vs_rotation: PointGraph<8>,
pub tilt_vs_slope: PointGraph<8>,
}
pub fn update(orientation: &mut GroundOrientation, frames: &mut ReckoningFrames,
body_spin: &mut BodySpinState, air: &mut AirState, settings: &Settings,
normal: V, heading: V, smoothing: f32, reverse_stance: bool,
physical_body_spin: f32) {
let old_up = lanes(orientation.up);
orientation.dynamic_up = orientation.up;
orientation.up_velocity = Vector3::ZERO;
frames.target_lean_angle = 0.;
air.secondary_lean_angle = 0.;
orientation.ground_blend = 0.;
let ground = orientation.ground_filter.update(settings.ground_normal_smoothing, normal);
orientation.ground_normal = xyz(ground);
frames.heading = heading;
let candidate: V = core::array::from_fn(|i| old_up[i].mul_add(smoothing, normal[i] * (1. - smoothing)));
// Native NormalizeSafe threshold is the original830BD350 splat1e-6;
// zero/short candidates retain the preceding up, not world-up.
let up = if length(candidate) > f32::from_bits(0x3586_37bd) {
let inverse = inverse_length_squared(dot3(candidate, candidate), 2);
candidate.map(|v| v * inverse)
} else { old_up };
orientation.up = xyz(up);
orientation.target = orientation.up;
orientation.slow_filter.filter(up);
orientation.fast_filter.filter(up);
orientation.slow_filter.publish_current(up);
orientation.fast_filter.publish_current(up);
frames.calculate_transform(up, ground);
frames.calculate_tilt(reverse_stance, &settings.tilt_vs_rotation, &settings.tilt_vs_slope);
// Caller82D3F4E8 passes updateBodySpin=false, hence0. Other native callers
// may supply their actual PhysBodySpin; this is still the ground branch.
body_spin::update_ground(body_spin, physical_body_spin);
// No ResetSpin: Air's spin_angle/spin_speed survive this function.
}
fn xyz(v: V) -> Vector3 { Vector3::new(v[0], v[1], v[2]) }
fn lanes(v: Vector3) -> V { [v.x, v.y, v.z, 0.] }
@@ -0,0 +1,33 @@
//! Exit nudge82D3F850 / S2 82D85330. Family dispatch remains separate.
use super::{V, dot3, scale, sub};
#[derive(Clone, Copy, Debug)]
pub struct Input {
pub position: V,
pub point: V,
pub across: V,
pub normal: V,
pub velocity: V,
pub geometry_kind: u32,
pub high_side: V,
pub force_across: bool,
/// Original r7. It is independent of r8/force_across; slides suppress lift.
pub enable_lift: bool,
pub strength: f32,
pub speed_limit: f32,
pub lift: f32,
}
/// None means no AddForce call, including no accumulator wakeup.
pub fn force(input: Input) -> Option<V> {
let outward = if input.force_across || input.geometry_kind == 0 {
scale(input.across, if dot3(input.across, sub(input.position, input.point)) > 0.0 {
1.0
} else { -1.0 })
} else { scale(input.high_side, -1.0) };
if !(dot3(input.velocity, outward) < input.speed_limit) { return None; }
let lateral = scale(outward, input.strength);
if !input.enable_lift { return Some(lateral); }
let upward = input.normal[1].max(0.0) * input.lift;
Some(core::array::from_fn(|i| input.normal[i].mul_add(upward, lateral[i])))
}
@@ -0,0 +1,57 @@
//! Translation branches82D419A0/82D41438; S2 boardslide82D90970.
use super::{V, dot3, scale, sub};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Slide { Boardslide, Darkslide }
/// All vectors are the physical-state/processed observations. Preparing-jump
/// is the retained state byte241, not a newly sampled analog input.
#[derive(Clone, Copy, Debug)]
pub struct Input {
pub position: V,
pub point: V,
pub direction: V,
pub across: V,
pub velocity: V,
pub translation_2796: f32,
pub total_mass_2660: f32,
/// Source global update frequency, not an invented fixed60 or render rate.
pub update_frequency: f32,
pub preparing_jump: bool,
pub geometry_kind: u32,
}
/// Forces at the processed deck position, in original application order.
/// The caller applies these directly, not through the21-entry tagged queue.
pub fn control(slide: Slide, input: Input) -> Vec<V> {
let Input { position, point, direction, across, velocity,
translation_2796: translation, total_mass_2660: mass,
update_frequency, preparing_jump, geometry_kind } = input;
let offset = dot3(sub(position, point), across);
let inward = scale(across, if offset > 0.0 { -1.0 } else { 1.0 });
if preparing_jump {
return if offset.abs() > 0.12 { vec![scale(inward, 201.0)] } else { Vec::new() };
}
let translation_strength = match slide {
Slide::Boardslide => 25.0,
Slide::Darkslide => (1.0 - offset.abs() * 6.25) * 40.0,
};
let translate = scale(across, translation * translation_strength);
if geometry_kind == 2 {
return vec![translate];
}
let perpendicular = sub(velocity, scale(direction, dot3(velocity, direction)));
if offset.abs() > 0.16 {
let mut forces = vec![scale(inward, 10.0)];
if dot3(inward, perpendicular) < 0.0 {
let mut stop = scale(perpendicular, -(mass * update_frequency));
stop[1] = 0.0;
forces.push(stop);
}
forces
} else {
let mut damping = scale(perpendicular, -20.0);
damping[1] = 0.0;
vec![translate, damping]
}
}
@@ -0,0 +1,24 @@
//! Common force leaves82D40080/82D3FFC0/82D40130/82D401F8.
//! These consume manager output; they do not duplicate its assistance update.
use super::{scale, V};
use crate::{physics::native_arithmetic::dot3, point_graph::PointGraph};
pub fn pin(normal: V, gravity_relief: f32) -> Option<V> {
if !(gravity_relief > 0.) { return None; }
let mut force = scale(normal, -40.);
force[1] = 0.;
Some(force)
}
pub fn coping(upmost: V, velocity: V, gravity_relief: f32) -> Option<V> {
(gravity_relief > 0. && dot3(upmost, velocity) < 0.).then_some([0., 120., 0., 0.])
}
pub fn exit_lean(high_side: V, angle_1500: f32, graph: &PointGraph<4>) -> Option<V> {
(angle_1500 > 0.).then(|| scale(high_side, graph.evaluate(angle_1500) * -65.))
}
///Unlike conditional pin/coping, original always calls AddForce, even at0.
pub fn truck_compensation(normal: V, pitch_1508: f32) -> V {
scale(normal, pitch_1508.abs() * -600.)
}
@@ -0,0 +1,82 @@
use super::*;
const X: V = [1.0, 0.0, 0.0, 0.0];
const Y: V = [0.0, 1.0, 0.0, 0.0];
const Z: V = [0.0, 0.0, 1.0, 0.0];
const O: V = [0.0; 4];
fn near(a: f32, b: f32) { assert!((a - b).abs() < 0.0001, "{a} != {b}"); }
#[test]
fn material_and_geometry_are_independent_and_support_is_not_the_grind_normal() {
assert_eq!(material_multiplier(13), material_multiplier(2));
assert_eq!(material_multiplier(999), 1.3);
for (kind, strength) in [(0, 50.0), (1, 40.0), (2, 30.0), (99, 30.0)] {
let force = friction([4.0, 9.0, 0.0, 0.0], Y, [0.0, 0.25, 0.0, 0.0],
0.5, true, material_multiplier(4), kind, [50.0, 40.0, 30.0]);
near(force[0], -0.25 * 0.5 * 1.9 * 0.6 * strength * 0.48);
near(force[1], 0.0);
}
assert!(!friction_applies([0.001, 10.0, 0.0, 0.0], Y));
assert!(friction_applies([0.002, 10.0, 0.0, 0.0], Y));
}
#[test]
fn pin_uses_signed_reference_and_skips_zero_error_instead_of_waking_body() {
assert_eq!(lateral_pin([X, Y, Z, O], O, X, X, 800.0, 0.0, 0.0, true, 1.0), None);
let force = lateral_pin([X, Y, Z, O], X, X, [2.0, 0.0, 0.0, 0.0],
100.0, 0.0, 0.0, true, 0.5).unwrap();
near(force[0], (100.0 - 100.0 * 0.133 * 2.0) * 0.5);
let graph = crate::point_graph::PointGraph { x: [-1.0, 0.0, 0.5, 1.0], y: [0.0, 0.0, 0.5, 0.75] };
assert_eq!(pin_slope(Y, Y, 0.0, &graph), 0.75);
assert_eq!(pin_slope(Y, Y, 0.01, &graph), 1.0);
}
fn slide_input() -> slide::Input {
slide::Input { position: O, point: O, direction: Z, across: X,
velocity: [2.0, 3.0, 4.0, 0.0], translation_2796: 0.5,
total_mass_2660: 10.0, update_frequency: 30.0,
preparing_jump: false, geometry_kind: 0 }
}
#[test]
fn slide_force_order_mass_frequency_and_darkslide_translation() {
let mut input = slide_input();
let f = slide::control(slide::Slide::Boardslide, input);
assert_eq!(f, vec![[12.5, 0.0, 0.0, 0.0], [-40.0, 0.0, 0.0, 0.0]]);
input.position[0] = 0.08;
let f = slide::control(slide::Slide::Darkslide, input);
near(f[0][0], 10.0); //(.5 remaining)*40*.5 translation.
input.position[0] = 0.2;
let f = slide::control(slide::Slide::Boardslide, input);
near(f[0][0], -10.0);
near(f[1][0], -600.0); //Actual input frequency30, not ZIP fixed60.
near(f[1][1], 0.0);
input.preparing_jump = true;
input.position[0] = 0.12;
assert!(slide::control(slide::Slide::Boardslide, input).is_empty());
}
#[test]
fn exit_lift_flag_is_independent_and_speed_equality_skips_force() {
let mut input = release::Input { position: X, point: O, across: X, normal: Y,
velocity: O, geometry_kind: 2, high_side: Z, force_across: false,
enable_lift: false, strength: 90.0, speed_limit: 0.75, lift: 125.0 };
assert_eq!(release::force(input), Some([0.0, 0.0, -90.0, 0.0]));
input.enable_lift = true;
assert_eq!(release::force(input), Some([0.0, 125.0, -90.0, 0.0]));
input.force_across = true;
input.velocity = [0.75, 0.0, 0.0, 0.0];
assert_eq!(release::force(input), None);
}
#[test]
fn noise_preserves_draw_order_modulo_speed_cap_and_position() {
let a = noise::angles(2.0, 1.0, [0, 50_000, 99_999]);
near(a[0], -0.06); near(a[1], 0.0); near(a[2], 0.0599988);
assert_eq!(a, noise::angles(20.0, 1.0, [100_000, 150_000, 199_999]));
let frame = [X, Y, Z, [8.0, 9.0, 10.0, 1.0]];
let n = noise::apply(frame, 2.0, 1.0, [12_345, 54_321, 87_654]);
assert_eq!(n[3], frame[3]);
for column in &n[..3] { near(dot3(*column, *column), 1.0); }
near(dot3(n[0], n[1]), 0.0);
assert_eq!(noise::apply(frame, 0.0, 1.0, [0, 1, 2]), frame);
}
@@ -32,11 +32,14 @@ pub struct ManualInput {
pub reference_z: [f32; 4],
/// Deck part6 transform's z-axis from 82585CB0, not an animation pose.
pub deck_z: [f32; 4],
/// Processed transform third column+224 and angular velocity+416.
/// Processed effective transform third column+224 and ground velocity+416.
pub velocity_frame_z: [f32; 4],
/// Legacy field name: this is Processed416, NOT angular velocity720.
/// Native82C04C88 projects it into the effective frame;82C04E60..EC4
/// emits -400 times this same translational velocity for correction.
pub angular_velocity_world: [f32; 4],
/// Native body caches. Producers/original names remain unresolved; these
/// layout identities stay at the input boundary instead of guessed levers.
/// Front/back truck drive-frame translations (7840+48 and7904+48).
///82C0B9C0 steering rotates the bases and preserves these translations.
pub correction_point_7888: [f32; 4],
pub correction_point_7952: [f32; 4],
}
@@ -171,8 +174,8 @@ fn apply_correction(
measured: f32,
) {
let frame = input.velocity_frame_z;
let omega = input.angular_velocity_world;
let local_z = frame[2].mul_add(omega[2], frame[1].mul_add(omega[1], frame[0] * omega[0]));
let velocity = input.angular_velocity_world;
let local_z = frame[2].mul_add(velocity[2], frame[1].mul_add(velocity[1], frame[0] * velocity[0]));
if !input.braking || !(0.0 > local_z * input.balance) {
return;
}
@@ -198,7 +201,7 @@ fn apply_correction(
} else {
input.correction_point_7952
};
effect.corrective_force_world = omega.map(|component| (component * -1.0) * 400.0);
effect.corrective_force_world = velocity.map(|component| (component * -1.0) * 400.0);
effect.correction_active = true;
}
@@ -34,8 +34,9 @@ pub struct ManualSettings {
#[derive(Clone, Copy, Debug)]
pub struct ManualMode {
/// physics_mode+32 Hash_84CB2F459F3FC811, compared to abs(local angular Z).
/// Original field name remains unresolved; this name describes its use.
/// physics_mode+32 Hash_84CB2F459F3FC811, compared to abs(local ground
/// velocity Z). Legacy Rust field name; the source reads Processed416,
/// not the angular velocity at720. Original stock name remains unresolved.
pub correction_angular_speed_threshold: f32,
/// physics_mode+36 Hash_F8CBC0F5FEF2240E, read as a byte.
pub corrective_force_enabled: bool,
+15 -5
View File
@@ -84,7 +84,9 @@ fn inverse_mass_frame(forward: RetailLocalMassFrame, inertia: Vector3) -> Retail
);
let minimum_offset_squared =
((inertia.y + inertia.z) + inertia.x) * f32::from_bits(0x3586_37BE);
let translated = center_squared >= minimum_offset_squared;
// S3 82AE78EC / S2 82AE9BE0: bge after fcmpu means LT is clear,
// so an unordered comparison also retains the inverse mass frame.
let translated = !(center_squared < minimum_offset_squared);
let rotated = forward
.basis
.columns
@@ -219,10 +221,18 @@ fn finalize_principal_mass(
refined_reciprocal(principal_moments.y),
refined_reciprocal(principal_moments.z),
);
let smallest_inverse = native_arithmetic::vector_min(
native_arithmetic::vector_min(inverse_tensor.x, inverse_tensor.y),
inverse_tensor.z,
);
// S3 82AE7A98/7AAC and S2 82AE9DA0/9DB4 retain the left operand
// only for ordered LT. Rust min would discard a right-hand NaN.
let smallest_xy = if inverse_tensor.x < inverse_tensor.y {
inverse_tensor.x
} else {
inverse_tensor.y
};
let smallest_inverse = if smallest_xy < inverse_tensor.z {
smallest_xy
} else {
inverse_tensor.z
};
RetailBodyMassProperties {
local_mass_frame,
dynamics: RetailInertiaDynamics {
+12 -12
View File
@@ -3,8 +3,8 @@
pub mod assembly;
pub mod board;
pub mod board_animation;
pub mod board_dynamic_normal;
pub mod board_ground;
pub mod board_dynamic_normal;
pub mod board_input_output;
pub mod board_motion_output;
pub mod board_pose;
@@ -12,11 +12,14 @@ pub mod board_runtime;
pub mod board_step;
pub mod board_toolkit;
pub mod board_world;
pub mod centre_of_mass_filter;
pub mod collision;
pub mod centre_of_mass_filter;
pub mod constraint_batch;
mod constraint_frames;
pub mod contact;
pub mod grind_contact;
pub mod grind_air;
pub mod grind_forces;
pub mod contact_feedback;
mod contact_layout;
pub mod contact_solver;
@@ -24,8 +27,8 @@ pub mod drive_frames;
pub mod drive_parameters;
pub mod drive_preparation;
pub mod drive_solver;
pub mod foot_physical_output;
pub mod force_queue;
pub mod foot_physical_output;
pub mod hook_drive;
pub mod joint_builder;
pub mod joint_records;
@@ -41,8 +44,6 @@ pub mod solver;
pub mod startup;
mod triangle_closest;
pub mod triangle_query;
pub mod grind_contact;
pub mod grind_forces;
mod triangle_sweep;
mod triangle_sweep_roots;
mod triangle_sweep_walk;
@@ -60,17 +61,16 @@ pub mod ground_hang_geometry;
#[cfg(test)]
#[path = "tests/primitive_query.rs"]
mod primitive_query_tests;
pub mod skateboard_controller;
pub mod skeleton_air_frames;
pub mod skeleton_animation_record;
pub mod skeleton_biped_air;
pub mod skeleton_air_frames;
pub mod skeleton_board_frames;
pub mod skeleton_output;
pub mod skeleton_board_offset;
pub mod skeleton_body;
pub mod skeleton_general;
pub mod skeleton_landing;
pub mod skeleton_motion;
pub mod skeleton_output;
pub mod skeleton_root;
pub mod grind_air;
pub mod skeleton_general;
pub mod skeleton_biped_air;
pub mod skeleton_landing_on_board;
pub mod skateboard_controller;
+5 -3
View File
@@ -133,8 +133,9 @@ pub struct RetailBodyRateStep {
/// - `mIfull = (Ixx, Ixy, Ixz)`;
/// - `mIsplt = (Izz, Iyy, Iyz)`.
///
/// This retained lane mapping requires independent TU3 instruction review.
/// Earlier generated-code validation has been withdrawn.
/// Independently traced through original S3 DynamicUpdate82AE6778..6804 and
/// S2 DynamicUpdate82AE467C..46FC. This verifies the six-component layout,
/// not bit-exact Xenon arithmetic; generated-code validation is withdrawn.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RetailPackedWorldInverseInertia {
pub full: Vector3,
@@ -278,7 +279,8 @@ pub fn integrate_body_rates(
}
}
/// Quaternion branch from TU3 `0x82AE66A8..0x82AE67B8`.
/// Quaternion branch from TU3 `0x82AE6668..0x82AE66C8`, paired with
/// Skate 2 `0x82AE455C..0x82AE45C8`.
///
/// Mapping the VMX128 word permutations back to RenderWare's public
/// `(x,y,z,w)` lane order gives:
@@ -60,6 +60,8 @@ pub fn dynamic_update_packed(
// rather than recalculating squared speed from the rounded scaled vector.
let factor = f32::from_bits(inertia[5]) * f32::from_bits(body[31]);
let energy = factor.mul_add(angular_squared, linear_squared);
// S3 82AE6974 / S2 82AE487C: bge tests LT clear, including unordered.
// Do not replace this with >=; the host predicate below is intentional.
body[43] = if !(energy < simulation.minimum_energy) {
0
} else {
@@ -85,7 +87,7 @@ pub fn dynamic_update_packed(
}
pub(super) fn orientation(q: V, rotation: V) -> V {
// 82AE6680..67B8: preserve the native cross-product and normalization order.
// S3 82AE6668..66C8 / S2 82AE455C..45C8: cross product then normalization.
let cross = perm(
nmsub(
perm(rotation, [1, 2, 0, 3]),
@@ -135,6 +137,8 @@ pub(super) fn inverse_inertia(basis: [V; 3], tensor: [u32; 3]) -> (V, V) {
fn cap(value: V, maximum: f32) -> (V, f32) {
let squared = dot(value, value, false);
let maximum_squared = maximum * maximum;
// S3 82AE6888/68FC and S2 82AE478C/4804 skip on GT clear (ble),
// including unordered. Thus ordered >, not !(<=), enters the cap path.
if squared > maximum_squared {
let ratio = maximum_squared / squared;
let root = ratio * refined_rsqrt(ratio);
@@ -33,7 +33,7 @@ impl SkateboardController {
let previous = self.fields.state_448;
self.fields.word_444 = 0;
if previous != 0 {
actions.let_go_of_skateboard(&mut self.fields);
actions.let_go_of_skateboard();
self.fields.state_448 = 0;
}
self.fields.system_on_452 = false;
@@ -1,32 +1,33 @@
//! Skeleton BipedAir frame production82BDDD70..82BDDFC0.
//! Biped Skeleton82BDDD70: COM-constrained animation root, not a body teleport.
//! Original S3 SHA431b8eba...0395a; ordinary PC arithmetic, not Xenon parity.
use super::{
skeleton_animation_record::{AnimationPartTransform as Frame, compose_affine},
skeleton_root::{SkeletonRootFrames, orthonormalize},
};
///82BDDDC8..DF54. The frame's translation is replaced by the trajectory COM
///minus the rotated LOCAL animation COM10960. Mapped board12624 is independent.
pub fn prepare(
roots: &mut SkeletonRootFrames,
frame: Frame,
body_position: [f32; 4],
mut frame: Frame,
trajectory_position: [f32; 4],
local_com: [f32; 4],
mapped: &Frame,
mapped_board: &Frame,
flags_2476: u32,
flags_2468: &mut u32,
) -> Frame {
let mut frame = frame;
roots.initialize_heading = true;
if flags_2476 & 4 != 0 {
for axis in [0, 2] {
frame[axis] = frame[axis].map(|v| -v);
frame[axis] = frame[axis].map(|lane| -lane);
}
}
frame[3] = std::array::from_fn(|i| {
body_position[i]
- frame[2][i].mul_add(
local_com[2],
frame[1][i].mul_add(local_com[1], frame[0][i] * local_com[0]),
)
frame[3] = core::array::from_fn(|lane| {
let x = frame[0][lane] * local_com[0];
let y = frame[1][lane].mul_add(local_com[1], x);
trajectory_position[lane] - frame[2][lane].mul_add(local_com[2], y)
});
roots.initialize_heading = true;
roots.reset_initial_alignment(orthonormalize(frame));
*flags_2468 |= 0x80000;
compose_affine(&roots.animation_to_world, mapped)
compose_affine(&roots.animation_to_world, mapped_board)
}
@@ -13,8 +13,6 @@ pub struct SkeletonBoardFrames {
pub physical_board: Transform,
pub skate_root: Transform,
pub animation_target: Transform,
/// Skeleton16016: authored deck frame retained during off-board transitions.
pub retained_board_16016: Transform,
pub com_frame: Transform,
pub lifted_com_frame: Transform,
pub centre_of_mass: [f32; 4],
@@ -31,7 +29,6 @@ impl Default for SkeletonBoardFrames {
physical_board: IDENTITY,
skate_root: IDENTITY,
animation_target: IDENTITY,
retained_board_16016: IDENTITY,
com_frame: IDENTITY,
lifted_com_frame: IDENTITY,
centre_of_mass: [0.; 4],
@@ -45,18 +45,26 @@ pub struct SkeletonTargetUpdate {
}
impl SkeletonTargets {
///82BE1618 updates only the two hook targets and returns the board-space
/// transform consumed by the skeleton input owner.
pub fn update_hook_positions(&mut self, input: &SkeletonTargetInput<'_>) -> Transform {
let hips = compose_affine(input.animation_to_world, input.animation_hips);
let board = compose_affine(input.animation_to_world, input.animation_board);
let animation_board_to_physics = compose_affine(input.inverse_board, &board);
self.set_transform(0, orthonormalize(hips));
self.set_transform(1, orthonormalize(*input.skate_root));
animation_board_to_physics
}
pub fn update_positions(
&mut self,
input: SkeletonTargetInput<'_>,
skeleton: &mut SkeletonBody,
) -> SkeletonTargetUpdate {
let hips = compose_affine(input.animation_to_world, input.animation_hips);
let board = compose_affine(input.animation_to_world, input.animation_board);
let animation_board_to_physics = compose_affine(input.inverse_board, &board);
let previous = self.transform(0)[3];
let change = std::array::from_fn(|i| previous[i] - hips[3][i]);
self.set_transform(0, orthonormalize(hips));
self.set_transform(1, orthonormalize(*input.skate_root));
let animation_board_to_physics = self.update_hook_positions(&input);
let hips = self.transform(0)[3];
let change = std::array::from_fn(|i| previous[i] - hips[i]);
let positions =
self.update_extra_targets(skeleton, input.com_frame, input.lifted_com_frame);
//82BE18A0 checks strictly greater; unordered distance alone does not
@@ -0,0 +1,72 @@
//! Skeleton82BDDC68,82BE0858,82BE0CC0 and82BDCC28, original S3 TU3.
//! These calculate animation targets; the existing skeleton drives own physics.
use super::{
skeleton_animation_record::{AnimationPartTransform as Frame, compose_affine, transform_point},
skeleton_root::{SkeletonRootFrames, orthonormalize, inverse_rigid},
board_motion_output::inverse_length_squared,
};
use crate::{animation::foot_ik::interpolate_affine, point_graph::PointGraph,
player::wipeout_state::math::{cross,dot,scale,sub}};
pub type Vector=[f32;4];
const UP:Vector=[0.,1.,0.,0.];
const EPSILON:f32=f32::from_bits(0x37800000);
#[derive(Clone,Copy)]
pub struct Settings { pub root_y_offset:f32, pub capsule_radius:f32, pub capsule_length:f32 }
///82BDDC68 changes only skate-root and invokes COM-lift on the OLD animation root.
pub fn skate_root(old:Frame, physical_com:Vector, flags2480:u32, ground_y:f32,
settings:Settings)->Frame {
let mut position=physical_com;
position[1]=old[3][1]+settings.root_y_offset;
if flags2480&1!=0 {
let requested=(ground_y+settings.capsule_length)+settings.capsule_radius;
position[1]=requested.clamp(position[1],position[1]+0.3);
}
[old[2],old[0],old[1],position]
}
///82BE0858. Packet trajectory reset is performed by the owning host before this.
pub fn update_root(roots:&mut SkeletonRootFrames, com:Vector, local_com:Vector,
spin:f32, revert_frames:Option<u32>, reversed:bool) {
roots.initialize_heading=true;
let angle=revert_frames.map_or(spin,|frames| {
let magnitude=std::f32::consts::PI/frames as f32;
if reversed {magnitude} else {-magnitude}
});
let (sin,cos)=crate::trigonometry::sin_cos(angle);
let yaw=[[cos,0.,-sin,0.],[0.,1.,0.,0.],[sin,0.,cos,0.],[0.;4]];
let mut frame=compose_affine(&roots.animation_to_world,&yaw);
if revert_frames.is_some() {frame=orthonormalize(frame);}
//82BE0CC0: world-up correction with two independent degeneracy tests.
let deviation=cross(frame[1],UP);
if dot(deviation,deviation)>EPSILON {
let right=cross(UP,frame[2]);
let sq=dot(right,right);
if sq>EPSILON {
let right=scale(right,inverse_length_squared(sq,2));
let target=[right,UP,cross(right,UP),frame[3]];
frame=interpolate_affine(&frame,&target,0.1);
}
}
let mut rotation=frame;
rotation[3]=[0.;4];
frame[3]=sub(com,transform_point(&rotation,local_com));
roots.reset_initial_alignment(orthonormalize(frame));
}
///82BDCC28 returns no write outside its descending/unsuppressed branch.
pub fn pose_adjustment(world_to_animation:&Frame, actual_board:&Frame, mapped_board:&Frame,
ik_offset:Vector, com_velocity_y:f32, flags2480:u32,
time:f32, blend:&PointGraph<8>)->Option<Frame> {
if !(com_velocity_y<0.) || flags2480&2!=0 {return None;}
let mut target=compose_affine(world_to_animation,actual_board);
let mut rotation=*world_to_animation;
rotation[3]=[0.;4];
let mut correction=transform_point(&rotation,ik_offset);
correction[1]=0.;
if target[3][1]>mapped_board[3][1] {
correction[1]=target[3][1]-mapped_board[3][1];
}
target[3]=std::array::from_fn(|lane|mapped_board[3][lane]+correction[lane]);
let blended=interpolate_affine(mapped_board,&target,blend.evaluate(time));
let mut offset=compose_affine(&blended,&inverse_rigid(mapped_board));
offset[3][1]=-offset[3][1];
Some(offset)
}
@@ -1,5 +1,30 @@
use super::*;
/// Regression for the paired postphysics branch contract, not fidelity proof.
/// S2 82B372E0 names mAngularDrag; S3 82C08634 stores inertia+36.
#[test]
fn wheel_angular_drag_tracks_physical_contact_and_wipeout() {
let mut ground = BoardGroundState::default();
let mut lines = WheelLineState::default();
// A successful line query is not physical wheel contact for drag selection.
lines.publish([Some(WheelLineHit { fraction: 0.1, normal: UP, surface_tag: 0 }); 4]);
let frequency = f32::from_bits(0x426F_FFFF);
let free = f32::from_bits(0x3BC4_9BA6) * frequency;
let wiping_out = f32::from_bits(0x3D23_D70A) * frequency;
let contacts = [
report(BodyId::RightFrontWheel, UP),
report(BodyId::LeftBackWheel, UP),
];
ground.update(&contacts, &lines, UP, 80.0, false);
assert_eq!(ground.wheel_angular_drag, [0.0, free, free, 0.0]);
ground.update(&contacts, &lines, UP, 80.0, true);
assert_eq!(ground.wheel_angular_drag, [wiping_out, free, free, wiping_out]);
ground.update(&[], &lines, UP, 80.0, true);
assert_eq!(ground.wheel_angular_drag, [free; 4]);
ground.update(&contacts, &lines, UP, 80.0, false);
assert_eq!(ground.wheel_angular_drag, [0.0, free, free, 0.0]);
}
fn report(part: BodyId, normal: Vector3) -> BoardContactReport {
BoardContactReport {
part, other: CollisionBody::StaticWorld, is_body_a: true,
@@ -90,151 +90,6 @@ fn close(a: f32, b: f32) {
assert!((a - b).abs() < 2e-6, "{a} != {b}");
}
fn clustered(triangles: Vec<WorldTriangle>) -> BoardWorld {
use crate::physics::board_world::query_metadata::{
Bounds, QueryMesh, QueryMetadata, QueryPool,
};
let metadata = QueryMetadata {
packed_surfaces: vec![0; triangles.len()],
meshes: triangles
.iter()
.enumerate()
.map(|(i, t)| QueryMesh {
triangle_range: i..i + 1,
local_to_world: RetailAffineTransform::IDENTITY,
world_to_local: RetailAffineTransform::IDENTITY,
local_bounds: Bounds::from_points(t.triangle.vertices).unwrap(),
matching_group: -1,
pool: QueryPool::Ground,
})
.collect(),
static_edges: vec![],
island_flags: 0,
};
BoardWorld::with_query_metadata(triangles, metadata).unwrap()
}
#[test]
fn cluster_culling_preserves_swept_hits_and_equal_hit_order() {
let mut rounded = triangle();
rounded.triangle.fatness = 0.5;
let mut later = rounded;
later.tag = 999;
let indexed = clustered(vec![rounded, later]);
let full = BoardWorld::new(vec![rounded, later]);
for x in [-100., -10., -1., 0., 9., 100.] {
for radius in [0., 0.2, 1.] {
let start = Vector3::new(x, 2., 0.);
let end = Vector3::new(x, -2., 0.);
assert_eq!(
format!("{:?}", indexed.query_swept_line(start, end, radius)),
format!("{:?}", full.query_swept_line(start, end, radius))
);
}
}
assert_eq!(
indexed
.query_thin_line(Vector3::new(0., 2., 0.), Vector3::new(0., -2., 0.))
.unwrap()
.unwrap()
.tag,
42
);
assert_eq!(
indexed
.line_candidates(Vector3::new(100., 2., 0.), Vector3::new(100., -2., 0.), 0.)
.count(),
0
);
}
#[test]
fn cluster_culling_preserves_predictive_contacts_for_every_shape() {
let mut floor = triangle();
floor.triangle.fatness = 0.1;
let mut indexed = clustered(vec![floor]);
let mut full = BoardWorld::new(vec![floor]);
let mut settings = config();
settings.query.volume_padding = 0.05;
settings.query.maximum_separating_distance = 0.8;
for y in [0.1, 0.4, 0.9, 2.] {
let center = Vector3::new(0., y, 0.);
let basis = Basis3 {
columns: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
};
let mut moving_triangle = triangle().triangle;
for p in &mut moving_triangle.vertices {
p.y += y;
}
for primitive in [
ContactPrimitive::Sphere(Sphere {
center,
radius: 0.2,
}),
ContactPrimitive::Capsule {
center,
axis: Vector3::new(1., 0., 0.),
half_length: 0.4,
radius: 0.2,
},
ContactPrimitive::RoundedBox {
center,
basis,
half_extents: Vector3::new(0.4, 0.2, 0.3),
radius: 0.05,
},
ContactPrimitive::Triangle(moving_triangle),
] {
let volumes = [BoardWorldVolume {
body: CollisionBody::Board(BodyId::ORDER[0]),
primitive,
linear_velocity: Vector3::new(0., -60., 0.),
material: settings.material,
}];
assert_eq!(
format!(
"{:?}",
indexed.query_primitives(&volumes, settings.query, settings.retention)
),
format!(
"{:?}",
full.query_primitives(&volumes, settings.query, settings.retention)
)
);
}
}
}
#[test]
fn triangle_bounds_match_unfiltered_contacts_near_edges_and_rotated_boxes() {
let mut floor = triangle();
floor.triangle.fatness = 0.15;
let mut indexed = clustered(vec![floor]);
let mut full = BoardWorld::new(vec![floor]);
let mut settings = config();
settings.query.volume_padding = 0.1;
settings.query.maximum_separating_distance = 0.8;
for x in [-10.5, -10., -5., 0., 5., 10., 10.5] {
for z in [-10.5, -10., 0., 10., 10.5] {
for y in [-0.1, 0.2, 1., 2.] {
let center = Vector3::new(x, y, z);
for primitive in [
ContactPrimitive::Sphere(Sphere { center, radius: 0.2 }),
ContactPrimitive::Capsule { center, axis: Vector3::new(0.6, 0.8, 0.), half_length: 0.7, radius: 0.2 },
ContactPrimitive::RoundedBox { center,
basis: Basis3 { columns: [[0.6, 0., 0.8], [0., 1., 0.], [-0.8, 0., 0.6]] },
half_extents: Vector3::new(0.6, 0.2, 0.1), radius: 0.05 },
] {
let volumes = [BoardWorldVolume { body: CollisionBody::Board(BodyId::ORDER[0]), primitive,
linear_velocity: Vector3::new(20., -60., -30.), material: settings.material }];
assert_eq!(format!("{:?}", indexed.query_primitives(&volumes, settings.query, settings.retention)),
format!("{:?}", full.query_primitives(&volumes, settings.query, settings.retention)));
}
}
}
}
}
#[test]
fn finite_world_geometry_produces_four_ordered_wheel_contacts_with_combined_materials() {
let mut world = BoardWorld::new(vec![triangle()]);
@@ -386,11 +241,7 @@ fn attached_volume_contact_reaches_its_actual_solver_body() {
let mut world = BoardWorld::new(vec![triangle(), triangle()]);
let config = config();
let contacts = world.query_primitives(&[volume], config.query, config.retention);
assert_eq!(
contacts.len(),
1,
"retention must preserve attached body identity"
);
assert_eq!(contacts.len(), 1, "retention must preserve attached body identity");
assert_eq!(contacts[0].body_a, CollisionBody::Attached(0));
let settings = BoardStepSettings {
simulation: RetailSimulationStep::fixed_60_hz(30, 0.001, Vector3::ZERO),
@@ -399,20 +250,12 @@ fn attached_volume_contact_reaches_its_actual_solver_body() {
truck_dynamics: retail_truck_drive_dynamics(RetailTruckDriveSettings::STOCK),
force_point_y_offset: 0.0,
};
board.advance_attached(
contacts,
[0.0; 2],
settings,
AttachedStep {
bodies: vec![&mut attached],
contacts: &mut [],
joints: &mut [],
drives: &mut [],
},
);
board.advance_attached(contacts, [0.0; 2], settings, AttachedStep {
bodies: vec![&mut attached],
contacts: &mut [],
joints: &mut [],
drives: &mut [],
});
assert!(attached.rates.linear_velocity.y > before + 0.5);
assert!(
board.contact_reports().is_empty(),
"skater contacts are not board observations"
);
assert!(board.contact_reports().is_empty(), "skater contacts are not board observations");
}
@@ -21,6 +21,8 @@ pub struct AirOutputFields {
pub trajectory_plane_samples_336: [f32; 25],
pub known_air_valid_437: u8,
pub launched_442: u8,
/// Common ProcessOutput82DB703C: TrajectorySelector9653.
pub flag_443: u8,
pub flag_444: u8,
/// Air324 high bits used by IsHandPlanting82BA55A8.
pub handplant_flags_324: u32,
@@ -0,0 +1,45 @@
//! Value snapshot copied by82D87E80 into Processed1104..1519 after manager post.
//! Native pointers are stable host identities; all geometry/control values are
//! retained independently. Zero is the native416-byte investigation reset.
use super::RawVector;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GrindInvestigationFields {
pub tangent_1104: RawVector,
pub point_1120: RawVector,
pub direction_1136: RawVector,
pub normal_1152: RawVector,
pub target_up_1168: RawVector,
pub entry_velocity_1184: RawVector,
pub front_contact_1200: RawVector,
pub rear_contact_1216: RawVector,
pub vector_1232: RawVector,
pub family_1248: u32,
pub entry_kind_1252: u32,
pub primitive_start_1264: RawVector,
pub primitive_end_1280: RawVector,
pub owner_1296: Option<u64>,
pub primitive_flags_1300: u32,
pub second_start_1312: RawVector,
pub second_end_1328: RawVector,
pub second_owner_1344: Option<u64>,
pub second_flags_1348: u32,
pub center_1360: RawVector,
pub far_points_1376_1392: [RawVector; 2],
pub upmost_normal_1408: RawVector,
pub surface_direction_1424: RawVector,
pub high_side_1440: RawVector,
pub normal_limits_1456_1460: [f32; 2],
pub geometry_kind_1464: u32,
pub audio_surface_1468: u32,
pub physics_surface_1472: u32,
pub geometry_flags_1476: u32,
pub valid_1488: bool,
pub impact_speed_1492: f32,
pub friction_1496: f32,
pub exit_lean_1500: f32,
pub yaw_1504: f32,
pub pitch_1508: f32,
pub gravity_relief_1512: f32,
pub flags_1516: u32,
}
@@ -0,0 +1,106 @@
//! Typed Grinds PhysOut block. Offsets identify native producers, not Rust ABI.
//! Reset82DE3518; common Fill82D40EA0; tipslide Fill82D42788;
//! chromosome publication82DEE508; contact conditioner82DF0640.
use super::types::RawVector;
use crate::animation::output::attributes::AttributeName;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GrindOutputFields {
pub direction_0: RawVector,
pub point_16: RawVector,
pub normal_32: RawVector,
pub across_48: RawVector,
pub primitive_start_64: RawVector,
pub primitive_end_80: RawVector,
pub camera_target_96: RawVector,
pub high_side_112: RawVector,
pub impact_speed_128: f32,
pub crouch_132: f32,
/// Physical family0..5 (not admission kind), then substate.
pub words_136_140: [u32; 2],
pub animation_id_144: u32,
pub scoring_id_148: u32,
pub scorable_id_152: u32,
/// Five-word stock FastString representation, not a hash of the display name.
/// None means the external reset-name template has not been supplied.
pub animation_name_156: Option<AttributeName>,
pub scoring_name_176: Option<AttributeName>,
pub surface_name_196: Option<AttributeName>,
pub audio_surface_216: u32,
pub spline_guids_224_232: [u64; 2],
/// Common physical classification104, consumed by GrindTrickOutTypeAllowed.
pub trick_out_240: u32,
/// Approach, location, twist, tilt, orientation, family. No game enum here.
pub volatile_chromosome_244: [u32; 6],
/// First component at268 is the graph's filtered approach observation.
pub animation_chromosome_268: [u32; 6],
pub scoring_chromosome_292: [u32; 6],
/// Selector-owned; the camera conditioner must not infer this flag.
pub grinding_316: u8,
pub leaving_317: u8,
pub flag_318: u8,
pub flag_319: u8,
pub is_ledge_320: u8,
pub curb_321: u8,
pub flag_322: u8,
pub flag_323: u8,
pub dropping_in_324: u8,
pub tipslide_325: u8,
pub tipslide_326: u8,
}
impl Default for GrindOutputFields {
fn default() -> Self {
const X: RawVector = [0x3f80_0000, 0, 0, 0];
const Y: RawVector = [0, 0x3f80_0000, 0, 0];
const EMPTY: [u32; 6] = [0, 0, 0, 0, 0, u32::MAX];
Self {
direction_0: X,
point_16: [0; 4],
normal_32: Y,
across_48: Y,
primitive_start_64: [0; 4],
primitive_end_80: [0; 4],
camera_target_96: [0; 4],
high_side_112: [0; 4],
impact_speed_128: 0.,
crouch_132: 0.,
words_136_140: [u32::MAX, 0],
animation_id_144: 0,
scoring_id_148: 0,
scorable_id_152: u32::MAX,
//82F8AF08 initializes template830C0D60 from empty string82060799.
animation_name_156: Some(crate::animation::skeleton_input::name::encode(b"")),
scoring_name_176: Some(crate::animation::skeleton_input::name::encode(b"")),
surface_name_196: None,
audio_surface_216: 0,
spline_guids_224_232: [u64::MAX; 2],
trick_out_240: 0,
volatile_chromosome_244: EMPTY,
animation_chromosome_268: EMPTY,
scoring_chromosome_292: EMPTY,
grinding_316: 0,
leaving_317: 0,
flag_318: 0,
flag_319: 0,
is_ledge_320: 0,
curb_321: 0,
flag_322: 0,
flag_323: 0,
dropping_in_324: 0,
tipslide_325: 0,
tipslide_326: 0,
}
}
}
impl GrindOutputFields {
/// The native reset copies two external FastString templates; it does not
/// construct a grind name from family0. Supply decoded original templates.
/// Keeping these optional distinguishes an unavailable template from zeros.
pub fn reset_names(&mut self, grind: Option<AttributeName>, surface: Option<AttributeName>) {
self.animation_name_156 = grind;
self.scoring_name_176 = grind;
self.surface_name_196 = surface;
}
}
@@ -11,13 +11,16 @@ mod types;
mod air_output;
mod pose_output;
mod requests;
mod grind_input;
mod grind_output;
pub use runtime::{InputContinuation, InputPhaseError, InputPhaseServices, process_input, start_input, finish_input};
pub use types::*;
pub use air_output::AirOutputFields;
pub use pose_output::{AnimationOutputFields, ScoringOutputFields, SkeletonOutputFields};
pub use requests::*;
pub use grind_input::GrindInvestigationFields;
pub use grind_output::GrindOutputFields;
#[cfg(test)]
mod tests;
@@ -6,6 +6,8 @@ use super::types::RawMatrix;
pub struct SkeletonOutputFields {
pub hips_right_angle_496: f32,
pub hips_up_angle_500: f32,
/// Skeleton Fill82BE2268: signed physical twist, before animation mirroring.
pub twist_504: f32,
pub scalar_544: f32,
pub no_support_time_548: f32,
pub time_until_teleport_576: f32,
@@ -21,20 +23,48 @@ pub struct SkeletonOutputFields {
pub anim_to_world_11920: RawMatrix,
}
impl SkeletonOutputFields {
/// Skeleton Fill82BE2148..2268. Read the completed physical record, the
/// current input toolkit's Processed352 and Reckoning1152. No pose fitting
/// or animation mirror: GrindControlFade applies that mirror once on Begin.
pub fn publish_twist(
&mut self,
record: &crate::physics::skeleton_body::SkeletonPhysicalRecord,
processed_forward_352: [f32; 4],
reckoning_up_1152: [f32; 4],
) {
use crate::physics::{board_motion_output::inverse_length_squared, native_arithmetic::dot3};
// SkeletonState at6464, physical pose at+1552: translations of
// parts10/6 are Skeleton8704/8448, respectively.
let across: [f32; 4] = std::array::from_fn(|i| record.pose[10][3][i] - record.pose[6][3][i]);
let squared = dot3(across, across);
let inverse = inverse_length_squared(squared, 2);
let length = if squared == 0.0 { 0.0 } else { squared * inverse };
// Original830BD350 is a zero vector. Preserve the ordered comparison,
// including the zero/NaN path selecting the zero direction.
let direction = if length > 0.0 { across.map(|v| v * inverse) } else { [0.0; 4] };
let angle = crate::player::wipeout_state::orientation::projected_angle(
processed_forward_352, direction, reckoning_up_1152,
);
// Original82139A60/50; wrap via fractional turns, not angle modulo pi.
let turns = angle * f32::from_bits(0x3E22_F983);
let fraction = turns - turns.floor();
self.twist_504 = (fraction - if fraction > 0.5 { 1.0 } else { 0.0 })
* f32::from_bits(0x40C9_0FDB);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnimationOutputFields {
pub collision_time_144: f32,
pub profile_148: u32,
/// PhysOutAnimation+166, tested by OkToDoTrickOnStairs82BA6930.
pub tricks_blocked_on_stairs_166: u8,
/// Ground manual opposition, read by PhysicsWantsManualExit82BA7930.
/// Ground Fill82D3A4F0 -> PhysicsWantsManualExit82BA7988.
pub manual_opposition_168: u8,
}
impl Default for AnimationOutputFields {
fn default() -> Self {
// Animation82DE3F38 stores FLT_MAX at144 and zero at148.
// Reset82DE3F38 clears byte166. Ordinary Ground Fill82D3A388
// and common ProcessOutput82DB6EC0 do not overwrite this byte.
Self { collision_time_144: f32::MAX, profile_148: 0, tricks_blocked_on_stairs_166: 0, manual_opposition_168: 0 }
}
}
@@ -39,6 +39,8 @@ pub struct CurrentStateFields {
pub category_12: u32,
pub state_16: u32,
pub surface_height_32: f32,
///BipedGround Fill82D32D38, independent of State40.
pub counter_36: u32,
pub skitch_value_40: u32,
pub flag_61: u8,
pub flag_66: u8,
@@ -73,7 +75,16 @@ pub struct SystemReckoningFields {
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GroundOutputFields {
///State503 Fill82D4E090..E0C8.
pub landing_half_turns_312: i32,
pub hippy_jumping_322: u8,
pub hippy_takeoff_323: u8,
pub vector_64: RawVector,
/// Board Fill82C03318: wheel-contact normal (not Motion80 velocity).
pub vector_80: RawVector,
/// Host storage for Motion273, NOT native Ground273. Common82DB7598
/// publishes Processed2468 bit20; keep this distinct from the normal.
pub flag_273: u8,
pub vector_96: RawVector,
pub vector_128: RawVector,
pub scalar_276: f32,
@@ -87,6 +98,8 @@ pub struct GroundOutputFields {
pub struct CollisionOutputFields {
pub wheel_count_0: u32,
pub scalar_28: f32,
///Offboard landing manager82D79498; reset82DE3290 clears this position.
pub vector_48: RawVector,
pub predicted_position_64: RawVector,
pub flag_215: u8,
pub flag_216: u8,
@@ -98,6 +111,8 @@ pub struct CollisionOutputFields {
pub flag_3479: u8,
pub flag_3480: u8,
pub flag_3481: u8,
///Offboard landing manager82D7948C; reset82DE32E4 clears availability.
pub flag_3482: u8,
pub flag_3483: u8,
}
@@ -108,34 +123,11 @@ pub struct PhysicsOutputFields {
pub vector_16: RawVector,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GrindOutputFields {
pub words_136_140: [u32; 2],
pub flag_318: u8,
pub flag_322: u8,
/// PhysOut.Grinds324: reset82DE3518 clears it; tipslide Fill82D42788
/// publishes its retained drop-in byte97. Read by IsDroppingIn82BA41F0.
pub dropping_in_324: u8,
}
use super::GrindOutputFields;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct OffBoardOutputFields {
pub landed_320: u8,
pub flag_331: u8,
pub air_duration_92: f32,
pub landing_direction_96: [f32; 4],
pub air_surface_144: u32,
pub impact_y_148: f32,
pub air_time_152: f32,
pub apex_time_156: f32,
pub launch_up_160: [f32; 4],
pub launch_position_176: [f32; 4],
pub landing_normal_192: [f32; 4],
pub landing_position_208: [f32; 4],
pub landing_forward_224: [f32; 4],
pub apex_position_240: [f32; 4],
pub phase_80: f32,
pub locomotion_84: u32,
///BipedGround Fill82D32D38 and shared Feet Fill82D785F8.
pub kind_88: u32,
pub scalar_112: f32,
pub distance_116: f32,
@@ -143,15 +135,25 @@ pub struct OffBoardOutputFields {
pub flag_329: u8,
pub flag_330: u8,
pub flag_334: u8,
///Common ProcessOutput82DB724C..7258 from the shared Biped720/716.
pub cadence_phase_80: f32,
pub locomotion_state_84: u32,
/// SkateboardController FillPhysOut82D76D20 orientation outputs +36/+40.
pub angle_36: f32,
pub angle_40: f32,
/// Reset82DE40C0; Biped trajectory publication writes the duration here.
pub trajectory_time_120: f32,
/// Reset82DE4170; BipedAir Fill82D30324 publishes trajectory availability.
pub trajectory_valid_331: u8,
pub scalar_32: f32,
pub board_angle_36: f32,
pub board_angle_40: f32,
pub flag_304: u8,
///Common ProcessOutput82DB76F4: Processed2480 bit19, not grab-object304.
pub flag_308: u8,
pub flag_311: u8,
///SkateboardController82D76D88: state448 is FREE/HIDING/RETURNING.
pub free_board_312: u8,
///82D76DA0: physical RETURNING state, distinct from animation activity323.
pub returning_board_313: u8,
pub hidden_board_321: u8,
pub board_request_324: u8,
pub flag_314: u8,
pub flag_315: u8,
pub flag_316: u8,
@@ -161,10 +163,28 @@ pub struct OffBoardOutputFields {
pub hippy_hurdling_317: u8,
pub flag_318: u8,
pub dropping_board_322: u8,
///82D76ED4: state448 is HIDING.
pub hiding_board_321: u8,
pub flag_319: u8,
pub retrieving_board_323: u8,
///82D76F68: retrieving animation OR retrieve pulse while RETURNING.
pub flag_324: u8,
pub flag_328: u8,
pub vector_64: RawVector,
///BipedAir Fill82D30300: distinct from the on-board Air output block.
pub scalar_92: f32,
pub vector_96: RawVector,
pub word_144: u32,
pub scalar_148: f32,
pub scalar_152: f32,
pub scalar_156: f32,
pub vector_160: RawVector,
pub vector_176: RawVector,
pub vector_192: RawVector,
pub vector_208: RawVector,
pub vector_224: RawVector,
pub vector_240: RawVector,
pub flag_320: u8,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -313,8 +333,6 @@ impl Default for PlayerInputState {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ProcessedPhysicsInput {
pub grind_position_1120: RawVector,
pub grind_direction_1136: RawVector,
pub effective_anim_transform_192: RawMatrix,
pub vectors_400_416: [RawVector; 2],
pub vectors_464_480_496_512_528: [RawVector; 5],
@@ -328,11 +346,16 @@ pub struct ProcessedPhysicsInput {
pub vectors_720_784_800_816_832_864: [RawVector; 6],
pub vectors_880_896_912_928_944: [RawVector; 5],
pub line_tests_960_1008_1056: [LineTestFields; 3],
pub grind: super::GrindInvestigationFields,
pub vector_1520: RawVector,
pub matrix_1536: RawMatrix,
pub byte_1600: u8,
pub external_physics_1616: ExternalPhysicsInput,
pub probe_1792: ProbeFields,
/// Actual grab-spline publication payloads; flags2480 bits22/21 carry readiness.
pub grab_records_1888_2176: [[u32; 72]; 2],
/// Interactable identity published by82D740F8; bit20 carries validity.
pub object_2464: u32,
pub flags_2468: u32,
pub flags_2472: u32,
pub flags_2476: u32,
@@ -362,6 +385,8 @@ pub struct ProcessedPhysicsInput {
pub right_surface_2600: u32,
pub timestep_2604: f32,
pub scalar_2612: f32,
///PrepareBoardToolkit82C01744: signed speed times its ordered travel sign.
pub scalar_2616: f32,
pub transition_2636: f32,
///Skeleton82BDD008: BodySpin plus the actual grind-air adjustment result.
///Read only with flags2484bit15; Reset82BFA394 clears it each input tick.
@@ -416,8 +441,6 @@ impl ProcessedPhysicsSnapshot {
impl Default for ProcessedPhysicsInput {
fn default() -> Self {
Self {
grind_position_1120: [0;4],
grind_direction_1136: [0;4],
effective_anim_transform_192: [[0; 4]; 4],
vectors_400_416: [[0; 4]; 2],
vectors_464_480_496_512_528: [[0; 4]; 5],
@@ -430,11 +453,14 @@ impl Default for ProcessedPhysicsInput {
vectors_720_784_800_816_832_864: [[0; 4]; 6],
vectors_880_896_912_928_944: [[0; 4]; 5],
line_tests_960_1008_1056: [LineTestFields::default(); 3],
grind: super::GrindInvestigationFields::default(),
vector_1520: [0; 4],
matrix_1536: [[0; 4]; 4],
byte_1600: 0,
external_physics_1616: empty_external_physics(),
probe_1792: ProbeFields::default(),
grab_records_1888_2176: [[0; 72]; 2],
object_2464: 0,
flags_2468: 0,
flags_2472: 0,
flags_2476: 0,
@@ -464,6 +490,7 @@ impl Default for ProcessedPhysicsInput {
right_surface_2600: 0,
timestep_2604: 0.0,
scalar_2612: 0.0,
scalar_2616: 0.0,
transition_2636: 0.0,
grind_adjusted_body_spin_2644: 0.0,
gravity_2648: 0.0,
+5 -5
View File
@@ -79,8 +79,8 @@ pub struct StateChangeData {
/// implementations are `HoldSkateboard` `0x82D75370` and
/// `LetGoOfSkateboard` `0x82D75440`; this lifecycle never replaces them.
pub trait SkateboardControllerActions {
fn hold_skateboard(&mut self, _fields: &mut SkateboardControllerFields);
fn let_go_of_skateboard(&mut self, _fields: &mut SkateboardControllerFields);
fn hold_skateboard(&mut self);
fn let_go_of_skateboard(&mut self);
}
/// Context supplied to an owned state's Enter or Exit implementation.
@@ -196,11 +196,11 @@ fn prepare_skateboard_controller<C: SkateboardControllerActions>(
controller.word_444 = 0;
if processed_flags_2480 & (0x80 | 0x100) != 0 {
if controller.state_448 != 2 {
actions.let_go_of_skateboard(controller);
actions.let_go_of_skateboard();
controller.state_448 = 2;
}
} else if controller.state_448 != 1 {
actions.hold_skateboard(controller);
actions.hold_skateboard();
controller.state_448 = 1;
}
controller.system_on_452 = true;
@@ -218,7 +218,7 @@ fn stop_skateboard_controller<C: SkateboardControllerActions>(
let previous_state = controller.state_448;
controller.word_444 = 0;
if previous_state != 0 {
actions.let_go_of_skateboard(controller);
actions.let_go_of_skateboard();
controller.state_448 = 0;
}
controller.system_on_452 = false;
@@ -1,276 +1,151 @@
//! TU3 Biped launch preparation82D7BA78. This packet is consumed by the
//! off-board trajectory owner82D6CA58; it never substitutes a landing query.
//! Original Biped launch82D7BA78. This is separate from onboard LaunchInfo.
//! Produces the full packet before Sync replaces position32 with its body point.
use super::controller;
use crate::point_graph::PointGraph;
pub type V = [f32; 4];
const UP: V = [0., 1., 0., 0.];
const ZERO: V = [0.; 4];
mod jump;
pub(super) mod math;
mod packet;
use math::*;
pub use packet::Packet;
#[cfg(test)]
mod tests;
pub type Vector = [f32; 4];
/// Actual physics_biped scalar fields, validated by the stock loader.
#[derive(Clone, Copy, Debug)]
pub struct Launch {
pub velocity: V,
pub secondary_velocity: V,
pub position: V,
pub up: V,
pub forward: V,
pub target: V,
pub angles: [f32; 3],
pub primary_count: u32,
pub secondary_count: u32,
pub has_target: bool,
pub flag_117: bool,
pub struct Settings {
/// Biped796 <- layout896 (0x380), JumpSpeedScalar.
pub jump_speed_scalar: f32,
/// Biped792 <- layout900 (0x384), JumpHeight.
pub jump_height: f32,
}
impl Default for Launch {
fn default() -> Self {
Self {
velocity: ZERO,
secondary_velocity: ZERO,
position: ZERO,
up: UP,
forward: ZERO,
target: ZERO,
angles: [0.; 3],
primary_count: 1,
secondary_count: 0,
has_target: false,
flag_117: false,
}
}
/// A per-call snapshot from the canonical processed input; no persistent copy.
///Source1120 is a point and1136 is the axis used by mode1's rejection.
///Absence means the host has no source-backed departure geometry.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DepartureGeometry {
pub point_1120: Vector,
pub axis_1136: Vector,
}
pub struct Input {
pub previous_state: u32,
pub previous_category: u32,
pub current_state: u32,
pub current_category: u32,
#[derive(Clone, Copy, Debug)]
pub struct Processed {
pub board_position_112: Vector,
pub forward_224: Vector,
pub up_544: Vector,
pub position_592: Vector,
pub velocity_608: Vector,
pub velocity_912: Vector,
pub departure_geometry: Option<DepartureGeometry>,
pub flags_2472: u32,
pub flags_2476: u32,
pub flags_2480: u32,
pub frame_forward_224: V,
pub up_544: V,
pub velocity_608: V,
pub position_592: V,
pub target_112: V,
pub velocity_912: V,
pub grind_position_1120: V,
pub grind_axis_1136: V,
pub stick: [f32; 2],
pub previous_state_2504: u32,
pub current_state_2508: u32,
pub current_category_2512: u32,
pub previous_category_2516: u32,
pub raw_x_2692: f32,
pub raw_z_2688: f32,
}
pub struct JumpInput<'a> {
pub reference_up_144: V,
pub contact_active_708: bool,
pub height_792: f32,
pub velocity_scalar_796: f32,
pub speed_704: f32,
pub steering_760: f32,
pub angular_velocity_688: f32,
pub turn_curve_1328: &'a PointGraph<8>,
}
pub fn prepare(p: &Input, j: &JumpInput<'_>, current: bool) -> Launch {
///82D7BA90..BB38, including the current/previous selector used by its callers.
pub fn mode(p: &Processed, current: bool) -> u8 {
let (state, category) = if current {
(p.current_state, p.current_category)
(p.current_state_2508, p.current_category_2512)
} else {
(p.previous_state, p.previous_category)
};
let mut forward = p.frame_forward_224;
forward[1] = 0.;
let forward = unit(forward, ZERO);
let mut out = Launch {
velocity: p.velocity_608,
secondary_velocity: p.velocity_608,
position: p.position_592,
up: p.up_544,
forward,
angles: [
f32::from_bits(0x3db2b8c2),
f32::from_bits(0x3f5f66f3),
f32::from_bits(0x3f32b8c2),
],
..Launch::default()
(p.previous_state_2504, p.previous_category_2516)
};
match category {
100 => {
out.target = p.target_112;
out.has_target = true;
let y = out.velocity[1];
let lower = select(y - 3., y, 3.);
out.velocity[1] = select(y + 3. - lower, lower, y + 3.);
out.position = madd(out.velocity, f32::from_bits(0x3c888889), out.position);
100 => 0,
400 => 1,
200 => 2,
500 if p.flags_2480 & 0x80 != 0 || state == 503 => 3,
500 if p.flags_2476 & 0x80000 != 0 => 4,
500 => 5,
_ => 6,
}
}
/// Borrow the existing controller state and its movement_velocity.turn_vs_speed.
/// Fields80/116/117 retain the caller's packet unless the original mode writes
/// them. Common scalars, both velocities, position, up and forward always write.
pub fn produce(
packet: &mut Packet,
state: &controller::State,
turn_vs_speed: &PointGraph<8>,
settings: Settings,
p: &Processed,
current: bool,
) -> Result<(), &'static str> {
let selected_mode = mode(p, current);
//Validate the host contract before ANY packet field changes. No numerical
//fallback or inferred geometry is substituted for an unavailable producer.
let departure =
if selected_mode == 1 {
Some(p.departure_geometry.ok_or(
"Offboard launch mode1 requires source-backed departure geometry1120/1136",
)?)
} else {
None
};
let forward = normalize_or(flatten(p.forward_224), ZERO);
packet.up_48 = p.up_544;
packet.forward_64 = forward;
packet.scalar_96 = f32::from_bits(0x3db2_b8c2);
packet.scalar_100 = f32::from_bits(0x3f5f_66f3);
packet.scalar_104 = f32::from_bits(0x3f32_b8c2);
packet.kind_108 = 1;
packet.kind_112 = 0;
packet.position_32 = p.position_592;
let mut velocity = p.velocity_608;
let mut secondary = velocity;
match selected_mode {
0 => {
//BC84..BCC0 is a boost, not a conventional [-3,3] clamp.
let lower = select(velocity[1] - 3.0, velocity[1], 3.0);
velocity[1] = select((velocity[1] + 3.0) - lower, lower, velocity[1] + 3.0);
packet.board_position_80 = p.board_position_112;
packet.has_board_position_116 = true;
packet.position_32 = madd(velocity, DT, packet.position_32);
}
400 => {
out.primary_count = 6;
let delta = sub(p.position_592, p.grind_position_1120);
let mut perpendicular = sub(
1 => {
let departure = departure.expect("Mode1 geometry validated before packet writes");
let delta = sub(p.position_592, departure.point_1120);
let projected = sub(
delta,
scale(p.grind_axis_1136, dot(delta, p.grind_axis_1136)),
scale(departure.axis_1136, dot(delta, departure.axis_1136)),
);
perpendicular[1] = 0.;
out.velocity = madd(unit(perpendicular, ZERO), 2., out.velocity);
out.velocity = add(out.velocity, UP);
out.position = madd(out.velocity, f32::from_bits(0x3c888889), out.position);
let normal = normalize_or(flatten(projected), ZERO);
velocity = add(madd(normal, 2.0, velocity), UP);
packet.position_32 = madd(velocity, DT, packet.position_32);
packet.kind_108 = 6;
}
500 if p.flags_2480 & 0x80 != 0 || state == 503 => {
let side = cross(UP, out.velocity);
let n = length(side);
if n > f32::from_bits(0x3a83126f) {
out.velocity = madd(
side,
(if p.flags_2476 & 4 != 0 { 0.75 } else { -0.75 }) / n,
out.velocity,
);
3 => {
let side = cross(UP, velocity);
let speed = length(side);
if !(speed <= f32::from_bits(0x3a83_126f)) {
let offset = if p.flags_2476 & 4 != 0 { 0.75 } else { -0.75 };
velocity = madd(side, offset / speed, velocity);
}
}
500 if p.flags_2476 & 0x80000 != 0 => jump(&mut out, p, j),
500 => {
let mut horizontal = out.velocity;
horizontal[1] = 0.;
out.primary_count = 6;
if length(horizontal) < 1.875 {
out.angles[0] = f32::from_bits(0x3f060a92);
out.velocity = scale(unit(horizontal, forward), 2.5);
out.velocity[1] = select(1. - out.velocity[1], 1., out.velocity[1]);
4 => {
(velocity, secondary) = jump::calculate(state, turn_vs_speed, settings, p, forward);
packet.kind_108 = 6;
packet.kind_112 = 3;
}
5 => {
let horizontal = flatten(velocity);
if !(length(horizontal) >= 1.875) {
velocity = scale(normalize_or(horizontal, forward), 2.5);
velocity[1] = select(1.0 - velocity[1], 1.0, velocity[1]);
packet.scalar_96 = f32::from_bits(0x3f06_0a92);
} else {
out.velocity = scale(p.velocity_912, 0.75);
out.velocity[1] = bounded(out.velocity[1], -10., 5.);
velocity = scale(p.velocity_912, 0.75);
velocity[1] = clamp(velocity[1], -10.0, 5.0);
}
packet.kind_108 = 6;
}
_ => {}
}
out
}
fn jump(out: &mut Launch, p: &Input, j: &JumpInput<'_>) {
let up = unit(j.reference_up_144, j.reference_up_144);
let tangent = sub(p.velocity_912, scale(up, dot(up, p.velocity_912)));
let amount = dot(tangent, out.forward);
let mut initial = madd(out.forward, select(-amount, 0., amount) - amount, tangent);
let mut redirected = initial;
let speed2 = dot(initial, initial);
if p.flags_2472 & 0x10000000 == 0 {
let stick = [p.stick[0], 0., p.stick[1], 0.];
if speed2 < 1. {
initial = limit_length(add(initial, stick), 1.);
redirected = initial;
} else if j.contact_active_708 && dot(stick, stick) > f32::from_bits(0x3f4f5c28) {
let angle =
crate::player::wipeout_state::orientation::projected_angle(stick, initial, up);
let turns = angle * f32::from_bits(0x3e22f983);
let f = turns - turns.floor();
let angle = (f - if f > 0.5 { 1. } else { 0. }) * f32::from_bits(0x40c90fdb);
let projected = sub(stick, scale(unit(up, ZERO), dot(stick, unit(up, ZERO))));
if angle < 45. * f32::from_bits(0x3c8efa35) {
redirected = scale(unit_unchecked(projected), sqrt(speed2));
} else if angle < 90. * f32::from_bits(0x3c8efa35) {
redirected = scale(
limit_angle(
unit_unchecked(projected),
initial,
45. * f32::from_bits(0x3c8efa35),
),
sqrt(speed2),
);
}
}
}
let vertical = select(
-(up[1] * sqrt(j.height_792 * f32::from_bits(0x419ccccd))),
0.,
up[1] * sqrt(j.height_792 * f32::from_bits(0x419ccccd)),
);
let mut horizontal = redirected;
horizontal[1] = 0.;
out.secondary_velocity = unit(horizontal, ZERO);
let requested =
j.turn_curve_1328.evaluate(j.speed_704) * j.steering_760 * f32::from_bits(0x3c8efa35);
let angle = bounded(
bounded(
requested,
j.angular_velocity_688 - f32::from_bits(0x3fdf66f3),
j.angular_velocity_688 + f32::from_bits(0x3fdf66f3),
) * 0.4,
f32::from_bits(0xbe860a92),
f32::from_bits(0x3e860a92),
);
out.velocity = rotate(
madd(initial, j.velocity_scalar_796, scale(up, vertical)),
up,
angle,
);
out.primary_count = 6;
out.secondary_count = 3;
}
fn select(t: f32, a: f32, b: f32) -> f32 {
if t >= 0. { a } else { b }
}
fn bounded(v: f32, lo: f32, hi: f32) -> f32 {
let v = select(lo - v, lo, v);
select(hi - v, v, hi)
}
pub(super) fn dot(a: V, b: V) -> f32 {
crate::physics::native_arithmetic::dot3(a, b)
}
fn add(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] + b[i])
}
pub(super) fn sub(a: V, b: V) -> V {
std::array::from_fn(|i| a[i] - b[i])
}
pub(super) fn scale(v: V, s: f32) -> V {
v.map(|x| x * s)
}
pub(super) fn madd(v: V, s: f32, b: V) -> V {
std::array::from_fn(|i| v[i].mul_add(s, b[i]))
}
fn inverse(q: f32) -> f32 {
let mut r = crate::physics::reciprocal_sqrt::estimate(q);
for _ in 0..2 {
r = (r * 0.5).mul_add((-q).mul_add(r * r, 1.), r);
}
r
}
fn sqrt(q: f32) -> f32 {
if q == 0. { 0. } else { q * inverse(q) }
}
pub(super) fn length(v: V) -> f32 {
sqrt(dot(v, v))
}
pub(super) fn unit(v: V, fallback: V) -> V {
let q = dot(v, v);
let inv = inverse(q);
if sqrt(q) > f32::from_bits(0x358637bd) {
scale(v, inv)
} else {
fallback
}
}
fn unit_unchecked(v: V) -> V {
scale(v, inverse(dot(v, v)))
}
pub(super) 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.,
]
}
pub(super) fn rotate(v: V, axis: V, angle: f32) -> V {
let (s, c) = crate::trigonometry::sin_cos(angle * 0.5);
let mut q = scale(axis, s);
q[3] = c;
crate::player::wipeout_state::orientation::rotate(q, v)
}
fn limit_length(v: V, limit: f32) -> V {
let n = length(v);
if n < f32::from_bits(0x37800000) {
return v;
}
scale(v, select(limit - n, n, limit) / n)
}
pub(super) fn limit_angle(proposed: V, reference: V, limit: f32) -> V {
let a = unit_unchecked(proposed);
let b = unit_unchecked(reference);
let angle = crate::trigonometry::acos(dot(a, b).max(-1.).min(1.));
let axis = cross(a, b);
if angle.abs() < limit || dot(axis, axis) < f32::from_bits(0x37800000) {
proposed
} else {
scale(rotate(b, unit_unchecked(axis), -limit), length(proposed))
2 | 6 => {}
_ => unreachable!("mode returns only the seven native cases"),
}
packet.velocity_0 = velocity;
packet.secondary_velocity_16 = secondary;
Ok(())
}
@@ -0,0 +1,54 @@
//! Mode4:82D7C0C0..C7F4. Secondary obstacle direction and primary impulse
//! intentionally have different dataflow, and the source compares signed angles.
use super::{PointGraph, Processed, Settings, Vector, controller, math::*};
use crate::player::wipeout_state::orientation::projected_angle;
pub(super) fn calculate(
state: &controller::State,
turn: &PointGraph<8>,
settings: Settings,
p: &Processed,
forward: Vector,
) -> (Vector, Vector) {
let reference_up = state.frame_output.frame[1]; //Biped144
// Preserve the explicit zero multiply and fallback to the original reference.
let blend = madd(reference_up, 1.0, scale(state.motion.frame_0[1], 0.0));
let up = normalize_or(blend, reference_up);
let planar = sub(p.velocity_912, scale(up, dot(up, p.velocity_912)));
let backward = dot(planar, forward);
let amount = select(-backward, 0.0, backward) - backward;
let mut base = madd(forward, amount, planar);
let mut secondary = base;
let speed_squared = dot(base, base);
if p.flags_2472 & 0x1000_0000 == 0 {
let control = [p.raw_x_2692, 0.0, p.raw_z_2688, 0.0];
if !(speed_squared >= 1.0) {
base = super::super::contact_correction::clamp_length(madd(control, 1.0, base), 1.0);
secondary = base;
} else if state.contact.active && dot(control, control) > f32::from_bits(0x3f4f_5c28) {
let angle = wrap_angle(projected_angle(control, base, up));
if 45.0 * RADIANS > angle {
secondary = scale(normalize(reject(control, up)), magnitude(speed_squared));
} else if 90.0 * RADIANS > angle {
let direction = normalize(reject(control, up));
//C4E4 supplies45.0 exactly, with no radians conversion.
secondary = scale(limit_angle(direction, base, 45.0), magnitude(speed_squared));
}
}
}
let launch_speed = magnitude(settings.jump_height * f32::from_bits(0x419c_cccd));
let vertical = up[1] * launch_speed;
let vertical = select(-vertical, 0.0, vertical);
secondary = normalize_or(flatten(secondary), ZERO);
let requested_turn = (turn.evaluate(state.motion.speed_704) * state.intent.steering) * RADIANS;
//82D7C6C4 reads Biped+688; +672 is the unrelated edge-position vector.
let turn_center = state.motion.angular_velocity_688;
let window = f32::from_bits(0x3fdf_66f3);
let angle = clamp(
clamp(requested_turn, turn_center - window, turn_center + window) * 0.4,
f32::from_bits(0xbe86_0a92),
f32::from_bits(0x3e86_0a92),
);
let impulse = madd(base, settings.jump_speed_scalar, scale(up, vertical));
(rotate(impulse, up, angle), secondary)
}
@@ -0,0 +1,61 @@
//! Recovered leaf expressions. Ordinary PC estimates retain native refinements;
//! they are not claimed to be bit-exact Xenon arithmetic.
use super::Vector;
pub(in crate::player::offboard) use crate::player::wipeout_state::math::{
add, clamp, cross, dot, length, madd, normalize, normalize_or, scale, sub, wrap_angle,
};
pub(in crate::player::offboard) const ZERO: Vector = [0.0; 4];
pub(in crate::player::offboard) const UP: Vector = [0.0, 1.0, 0.0, 0.0];
pub(in crate::player::offboard) const DT: f32 = f32::from_bits(0x3c88_8889);
pub(in crate::player::offboard) const RADIANS: f32 = f32::from_bits(0x3c8e_fa35);
pub(in crate::player::offboard) fn select(test: f32, positive: f32, negative: f32) -> f32 {
if test >= 0.0 { positive } else { negative }
}
pub(in crate::player::offboard) fn flatten(mut v: Vector) -> Vector {
v[1] = 0.0;
v
}
pub(in crate::player::offboard) fn magnitude(q: f32) -> f32 {
let value = q * crate::physics::board_motion_output::inverse_length_squared(q, 2);
if q == 0.0 { 0.0 } else { value }
}
///82E0A4E8: normalize axis safely, then separate multiplication/subtraction.
pub(in crate::player::offboard) fn reject(v: Vector, axis: Vector) -> Vector {
let axis = normalize_or(axis, ZERO);
sub(v, scale(axis, dot(v, axis)))
}
///82BD3E78, including its strict angle comparison and raw quaternion W insert.
pub(in crate::player::offboard) fn limit_angle(target: Vector, from: Vector, maximum: f32) -> Vector {
let a = normalize_or(target, ZERO);
let b = normalize_or(from, ZERO);
let axis = cross(a, b);
let angle = crate::trigonometry::acos(dot(a, b).max(-1.0).min(1.0));
let turns = angle.mul_add(f32::from_bits(0x3e22_f983), 0.5).floor();
let closest = (-turns).mul_add(f32::from_bits(0x40c9_0fdb), angle);
if closest.abs() < maximum || dot(axis, axis) < f32::from_bits(0x3780_0000) {
return target;
}
let (s, c) = crate::trigonometry::sin_cos(-maximum * 0.5);
let mut q = scale(normalize(axis), s);
q[3] = c;
let first = cross(q, from);
let middle = madd(from, c, first);
scale(madd(cross(q, middle), 2.0, from), length(target))
}
///82D7C724..C7F4 XYZ Rodrigues rotation. Host representation adaptation:
///native permutation scratch lanes are not a fourth spatial velocity component.
///Keep the geometric vector W=0 at this producer, before trajectory feedback.
pub(in crate::player::offboard) fn rotate(v: Vector, axis: Vector, angle: f32) -> Vector {
let [x, y, z, _] = axis;
let (s, c) = crate::trigonometry::sin_cos(angle);
let t = 1.0 - c;
let (tx, ty, tz) = (t * x, t * y, t * z);
let (sx, sy, sz) = (s * x, s * y, s * z);
let xx = tx.mul_add(x, c);
let yx = ty * x - sz;
let zx = tz.mul_add(x, sy);
let right = [xx, tx.mul_add(y, sz), tx * z - sy, 0.];
let up = [yx, ty.mul_add(y, c), ty.mul_add(z, sx), 0.];
let forward = [zx, tz * y - sx, tz.mul_add(z, c), 0.];
madd(forward, v[2], madd(up, v[1], scale(right, v[0])))
}
@@ -0,0 +1,40 @@
use super::Vector;
/// Native118-byte logical packet. W lanes and the two trailing bytes are real
/// fields; this host struct makes no promise about the C ABI or native padding.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Packet {
pub velocity_0: Vector,
pub secondary_velocity_16: Vector,
pub position_32: Vector,
pub up_48: Vector,
pub forward_64: Vector,
pub board_position_80: Vector,
pub scalar_96: f32,
pub scalar_100: f32,
pub scalar_104: f32,
pub kind_108: u32,
pub kind_112: u32,
pub has_board_position_116: bool,
pub flag_117: bool,
}
impl Packet {
///82D2DB98 does not write104; the caller supplies its retained value.
/// The launch producer subsequently overwrites104 in every mode.
pub fn initialized(retained_104: f32) -> Self {
Self {
velocity_0: [0.0; 4],
secondary_velocity_16: [0.0; 4],
position_32: [0.0; 4],
up_48: [0.0, 1.0, 0.0, 0.0],
forward_64: [0.0; 4],
board_position_80: [0.0; 4],
scalar_96: 0.0,
scalar_100: 0.0,
scalar_104: retained_104,
kind_108: 1,
kind_112: 0,
has_board_position_116: false,
flag_117: false,
}
}
}
@@ -0,0 +1,371 @@
use super::*;
fn processed() -> Processed {
Processed {
board_position_112: [7.0, 8.0, 9.0, 2.0],
forward_224: [0.0, 0.0, 1.0, 0.0],
up_544: UP,
position_592: [10.0, 20.0, 30.0, 1.0],
velocity_608: [0.0; 4],
velocity_912: [0.0; 4],
departure_geometry: None,
flags_2472: 0,
flags_2476: 0,
flags_2480: 0,
previous_state_2504: 500,
current_state_2508: 500,
current_category_2512: 500,
previous_category_2516: 500,
raw_x_2692: 0.0,
raw_z_2688: 0.0,
}
}
fn curve() -> PointGraph<8> {
PointGraph {
x: [0., 1., 2., 3., 4., 5., 6., 7.],
y: [90.; 8],
}
}
#[test]
fn mode4_turn_clamp_reads_angular_velocity_not_edge_position() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.velocity_912 = [0., 0., 4., 0.];
let mut state = controller::State::new([None; 3]);
state.motion.angular_velocity_688 = 4.0;
state.intent.edge_target[0] = -100.0;
let mut first = Packet::initialized(0.0);
produce(&mut first, &state, &curve(), settings(), &p, true).unwrap();
state.intent.edge_target[0] = 100.0;
let mut second = Packet::initialized(0.0);
produce(&mut second, &state, &curve(), settings(), &p, true).unwrap();
//82D7C6C4 loads Biped+688, not the separate edge-position vector+672.
assert_eq!(first.velocity_0, second.velocity_0);
state.motion.angular_velocity_688 = -4.0;
let mut opposite = Packet::initialized(0.0);
produce(&mut opposite, &state, &curve(), settings(), &p, true).unwrap();
assert_ne!(second.velocity_0, opposite.velocity_0);
}
fn settings() -> Settings {
Settings {
jump_speed_scalar: 0.8,
jump_height: 0.9,
}
}
fn run(p: Processed) -> Packet {
let mut packet = Packet::initialized(f32::NAN);
produce(
&mut packet,
&controller::State::new([None; 3]),
&curve(),
settings(),
&p,
true,
)
.unwrap();
packet
}
fn near(a: f32, b: f32) {
assert!((a - b).abs() < 2.0e-5, "{a} != {b}");
}
fn vector(a: Vector, b: Vector) {
for i in 0..4 {
near(a[i], b[i]);
}
}
#[test]
fn all_seven_dispatch_modes_and_previous_state_selector() {
let mut p = processed();
for (category, expected) in [(100, 0), (400, 1), (200, 2), (700, 6)] {
p.current_category_2512 = category;
assert_eq!(mode(&p, true), expected);
}
p.current_category_2512 = 500;
assert_eq!(mode(&p, true), 5);
p.flags_2476 = 0x80000;
assert_eq!(mode(&p, true), 4);
p.flags_2480 = 0x80;
assert_eq!(mode(&p, true), 3);
p.flags_2480 = 0;
p.current_state_2508 = 503;
assert_eq!(mode(&p, true), 3);
p.previous_category_2516 = 200;
assert_eq!(mode(&p, false), 2);
}
#[test]
fn initializer_preserves_only_unwritten_scalar104() {
let p = Packet::initialized(123.0);
assert_eq!(p.scalar_104, 123.0);
assert_eq!(p.up_48, UP);
assert_eq!(p.board_position_80, ZERO);
assert!(!p.flag_117);
}
#[test]
fn passive_modes_publish_common_fields_and_preserve_caller_fields() {
for category in [200, 700] {
let mut p = processed();
p.current_category_2512 = category;
p.velocity_608 = [1., 2., 3., 4.];
p.forward_224 = [0., 99., 2., 6.];
let mut packet = Packet::initialized(f32::NAN);
packet.board_position_80 = [4.; 4];
packet.has_board_position_116 = true;
packet.flag_117 = true;
produce(
&mut packet,
&controller::State::new([None; 3]),
&curve(),
settings(),
&p,
true,
)
.unwrap();
assert_eq!(packet.velocity_0, p.velocity_608);
assert_eq!(packet.secondary_velocity_16, p.velocity_608);
vector(packet.forward_64, [0., 0., 1., 3.]);
assert_eq!(packet.position_32, p.position_592);
assert_eq!(packet.scalar_104.to_bits(), 0x3f32_b8c2);
assert_eq!(packet.board_position_80, [4.; 4]);
assert!(packet.has_board_position_116 && packet.flag_117);
}
}
#[test]
fn riding_launch_boost_is_not_a_clamp_and_advances_all_lanes() {
for (y, want) in [
(-10., -7.),
(-2., 1.),
(0., 3.),
(2., 3.),
(3., 3.),
(8., 8.),
] {
let mut p = processed();
p.current_category_2512 = 100;
p.velocity_608 = [2., y, 4., 6.];
let packet = run(p);
assert_eq!(packet.velocity_0[1], want);
assert_eq!(packet.secondary_velocity_16, p.velocity_608);
assert_eq!(packet.board_position_80, p.board_position_112);
assert!(packet.has_board_position_116);
near(packet.position_32[1], want.mul_add(DT, 20.));
near(packet.position_32[3], 6.0f32.mul_add(DT, 1.));
}
}
#[test]
fn mode1_projects_away_from_supplied_axis_before_side_impulse() {
let mut p = processed();
p.current_category_2512 = 400;
p.position_592 = [3., 50., 4., 0.];
p.departure_geometry = Some(DepartureGeometry {
point_1120: [0.; 4],
axis_1136: UP,
});
let packet = run(p);
vector(packet.velocity_0, [1.2, 1., 1.6, 0.]);
assert_eq!(packet.kind_108, 6);
vector(packet.secondary_velocity_16, ZERO);
}
#[test]
fn mode3_sideways_sign_and_stationary_guard() {
let mut p = processed();
p.current_state_2508 = 503;
assert_eq!(run(p).velocity_0, ZERO);
p.velocity_608 = [0., 1., 2., 0.];
near(run(p).velocity_0[0], -0.75);
p.flags_2476 = 4;
near(run(p).velocity_0[0], 0.75);
assert_eq!(run(p).position_32, p.position_592);
}
#[test]
fn mode5_speeds_either_side_and_fast_vertical_caps() {
let mut p = processed();
p.velocity_608 = [0., 0., 1.874, 0.];
p.velocity_912 = [4., 20., 8., 12.];
let slow = run(p);
vector(slow.velocity_0, [0., 1., 2.5, 0.]);
assert_eq!(slow.scalar_96.to_bits(), 0x3f06_0a92);
//BF84 compares the refined magnitude, not the raw input component.
//Use a value unambiguously above1.875 for the fast/capping contract.
p.velocity_608[2] = 1.876;
let fast = run(p);
vector(fast.velocity_0, [3., 5., 6., 9.]);
assert_eq!(fast.scalar_96.to_bits(), 0x3db2_b8c2);
p.velocity_912[1] = -20.;
assert_eq!(run(p).velocity_0[1], -10.);
}
#[test]
fn mode5_pc_refinement_at_raw_1875_is_below_comparison_threshold() {
//Independent Single/FMA evaluation of S3 BF34..BF88 with our documented
//PC seed: square3.515625; seed3F088889; two refinements3F088888;
//multiply gives3FEFFFFF, one ULP below threshold3FF00000.
//This is a regression for our PC arithmetic, NOT Xenon estimate parity.
let mut p = processed();
p.velocity_608 = [0., 0., 1.875, 0.];
p.velocity_912 = [4., 20., 8., 12.];
assert_eq!(length(p.velocity_608).to_bits(), 0x3fef_ffff);
let packet = run(p);
vector(packet.velocity_0, [0., 1., 2.5, 0.]);
assert_eq!(packet.scalar_96.to_bits(), 0x3f06_0a92);
}
#[test]
fn mode5_stationary_uses_flattened_forward_including_w() {
let mut p = processed();
p.forward_224 = [0., 7., 2., 4.];
vector(run(p).velocity_0, [0., 1., 2.5, 5.]);
}
#[test]
fn jump_stationary_control_and_impulse_use_separate_stock_scalars() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.raw_x_2692 = 0.6;
p.raw_z_2688 = 0.8;
let packet = run(p);
near(packet.velocity_0[0], 0.48);
near(packet.velocity_0[2], 0.64);
near(packet.velocity_0[1], (0.9f32 * 19.6).sqrt());
// Host geometric representation deliberately excludes native scratch W.
// XYZ stock impulse assertions above remain unchanged.
assert_eq!(packet.velocity_0[3], 0.);
vector(packet.secondary_velocity_16, [0.6, 0., 0.8, 0.]);
assert_eq!((packet.kind_108, packet.kind_112), (6, 3));
}
#[test]
fn jump_processed_gate_suppresses_stationary_stick_impulse() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.flags_2472 = 0x1000_0000;
p.raw_x_2692 = 1.;
let packet = run(p);
assert_eq!(packet.velocity_0[0], 0.);
assert_eq!(packet.secondary_velocity_16, ZERO);
}
#[test]
fn jump_backward_velocity_is_removed_before_control() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.flags_2472 = 0x1000_0000;
p.velocity_912 = [0., -9., -3., 0.];
let packet = run(p);
near(packet.velocity_0[2], 0.);
assert_eq!(packet.secondary_velocity_16, ZERO);
}
#[test]
fn obstacle_positive_sixty_degree_branch_keeps_raw_45_argument() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.velocity_912 = [0., 0., 2., 0.];
p.raw_x_2692 = -0.8660254;
p.raw_z_2688 = 0.5;
let mut state = controller::State::new([None; 3]);
state.contact.active = true;
let mut packet = Packet::initialized(0.);
produce(&mut packet, &state, &curve(), settings(), &p, true).unwrap();
near(packet.secondary_velocity_16[0], -0.8660254);
near(packet.secondary_velocity_16[2], 0.5);
// Secondary changes do not replace the base vector used for primary impulse.
near(packet.velocity_0[0], 0.);
near(packet.velocity_0[2], 1.6);
}
#[test]
fn obstacle_signed_negative_angle_and_over_ninety_branch() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.velocity_912 = [0., 0., 2., 0.];
let mut state = controller::State::new([None; 3]);
state.contact.active = true;
let mut packet = Packet::initialized(0.);
p.raw_x_2692 = 1.;
produce(&mut packet, &state, &curve(), settings(), &p, true).unwrap();
vector(packet.secondary_velocity_16, [1., 0., 0., 0.]);
p.raw_x_2692 = -1.;
p.raw_z_2688 = -0.1;
produce(&mut packet, &state, &curve(), settings(), &p, true).unwrap();
vector(packet.secondary_velocity_16, [0., 0., 1., 0.]);
}
#[test]
fn jump_slope_frame_reduces_vertical_impulse_by_up_y() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.flags_2472 = 0x1000_0000;
let mut state = controller::State::new([None; 3]);
state.frame_output.frame[1] = [0.6, 0.8, 0., 0.];
let mut packet = Packet::initialized(0.);
produce(&mut packet, &state, &curve(), settings(), &p, true).unwrap();
let lift = (0.9f32 * 19.6).sqrt() * 0.8;
near(packet.velocity_0[0], lift * 0.6);
near(packet.velocity_0[1], lift * 0.8);
assert_eq!(packet.up_48, UP); //Common output retains processed544.
}
#[test]
fn steering_uses_live760_then_angular_velocity_window_and_final_angle_cap() {
let mut p = processed();
p.flags_2476 = 0x80000;
p.flags_2472 = 0x1000_0000;
p.velocity_912 = [0., 0., 2., 0.];
let mut state = controller::State::new([None; 3]);
state.intent.steering = 1.;
state.intent.original_steering = -1.;
let mut packet = Packet::initialized(0.);
produce(&mut packet, &state, &curve(), settings(), &p, true).unwrap();
let (sin, cos) = crate::trigonometry::sin_cos(f32::from_bits(0x3e86_0a92));
near(packet.velocity_0[0], 1.6 * sin);
near(packet.velocity_0[2], 1.6 * cos);
//82D7C6C4: the previous fixture incorrectly populated Biped+672.
state.motion.angular_velocity_688 = -3.;
produce(&mut packet, &state, &curve(), settings(), &p, true).unwrap();
assert!(packet.velocity_0[0] < 0.);
}
#[test]
fn missing_departure_geometry_fails_before_any_packet_write() {
let mut p = processed();
p.previous_category_2516 = 400;
let mut packet = Packet::initialized(123.);
packet.velocity_0 = [9.; 4];
packet.position_32 = [8.; 4];
let before = packet;
let result = produce(
&mut packet,
&controller::State::new([None; 3]),
&curve(),
settings(),
&p,
false,
);
assert_eq!(
result,
Err("Offboard launch mode1 requires source-backed departure geometry1120/1136")
);
assert_eq!(packet, before);
}
#[test]
fn absent_departure_geometry_is_valid_outside_selected_mode1() {
let mut p = processed();
p.previous_category_2516 = 400;
p.current_category_2512 = 200;
p.velocity_608 = [1., 2., 3., 4.];
let mut packet = Packet::initialized(0.);
produce(
&mut packet,
&controller::State::new([None; 3]),
&curve(),
settings(),
&p,
true,
)
.unwrap();
assert_eq!(packet.velocity_0, p.velocity_608);
}
#[test]
fn geometric_launch_rotation_does_not_turn_x_into_a_fourth_velocity() {
let v = [-2.0225198, 4.1746917, 0.7100913, 0.];
let rotated = super::math::rotate(v, [0., 1., 0., 0.], 0.);
assert_eq!(rotated, v);
for angle in [-0.2, 0.2] {
let rotated = super::math::rotate(v, [0., 1., 0., 0.], angle);
assert_eq!(rotated[3], 0.);
let (s, c) = crate::trigonometry::sin_cos(angle);
assert!((rotated[0] - (v[0] * c + v[2] * s)).abs() < 1e-6);
assert_eq!(rotated[1], v[1]);
assert!((rotated[2] - (-v[0] * s + v[2] * c)).abs() < 1e-6);
}
}
@@ -0,0 +1,194 @@
//! Persistent offboard selector72: S3 82D6BD78..82D6EA6C.
//! Separate from onboard Air. World work is submitted/staged by the game host;
//! only explicit consume calls publish results. No physical solver ownership.
mod adjustment;
mod candidates;
mod completion;
pub mod ledge;
mod math;
#[cfg(test)]
mod tests;
use super::{air_launch::Packet, biped_air::recovered};
use crate::{
air::trajectory::{Prediction, QueryRequest, QueryResult, Trajectory},
point_graph::PointGraph,
};
pub use recovered::selection::Candidate;
pub use recovered::{TrajectoryResult, sampling::SelectorState};
pub type Vector = [f32; 4];
pub type Frame = [Vector; 4];
pub const DT: f32 = f32::from_bits(0x3c888889);
pub const MAX_CANDIDATES: usize = 16;
pub const MESH_REJECT_MASK: u32 = 0x6000;
/// Original stock attributes, not incoming material policy.
#[derive(Clone, Copy, Debug)]
pub struct Settings {
/// AB0D9EAEBFC584E9
pub height: f32,
/// 620EEA2E27A371A2
pub sphere_radius: f32,
/// 1B6E3FF7EC72CDE1
pub start_index: i32,
}
#[derive(Clone, Copy, Debug)]
pub struct Context {
pub selection_flags_2948: u32,
pub matching_group_2952: i32,
pub up_544: Vector,
pub forward_224: Vector,
}
#[derive(Clone, Debug)]
pub struct Selector {
pub sampling: SelectorState,
pub launch: Packet,
pub candidates: Vec<Candidate>,
pub predictions: Vec<Prediction>,
pub scores: Vec<f32>,
pub selected_index: Option<usize>,
///6080 is NOT the shifted/animation-adjusted sampling trajectory8144.
pub selected_candidate: Candidate,
pub offset_8272: Vector,
pub correction_8304: Vector,
pub ledge_normal_8320: Vector,
pub ledge_selected_8496: bool,
pub just_changed_8497: bool,
pub requery_pending_8499: bool,
pub requery_count_8488: i32,
pub requery_position_8352: Vector,
pub requery_normal_8368: Vector,
}
impl Default for Selector {
fn default() -> Self {
Self::new(Packet::initialized(0.))
}
}
impl Selector {
pub fn new(launch: Packet) -> Self {
//Constructor82D6B870 differs from Reset82D6BD78 at these fields.
let mut sampling = SelectorState::reset_sampling(launch.scalar_100);
sampling.restart_allowed_8493 = false;
sampling.selection.landing_frame_8480 = 0;
Self {
sampling,
launch,
candidates: Vec::new(),
predictions: Vec::new(),
scores: Vec::new(),
selected_index: None,
selected_candidate: Candidate::reset(),
offset_8272: [0.; 4],
correction_8304: [0.; 4],
ledge_normal_8320: math::UP,
ledge_selected_8496: false,
just_changed_8497: false,
requery_pending_8499: false,
requery_count_8488: 0,
requery_position_8352: [0.; 4],
requery_normal_8368: [0.; 4],
}
}
///82D6BD78 retains packet3904. Reset does not invent a new launch.
pub fn reset(&mut self) {
*self = Self::new(self.launch);
self.sampling = SelectorState::reset_sampling(self.launch.scalar_100);
}
///82D2EF20 clears only these two selector flags; retained predictions survive.
pub fn exit(&mut self) {
self.sampling.pending_8492 = false;
self.sampling.preinitialized_8494 = false;
}
///82D6CA58: once per launch. Invalid authored inputs fail, never get clamped
///into a different candidate count or padded collision request.
pub fn begin_launch(
&mut self,
launch: Packet,
gravity: Vector,
settings: Settings,
) -> Result<Vec<QueryRequest>, &'static str> {
let (candidates, offset, correction) = candidates::prepare(launch, gravity, settings)?;
self.reset();
self.launch = launch;
self.sampling.selection.candidate_scalar_100 = launch.scalar_100;
self.sampling
.seed_fallback(launch.position_32, launch.velocity_0, gravity);
self.offset_8272 = offset;
self.correction_8304 = correction;
self.candidates = candidates;
let requests: Vec<_> = self
.candidates
.iter()
.map(|c| request(c.trajectory, settings.sphere_radius))
.collect();
self.predictions = requests
.iter()
.map(|&request| Prediction {
request,
result: QueryResult::miss(),
})
.collect();
self.scores = vec![0.; requests.len()];
Ok(requests)
}
pub fn sample(
&mut self,
frame: i32,
timestep: f32,
curve: &PointGraph<8>,
out: &mut TrajectoryResult,
) {
self.sampling.sample(frame, timestep, curve, out);
}
///82D6E798. Caller supplies current processed suppression flags.
pub fn begin_requery(
&mut self,
flags_2472: u32,
flags_2488: u32,
radius: f32,
) -> Result<Option<QueryRequest>, &'static str> {
if flags_2472 & 0x10000000 != 0
|| flags_2488 & 0x400000 != 0
|| !self.sampling.restart_allowed_8493
{
return Ok(None);
}
let request = request(self.selected_candidate.trajectory, radius);
validate_request(request)?;
self.sampling.restart_allowed_8493 = false;
self.requery_pending_8499 = true;
//Native requery replaces slot0; it does not replace the winning candidate.
if let Some(slot) = self.predictions.first_mut() {
*slot = Prediction {
request,
result: QueryResult::miss(),
};
}
Ok(Some(request))
}
}
pub fn request(mut trajectory: Trajectory, radius: f32) -> QueryRequest {
//82D6CBxx/E844: f1 radius, f2 .25, f3 1.2, f4 duration2.
trajectory.duration = 2.;
QueryRequest {
trajectory,
radius,
start_error: 0.25,
end_error: 1.2,
}
}
pub fn validate_request(q: QueryRequest) -> Result<(), &'static str> {
let t = q.trajectory;
if [t.duration, q.radius, q.start_error, q.end_error]
.iter()
.any(|x| !x.is_finite() || *x <= 0.)
|| t.position
.into_iter()
.chain(t.velocity)
.chain(t.acceleration)
.any(|x| !x.is_finite())
{
Err("Invalid native BipedAir trajectory request")
} else {
Ok(())
}
}
@@ -0,0 +1,60 @@
//!82D6DE08/E018/E178: animation displacement adjusts the sampling arc only.
use super::{DT, Frame, Selector, Vector, math::*};
impl Selector {
pub fn adjust_animation(&mut self, frame: i32, animation: Vector, axes: Frame, radius: f32) {
self.sampling.adjustment_8336 = scale(axes[2], animation[2]);
self.correction_8304 = scale(
//82D6DF40 is one vmaddfp after the separate forward multiply.
madd(
axes[1], animation[1] - radius,
self.sampling.adjustment_8336,
),
-1.,
);
self.correction_8304[1] -= 0.08;
let s = &mut self.sampling.selection;
let delta = sub(
sub(s.position_6176, self.correction_8304),
s.trajectory_8144
.position_at(s.landing_frame_8480 as f32 * DT),
);
shift(&mut s.trajectory_8144, frame as f32 * DT);
adjust(
&mut s.trajectory_8144,
s.landing_frame_8480.wrapping_sub(frame),
delta,
0.5,
);
shift(&mut s.trajectory_8144, frame.wrapping_neg() as f32 * DT);
let t = s.trajectory_8144;
let end = t.position_at(s.landing_frame_8480 as f32 * DT);
if self.launch.up_48[1] < 0.8
&& dot(self.launch.up_48, flatten(t.velocity)) <= 0.
&& s.normal_6144[1] > 0.9
&& end[1] - t.position[1] > -0.2
{
//Runtime constants830BD400/330 are initialized by82F82588/826B0:
//splat(-9.8), [0,-9.8,0,0]; not the launch gravity attribute.
let g = f32::from_bits(0x411ccccd);
let distance = length(sub(t.position, end));
let square = 2. * distance * reciprocal(g);
let root = if square == 0. {
0.
} else {
square * crate::physics::board_motion_output::inverse_length_squared(square, 2)
};
let duration = root * 0.66 + s.landing_frame_8480 as f32 * 0.0056666667;
if duration > 0.2 {
let acceleration = [0., -g, 0., 0.];
s.trajectory_8144.velocity = sub(
scale(sub(end, t.position), reciprocal(duration)),
scale(scale(acceleration, 0.5), duration),
);
s.trajectory_8144.acceleration = acceleration;
s.trajectory_8144.duration = -1.;
s.landing_frame_8480 = (duration * 59.999996) as i32;
s.scalar_8392 = s.landing_frame_8480 as f32 * DT;
}
}
}
}
@@ -0,0 +1,112 @@
//!82D6BF90; S2 82DBB358 verifies packet/index-shift roles, not S3's new cone.
use super::{Candidate, DT, MAX_CANDIDATES, Packet, Settings, Vector, math::*};
use crate::air::trajectory::Trajectory;
pub(super) fn prepare(
p: Packet,
gravity: Vector,
s: Settings,
) -> Result<(Vec<Candidate>, Vector, Vector), &'static str> {
let count = p
.kind_108
.checked_add(p.kind_112)
.ok_or("BipedAir candidate count overflow")? as usize;
if p.kind_108 == 0
|| count > MAX_CANDIDATES
|| !s.height.is_finite()
|| !s.sphere_radius.is_finite()
|| s.sphere_radius <= 0.
{
return Err("Invalid authored BipedAir selector settings");
}
if [p.scalar_96, p.scalar_100, p.scalar_104]
.iter()
.any(|x| !x.is_finite())
|| [
p.velocity_0,
p.secondary_velocity_16,
p.position_32,
p.up_48,
p.forward_64,
p.board_position_80,
gravity,
]
.into_iter()
.flatten()
.any(|x| !x.is_finite())
{
return Err("Nonfinite BipedAir launch packet");
}
let initial = scale(UP, -(s.height - s.sphere_radius));
let mut correction = [0., 0.1, 0., 0.];
if p.has_board_position_116 {
correction = add(
correction,
sub(p.board_position_80, add(p.position_32, initial)),
);
}
let offset = add(initial, correction);
let base = sub(p.velocity_0, scale(correction, reciprocal(1.)));
let mut velocities = vec![base; count];
if count > 1 {
let speed = length(p.velocity_0);
let (axis, right) = if speed >= 0.001 {
let axis = scale(p.velocity_0, reciprocal(speed));
let mut right = cross(UP, axis);
if length(right) < 0.1 {
right = cross(axis, p.forward_64);
}
(axis, scale(right, reciprocal(length(right))))
} else {
(UP, scale(p.forward_64, -1.))
};
let vertical = cross(axis, right);
//8252D980 is tan; Biped launch already supplies radians.
let tangents = [p.scalar_96.tan(), p.scalar_100.tan(), p.scalar_104.tan()];
let base_speed = length(base);
let mut minimum = length(flatten(p.velocity_0));
for i in 1..p.kind_108 as usize {
let angle = (1. - (i as f32 / (p.kind_108 - 1) as f32) * 2.) * std::f32::consts::PI;
let sin = crate::trigonometry::sin(angle);
let cos = crate::trigonometry::cos(angle);
let cone = if cos <= 0. { tangents[1] } else { tangents[2] };
let v = madd(vertical, cos * cone, madd(right, sin * tangents[0], base));
velocities[i] = scale(v, base_speed * reciprocal(length(v)));
minimum = minimum.min(length(flatten(velocities[i])));
}
if p.kind_112 > 0 {
let secondary = flatten(p.secondary_velocity_16);
let n = length(secondary);
let direction = if n > 0.01 {
scale(secondary, reciprocal(n))
} else {
normalize_or(p.forward_64, [0.; 4])
};
let increment = minimum.max(1.).min(4.) / (p.kind_112 + 1) as f32;
let mut speed = 0.;
//Original822F9444=41A95811: fixed native secondary vertical seed.
let square = f32::from_bits(0x41a95811);
let vertical =
square * crate::physics::board_motion_output::inverse_length_squared(square, 2);
for i in p.kind_108 as usize..count {
speed += increment;
velocities[i] = madd(direction, speed, [0., vertical, 0., 0.]);
}
}
}
let mut candidates = Vec::with_capacity(count);
for velocity in velocities {
let mut trajectory = Trajectory {
position: add(p.position_32, offset),
velocity,
acceleration: gravity,
duration: 2.,
};
shift(&mut trajectory, s.start_index as f32 * DT);
super::validate_request(super::request(trajectory, s.sphere_radius))?;
let mut c = Candidate::reset();
c.trajectory = trajectory;
c.start_frame_112 = s.start_index;
candidates.push(c);
}
Ok((candidates, offset, initial))
}
@@ -0,0 +1,142 @@
//! S3 82D6D020/CC50/E8A0. Incoming scoring/requery are comparison only.
use super::{Candidate, Context, DT, Selector, ledge, math::*};
use crate::air::trajectory::{Prediction, QueryResult};
impl Selector {
///Populate native landing observations before candidate0's ledge stage.
pub fn observe_launch(&mut self, results: &[QueryResult]) -> Result<(), &'static str> {
if !self.sampling.pending_8492 || results.len() != self.candidates.len() {
return Err("BipedAir launch completion does not match pending batch");
}
for ((candidate, prediction), &result) in self
.candidates
.iter_mut()
.zip(&mut self.predictions)
.zip(results)
{
prediction.result = result;
if result.valid() {
candidate.contact_position_96 = result.contact_position;
candidate.normal_64 = result.suggested_normal();
prediction.result.contact_normal = candidate.normal_64;
candidate.contact_velocity_80 = candidate
.trajectory
.velocity_at(result.contact_frame as f32 * DT);
candidate.landing_frame_116 = result.contact_frame;
candidate.valid_120 = true;
} else {
candidate.normal_64 = UP;
candidate.contact_velocity_80 = [0.; 4];
candidate.landing_frame_116 = 120;
candidate.valid_120 = false;
}
}
Ok(())
}
///Native scoring uses the pre-ledge normal/displacement even if candidate0
///is subsequently adjusted. Caller passes the original observations.
pub fn select_launch(
&mut self,
context: Context,
original_first: Candidate,
ledge: Option<ledge::Adjustment>,
) -> Result<usize, &'static str> {
if !self.sampling.pending_8492 || self.candidates.is_empty() {
return Err("BipedAir select without pending launch");
}
if let Some(adjustment) = ledge {
adjustment.apply(&mut self.candidates[0]);
self.ledge_normal_8320 = self.candidates[0].normal_64;
self.ledge_selected_8496 = true;
}
for i in 0..self.candidates.len() {
let c = self.candidates[i];
self.scores[i] = 0.;
if !c.valid_120 {
continue;
}
let original = if i == 0 { original_first } else { c };
let displacement = sub(original.contact_position_96, original.trajectory.position);
let normal = original.normal_64;
let time = c.landing_frame_116 as f32 * DT;
let mut ledge_bonus = if i == 0 && self.ledge_selected_8496 {
1.
} else {
0.
};
let mut time_penalty = 0.;
if time <= 0.5 {
ledge_bonus = 0.;
time_penalty = -1. - (0.5 - time) * 2.;
}
let center_bonus = if i == 0 { 1. } else { 0. };
if self.predictions[i].result.surface & 0xf80 == 0x300 {
self.predictions[i].result.contact_normal = context.up_544;
}
let mut normal_penalty = (2.2222223 * ((normal[1] - 0.45) * 5.)).min(0.);
if dot(normal, self.launch.forward_64) > 0. {
normal_penalty = 0.;
}
let mut forward = dot(self.launch.forward_64, displacement).max(1.);
let mut height = displacement[1].max(0.);
if i >= self.launch.kind_108 as usize {
if displacement[1] < 0.4 {
height -= 10000.;
} else {
height *= 8.;
forward = dot(self.launch.secondary_velocity_16, displacement);
}
}
self.scores[i] = ((((0. + forward) + height) + normal_penalty) + center_bonus)
+ time_penalty
+ ledge_bonus;
}
let index = super::recovered::selection::selected_index(&self.scores)
.ok_or("BipedAir empty candidate batch")?;
self.selected_index = Some(index);
self.selected_candidate = self.candidates[index];
if self.sampling.commit(
self.selected_candidate,
&mut self.predictions[index],
self.offset_8272,
) {
self.just_changed_8497 = true;
}
Ok(index)
}
///82D6E8A0. A miss clears pending only. First hit seeds comparison state;
///subsequent moved hits update landing observations, not trajectory8144.
pub fn complete_requery(&mut self, prediction: Prediction) -> Result<(), &'static str> {
if !self.requery_pending_8499 {
return Err("BipedAir requery completion without submission");
}
self.requery_pending_8499 = false;
if let Some(slot) = self.predictions.first_mut() {
*slot = prediction;
}
if !prediction.result.valid() {
return Ok(());
}
let result = prediction.result;
if self.requery_count_8488 <= 0 {
self.requery_position_8352 = result.contact_position;
self.requery_normal_8368 = result.suggested_normal();
} else {
let delta = sub(self.requery_position_8352, result.contact_position);
if dot(delta, delta) > 0.0001 {
self.requery_position_8352 = result.contact_position;
self.requery_normal_8368 = result.suggested_normal();
let s = &mut self.sampling.selection;
s.landing_frame_8480 = result.contact_frame;
s.scalar_8392 = result.contact_frame as f32 * DT;
s.velocity_6160 = prediction
.request
.trajectory
.velocity_at(result.contact_frame as f32 * DT);
s.position_6176 = result.contact_position;
s.normal_6144 = result.suggested_normal();
}
}
self.requery_count_8488 = self.requery_count_8488.wrapping_add(1);
Ok(())
}
}
@@ -0,0 +1,282 @@
//! Air-only ledge correction82D6D4D0, filtering82C1F310, six-line82C20728.
//! Candidate0 only. Closest-point/plane formulas read from82E09D28/09DF0.
use super::{Candidate, Context, DT, Vector, math::*};
use crate::{
air::trajectory::{Prediction, Trajectory},
math::Vector3,
player::offboard::ground_query::{Edge, EdgeSearch, Frame, Line, LineHit, QueryContext},
};
fn lanes(v: Vector3) -> Vector {
[v.x, v.y, v.z, 0.]
}
fn vec3(v: Vector) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
pub fn search(
candidate: Candidate,
prediction: Prediction,
context: Context,
) -> Option<EdgeSearch> {
if !prediction.result.valid() || prediction.result.contact_frame < 16 {
return None;
}
let collision = prediction.collision_position();
let (apex, _) = candidate.trajectory.highest_position();
Some(EdgeSearch {
min: vec3(std::array::from_fn(|i| collision[i].min(apex[i]) - 2.)),
max: vec3(std::array::from_fn(|i| collision[i].max(apex[i]) + 2.)),
frame: Frame {
position: vec3(candidate.trajectory.position),
..Frame::IDENTITY
},
context: QueryContext {
selection_flags_2948: context.selection_flags_2948,
matching_id_2952: context.matching_group_2952,
},
narrow_forward: false,
})
}
///82C1F310 removes a parallel edge only with native separation, overlap,
/// reference-distance and oriented-height tests. Retains provider order.
pub fn filter_edges(edges: &[Edge], reference: Vector) -> Vec<Edge> {
let directions: Vec<_> = edges
.iter()
.map(|e| normalize(sub(lanes(e.end), lanes(e.start))))
.collect();
edges
.iter()
.enumerate()
.filter_map(|(i, &edge)| {
let start = lanes(edge.start);
let midpoint = scale(add(start, lanes(edge.end)), 0.5);
for (j, other) in edges.iter().enumerate() {
if i == j {
continue;
}
let parallel = cross(directions[i], directions[j]);
if !(dot(parallel, parallel) < 0.0001) {
continue;
}
let delta = sub(lanes(other.start), start);
let off = sub(delta, scale(directions[j], dot(directions[j], delta)));
let distance = dot(off, off);
if !(distance > 0.0001 && distance < 0.010000001) {
continue;
}
let from_mid = sub(lanes(other.start), midpoint);
if !(dot(from_mid, sub(lanes(other.end), midpoint)) < 0.) {
continue;
}
let other_point = add(
midpoint,
sub(from_mid, scale(directions[j], dot(directions[j], from_mid))),
);
let a = sub(midpoint, reference);
let b = sub(other_point, reference);
let normal = normalize_or(cross(directions[i], cross(UP, directions[i])), [0.; 4]);
let height = dot(delta, normal);
if (height > -0.02 && dot(a, a) > dot(b, b)) || height > 0.02 {
return None;
}
}
Some(edge)
})
.take(40)
.collect()
}
///82D60C80/B98: exact discriminant branches, larger root for two roots.
/// No generic epsilon-linear fallback, positive clamp, or duration rejection.
pub fn plane_time(t: Trajectory, point: Vector, normal: Vector) -> Option<f32> {
let a = dot(normal, scale(t.acceleration, 0.5));
let b = dot(normal, t.velocity);
let c = dot(normal, t.position) - dot(point, normal);
let discriminant = b * b - (4. * a) * c;
if discriminant < 0. {
return None;
}
let denominator = reciprocal(2. * a);
if discriminant > 0. {
let root = discriminant
* crate::physics::board_motion_output::inverse_length_squared(discriminant, 2);
Some((denominator * (-b + root)).max(denominator * (-b - root)))
} else {
let time = denominator * (-b);
(time > 0.).then_some(time)
}
}
fn closest(point: Vector, edge: Edge) -> Vector {
let start = lanes(edge.start);
let mut delta = sub(lanes(edge.end), start);
let n = length(delta);
if n > f32::from_bits(0x37800000) {
delta = scale(delta, reciprocal(n));
}
madd(delta, dot(delta, sub(point, start)).min(n).max(0.), start)
}
fn angle(new: Vector, old: Vector, up: Vector) -> f32 {
if !(dot(new, new) * dot(old, old) > f32::from_bits(0x37800000)) {
return 0.;
}
let a = sub(new, scale(up, dot(up, new)));
let b = sub(old, scale(up, dot(up, old)));
let (aa, bb) = (dot(a, a), dot(b, b));
if !(aa > 0.0001 && bb > 0.0001) {
return 0.;
}
//8296EC98 uses ONE refinement, not the two-refinement generic normalize.
let a = scale(
a,
crate::physics::board_motion_output::inverse_length_squared(aa, 1),
);
let b = scale(
b,
crate::physics::board_motion_output::inverse_length_squared(bb, 1),
);
let angle = crate::trigonometry::acos(dot(a, b).max(-1.).min(1.));
let angle = if dot(cross(a, b), up) < 0. {
std::f32::consts::TAU - angle
} else {
angle
};
wrap_angle(angle).abs()
}
#[derive(Clone, Copy, Debug)]
pub struct Adjustment {
pub edge: Edge,
pub point: Vector,
pub lowered_trajectory: Trajectory,
pub landing_frame: i32,
pub radius: f32,
}
impl Adjustment {
pub fn apply(self, candidate: &mut Candidate) {
candidate.trajectory = self.lowered_trajectory;
candidate.trajectory.position =
add(candidate.trajectory.position, [0., self.radius, 0., 0.]);
candidate.normal_64 = normalize_or(
cross(
sub(lanes(self.edge.end), lanes(self.edge.start)),
cross(UP, sub(lanes(self.edge.end), lanes(self.edge.start))),
),
UP,
);
candidate.contact_position_96 = self.point;
candidate.landing_frame_116 = self.landing_frame;
candidate.contact_velocity_80 = self
.lowered_trajectory
.velocity_at(self.landing_frame as f32 * DT);
candidate.special_121 = true;
}
}
pub fn choose(
candidate: Candidate,
prediction: Prediction,
context: Context,
radius: f32,
edges: &[Edge],
) -> Option<Adjustment> {
if !prediction.result.valid() || prediction.result.contact_frame < 16 {
return None;
}
let t = candidate.trajectory;
let mut lowered = t;
lowered.position = add(t.position, [0., -radius, 0., 0.]);
let mut best: Option<Adjustment> = None;
for &edge in edges {
let delta = sub(lanes(edge.end), lanes(edge.start));
let normal = normalize_or(cross(delta, cross(UP, delta)), UP);
let Some(time) = plane_time(t, lanes(edge.start), normal) else {
continue;
};
let intersection = t.position_at(time);
let point = closest(intersection, edge);
if !(normal[1] > 0.71) {
continue;
}
let horizontal = flatten(sub(point, lowered.position));
let distance = length(horizontal);
if !(distance > 0.3) {
continue;
}
let direction = scale(horizontal, 1. / distance);
if !(dot(
direction,
normalize_or(flatten(context.forward_224), [0.; 4]),
) > 0.71)
{
continue;
}
let frame = (time * 59.999996) as i32;
let mut adjusted = lowered;
//Original delta uses the UNLOWERED continuous intersection, not a
//rounded frame sample of the lowered working trajectory (incoming).
adjust(&mut adjusted, frame, sub(point, intersection), 10.);
let velocity_delta = sub(adjusted.velocity, t.velocity);
let height = point[1] - prediction.result.contact_position[1];
if !(height > -0.05 && dot(velocity_delta, velocity_delta) < 2.25) {
continue;
}
let near_height = height < 0.05;
let maximum = if near_height { 10. } else { 40. };
if near_height && !(dot(direction, normalize_or(flatten(delta), [0.; 4])) >= 0.71) {
continue;
}
if !(angle(adjusted.velocity, t.velocity, context.up_544)
< maximum * f32::from_bits(0x3c8efa35))
{
continue;
}
if best.as_ref().is_some_and(|old| old.point[1] > point[1]) {
continue;
}
best = Some(Adjustment {
edge,
point,
lowered_trajectory: adjusted,
landing_frame: frame,
radius,
});
}
best
}
///82C20530 clears byte64: Air does not append Ground's downward seventh line.
pub fn lines(adjustment: Adjustment, half_wheelbase: f32) -> Option<[Line; 6]> {
let edge = adjustment.edge;
let delta = sub(lanes(edge.end), lanes(edge.start));
let raw_up = cross(delta, cross(UP, delta));
if 0.00001 > length(raw_up) {
return None;
}
let up = normalize_or(raw_up, raw_up);
let tangent = normalize_or(delta, delta);
let start = lanes(edge.start);
let projection = madd(tangent, dot(sub(adjustment.point, start), tangent), start);
let center = sub(adjustment.point, sub(adjustment.point, projection));
let side = cross(up, tangent);
let near = scale(side, 0.09);
let far = scale(side, half_wheelbase);
let short_up = scale(up, 0.04);
let far_up = scale(up, half_wheelbase * f32::from_bits(0x3f87ae14));
let line = |c, d, radius| Line {
start: vec3(add(c, d)),
end: vec3(sub(c, d)),
radius,
};
Some([
line(add(center, near), short_up, 0.),
line(sub(center, near), short_up, 0.),
line(add(center, far), far_up, 0.),
line(sub(center, far), far_up, 0.),
line(center, short_up, 0.),
line(add(center, scale(up, 0.09)), near, 0.001),
])
}
///82C20C08: return1 after consuming a submitted batch, even with six misses.
///The general grind-classification output is temporary and discarded by Air.
pub fn consume_lines(hits: &[Option<LineHit>]) -> Result<(), &'static str> {
if hits.len() != 6 {
Err("BipedAir ledge completion requires exactly six lines")
} else {
Ok(())
}
}
@@ -0,0 +1,29 @@
//! Reuse ordinary native-arithmetic leaves; no new physical solver.
use super::{DT, Vector};
use crate::air::trajectory::Trajectory;
pub(super) use crate::player::wipeout_state::math::{
add, cross, dot, length, madd, normalize, normalize_or, reciprocal, scale, sub, wrap_angle,
};
pub(super) const UP: Vector = [0., 1., 0., 0.];
pub(super) fn flatten(mut v: Vector) -> Vector {
v[1] = 0.;
v
}
pub(super) fn shift(t: &mut Trajectory, time: f32) {
let p = t.position_at(time);
t.velocity = t.velocity_at(time);
t.position = p;
}
///82D609E0: velocity correction only, bounded at the caller's maximum.
pub(super) fn adjust(t: &mut Trajectory, frame: i32, delta: Vector, maximum: f32) {
if frame <= 0 {
return;
}
let correction = scale(delta, reciprocal(frame as f32 * DT));
let scalar = if dot(correction, correction) > maximum * maximum {
maximum / length(correction)
} else {
1.
};
t.velocity = madd(correction, scalar, t.velocity);
}
@@ -0,0 +1,275 @@
use super::math::*;
use super::*;
use crate::math::Vector3;
fn settings() -> Settings {
Settings {
height: 0.9,
sphere_radius: 0.3,
start_index: 3,
}
}
fn packet() -> Packet {
let mut p = Packet::initialized(0.);
p.position_32 = [0., 2., 0., 0.];
p.velocity_0 = [0., 3., 4., 0.];
p.forward_64 = [0., 0., 1., 0.];
p.scalar_100 = 0.2;
p
}
fn context() -> Context {
Context {
selection_flags_2948: 0,
matching_group_2952: -1,
up_544: UP,
forward_224: [0., 0., 1., 0.],
}
}
fn hit(frame: i32, position: Vector) -> QueryResult {
QueryResult {
contact_frame: frame,
contact_time: frame as f32 * DT,
contact_position: position,
surface: 7 << 7,
..QueryResult::miss()
}
}
fn launched() -> Selector {
let mut s = Selector::default();
s.begin_launch(packet(), [0., -9.8, 0., 0.], settings())
.unwrap();
s
}
fn selected() -> Selector {
let mut s = launched();
s.observe_launch(&[hit(60, [0., 0., 4., 0.])]).unwrap();
s.select_launch(context(), s.candidates[0], None).unwrap();
s
}
fn close(a: Vector, b: Vector) {
for i in 0..4 {
assert!((a[i] - b[i]).abs() < 0.0001, "{a:?} != {b:?}");
}
}
#[test]
fn constructor_reset_and_exit_have_distinct_native_effects() {
let mut s = Selector::default();
assert!(!s.sampling.restart_allowed_8493);
assert_eq!(s.sampling.selection.landing_frame_8480, 0);
s.launch = packet();
s.reset();
assert_eq!(s.launch, packet());
assert!(s.sampling.restart_allowed_8493);
assert_eq!(s.sampling.selection.landing_frame_8480, 1000);
let mut s = selected();
let before = s.predictions.clone();
let candidate = s.selected_candidate.trajectory;
s.exit();
assert!(!s.sampling.pending_8492 && !s.sampling.preinitialized_8494);
assert_eq!(s.predictions, before);
assert_eq!(s.selected_candidate.trajectory, candidate);
}
#[test]
fn launch_requests_are_native_valid_and_index_shifted() {
let s = launched();
let p = packet();
let trajectory = s.candidates[0].trajectory;
let original = Trajectory {
position: add(p.position_32, s.offset_8272),
velocity: sub(p.velocity_0, [0., 0.1, 0., 0.]),
acceleration: [0., -9.8, 0., 0.],
duration: 2.,
};
close(trajectory.position, original.position_at(3. * DT));
close(trajectory.velocity, original.velocity_at(3. * DT));
let q = s.predictions[0].request;
assert_eq!(
(q.trajectory.duration, q.radius, q.start_error, q.end_error),
(2., 0.3, 0.25, 1.2)
);
assert!(validate_request(q).is_ok());
}
#[test]
fn invalid_authoring_is_not_capped_or_padded() {
let mut s = Selector::default();
let mut p = packet();
p.kind_108 = 17;
assert!(s.begin_launch(p, [0., -9.8, 0., 0.], settings()).is_err());
p.kind_108 = 0;
assert!(s.begin_launch(p, [0., -9.8, 0., 0.], settings()).is_err());
let mut bad = settings();
bad.sphere_radius = 0.;
assert!(s.begin_launch(packet(), [0., -9.8, 0., 0.], bad).is_err());
assert!(s.candidates.is_empty());
}
#[test]
fn pending_sample_uses_original_fallback_without_consuming() {
let mut s = launched();
let fallback = s.sampling.fallback_8208;
let graph = PointGraph {
x: [0.; 8],
y: [1.; 8],
};
let mut out = TrajectoryResult::default();
s.sample(10, DT, &graph, &mut out);
close(out.position_272, fallback.position_at(10. * DT));
assert!(!out.valid_404);
assert_eq!(out.normal_304, UP);
assert!(s.sampling.pending_8492);
assert_eq!(out.scalar_392, packet().scalar_100);
}
#[test]
fn scoring_keeps_first_tie_and_secondary_low_height_penalty() {
let mut p = packet();
p.kind_108 = 2;
p.kind_112 = 1;
p.secondary_velocity_16 = [0., 0., 100., 0.];
let mut s = Selector::default();
s.begin_launch(p, [0., -9.8, 0., 0.], settings()).unwrap();
let results: Vec<_> = s
.candidates
.iter()
.map(|c| hit(60, add(c.trajectory.position, [0., 0.1, 2., 0.])))
.collect();
s.observe_launch(&results).unwrap();
s.select_launch(context(), s.candidates[0], None).unwrap();
assert_eq!(s.selected_index, Some(0));
assert!(s.scores[2] < -9000.);
assert_eq!(recovered::selection::selected_index(&[2., 2., 1.]), Some(0));
}
#[test]
fn selection_preserves_special_surface_and_unshifted_candidate() {
let mut s = launched();
s.observe_launch(&[hit(60, [0., 0., 4., 0.])]).unwrap();
let first = s.candidates[0];
let edge = crate::player::offboard::ground_query::Edge {
start: Vector3::new(-1., 0., 4.),
end: Vector3::new(1., 0., 4.),
};
let adjustment = ledge::Adjustment {
edge,
point: [0., 0., 4., 0.],
lowered_trajectory: first.trajectory,
landing_frame: 55,
radius: 0.3,
};
s.select_launch(context(), first, Some(adjustment)).unwrap();
assert_eq!(s.sampling.selection.word_8396, 2);
assert_eq!(s.predictions[0].result.contact_frame, 63);
assert_eq!(s.sampling.selection.landing_frame_8480, 55); //S3, not S2 index sum.
assert_eq!(
s.selected_candidate.trajectory.velocity,
first.trajectory.velocity
);
assert_ne!(
s.sampling.selection.trajectory_8144.position,
s.selected_candidate.trajectory.position
);
}
#[test]
fn animation_adjusts_sample_only_and_preserves_current_point() {
let mut s = selected();
let arc = s.sampling.selection.trajectory_8144;
let candidate = s.selected_candidate.trajectory;
let prediction = s.predictions[0];
let frame = 10;
let before = arc.position_at(frame as f32 * DT);
let axes = [[1., 0., 0., 0.], UP, [0., 0., 1., 0.], [0.; 4]];
s.adjust_animation(frame, [0., 0.9, 0.3, 0.], axes, 0.3);
close(
s.sampling
.selection
.trajectory_8144
.position_at(frame as f32 * DT),
before,
);
assert_eq!(s.selected_candidate.trajectory, candidate);
assert_eq!(s.predictions[0], prediction);
}
#[test]
fn animation_correction_retains_native_fused_up_plus_forward_operation() {
let mut s = selected();
//Finite orthogonal axes deliberately exercise cancellation. This is an
//arithmetic regression for82D6DF40, not a stock gameplay tuning fixture.
let axes = [[0., 0., 1., 0.], [0.8, 0.6, 0., 0.],
[0.6, -0.8, 0., 0.], [0.; 4]];
s.adjust_animation(10, [0., 0.6, -0.8, 0.], axes, 0.);
let forward = 0.6_f32 * -0.8;
let expected = -0.8_f32.mul_add(0.6, forward);
assert_ne!(expected, -(0.8_f32 * 0.6 + forward));
assert_eq!(s.correction_8304[0].to_bits(), expected.to_bits());
}
#[test]
fn requery_gates_first_hit_changed_hit_and_miss_retention() {
let mut s = selected();
let old = s.sampling.selection;
assert!(s.begin_requery(0x10000000, 0, 0.3).unwrap().is_none());
assert!(s.sampling.restart_allowed_8493);
let request = s.begin_requery(0, 0, 0.3).unwrap().unwrap();
assert_eq!(
request.trajectory.position,
s.selected_candidate.trajectory.position
);
s.complete_requery(Prediction {
request,
result: hit(41, [3., 2., 1., 0.]),
})
.unwrap();
assert_eq!(s.requery_count_8488, 1);
assert_eq!(s.sampling.selection.position_6176, old.position_6176);
s.sampling.restart_allowed_8493 = true;
let request = s.begin_requery(0, 0, 0.3).unwrap().unwrap();
s.complete_requery(Prediction {
request,
result: hit(42, [4., 2., 1., 0.]),
})
.unwrap();
assert_eq!(s.sampling.selection.landing_frame_8480, 42);
assert_eq!(s.sampling.selection.trajectory_8144, old.trajectory_8144);
let retained = s.sampling.selection;
s.sampling.restart_allowed_8493 = true;
let request = s.begin_requery(0, 0, 0.3).unwrap().unwrap();
s.complete_requery(Prediction {
request,
result: QueryResult::miss(),
})
.unwrap();
assert_eq!(s.requery_count_8488, 2);
assert!(!s.requery_pending_8499);
assert_eq!(s.sampling.selection.position_6176, retained.position_6176);
}
#[test]
fn greatest_plane_root_is_not_limited_to_query_duration() {
let t = Trajectory {
position: [0., 1., 0., 0.],
velocity: [0., 2., 0., 0.],
acceleration: [0., -2., 0., 0.],
duration: 0.1,
};
let time = ledge::plane_time(t, [0.; 4], UP).unwrap();
assert!((time - (1. + 2f32.sqrt())).abs() < 0.0001);
let tangent = Trajectory {
position: [0., -1., 0., 0.],
..t
};
assert!((ledge::plane_time(tangent, [0.; 4], UP).unwrap() - 1.).abs() < 0.0001);
}
#[test]
fn air_six_lines_consume_even_when_all_miss() {
let edge = crate::player::offboard::ground_query::Edge {
start: Vector3::new(-1., 0., 0.),
end: Vector3::new(1., 0., 0.),
};
let adjustment = ledge::Adjustment {
edge,
point: [0.; 4],
lowered_trajectory: selected().selected_candidate.trajectory,
landing_frame: 60,
radius: 0.3,
};
let lines = ledge::lines(adjustment, 0.4).unwrap();
assert_eq!(lines.len(), 6);
assert_eq!(lines[5].radius, 0.001);
assert!(ledge::consume_lines(&[None; 6]).is_ok());
assert!(ledge::consume_lines(&[None; 7]).is_err());
}
@@ -0,0 +1,158 @@
//! Pre-migration runtime restored for the Ground build checkpoint.
//! Its known Air lifecycle defects are NOT repaired or validated by this rollback.
//! The separately compiled recovered module is the unfinished replacement.
//! Persistent BipedAir owner storage and lifecycle publication.
//!
//! The native object retains the launch packet, cadence, phase timing and
//! output vectors across Enter/Update/Post/Output/Exit. Keeping that state in
//! one owner prevents the old failure mode where a transient launch result was
//! mistaken for the whole off-board state.
use super::{air_launch::Packet, cadence::BipedCadence};
pub type Vector = [f32; 4];
pub type Frame = [Vector; 4];
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Input {
pub position_592: Vector,
pub velocity_608: Vector,
pub up_544: Vector,
pub forward_224: Vector,
pub board_position_112: Vector,
pub departure_geometry: Option<super::air_launch::DepartureGeometry>,
pub velocity_912: Vector,
pub flags_2472: u32,
pub flags_2476: u32,
pub flags_2480: u32,
pub previous_state_2504: u32,
pub current_state_2508: u32,
pub current_category_2512: u32,
pub previous_category_2516: u32,
pub raw_x_2692: f32,
pub raw_z_2688: f32,
pub current: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Output {
pub packet: Packet,
pub position_288: Vector,
pub velocity_304: Vector,
pub phase_444: f32,
pub phase_counter_448: f32,
pub update_count_452: u32,
pub landed_404: bool,
pub wipeout_547: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct State {
pub packet: Packet,
pub cadence: BipedCadence,
pub position_288: Vector,
pub velocity_304: Vector,
pub phase_444: f32,
pub phase_counter_448: f32,
pub update_count_452: u32,
pub landed_404: bool,
pub wipeout_547: bool,
pub active: bool,
}
impl Default for State {
fn default() -> Self {
Self {
packet: Packet::initialized(0.0),
cadence: BipedCadence::default(),
position_288: [0.0; 4],
velocity_304: [0.0; 4],
phase_444: 0.0,
phase_counter_448: 0.0,
update_count_452: 0,
landed_404: false,
wipeout_547: false,
active: false,
}
}
}
impl State {
///82D2E5C0: clear the retained landing/output flags and enter with a
///fresh launch packet. The caller supplies the actual retained scalar.
pub fn enter(&mut self, retained_packet_scalar_104: f32, position: Vector) {
self.packet = Packet::initialized(retained_packet_scalar_104);
self.position_288 = position;
self.velocity_304 = [0.0; 4];
self.phase_444 = 0.0;
self.phase_counter_448 = 0.0;
self.update_count_452 = 0;
self.landed_404 = false;
self.wipeout_547 = false;
self.active = true;
}
///82D2EF20: the native exit clears the trajectory selector and leaves no
///live BipedAir publication behind.
pub fn exit(&mut self) {
self.active = false;
self.landed_404 = false;
}
pub fn update(
&mut self,
input: &Input,
controller: &super::controller::State,
turn_vs_speed: &crate::point_graph::PointGraph<8>,
settings: super::air_launch::Settings,
) -> Result<(), &'static str> {
if !self.active {
return Ok(());
}
let processed = super::air_launch::Processed {
board_position_112: input.board_position_112,
forward_224: input.forward_224,
up_544: input.up_544,
position_592: input.position_592,
velocity_608: input.velocity_608,
velocity_912: input.velocity_912,
departure_geometry: input.departure_geometry,
flags_2472: input.flags_2472,
flags_2476: input.flags_2476,
flags_2480: input.flags_2480,
previous_state_2504: input.previous_state_2504,
current_state_2508: input.current_state_2508,
current_category_2512: input.current_category_2512,
previous_category_2516: input.previous_category_2516,
raw_x_2692: input.raw_x_2692,
raw_z_2688: input.raw_z_2688,
};
super::air_launch::produce(
&mut self.packet,
controller,
turn_vs_speed,
settings,
&processed,
input.current,
)?;
self.position_288 = self.packet.position_32;
self.velocity_304 = self.packet.velocity_0;
self.phase_counter_448 += f32::from_bits(0x3c888889);
self.update_count_452 = self.update_count_452.wrapping_add(1);
Ok(())
}
pub fn output(&self) -> Output {
Output {
packet: self.packet,
position_288: self.position_288,
velocity_304: self.velocity_304,
phase_444: self.phase_444,
phase_counter_448: self.phase_counter_448,
update_count_452: self.update_count_452,
landed_404: self.landed_404,
wipeout_547: self.wipeout_547,
}
}
}
pub mod recovered;
@@ -0,0 +1,124 @@
//! Air-owned response82D2F748; never changes the shared contact/joint solver.
use super::{DT, Frame, State, Vector, math::*};
use crate::player::offboard::air_launch::Packet;
#[cfg(test)]
#[path = "collision_tests.rs"]
mod tests;
#[derive(Clone, Copy, Debug)]
pub struct Response {
///82D2F93C..950: byte52/value184/requestcounter200.
pub request_52: bool,
pub restart: Option<Vector>,
}
///82C1E170: remove only a positive component along the supplied direction.
///Its dot uses the normalized direction, subtraction uses the original one.
fn remove_positive(value: Vector, direction: Vector) -> Vector {
let component = dot(value, normalize_or(direction, ZERO));
if component > 0. {
sub(value, scale(direction, component))
} else {
value
}
}
impl State {
pub fn collision_response(
&mut self,
errors_16288_16304: [Vector; 2],
up_544: Vector,
restart_allowed_8493: bool,
) -> Response {
//82D2F7A8..888: the sign masks take ABSOLUTE projections and
//absolute component products. This is not ordinary plane rejection.
let vertical = errors_16288_16304.map(|v| dot(v, up_544).abs());
let residual = std::array::from_fn::<_, 2, _>(|i| {
length(sub(
errors_16288_16304[i],
scale(up_544, vertical[i]).map(f32::abs),
))
});
//82D2F918..938 tests BOTH projections and BOTH residual lengths.
let request_52 = vertical.iter().chain(&residual).any(|&v| v > 0.2);
//82D2F810/818/830/838: original permute inserts zeroY and repeatsX inW.
let planar = errors_16288_16304.map(|v| [v[0], 0., v[2], v[0]]);
let sizes = planar.map(length);
let mut response = Response {
request_52,
restart: None,
};
//82098E1C,82098E28,82098E2C all contain3C23D70A in original S3.
if !(sizes[0] > 0.01 || sizes[1] > 0.01) {
return response;
}
if self.flags_544_550[6] {
self.flags_544_550[3] = true; //82D2FA10..20
return response;
}
//82D2FA24..40: strict greater; a tie chooses16304.
let normal = normalize(if sizes[0] > sizes[1] {
planar[0]
} else {
planar[1]
});
let velocity = self.result.velocity_288;
if (sizes[0] > 0.01 || sizes[1] > 0.01) && dot(normal, velocity) < -10. {
self.flags_544_550[3] = true; //82D2FA70..FB44
}
if !(dot(velocity, normal) < 0.) {
return response;
} //82D2FB4C..5C
let above_root =
self.result.scalar_392 > self.frame_208[3][1] && self.result.normal_304[1] > 0.7; //82D2FB64..BF0
if restart_allowed_8493 && !above_root {
response.restart = Some(normal); //82D2FBF8..FC14
}
response
}
///82D2FC18..FD58: caller first produces the complete native launch packet.
///Secondary velocity and all other producer fields retain their own values.
pub fn restart_packet(
&mut self,
mut packet: Packet,
normal: Vector,
position_592: Vector,
up_544: Vector,
foot_height: f32,
) -> Packet {
self.flags_544_550[2] = true;
//The source computes two normalized directions; the second explicitly
//zerosY. Both are planar here becauseF810..838 already removedY.
let horizontal = normal;
let first = remove_positive(self.result.velocity_288, scale(horizontal, -1.));
let second = remove_positive(first, scale(normal, -1.));
packet.velocity_0 = madd(normal, 1., second); //82D2FCC4
packet.board_position_80 = madd(up_544, foot_height, position_592); //FD44
packet.position_32 = madd(packet.velocity_0, DT, position_592); //FD48
packet.has_board_position_116 = true; //FD30
self.restart_normal_528 = normal; //FD28
packet
}
///Only after the host successfully submits the restart82D6CA58.
pub fn finish_restart(&mut self, effective_animation_frame: Frame, normal: Vector) {
self.frame_452 = 0; //82D2FD5C
self.flags_544_550[0] = false;
self.flags_544_550[1] = false;
self.frame_80 = effective_animation_frame; //82D2FD70..DA4
self.initial_up_480 = normal; //82D2FDA8, NOT frame80's up
}
///82D2F490..534 adjusts the current sample after a restart; it does NOT
///resample or overwrite the persistent selector's new trajectory.
pub fn correct_restarted_sample(&mut self) {
if self.flags_544_550[2] {
let step = scale(self.result.velocity_288, DT);
self.result.position_272 = add(
sub(self.result.position_272, step),
remove_positive(step, scale(self.restart_normal_528, -1.)),
);
}
}
}
@@ -0,0 +1,29 @@
use super::*;
#[test]
fn vertical_error_requests_bail_without_planar_restart() {
let mut state = State::default();
state.result.velocity_288 = [0., -3., 0., 0.];
let response = state.collision_response([[0., 0.21, 0., 0.], [0.; 4]], [0., 1., 0., 0.], true);
assert!(response.request_52);
assert!(response.restart.is_none());
}
#[test]
fn restart_changes_launch_and_current_sample_without_rewriting_secondary_velocity() {
let mut state = State::default();
state.result.velocity_288 = [-3., 1., 0., 0.];
state.result.position_272 = [2., 3., 4., 0.];
let response = state.collision_response([[0.02, 0., 0., 0.], [0.; 4]], [0., 1., 0., 0.], true);
let normal = response.restart.unwrap();
let mut packet = Packet::initialized(0.);
packet.secondary_velocity_16 = [5., 6., 7., 0.];
let result = state.restart_packet(packet, normal, [2., 3., 4., 0.], [0., 1., 0., 0.], -0.8);
assert_eq!(result.secondary_velocity_16, [5., 6., 7., 0.]);
for (actual, expected) in result.velocity_0[..3].iter().zip([1., 1., 0.]) {
assert!((actual - expected).abs() < 1e-5);
}
assert!(result.has_board_position_116);
let retained = state.result.velocity_288;
state.correct_restarted_sample();
assert_eq!(state.result.velocity_288, retained);
assert!((state.result.position_272[0] - (2. + 3. * DT)).abs() < 1e-6);
}
Loaded 100 of 434 files, more files were not shown because too many files have changed in this diff. Show more