diff --git a/crates/skate-game/src/app.rs b/crates/skate-game/src/app.rs index c668475f..1d770685 100644 --- a/crates/skate-game/src/app.rs +++ b/crates/skate-game/src/app.rs @@ -95,6 +95,7 @@ pub(crate) fn build( assets::GameAssetsPlugin, animation::AnimationPlugin, world::WorldPlugin, + crate::grind_world::GrindGeometryPlugin, camera::CameraPlugin, crate::graphics_menu::GraphicsMenuPlugin, crate::render_capacity::RenderCapacityPlugin, diff --git a/crates/skate-game/src/grind_world.rs b/crates/skate-game/src/grind_world.rs new file mode 100644 index 00000000..3f14c89b --- /dev/null +++ b/crates/skate-game/src/grind_world.rs @@ -0,0 +1,141 @@ +//! Authored test-course grind paths. Collision/visuals share these dimensions. +//! This asset owner does not claim a nearby spline is an acquired grind. +use skate_core::math::Vector3; +use bevy::prelude::*; + +/// Retained geometry for the forthcoming native contact/scorer adapter. This +/// deliberately does not publish selector candidate1488 or enter state400. +#[derive(Resource)] +pub(crate) struct GrindGeometry { + pub rails: Vec, + pub native_blob: Vec, +} + +pub(crate) struct GrindGeometryPlugin; +impl Plugin for GrindGeometryPlugin { + fn build(&self, app: &mut App) { + let test_world = app.world().resource::().map.is_none(); + let geometry = if test_world { + GrindGeometry { rails: rails().to_vec(), native_blob: spline_blob() } + } else { + GrindGeometry { rails: Vec::new(), native_blob: Vec::new() } + }; + if test_world { + info!("GRIND_GEOMETRY rails={} bytes={} native_acquisition=not_connected", + geometry.rails.len(), geometry.native_blob.len()); + for rail in &geometry.rails { + info!("GRIND_RAIL id={} name={} start={:?} end={:?}", + rail.id, rail.name, rail.start, rail.end); + } + } + app.insert_resource(geometry); + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct Rail { + pub id: u64, + pub name: &'static str, + pub start: Vector3, + pub end: Vector3, + pub width: f32, + pub thickness: f32, +} + +pub(crate) fn rails() -> [Rail; 7] { + let floor = crate::physics::ground::FLOOR_HEIGHT; + let top = crate::physics::ground::HEIGHT; + let v = Vector3::new; + [ + Rail { id: 1, name: "low_flat_rail", start: v(-7., floor + 0.45, 7.), + end: v(-7., floor + 0.45, 15.), width: 0.10, thickness: 0.10 }, + Rail { id: 2, name: "high_flat_rail", start: v(4.5, floor + 0.70, 8.), + end: v(4.5, floor + 0.70, 16.), width: 0.10, thickness: 0.10 }, + Rail { id: 3, name: "halfpipe_left_coping", start: v(7.5, floor + 2.5, -6.), + end: v(7.5, floor + 2.5, 6.), width: 0.08, thickness: 0.08 }, + Rail { id: 4, name: "halfpipe_right_coping", start: v(16.5, floor + 2.5, -6.), + end: v(16.5, floor + 2.5, 6.), width: 0.08, thickness: 0.08 }, + Rail { id: 5, name: "platform_back_edge", start: v(-3., top, -4.), + end: v(3., top, -4.), width: 0., thickness: 0. }, + Rail { id: 6, name: "platform_right_edge", start: v(3., top, -4.), + end: v(3., top, 4.), width: 0., thickness: 0. }, + Rail { id: 7, name: "platform_left_edge", start: v(-3., top, 4.), + end: v(-3., top, -4.), width: 0., thickness: 0. }, + ] +} + +/// Box rails have their spline on the top centre, not the centre of the tube. +/// Coping is flush with the deck lip; the existing transition remains intact. +pub(crate) fn surfaces() -> Vec<[Vector3; 4]> { + let mut out = Vec::new(); + let floor = crate::physics::ground::FLOOR_HEIGHT; + for rail in rails() { + // Ledge paths use the already authored platform collision. + if rail.width == 0. { continue; } + let x = rail.start.x; + let y = rail.start.y; + box_faces(&mut out, Vector3::new(x - rail.width * 0.5, y - rail.thickness, rail.start.z), + Vector3::new(x + rail.width * 0.5, y, rail.end.z)); + if rail.id <= 2 { + for z in [rail.start.z + 0.6, rail.end.z - 0.6] { + box_faces(&mut out, Vector3::new(x - 0.04, floor, z - 0.04), + Vector3::new(x + 0.04, y - rail.thickness, z + 0.04)); + } + } + } + out +} + +fn box_faces(out: &mut Vec<[Vector3; 4]>, min: Vector3, max: Vector3) { + let v = Vector3::new; + let top = [v(min.x, max.y, min.z), v(max.x, max.y, min.z), + v(max.x, max.y, max.z), v(min.x, max.y, max.z)]; + out.push(top); + for i in 0..4 { + let a = top[i]; let b = top[(i + 1) % 4]; + out.push([b, a, v(a.x, min.y, a.z), v(b.x, min.y, b.z)]); + } + out.push([v(min.x, min.y, max.z), v(max.x, min.y, max.z), + v(max.x, min.y, min.z), v(min.x, min.y, min.z)]); +} + +/// Relocatable Pegasus tSplineData, matching the project's established native +/// builder (owned/world/src/grind_spline.cpp). One straight segment per rail. +/// Guest pointers stay blob-relative; the Rust owner retains stable rail IDs. +pub(crate) fn spline_blob() -> Vec { + let rails = rails(); + let segments = 16 + rails.len() * 32; + let mut bytes = vec![0; segments + rails.len() * 144]; + word(&mut bytes, 0, rails.len() as u32); + word(&mut bytes, 4, rails.len() as u32); + word(&mut bytes, 8, 16); + word(&mut bytes, 12, segments as u32); + for (i, rail) in rails.iter().enumerate() { + let r = 16 + i * 32; + let s = segments + i * 144; + bytes[r..r + 8].copy_from_slice(&rail.id.to_be_bytes()); + bytes[r + 8..r + 16].copy_from_slice(&0x2c7017070007004au64.to_be_bytes()); + word(&mut bytes, r + 20, s as u32); + word(&mut bytes, r + 24, s as u32); + let delta = [rail.end.x - rail.start.x, rail.end.y - rail.start.y, + rail.end.z - rail.start.z]; + let length = delta.iter().map(|v| v * v).sum::().sqrt(); + vector(&mut bytes, s, [delta[0], delta[1], delta[2], 0.]); + vector(&mut bytes, s + 48, [rail.start.x, rail.start.y, rail.start.z, 1.]); + vector(&mut bytes, s + 64, [1. / length, 0., 0., 0.]); + vector(&mut bytes, s + 80, [rail.start.x.min(rail.end.x), + rail.start.y.min(rail.end.y), rail.start.z.min(rail.end.z), 0.]); + vector(&mut bytes, s + 96, [rail.start.x.max(rail.end.x), + rail.start.y.max(rail.end.y), rail.start.z.max(rail.end.z), 0.]); + word(&mut bytes, s + 112, length.to_bits()); + word(&mut bytes, s + 120, r as u32); + } + bytes +} + +fn word(bytes: &mut [u8], at: usize, value: u32) { + bytes[at..at + 4].copy_from_slice(&value.to_be_bytes()); +} +fn vector(bytes: &mut [u8], at: usize, value: [f32; 4]) { + for (i, v) in value.into_iter().enumerate() { word(bytes, at + i * 4, v.to_bits()); } +} diff --git a/crates/skate-game/src/main.rs b/crates/skate-game/src/main.rs index 67f9ca32..4b1f4e82 100644 --- a/crates/skate-game/src/main.rs +++ b/crates/skate-game/src/main.rs @@ -17,6 +17,7 @@ mod render_capacity; mod presentation; mod replay; mod world; +mod grind_world; mod skate_world; fn main() -> bevy::app::AppExit { diff --git a/crates/skate-game/src/physics/ground.rs b/crates/skate-game/src/physics/ground.rs index 55b7ffb1..098b03f9 100644 --- a/crates/skate-game/src/physics/ground.rs +++ b/crates/skate-game/src/physics/ground.rs @@ -64,6 +64,7 @@ pub(crate) fn surfaces() -> [Vec<[Vector3; 4]>; 3] { v(-3.0, y1, z1), ]); } + platform.extend(crate::grind_world::surfaces()); // Open ends at z +/-6 allow entry from the landing floor. Circular // transitions meet the four-metre flat bottom tangentially. let radius = 2.5; diff --git a/docs/GRIND_INTEGRATION.md b/docs/GRIND_INTEGRATION.md new file mode 100644 index 00000000..909b35e6 --- /dev/null +++ b/docs/GRIND_INTEGRATION.md @@ -0,0 +1,58 @@ +# Grinding integration + +## Current milestone: authored geometry, not playable grinding + +The test course now owns seven named paths in `grind_world.rs`: two flat bars, +both half-pipe lips, and the three exposed platform edges. The downhill entrance +is not an edge, and triangle diagonals/transition tessellation are not splines. +Bar top centres and coping paths share the collision/render dimensions. +Climbing geometry from the other task is preserved. + +`GrindGeometry` retains a relocatable Pegasus tSplineData blob. Its layout matches +the existing recomp's `owned/world/src/grind_spline.cpp`: 16-byte header, +32-byte rail records, 144-byte segments, signature `2C7017070007004A`, straight +segment delta at +0, origin at +48, inverse length at +64, bounds +80/+96, +length +112, cumulative length +116, and owner/previous/next +120/+124/+128. +IDs are stable host-authored integers, not guessed retail asset IDs. + +This registry is not yet connected to contact acquisition or state selection. +Startup explicitly logs `native_acquisition=not_connected`. The game therefore +still treats the bars as ordinary collision geometry. Do not call this playable +grinding or replace the existing empty-edge providers with a proximity snap. + +## Native integration audit + +Evidence image: TU3 `default_82000000_011B0000.bin`, base `82000000`, SHA-256 +`f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`. +Community symbols plus generated PPC instruction comments were consulted. +The disposable IDA database's decompiler misdecodes some VMX128 operations; +its truncated pseudocode is not sufficient to port the nonempty branches. + +- `82D8A828` GrindManager input update: constructs an AABB around processed + position112 with half-extent1.2 (`821659F4`), unless byte2476 bit0 disables it. +- `82C1EAD8`: static asset/segment bounds collection, source order, maximum40; + entries are48 bytes with endpoints and retained owner/flags. The final16 + bytes must not be invented as a surface normal. +- `82D875A8`: candidate acquisition/scoring. Dispatches truck/deck contact + queries (`82C1FDC0`, `82C20130`, `82C1FB98`, `82C1F968`) before scoring. For + example `82D89150` handles a two-truck candidate; it is not a nearest-rail + angle classifier. Selection output has candidate byte384 in a416-byte record. +- `82D8AB08`: post-input manager publication precedes state selection. +- Native selector maps actual candidate grind type0..5 to states401,400,402, + 403,404,405. `player_state/selection.rs` currently publishes no candidate. +- Boardslide Enter `82D41100` calls shared grind entry `82D3F318`, then clears + bytes240/241. Shared entry resets board controller settings, skeleton state + and the grind manager; merely allowing state400 in transition.rs is unsafe. +- The active force/contact owner, output conditioner and graph attributes + still need wiring. `player_state/publication.rs` currently uses default grind + output; camera output has the same inactive assumption. +- Air acquisition also needs the nonempty trajectory path: `82D69F80`, + `82D6A168`, `82D6A398`, `82D6A840` and GrindAirAdjust `82D712E0` onward. + The clone's GRIND_CONTACT_SPEC explicitly leaves parts of this arbitration + unresolved, so it cannot serve as a complete implementation. + +Next implementation boundary: native endpoint/contact queries and candidate +record/scorer, then lifecycle/constraint/output integration as one coherent +vertical slice. Do not enable selector candidates before those owners exist. + +Validation: build only; no gameplay or automated tests, per user instruction.