mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Collision: skip meshes with no surface IDs (zone/trigger volumes)
Retail collision includes clustered meshes in which no unit carries a surface ID (unit flag 0x80 unset). Across every shipped district these are only 12-triangle boxes: zone, trigger and level-bounds volumes. The loader made them solid, creating invisible walls (SkateSchool spawn area, DownTown's Kube Tower plaza, MegaPark's stadium entrance, Maloof). Authored player positions lie inside some of them, so they are not solid in the original game. visit_clusters now decodes and validates each mesh fully and skips it when no unit has a surface ID. Meshes mixing surfaced and surfaceless units are ordinary geometry and stay whole. RetailTriangle records has_surface. Collision triangles loaded: SkateSchool -84, DownTown -24, Maloof -24, MegaPark -12, all other maps unchanged. Confirmed in play. See docs/hails-additions/05-collision-volumes.md.
This commit is contained in:
4 files changed
+160
-4
No files matched your search
@@ -9,6 +9,8 @@ pub struct RetailTriangle {
|
||||
pub points: [[f32; 3]; 3],
|
||||
pub edges: Option<[u8; 3]>,
|
||||
pub surface: u16,
|
||||
/// Unit flag 0x80: the unit carries a surface ID (`surface` is 0 otherwise).
|
||||
pub has_surface: bool,
|
||||
pub group: u16,
|
||||
pub one_sided: bool,
|
||||
}
|
||||
@@ -38,8 +40,16 @@ fn id(data: &[u8], at: &mut usize, width: u8) -> Result<u16, String> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Visits complete clusters in archive order, retaining only one decoded
|
||||
/// cluster at a time. The callback can build its own spatial query index.
|
||||
/// Visits complete clusters in archive order, retaining one decoded mesh at a
|
||||
/// time. The callback can build its own spatial query index. Returns the number
|
||||
/// of triangles visited.
|
||||
///
|
||||
/// Meshes in which no unit carries a surface ID are skipped. Across every
|
||||
/// shipped district these are only 12-triangle boxes (zone/trigger/bounds
|
||||
/// volumes); authored player positions lie inside some of them, e.g. the
|
||||
/// SkateSchool area and DownTown's Kube Tower landing, so they are not solid.
|
||||
/// Meshes that mix surfaced and surfaceless units are ordinary geometry and
|
||||
/// are kept whole.
|
||||
pub fn visit_clusters(
|
||||
data: &[u8],
|
||||
mut visit: impl FnMut(&str, &[RetailTriangle]) -> Result<(), String>,
|
||||
@@ -96,6 +106,7 @@ fn mesh_clusters(
|
||||
let one_sided = be16(data, 60)? & 0x10 != 0;
|
||||
let mut previous_end = table + count * 4;
|
||||
let mut total = 0;
|
||||
let mut clusters = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let offset = be32(data, table + i * 4)? as usize;
|
||||
if offset < previous_end {
|
||||
@@ -109,11 +120,17 @@ fn mesh_clusters(
|
||||
previous_end = offset + size;
|
||||
let triangles = decode_cluster(cluster, granularity, data[62], data[63], one_sided)?;
|
||||
total += triangles.len();
|
||||
visit(&triangles)?;
|
||||
clusters.push(triangles);
|
||||
}
|
||||
if total != be32(data, 40)? as usize {
|
||||
return Err("RWCM triangle count mismatch".into());
|
||||
}
|
||||
if !clusters.iter().flatten().any(|t| t.has_surface) {
|
||||
return Ok(0); // volume, not collision (see visit_clusters)
|
||||
}
|
||||
for triangles in &clusters {
|
||||
visit(triangles)?;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
@@ -192,6 +209,7 @@ fn decode_cluster(
|
||||
points,
|
||||
edges,
|
||||
surface,
|
||||
has_surface: flags & 0x80 != 0,
|
||||
group,
|
||||
one_sided,
|
||||
});
|
||||
|
||||
@@ -64,6 +64,44 @@ fn preserves_all_vertex_encodings_and_little_endian_unit_ids() {
|
||||
assert_eq!(clusters, 3);
|
||||
}
|
||||
|
||||
/// Removes the surface ID from the first unit of fixture mesh `index`: clears
|
||||
/// unit flag 0x80 and shortens the unit stream by the 2-byte ID (its last field).
|
||||
fn strip_surface(bytes: &mut [u8], index: usize) {
|
||||
let be16 = |b: &[u8], at: usize| u16::from_be_bytes(b[at..at + 2].try_into().unwrap());
|
||||
let be32 = |b: &[u8], at: usize| u32::from_be_bytes(b[at..at + 4].try_into().unwrap()) as usize;
|
||||
let mut at = 12;
|
||||
let mut mesh = 0;
|
||||
for _ in 0..=index {
|
||||
let name_len = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
|
||||
at += 4 + name_len;
|
||||
let size = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
|
||||
mesh = at + 4;
|
||||
at = mesh + size;
|
||||
}
|
||||
assert_eq!(bytes[mesh + 63], 2, "fixture surface IDs are 2 bytes");
|
||||
let cluster = mesh + be32(bytes, mesh + be32(bytes, mesh + 52));
|
||||
let unit_start = (usize::from(be16(bytes, cluster + 4)) + 1) * 16;
|
||||
assert_ne!(bytes[cluster + unit_start] & 0x80, 0);
|
||||
bytes[cluster + unit_start] &= !0x80;
|
||||
let length = be16(bytes, cluster + 2) - 2;
|
||||
bytes[cluster + 2..cluster + 4].copy_from_slice(&length.to_be_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_meshes_without_any_surface_ids() {
|
||||
let mut bytes = FIXTURE.to_vec();
|
||||
strip_surface(&mut bytes, 1);
|
||||
let mut names = Vec::new();
|
||||
let total = visit_clusters(&bytes, |name, triangles| {
|
||||
assert!(triangles.iter().all(|t| t.has_surface));
|
||||
names.push(name.to_owned());
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(names, ["compression-0", "compression-2"]);
|
||||
assert_eq!(total, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_every_truncated_prefix_and_trailing_bytes() {
|
||||
for end in 0..FIXTURE.len() {
|
||||
|
||||
Reference in new issue
Block a user