From d39703e48c5c949c44ed16a50e1e16c048d50402 Mon Sep 17 00:00:00 2001 From: chazmm Date: Mon, 7 Sep 2026 20:30:09 +0100 Subject: [PATCH] Publish native grind direction and conditioned camera contact --- .../skate-game/src/physics/camera_output.rs | 18 +++--- crates/skate-game/src/physics/grind.rs | 21 +++++++ crates/skate-game/src/physics/grind_camera.rs | 58 +++++++++++++++++++ docs/grind-camera.md | 32 ++++++++++ 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 crates/skate-game/src/physics/grind_camera.rs create mode 100644 docs/grind-camera.md diff --git a/crates/skate-game/src/physics/camera_output.rs b/crates/skate-game/src/physics/camera_output.rs index 514e079d..ed147e77 100644 --- a/crates/skate-game/src/physics/camera_output.rs +++ b/crates/skate-game/src/physics/camera_output.rs @@ -2,7 +2,7 @@ //! Reset82DE53F0 runs before the actual producers82DB6EC0/82D3A388. use super::{GamePhysics, SkaterRuntime}; use crate::camera::{ - CameraAirOutput, CameraAnimationOutput, CameraEventsOutput, CameraGrindOutput, + CameraAirOutput, CameraAnimationOutput, CameraEventsOutput, CameraOffboardOutput, CameraPreferences, CameraPublicationInputs, CameraStateOutput, }; use skate_core::{ @@ -15,11 +15,16 @@ use skate_core::{ /// The user's selected normal High camera is graph type1. This custom world /// has no road/ledge/camera-volume annotations or other moving actors. pub(crate) fn advance( - physics: &GamePhysics, + physics: &mut GamePhysics, skater: &SkaterRuntime, feedback: &PhysicalFeedback, camera: &mut crate::camera::CameraRuntime, ) -> Result<(), String> { + //Once per physical publication, preserving conditioner history across + //direct grind-type changes and invalidating it on the first non-grind tick. + physics.grind.advance_camera( + skater.player_input.processed.vectors_400_416[0].map(f32::from_bits), + )?; let inputs = publish( physics, skater, @@ -137,12 +142,9 @@ pub(crate) fn publish( use_trajectory_331: 0, dropping_in_334: 0, }, - grinds: CameraGrindOutput { - //Reset82DE3518 loads original world-X literal82139A10. - direction_0: [1.0, 0.0, 0.0, 0.0], - camera_target_96: [0.0; 4], - grinding_316: u8::from(matches!(physical.grinds.words_136_140[1], 1 | 2)), - }, + //Shared grind Fill82D40EA0 publishes the directed rail tangent; + //conditioner82DF0640 supplies the continuous contact anchor. + grinds: physics.grind.camera_output(), events: CameraEventsOutput { intent_51: bit(intents, 28), preparing_52: bit(intents, 27), diff --git a/crates/skate-game/src/physics/grind.rs b/crates/skate-game/src/physics/grind.rs index adc6ee99..74488a18 100644 --- a/crates/skate-game/src/physics/grind.rs +++ b/crates/skate-game/src/physics/grind.rs @@ -17,7 +17,10 @@ use skate_core::{ }; use skate_data::collections::Collections; type V = [f32; 4]; +#[path = "grind_camera.rs"] +mod camera; pub(crate) struct Runtime { + camera: camera::GrindCamera, primitives: Vec, pub candidate: Option, pub name: String, @@ -50,6 +53,7 @@ impl Runtime { .words::<8>("physics_grinds", "default", "PinVsSlope")? .map(f32::from_bits); Ok(Self { + camera: camera::GrindCamera::default(), primitives: crate::grind_world::primitives(map)?, candidate: None, name: String::new(), @@ -98,6 +102,23 @@ impl Runtime { diagnostic_tick: 0, }) } + pub fn advance_camera(&mut self, velocity: V) -> Result<(), String> { + if self.active { + let contact = self.candidate.ok_or("Grind camera needs active contact")?; + self.camera.update(contact, self.primitives[contact.primitive], self.kind, velocity); + } else { + self.camera.exit(); + } + Ok(()) + } + pub fn camera_output(&self) -> crate::camera::CameraGrindOutput { + crate::camera::CameraGrindOutput { + //Reset82DE3518 defaults still apply outside the active state. + direction_0: if self.active { self.camera.direction } else { [1.0, 0.0, 0.0, 0.0] }, + camera_target_96: if self.active { self.camera.target } else { [0.0; 4] }, + grinding_316: u8::from(self.active), + } + } pub fn output(&self) -> GrindState { if !self.active { return GrindState::default(); diff --git a/crates/skate-game/src/physics/grind_camera.rs b/crates/skate-game/src/physics/grind_camera.rs new file mode 100644 index 00000000..d3ba0465 --- /dev/null +++ b/crates/skate-game/src/physics/grind_camera.rs @@ -0,0 +1,58 @@ +//! TU3 grind direction82D87460 and contact-point conditioner82DF0640. +use skate_core::physics::grind_contact::{FiftyFiftyCandidate, Primitive}; +use skate_core::riding::ground_correction_math::dot_product; +type V = [f32; 4]; + +#[derive(Default)] +pub(super) struct GrindCamera { + pub direction: V, + pub target: V, + previous_target: V, + offset: V, + midpoint: V, + kind: u32, + active: bool, +} +impl GrindCamera { + pub fn exit(&mut self) { + //82DF0610/82DF07EC invalidate history without erasing the rail tangent. + self.active = false; + } + + pub fn update(&mut self, contact: FiftyFiftyCandidate, edge: Primitive, kind: u32, velocity: V) { + //82D37048 supplies the normalized primitive tangent, independently of + //the deck's facing direction (including backwards50-50s/boardslides). + let delta: V = core::array::from_fn(|i| edge.end[i] - edge.start[i]); + let length = dot_product(delta, delta).sqrt(); + let mut direction = if length > 0.0 { delta.map(|v| v / length) } else { self.direction }; + let along = dot_product(direction, velocity); + if along < 0.0 { + direction = direction.map(|v| -v); + } + //82D874F0: native literals820641A8=.1 and822F8E94=-.9. + if along.abs() < 0.1 && dot_product(self.direction, direction) < -0.9 { + direction = direction.map(|v| -v); + } + self.direction = direction; + + let midpoint: V = core::array::from_fn(|i| (edge.start[i] + edge.end[i]) * 0.5); + if !self.active { + self.offset = [0.0; 4]; + self.target = contact.centre; + self.previous_target = contact.centre; + } else if kind != self.kind || midpoint[..3] != self.midpoint[..3] { + //Preserve extrapolated motion across a change of contact owner. + self.offset = core::array::from_fn(|i| { + self.target[i] + (self.target[i] - self.previous_target[i]) - contact.centre[i] + }); + } + //Constructor82DF24CC loads literal82072818, .95 per physical tick. + //This decays a contact discontinuity, not ordinary camera movement. + self.offset = self.offset.map(|v| v * 0.95); + self.previous_target = self.target; + self.target = core::array::from_fn(|i| contact.centre[i] + self.offset[i]); + self.midpoint = midpoint; + self.kind = kind; + self.active = true; + } +} diff --git a/docs/grind-camera.md b/docs/grind-camera.md new file mode 100644 index 00000000..23578b4b --- /dev/null +++ b/docs/grind-camera.md @@ -0,0 +1,32 @@ +# Grind camera publication + +The camera adapter previously published Grinds0 as world X and Grinds96 as +world origin even during active grinds. The native movement compass82DF3AC0 +uses Grinds0 directly while grinding, and GrindAnchor82DF2C78 consumes Grinds96. +This explains a heading snap followed by an unchanging direction, with an +incorrect positional anchor as well. + +The live adapter now supplies: + +- Directed primitive tangent:82D87460 calls82D37048, flips against velocity, + and preserves the previous direction when absolute along-speed is below + .1 and the direction dot is below -.9. These literals are820641A8/822F8E94. +- Contact target:82DF0640 initializes on entry, extrapolates the previous two + targets on changes to grind type or primitive midpoint, then decays the + resulting offset by .95 each physical tick. Constructor82DF24CC loads this + coefficient from82072818. Inactive ticks invalidate target history. + +This is contact discontinuity conditioning, not a general camera latency +filter. Existing camera compass, anchors and authored camera graph remain the +consumers. Straight authored/imported primitives supply their actual endpoints. + +Evidence: local TU3 native image +`Skate3Research/research/reverse-engineering/XexDump/default_82000000_011B0000.bin`, +base82000000, SHA256 +`f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`. +Instruction listings were extracted from the generated PPC source into +`logs/biped-82D87460.txt`, `logs/biped-82D37048.txt`, +`logs/biped-82DF0640.txt`, and `logs/biped-82DF2130.txt`; coefficients were +cross-checked directly against image bytes. Behavior above is observed static +code; the reported visual symptom is consistent with the missing publications. +No reference gameplay trace or automated gameplay testing was performed.