mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Read SKATE15 custom maps with lossless compressed storage transforms
This commit is contained in:
1 parent
4991832653
commit
a4666424cf
3 files changed
+216
-5
No files matched your search
@@ -1,8 +1,9 @@
|
||||
//! Little-endian SKATE01..14 reader. Layout follows the supplied
|
||||
//! Little-endian SKATE01..15 reader. Layout follows the supplied
|
||||
//! SK8R15/Source/tools/blender_owned_map/SKATE_FORMAT.md and
|
||||
//! owned/world/src/owned_map_package.cpp. No geometry or physics is inferred.
|
||||
use std::{io::Read, path::Path, time::Instant};
|
||||
mod texture_decode;
|
||||
mod storage_v15;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct SkateMap {
|
||||
@@ -314,6 +315,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
/// Borrow compressed payloads while scanning; decompression does not mutate
|
||||
/// the package or depend on any other texture.
|
||||
#[derive(Clone, Copy)]
|
||||
struct StoredBlock<'a> { expected: usize, method: u32, bytes: &'a [u8] }
|
||||
impl StoredBlock<'_> {
|
||||
fn decode(&self) -> Result<Vec<u8>, String> {
|
||||
@@ -342,6 +344,7 @@ impl StoredBlock<'_> {
|
||||
.map_err(|e| format!("SKATE Zstandard: {e}"))?;
|
||||
result
|
||||
}
|
||||
3..=10 => return storage_v15::decode(method, bytes, expected),
|
||||
_ => return Err(format!("Unsupported SKATE storage method {method}")),
|
||||
};
|
||||
if decoded.len() != expected {
|
||||
@@ -395,8 +398,8 @@ impl SkateMap {
|
||||
return Err("Invalid SKATE magic/version".into());
|
||||
}
|
||||
let version = (magic[5] - b'0') * 10 + magic[6] - b'0';
|
||||
if !(1..=14).contains(&version) {
|
||||
return Err("Unsupported SKATE version: reader supports 01 through 14; newer packages require an update".into());
|
||||
if !(1..=15).contains(&version) {
|
||||
return Err("Unsupported SKATE version: reader supports 01 through 15; newer packages require an update".into());
|
||||
}
|
||||
if r.u()? != 0x12345678 {
|
||||
return Err("Invalid SKATE endian marker".into());
|
||||
@@ -428,6 +431,15 @@ impl SkateMap {
|
||||
for c in &mut counts[..n] {
|
||||
*c = r.u()?;
|
||||
}
|
||||
let material_bytes = if version >= 15 {
|
||||
let size = r.u()? as usize;
|
||||
Some(r.stored(size)?)
|
||||
} else { None };
|
||||
let mut package_reader = None;
|
||||
if let Some(bytes) = &material_bytes {
|
||||
package_reader = Some(r);
|
||||
r = Reader { bytes, at: 0 };
|
||||
}
|
||||
r.check_count(counts[0], if version >= 2 { 80 } else { 48 })?;
|
||||
let mut materials = Vec::new();
|
||||
for _ in 0..counts[0] {
|
||||
@@ -512,10 +524,14 @@ impl SkateMap {
|
||||
}
|
||||
materials.push(m);
|
||||
}
|
||||
if let Some(package) = package_reader {
|
||||
if r.at != r.bytes.len() { return Err("SKATE material block has trailing bytes".into()); }
|
||||
r = package;
|
||||
}
|
||||
let texture_started = Instant::now();
|
||||
r.check_count(counts[1], 20)?;
|
||||
let mut textures = Vec::new();
|
||||
let mut texture_blocks = Vec::new();
|
||||
let mut texture_blocks: Vec<StoredBlock<'_>> = Vec::new();
|
||||
for _ in 0..counts[1] {
|
||||
let name = r.string()?;
|
||||
let width = r.u()?;
|
||||
@@ -534,6 +550,13 @@ impl SkateMap {
|
||||
}
|
||||
StoredBlock { expected, method: 0, bytes: r.take(bytes)? }
|
||||
};
|
||||
let block = if version >= 15 && block.method == 11 {
|
||||
let index = u32::from_le_bytes(block.bytes.try_into()
|
||||
.map_err(|_| "Invalid SKATE texture reference size")?) as usize;
|
||||
let source = *texture_blocks.get(index).ok_or("Invalid SKATE forward texture reference")?;
|
||||
if source.expected != expected { return Err("SKATE texture reference size mismatch".into()); }
|
||||
source
|
||||
} else { block };
|
||||
texture_blocks.push(block);
|
||||
textures.push(Texture {
|
||||
name,
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Reversible SKATE15 storage transforms, matching the bundled exporter.
|
||||
use super::{Reader, StoredBlock};
|
||||
|
||||
pub(super) fn decode(method: u32, bytes: &[u8], expected: usize) -> Result<Vec<u8>, String> {
|
||||
let mut input = Reader { bytes, at: 0 };
|
||||
let size = input.u()? as usize;
|
||||
// Transformed records are at most a small multiple of their decoded size.
|
||||
// Bound both lengths before allocating or decompressing untrusted data.
|
||||
if size > expected.saturating_mul(3).saturating_add(16) || size > 2_147_483_648 {
|
||||
return Err("SKATE transformed block exceeds size limit".into());
|
||||
}
|
||||
let data = StoredBlock {
|
||||
expected: size, method: if method <= 6 { 2 } else { 1 },
|
||||
bytes: &bytes[4..],
|
||||
}.decode()?;
|
||||
let mut source = Reader { bytes: &data, at: 0 };
|
||||
let mut result = Vec::new();
|
||||
match method {
|
||||
3 | 7 => {
|
||||
let width = source.u()? as usize;
|
||||
let height = source.u()? as usize;
|
||||
let row = width.checked_mul(4).ok_or("SKATE filtered width overflow")?;
|
||||
if row.checked_mul(height) != Some(expected) || width > 16384 || height > 16384 {
|
||||
return Err("SKATE filtered dimensions mismatch".into());
|
||||
}
|
||||
result.resize(expected, 0u8);
|
||||
for y in 0..height {
|
||||
let filter = source.take(1)?[0];
|
||||
if filter > 4 { return Err("Invalid SKATE RGBA filter".into()); }
|
||||
for (x, &value) in source.take(row)?.iter().enumerate() {
|
||||
let at = y * row + x;
|
||||
let left = if x >= 4 { result[at - 4] as i32 } else { 0 };
|
||||
let above = if y > 0 { result[at - row] as i32 } else { 0 };
|
||||
let upper_left = if x >= 4 && y > 0 { result[at - row - 4] as i32 } else { 0 };
|
||||
let predictor = match filter {
|
||||
1 => left, 2 => above, 3 => (left + above) / 2,
|
||||
4 => {
|
||||
let p = left + above - upper_left;
|
||||
let a = (p - left).abs();
|
||||
let b = (p - above).abs();
|
||||
let c = (p - upper_left).abs();
|
||||
if a <= b && a <= c { left } else if b <= c { above } else { upper_left }
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
result[at] = value.wrapping_add(predictor as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
4 | 8 => {
|
||||
if expected % 56 != 0 || data.len() != expected { return Err("Invalid SKATE vertex streams".into()); }
|
||||
result.resize(expected, 0);
|
||||
for (offset, length) in [(0,12), (12,12), (24,8), (32,8), (40,4), (44,8), (52,4)] {
|
||||
for vertex in 0..expected / 56 {
|
||||
let at = vertex * 56 + offset;
|
||||
result[at..at + length].copy_from_slice(source.take(length)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
5 | 9 => {
|
||||
if expected % 4 != 0 { return Err("Invalid SKATE index block size".into()); }
|
||||
let mut previous = 0i64;
|
||||
for _ in 0..expected / 4 {
|
||||
let mut zigzag = 0u64;
|
||||
// A delta between u32 indices requires at most 33 bits.
|
||||
for shift in (0..=28).step_by(7) {
|
||||
let byte = source.take(1)?[0];
|
||||
if shift == 28 && byte > 31 { return Err("Invalid SKATE index varint".into()); }
|
||||
zigzag |= u64::from(byte & 127) << shift;
|
||||
if byte & 128 == 0 { break; }
|
||||
}
|
||||
let current = previous + ((zigzag >> 1) as i64 ^ -((zigzag & 1) as i64));
|
||||
let current = u32::try_from(current).map_err(|_| "SKATE index delta overflow")?;
|
||||
result.extend(current.to_le_bytes());
|
||||
previous = i64::from(current);
|
||||
}
|
||||
}
|
||||
6 | 10 => {
|
||||
if expected % 48 != 0 { return Err("Invalid SKATE collision block size".into()); }
|
||||
let count = source.u()? as usize;
|
||||
let vertices = source.take(count.checked_mul(12).ok_or("SKATE collision vertex overflow")?)?;
|
||||
for _ in 0..expected / 48 {
|
||||
for _ in 0..3 {
|
||||
let index = source.u()? as usize;
|
||||
if index >= count { return Err("Invalid SKATE collision vertex reference".into()); }
|
||||
result.extend_from_slice(&vertices[index * 12..index * 12 + 12]);
|
||||
}
|
||||
result.extend_from_slice(source.take(12)?);
|
||||
}
|
||||
}
|
||||
_ => return Err("Invalid SKATE storage transform".into()),
|
||||
}
|
||||
if source.at != data.len() || result.len() != expected { return Err("SKATE transformed size mismatch or trailing data".into()); }
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
fn unpack(method: u32, data: &[u8], expected: usize) -> Result<Vec<u8>, String> {
|
||||
let mut bytes = (data.len() as u32).to_le_bytes().to_vec();
|
||||
if method <= 6 { bytes.extend(zstd::stream::encode_all(data, 1).unwrap()); }
|
||||
else {
|
||||
let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
encoder.write_all(data).unwrap();
|
||||
bytes.extend(encoder.finish().unwrap());
|
||||
}
|
||||
decode(method, &bytes, expected)
|
||||
}
|
||||
#[test]
|
||||
fn both_codecs_restore_vertices_indices_collision_and_rgba_exactly() {
|
||||
let vertices: Vec<u8> = (0..112).collect();
|
||||
let mut soa = Vec::new();
|
||||
for (offset, len) in [(0,12), (12,12), (24,8), (32,8), (40,4), (44,8), (52,4)] {
|
||||
for v in 0..2 { soa.extend_from_slice(&vertices[v * 56 + offset..v * 56 + offset + len]); }
|
||||
}
|
||||
let mut collision = 3u32.to_le_bytes().to_vec();
|
||||
collision.extend(0u8..36);
|
||||
for index in [2u32, 0, 1] { collision.extend(index.to_le_bytes()); }
|
||||
collision.extend(36u8..48);
|
||||
let expected_collision: Vec<u8> = (24..36).chain(0..24).chain(36..48).collect();
|
||||
let mut rgba = 2u32.to_le_bytes().to_vec();
|
||||
rgba.extend(5u32.to_le_bytes());
|
||||
for filter in 0..5u8 {
|
||||
rgba.push(filter);
|
||||
// Constant RGBA rows exercise all five PNG-style predictors.
|
||||
rgba.extend(if filter == 0 { [10; 8] } else if filter == 1 { [10,10,10,10,0,0,0,0] }
|
||||
else if filter == 3 { [5,5,5,5,0,0,0,0] } else { [0; 8] });
|
||||
}
|
||||
for shift in [0, 4] {
|
||||
assert_eq!(unpack(3 + shift, &rgba, 40).unwrap(), vec![10; 40]);
|
||||
assert_eq!(unpack(4 + shift, &soa, 112).unwrap(), vertices);
|
||||
assert_eq!(unpack(5 + shift, &[0, 4, 1], 12).unwrap(), [0u32,2,1].into_iter().flat_map(u32::to_le_bytes).collect::<Vec<_>>());
|
||||
assert_eq!(unpack(6 + shift, &collision, 48).unwrap(), expected_collision);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn malformed_transforms_fail_without_panics() {
|
||||
for method in 3..=10 {
|
||||
assert!(unpack(method, &[], 48).is_err());
|
||||
assert!(decode(method, &[255; 4], 48).is_err());
|
||||
}
|
||||
assert!(unpack(9, &[1], 4).is_err()); // negative first index
|
||||
assert!(unpack(9, &[128; 5], 4).is_err());
|
||||
assert!(unpack(9, &[0, 0], 4).is_err()); // trailing varint
|
||||
assert!(unpack(8, &[0; 56], 55).is_err());
|
||||
let mut bad_collision = 0u32.to_le_bytes().to_vec();
|
||||
bad_collision.extend([0; 24]);
|
||||
assert!(unpack(10, &bad_collision, 48).is_err());
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ fn fixture(version: u8) -> Vec<u8> {
|
||||
if version >= 8 {
|
||||
u(&mut b, 1);
|
||||
}
|
||||
let material_start = b.len();
|
||||
s(&mut b, "Surface");
|
||||
u(&mut b, 1);
|
||||
fs(&mut b, &[0.6, 0.1, 1., 0.5, 0.25, 0.8, 0.]);
|
||||
@@ -78,6 +79,11 @@ fn fixture(version: u8) -> Vec<u8> {
|
||||
if version >= 12 {
|
||||
u(&mut b, 0);
|
||||
}
|
||||
if version >= 15 {
|
||||
let material = b.split_off(material_start);
|
||||
u(&mut b, material.len() as u32);
|
||||
stored(&mut b, &material, 2);
|
||||
}
|
||||
s(&mut b, "Pixel");
|
||||
for n in [1, 1, 1] {
|
||||
u(&mut b, n);
|
||||
@@ -172,7 +178,7 @@ fn stored(b: &mut Vec<u8>, data: &[u8], method: u8) {
|
||||
}
|
||||
#[test]
|
||||
fn reads_all_documented_versions_without_moving_geometry() {
|
||||
for version in 1..=14 {
|
||||
for version in 1..=15 {
|
||||
let map = SkateMap::parse(&fixture(version)).unwrap();
|
||||
assert_eq!(map.version, version);
|
||||
assert_eq!(map.spawn, [2., 0., 3.]);
|
||||
@@ -194,6 +200,36 @@ fn reads_all_documented_versions_without_moving_geometry() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v15_texture_references_preserve_order_and_reject_forward_or_wrong_size() {
|
||||
let mut data = fixture(15);
|
||||
let header_counts = 8 + 4 + 4 + "Fixture".len() + 49 * 4;
|
||||
data[header_counts + 4..header_counts + 8].copy_from_slice(&3u32.to_le_bytes());
|
||||
let mut needle = Vec::new();
|
||||
s(&mut needle, "Pixel");
|
||||
let start = data.windows(needle.len()).position(|w| w == needle).unwrap();
|
||||
let metadata_end = start + needle.len() + 12;
|
||||
let texture_end = metadata_end + 8 + 4; // fixture v15 uses raw RGBA
|
||||
let mut references = Vec::new();
|
||||
for source in [0, 1] {
|
||||
references.extend_from_slice(&data[start..metadata_end]);
|
||||
u(&mut references, 11);
|
||||
u(&mut references, 4);
|
||||
u(&mut references, source);
|
||||
}
|
||||
data.splice(texture_end..texture_end, references);
|
||||
let map = SkateMap::parse(&data).unwrap();
|
||||
assert_eq!(map.textures.len(), 3);
|
||||
assert!(map.textures.iter().all(|t| t.rgba == [255, 128, 64, 255]));
|
||||
let reference = texture_end + (metadata_end - start) + 8;
|
||||
data[reference..reference + 4].copy_from_slice(&1u32.to_le_bytes());
|
||||
assert!(SkateMap::parse(&data).unwrap_err().contains("forward texture reference"));
|
||||
data[reference..reference + 4].copy_from_slice(&0u32.to_le_bytes());
|
||||
let width = texture_end + needle.len();
|
||||
data[width..width + 4].copy_from_slice(&2u32.to_le_bytes());
|
||||
assert!(SkateMap::parse(&data).unwrap_err().contains("reference size mismatch"));
|
||||
}
|
||||
#[test]
|
||||
fn rejects_every_truncated_prefix_and_trailing_data() {
|
||||
let data = fixture(8);
|
||||
|
||||
Reference in new issue
Block a user