mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Connect authored and retail map splines to live grind queries
This commit is contained in:
1 parent
2c2ba09877
commit
411a0f652d
7 files changed
+180
-84
No files matched your search
@@ -33,13 +33,28 @@ pub fn fifty_fifty_candidate(
|
||||
board: [V; 4],
|
||||
balance: [f32; 2],
|
||||
contacts: [Option<TruckContact>; 2],
|
||||
) -> Option<FiftyFiftyCandidate> {
|
||||
fifty_fifty_candidate_on_splines(board, balance, contacts, &[])
|
||||
}
|
||||
|
||||
///82D89150 also accepts two segment indices when their hits pass its signed
|
||||
/// cross-rail separation check. Splines with multiple segments need this path.
|
||||
pub fn fifty_fifty_candidate_on_splines(
|
||||
board: [V; 4], balance: [f32; 2],
|
||||
contacts: [Option<TruckContact>; 2], primitives: &[Primitive],
|
||||
) -> 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
|
||||
{
|
||||
if balance[0].abs().max(balance[1].abs()) >= 0.9 {
|
||||
return None;
|
||||
}
|
||||
if front.primitive != rear.primitive {
|
||||
let edge = primitives.get(front.primitive)?;
|
||||
let delta = sub(edge.end, edge.start);
|
||||
let direction = scale(delta, dot3(delta, delta).sqrt().recip());
|
||||
let across = cross(upright_normal(direction), direction);
|
||||
//820641A8. This comparison is signed in the original routine.
|
||||
if dot3(sub(front.position, rear.position), across) > 0.1 { 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 {
|
||||
|
||||
@@ -99,43 +99,10 @@ fn box_faces(out: &mut Vec<[Vector3; 4]>, min: Vector3, max: Vector3) {
|
||||
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
|
||||
}
|
||||
mod spline;
|
||||
pub(crate) use spline::primitives;
|
||||
|
||||
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()); }
|
||||
/// Relocatable Pegasus tSplineData used by the live default-world query.
|
||||
pub(crate) fn spline_blob() -> Vec<u8> {
|
||||
spline::build(None).expect("static test-course splines are valid")
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//! SK8R15 owned/world/src/grind_spline.cpp and TU3 AssetRecord82C1E568.
|
||||
//! Keep the native cubic payload; the game's contact primitive is its chord
|
||||
//! D -> (A+B)+(C+D), including for retail cubic segments.
|
||||
use skate_core::physics::grind_contact::Primitive;
|
||||
use skate_data::skate_map::{Rail, SkateMap};
|
||||
|
||||
pub(super) fn build(map: Option<&SkateMap>) -> Result<Vec<u8>, String> {
|
||||
let defaults;
|
||||
let rails = if let Some(map) = map { &map.rails } else {
|
||||
defaults = super::rails().iter().map(|r| Rail {
|
||||
name: r.name.into(), closed: false, native: None,
|
||||
points: vec![[r.start.x,r.start.y,r.start.z], [r.end.x,r.end.y,r.end.z]],
|
||||
}).collect();
|
||||
&defaults
|
||||
};
|
||||
let mut records = Vec::new();
|
||||
for rail in rails {
|
||||
let mut header = [0u32; 6];
|
||||
let mut segments = Vec::new();
|
||||
if let Some(raw) = &rail.native {
|
||||
if raw.len() < 28 { return Err(format!("Truncated native rail {}", rail.name)); }
|
||||
// .skate is little endian; Pegasus blobs are big endian.
|
||||
let words: Vec<u32> = raw.chunks_exact(4)
|
||||
.map(|w| u32::from_le_bytes(w.try_into().unwrap())).collect();
|
||||
header.copy_from_slice(&words[..6]);
|
||||
// u64 fields use low/high words in the package.
|
||||
header.swap(0,1); header.swap(2,3);
|
||||
if words[6] as usize != (raw.len()-28)/120 || (raw.len()-28)%120 != 0 {
|
||||
return Err(format!("Invalid native rail segment count: {}", rail.name));
|
||||
}
|
||||
for segment in words[7..].chunks_exact(30) {
|
||||
segments.push(<[u32;30]>::try_from(segment).unwrap());
|
||||
}
|
||||
} else {
|
||||
if rail.points.len() < 2 { return Err(format!("Rail {} needs two points", rail.name)); }
|
||||
let mut id = 1469598103934665603u64;
|
||||
for byte in rail.name.bytes().chain(rail.points.iter().flat_map(|p|
|
||||
p.iter().flat_map(|f| f.to_bits().to_be_bytes()))) {
|
||||
id = (id ^ byte as u64).wrapping_mul(1099511628211);
|
||||
}
|
||||
header = [(id>>32) as u32,id as u32,0x2c701707,0x0007004a,0,0];
|
||||
let mut points = rail.points.clone();
|
||||
let first = points[0]; let last = *points.last().unwrap();
|
||||
let distance: f32 = (0..3).map(|i| (first[i]-last[i]).powi(2)).sum();
|
||||
if rail.closed && distance > 0.001*0.001 { points.push(first); }
|
||||
for pair in points.windows(2) {
|
||||
let [start,end] = [pair[0],pair[1]];
|
||||
let mut segment = [0u32;30];
|
||||
for i in 0..3 {
|
||||
let delta = end[i]-start[i];
|
||||
segment[i] = (-2.*delta).to_bits();
|
||||
segment[4+i] = (3.*delta).to_bits();
|
||||
segment[12+i] = start[i].to_bits();
|
||||
segment[20+i] = start[i].min(end[i]).to_bits();
|
||||
segment[24+i] = start[i].max(end[i]).to_bits();
|
||||
}
|
||||
segment[15] = 1f32.to_bits();
|
||||
segments.push(segment);
|
||||
}
|
||||
}
|
||||
if segments.is_empty() { return Err(format!("Rail {} has no segments",rail.name)); }
|
||||
for s in &segments {
|
||||
if s[..28].iter().any(|w| !f32::from_bits(*w).is_finite()) {
|
||||
return Err(format!("Non-finite spline data in {}",rail.name));
|
||||
}
|
||||
}
|
||||
records.push((header,segments));
|
||||
}
|
||||
let count: usize = records.iter().map(|r| r.1.len()).sum();
|
||||
let base = 16+records.len()*32;
|
||||
let mut bytes = vec![0; base+count*144];
|
||||
put(&mut bytes,0,records.len() as u32); put(&mut bytes,4,count as u32);
|
||||
put(&mut bytes,8,16); put(&mut bytes,12,base as u32);
|
||||
let mut at = base;
|
||||
for (index,(header,segments)) in records.iter().enumerate() {
|
||||
let r = 16+index*32;
|
||||
for i in 0..5 { put(&mut bytes,r+i*4,header[i]); }
|
||||
put(&mut bytes,r+20,at as u32);
|
||||
put(&mut bytes,r+24,(at+(segments.len()-1)*144) as u32);
|
||||
put(&mut bytes,r+28,header[5]);
|
||||
for (i,segment) in segments.iter().enumerate() {
|
||||
for (j,w) in segment.iter().enumerate() { put(&mut bytes,at+j*4,*w); }
|
||||
put(&mut bytes,at+120,r as u32);
|
||||
if i>0 { put(&mut bytes,at+124,(at-144) as u32); }
|
||||
if i+1<segments.len() { put(&mut bytes,at+128,(at+144) as u32); }
|
||||
at+=144;
|
||||
}
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn primitives(map: Option<&SkateMap>) -> Result<Vec<Primitive>,String> {
|
||||
let bytes = build(map)?;
|
||||
let word = |at| u32::from_be_bytes(bytes[at..at+4].try_into().unwrap());
|
||||
let mut result = Vec::new();
|
||||
let base = word(12) as usize;
|
||||
for i in 0..word(4) as usize {
|
||||
let s = base+i*144;
|
||||
let r = word(s+120) as usize;
|
||||
let coefficients: [[f32;4];4] = std::array::from_fn(|v|
|
||||
std::array::from_fn(|j| f32::from_bits(word(s+v*16+j*4))));
|
||||
let [a,b,c,d] = coefficients;
|
||||
let end = std::array::from_fn(|j| (a[j]+b[j])+(c[j]+d[j]));
|
||||
let length: f32 = (0..3).map(|j| (end[j]-d[j]).powi(2)).sum();
|
||||
if !length.is_finite() || length <= 0.000001 {
|
||||
return Err(format!("Spline segment {i} has a degenerate contact chord"));
|
||||
}
|
||||
result.push(Primitive { start:d,end,owner:((word(r) as u64)<<32)|word(r+4) as u64 });
|
||||
}
|
||||
bevy::log::info!("GRIND_RUNTIME splines={} primitives={} source={}",word(0),result.len(),
|
||||
map.map(|m|m.name.as_str()).unwrap_or("default test world"));
|
||||
Ok(result)
|
||||
}
|
||||
fn put(bytes:&mut [u8],at:usize,w:u32) { bytes[at..at+4].copy_from_slice(&w.to_be_bytes()); }
|
||||
@@ -146,7 +146,7 @@ impl GamePhysics {
|
||||
let (query, retention) = ground::query_settings();
|
||||
Ok(Self {
|
||||
clock: clock::SimulationClock::default(),
|
||||
grind: grind::Runtime::load(&data, map.is_none())?,
|
||||
grind: grind::Runtime::load(&data, map)?,
|
||||
board,
|
||||
riding,
|
||||
world,
|
||||
|
||||
@@ -32,25 +32,15 @@ pub(crate) struct Runtime {
|
||||
pub launch_velocity: V,
|
||||
cooldown: u32,
|
||||
previous_position: V,
|
||||
diagnostic_tick: u32,
|
||||
}
|
||||
impl Runtime {
|
||||
pub fn load(data: &Collections, test_world: bool) -> Result<Self, String> {
|
||||
pub fn load(data: &Collections, map: Option<&skate_data::skate_map::SkateMap>) -> Result<Self, String> {
|
||||
let graph = data
|
||||
.words::<8>("physics_grinds", "default", "PinVsSlope")?
|
||||
.map(f32::from_bits);
|
||||
Ok(Self {
|
||||
primitives: if test_world {
|
||||
crate::grind_world::rails()
|
||||
.iter()
|
||||
.map(|r| Primitive {
|
||||
start: lanes(r.start),
|
||||
end: lanes(r.end),
|
||||
owner: r.id,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
primitives: crate::grind_world::primitives(map)?,
|
||||
candidate: None,
|
||||
name: String::new(),
|
||||
active: false,
|
||||
@@ -77,6 +67,7 @@ impl Runtime {
|
||||
cooldown: 0,
|
||||
distance: 0.0,
|
||||
previous_position: [0.; 4],
|
||||
diagnostic_tick: 0,
|
||||
})
|
||||
}
|
||||
pub fn output(&self) -> GrindState {
|
||||
@@ -107,25 +98,36 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
runtime.manager_age = (runtime.manager_age + 0.0035).min(1.0);
|
||||
let old = runtime.candidate;
|
||||
runtime.candidate = None;
|
||||
if runtime.cooldown > 0 || skater.player_input.grind.disabled || p.flags_2476 & 0x01000000 != 0
|
||||
{
|
||||
return;
|
||||
}
|
||||
let hits = grind_contact::truck_contacts(
|
||||
board,
|
||||
p.flags_2484,
|
||||
runtime.truck_to_wheel,
|
||||
runtime.deck_to_truck,
|
||||
runtime.diagnostic_tick = runtime.diagnostic_tick.wrapping_add(1);
|
||||
let gated = runtime.cooldown > 0 || skater.player_input.grind.disabled
|
||||
|| p.flags_2476 & 0x01000000 != 0;
|
||||
// Native GrindData query82D8A828: a 1.2m half-extent, at most40 chords.
|
||||
let nearby: Vec<usize> = runtime.primitives.iter().enumerate().filter(|(_,edge)|
|
||||
(0..3).all(|i| edge.start[i].min(edge.end[i]) <= board[3][i]+1.2
|
||||
&& edge.start[i].max(edge.end[i]) >= board[3][i]-1.2))
|
||||
.map(|(i,_)|i).take(40).collect();
|
||||
if nearby.is_empty() { return; }
|
||||
let edges: Vec<Primitive> = nearby.iter().map(|i|runtime.primitives[*i]).collect();
|
||||
let mut hits = grind_contact::truck_contacts(
|
||||
board, p.flags_2484, runtime.truck_to_wheel, runtime.deck_to_truck, &edges,
|
||||
);
|
||||
for hit in hits.iter_mut().flatten() { hit.primitive = nearby[hit.primitive]; }
|
||||
let extra = &skater.animation_input.extra;
|
||||
let candidate = grind_contact::fifty_fifty_candidate_on_splines(
|
||||
board, [extra.grind_translation, extra.grind_stability_nudge], hits,
|
||||
&runtime.primitives,
|
||||
);
|
||||
let extra = &skater.animation_input.extra;
|
||||
let Some(candidate) = grind_contact::fifty_fifty_candidate(
|
||||
board,
|
||||
[extra.grind_translation, extra.grind_stability_nudge],
|
||||
hits,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
// Passive diagnostics for the user's visual run; no input simulation.
|
||||
if runtime.diagnostic_tick % 30 == 0 || (!gated && candidate.is_some()) != old.is_some() {
|
||||
bevy::log::info!("GRIND_QUERY state={} gated={} nearby={} hits={:?} depth={:?} candidate={} board={:?} up={:?} forward={:?} probe={:?} flags={:08x}/{:08x}/{:08x}/{:08x}/{:08x}",
|
||||
p.state_2508, gated, nearby.len(), hits.map(|h|h.map(|h|h.primitive)),
|
||||
hits.map(|h|h.map(|h|dot3(sub(board[3],h.position),board[1]))),
|
||||
candidate.is_some(),board[3],board[1],board[2],
|
||||
[runtime.truck_to_wheel,runtime.deck_to_truck],
|
||||
p.flags_2468,p.flags_2472,p.flags_2476,p.flags_2480,p.flags_2484);
|
||||
}
|
||||
if gated { return; }
|
||||
let Some(candidate) = candidate else { return; };
|
||||
let velocity = p.vectors_400_416[0].map(f32::from_bits);
|
||||
//82D886B8: air accepts an actual intersection; same active grind uses90deg.
|
||||
//The ordinary level-ground admission uses17deg.
|
||||
@@ -311,9 +313,7 @@ fn append(physics: &mut GamePhysics, force: V) -> Result<(), String> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn lanes(v: Vector3) -> V {
|
||||
[v.x, v.y, v.z, 0.]
|
||||
}
|
||||
|
||||
fn xyz(v: V) -> Vector3 {
|
||||
Vector3::new(v[0], v[1], v[2])
|
||||
}
|
||||
|
||||
@@ -69,12 +69,6 @@ pub(crate) fn validate_runtime(map: &SkateMap) -> Result<(), String> {
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if !map.rails.is_empty() {
|
||||
eprintln!(
|
||||
"SKATE LIMITATION: {} rail records parsed, but supplied game has no nonempty grind-edge controller. Rails will not engage grinds.",
|
||||
map.rails.len()
|
||||
);
|
||||
}
|
||||
if !map.routes.is_empty() {
|
||||
eprintln!(
|
||||
"SKATE LIMITATION: {} NPC routes parsed; supplied game has no NPC controller.",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Live grind integration
|
||||
|
||||
The default world now supplies seven straight grind paths shared with rail/coping collision geometry. This pass connects the paired-truck 50-50 acquisition path to the production state selector, board forces, skeleton update, stock grind animations and physical output. Imported map grind descriptors are not wired into this owner yet.
|
||||
The default world now supplies seven straight grind paths shared with rail/coping collision geometry. This pass connects the paired-truck 50-50 acquisition path to the production state selector, board forces, skeleton update, stock grind animations and physical output. The runtime now builds and consumes the same Pegasus spline data for default-world and imported `.skate` rails (authored polylines, closed paths, and preserved retail cubic records).
|
||||
|
||||
## Source and implementation
|
||||
|
||||
TU3 image: default_82000000_011B0000.bin, SHA256 f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4. Analysis used the local generated PPC and the copied IDA database; the reference game was not launched for this implementation.
|
||||
|
||||
- 82C1FDC0 and 82D89150: actual truck probe rectangles, paired same-primitive contacts, depth and balance rejection. No nearest-distance snap or positional teleport.
|
||||
- 82D886B8: air and ordinary level-ground admission. The host currently covers straight level rails; sloped approach classification, adjacency arbitration, tipslides, boardslides and darkslides still need their own live adapters.
|
||||
- 82C1FDC0 and 82D89150: actual truck probe rectangles, paired contacts including the native cross-segment separation branch, depth and balance rejection. No nearest-distance snap or positional teleport.
|
||||
- 82D886B8: air and ordinary level-ground admission. The host currently covers straight level rails; sloped approach classification, tipslides, boardslides and darkslides still need their own live adapters.
|
||||
- 82D41D70 / 82D3FA18 / 82D3FD88: 50-50 lateral pin and friction, using the stock PinVsSlope and FrictionVsTime curves. Authored static rail support uses the upward normal and default surface multiplier.
|
||||
- 82D41E38 / 82D40890: rail-aligned target blended through the existing native matrix interpolation and angular-only board drive. User-driven grind twist forces and the remaining grind-manager transition scoring are not complete.
|
||||
- 82D73AB0: straight static 50-50 pop branch, stock physics-mode launch speeds, lateral nudge and launch velocity publication.
|
||||
@@ -20,3 +20,9 @@ TU3 image: default_82000000_011B0000.bin, SHA256 f4aa113eb541bfba03dbc108cf5ab43
|
||||
Build with Build.ps1; launch bin/skate-game.exe --assets assets (no map argument). The low straight rail is at x=-7, z=7..15, 0.45m above the lower floor; the higher rail is at x=4.5, z=8..16, 0.70m above it. Approach along the rail and land both trucks on its top. The halfpipe coping and three starting-platform edges also have authored paths.
|
||||
|
||||
Build completed. No automated tests or controller-driven gameplay tests were run. Visual behavior, catch consistency, pop timing, naming and solver stability while grinding require the user's playtest; this is not a claim of complete Skate 3 grind parity.
|
||||
|
||||
## Spline integration correction (2026-09-07)
|
||||
|
||||
The newer `SK8R15/Source/owned/world/src/grind_spline.cpp` revealed that the previous default-world blob incorrectly put delta in coefficient A and populated auxiliary words as length fields. Authored segments now use A=-2*delta, B=3*delta, C=0, D=start. The old independent endpoint list bypassed that blob, so this malformed blob alone does not explain the failed default-world catches.
|
||||
|
||||
Both map paths now build Pegasus data and feed the live query from it. TU3 AssetRecord82C1E568 constructs exactly one contact chord per cubic, D to (A+B)+(C+D); curved payloads are retained without an invented tessellation scheme. Imported native IDs, flags and all30 payload words are preserved. The query uses the native local1.2m bounds and40-entry limit. The latest user run recorded no entered grind; the default-world acquisition failure still needs a near-rail observation to identify the rejecting gate/contact. Passive GRIND_QUERY logging now records those observations during the user's visual run. No gameplay success is claimed from a build.
|
||||
Reference in new issue
Block a user