Author test-course rails and native grind spline geometry

This commit is contained in:
chazmm committed 2026-09-07 17:21:57 +01:00
1 parent a5fab9828a
commit cca746fbde
5 files changed
+202

No files matched your search

+1
View File
@@ -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,
+141
View File
@@ -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<Rail>,
pub native_blob: Vec<u8>,
}
pub(crate) struct GrindGeometryPlugin;
impl Plugin for GrindGeometryPlugin {
fn build(&self, app: &mut App) {
let test_world = app.world().resource::<crate::config::Config>().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<u8> {
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::<f32>().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()); }
}
+1
View File
@@ -17,6 +17,7 @@ mod render_capacity;
mod presentation;
mod replay;
mod world;
mod grind_world;
mod skate_world;
fn main() -> bevy::app::AppExit {
+1
View File
@@ -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;
+58
View File
@@ -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.