mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Reconcile archive offboard and grind owners with retained game features
This commit is contained in:
1 parent
ae452ba3fe
commit
edce9c8e7d
434 files changed
+35475
-6522
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(¢er(b)).then(a.cmp(&b)));
|
||||
self.order[range.clone()].select_nth_unstable_by(range.len() / 2, |&a, &b| {
|
||||
center(a).total_cmp(¢er(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);
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in new issue
Block a user