diff --git a/crates/skate-core/src/animation/grind_control.rs b/crates/skate-core/src/animation/grind_control.rs new file mode 100644 index 00000000..2475aa2e --- /dev/null +++ b/crates/skate-core/src/animation/grind_control.rs @@ -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 { + 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 } +} diff --git a/crates/skate-core/src/animation/mod.rs b/crates/skate-core/src/animation/mod.rs index d6ec12b0..8a5b74f8 100644 --- a/crates/skate-core/src/animation/mod.rs +++ b/crates/skate-core/src/animation/mod.rs @@ -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; diff --git a/crates/skate-core/src/physics/grind_contact.rs b/crates/skate-core/src/physics/grind_contact.rs index 9f2442da..baf6c616 100644 --- a/crates/skate-core/src/physics/grind_contact.rs +++ b/crates/skate-core/src/physics/grind_contact.rs @@ -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; 2], +) -> Option { + 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. diff --git a/crates/skate-core/src/physics/grind_forces.rs b/crates/skate-core/src/physics/grind_forces.rs new file mode 100644 index 00000000..c93cb5ee --- /dev/null +++ b/crates/skate-core/src/physics/grind_forces.rs @@ -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) } diff --git a/crates/skate-core/src/physics/mod.rs b/crates/skate-core/src/physics/mod.rs index 4daaf636..411821aa 100644 --- a/crates/skate-core/src/physics/mod.rs +++ b/crates/skate-core/src/physics/mod.rs @@ -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; diff --git a/docs/GRIND_INTEGRATION.md b/docs/GRIND_INTEGRATION.md index f30c835f..67331d9c 100644 --- a/docs/GRIND_INTEGRATION.md +++ b/docs/GRIND_INTEGRATION.md @@ -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.