Recover native fifty-fifty forces and grind animation controls

This commit is contained in:
chazmm committed 2026-09-07 18:07:55 +01:00
1 parent 6bb16a6827
commit 88b51467e9
6 files changed
+234

No files matched your search

@@ -0,0 +1,69 @@
//! TU3 GrindControlFade82BB0D40 and ControlGrindCrouch82BB10F8.
//! Animation attributes, physical observations and collection values remain
//! explicit inputs; these routines do not choose the grind or animation.
#[derive(Clone, Copy, Debug)]
pub struct FadeSettings {
///Collection owner312: offsets1640,1644,1648,1652.
pub response: f32,
pub input_scale: f32,
pub acceleration: f32,
pub maximum_step: f32,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Fade {
pub just_began: bool,
pub elapsed: f32,
pub value: f32,
pub target: f32,
pub step: f32,
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.
pub fn begin(&mut self, minimum: f32, maximum: f32, twist: f32) -> f32 {
*self = Self {
just_began: true,
minimum,
maximum,
value: bound(twist, minimum, maximum),
target: bound(twist, minimum, maximum),
..Self::default()
};
self.value
}
///The first Update after Begin only clears byte8; it emits no attribute.
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.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));
let step = dt * rate;
bound(desired, previous - step, previous + step)
}
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 }
}
+1
View File
@@ -13,6 +13,7 @@ pub mod phase_blend;
pub mod pose_blend;
pub mod playback_transition;
pub mod crouching;
pub mod grind_control;
pub mod pose_sample;
pub mod pose_trajectory;
pub mod pose_add;
@@ -17,6 +17,68 @@ pub struct TruckContact {
pub primitive: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct FiftyFiftyCandidate {
pub direction: V,
pub centre: V,
pub front: V,
pub rear: V,
pub primitive: usize,
}
///82D89150's same-primitive contact branch, before shared admission82D886B8.
/// Linked/adjacent primitive arbitration belongs to the caller; this boundary
/// explicitly returns none for that case instead of guessing an owner.
pub fn fifty_fifty_candidate(
board: [V; 4],
balance: [f32; 2],
contacts: [Option<TruckContact>; 2],
) -> Option<FiftyFiftyCandidate> {
let [Some(front), Some(rear)] = contacts else { return None };
if balance[0].abs().max(balance[1].abs()) >= 0.9
|| front.primitive != rear.primitive
{
return None;
}
let front_depth = dot3(sub(board[3], front.position), board[1]);
let rear_depth = dot3(sub(board[3], rear.position), board[1]);
if front_depth.max(rear_depth) >= 0.13 {
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()),
centre: scale(add(front.position, rear.position), 0.5),
front: front.position,
rear: rear.position,
primitive: front.primitive,
})
}
///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();
if length <= 0.0 { return [1.0, 0.0, 0.0, 0.0]; }
if normal[1] < 0.0 { normal = scale(normal, -1.0); }
scale(normal, length.recip())
}
///82D88518, common active-grind angular admission. Plane projection occurs
/// before the direction comparison; vertical speed is not an approach angle.
pub fn within_approach_angle(direction: V, velocity: V, degrees: f32) -> bool {
let normal = upright_normal(direction);
let projected = sub(velocity, scale(normal, dot3(velocity, normal)));
let length = dot3(projected, projected).sqrt();
if length <= 0.001 { return true; }
let alignment = dot3(scale(projected, length.recip()), direction).abs();
alignment > (degrees * f32::from_bits(0x3c8e_fa35)).cos()
}
/// Truck rectangles from82C1FDC0. The geometry dimensions come from the
/// physics_grinds collection (TruckToWheel584, DeckCenterToTruck636).
/// Output order is positive board-forward truck, negative board-forward truck.
@@ -0,0 +1,59 @@
//! 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];
///82D3FA18, called by the 50-50 update82D41D70 with800,0,.07.
/// Returns a force at the deck origin; never moves the body to a spline.
pub fn lateral_pin(
board: [V; 4],
point: V,
across: V,
velocity: V,
strength: f32,
forward_offset: f32,
up_offset: f32,
forward_selected: bool,
slope_multiplier: f32,
) -> V {
let direction = if forward_selected { board[2] } else { scale(board[2], -1.0) };
let reference = core::array::from_fn(|i| {
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 axis = scale(force, length.recip());
let damping = scale(axis, strength * f32::from_bits(0x3e08_3127) * dot3(velocity, axis));
scale(sub(force, damping), slope_multiplier)
}
///82D3FD88 after its native surface/material selection. It damps velocity in
/// the plane perpendicular to the grind normal, including the along-rail lane.
pub fn friction(
velocity: V,
grind_normal: V,
support: V,
time_multiplier: f32,
flagged_surface: bool,
surface_multiplier: f32,
engagement: 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();
if speed <= 0.001 {
return [0.0; 4];
}
let strength = strengths[match engagement { 0 => 0, 1 => 1, _ => 2 }];
let load = support[1].max(0.0);
let surface_flag_multiplier = if flagged_surface { 1.9 } else { 1.0 };
let multiplier = load / speed * time_multiplier * surface_flag_multiplier
* surface_multiplier * strength * f32::from_bits(0xbef5_c28f);
scale(tangent_velocity, multiplier)
}
fn sub(a: V, b: V) -> V { core::array::from_fn(|i| a[i] - b[i]) }
fn scale(a: V, scale: f32) -> V { a.map(|v| v * scale) }
+1
View File
@@ -41,6 +41,7 @@ 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;
+42
View File
@@ -84,3 +84,45 @@ capture infrastructure rather than a standalone grind controller.
The remaining native state/force and graph-attribute contracts are unresolved.
Do not advertise this build as grind-ready. Build log:
logs/grind-collision-build.log (successful,35 warnings); no tests executed.
## Additional static recovery
The reference process was closed at the user's request. Continue using static
analysis; do not relaunch it. IDA database
`research/reverse-engineering/XexDump/Skate3TU3_dump.i64` was copied into local
ignored `logs/grind-research-copy.i64`. The original was not modified. The
database has sparse function boundaries; bounded definitions are needed even
where the community symbol map supplies a name. VMX128 decompilation remains
unreliable; cross-check vector operations against generated instruction text.
New core routines (not yet called by the game's active state):
- `fifty_fifty_candidate`:82D89150 same-primitive two-truck branch. Rejects
balance magnitude >=.9 and local-up contact depth >=.13. Adjacent-segment
arbitration is deliberately outside this routine.
- `upright_normal`:82D370F8; the vertical-line fallback is the actual constant
X axis at82139A10. `within_approach_angle`:82D88518's plane projection and
strict cosine comparison.
- `grind_forces::lateral_pin`:82D3FA18. Native50-50 caller82D41D70 supplies
strength800, forward offset0, up offset.07, damping scalar.133.
- `grind_forces::friction`:82D3FD88 after material selection. Receives actual
support, material and engagement inputs; does not invent them. Native50-50
caller supplies strength50 for engagement0,40 otherwise.
- `animation::grind_control::Fade`:82BB0D40; Begin82BB0A30 evaluates attribute
bounds. First Update only clears the Begin latch. Later updates apply the
native target, response, acceleration and step bounds.
- `grind_control::crouch`:82BB10F8. Begin82BB1078 seeds from physical crouching
output. Runtime collection names for owner312 offsets1640..1652 and owner340
offsets1760/1792/1816 still need resolving before host integration.
Vtable823283D0 identifies50-50: Enter82D3F318, Exit82D3F430,
PreUpdate82D3F4E8, Update82D41D70, Fill82D41FF8. Its angular target path
82D41E38 ->82D40890 uses existing matrix interpolation82BD3150 with weight.1,
then hook target82BD4318 and angular-only hook drive82C05658. It does not
teleport the deck to a spline. This should use the existing core interpolation
and hook implementation when connected.
The playable-state integration is still incomplete. In particular no selector
candidate is published, the active state adapter is absent, and MotionHost
still rejects the unbound stock grind behaviors. These ports alone are not a
grind-ready game build.