Parse the shrub class header
Shrub is the third class-geometry family and is not the moby one: all 358 spans on the disc have a leading word of 0 or one too large to index a table into, so none contain a moby-style part table. The rule that holds for 43,467 of 43,467 moby parts holds for zero of them. Two header fields do self-identify, exactly, on all 358 spans: +0x24 equals the owning class entry's own o_class, and +0x2c is a data offset landing inside the span. Both are checked rather than merely reported -- a span naming the wrong class is a different thing, and quietly returning the mismatched value would hide that. The body past +0x2c is not decoded. It tiles at no stride consistent with the count at +0x28 (tails of 240, 240, 192 and 224 bytes against counts of 5, 6, 5 and 3), and the header's leading bytes are packed rather than float even though +0x10 lands exactly on 5.0f on 281 of 358 spans. Shrub class entries are 0x30 bytes rather than 0x20, because they carry billboard texture fields the other two tables lack; the parser records that so the stride is not re-derived.
This commit is contained in:
@@ -1619,3 +1619,70 @@ fn moby_parts_tile_on_every_level() {
|
||||
({empties} with no geometry) across {levels} levels"
|
||||
);
|
||||
}
|
||||
|
||||
/// Shrub class headers.
|
||||
///
|
||||
/// Two fields self-identify exactly on every span, and the parser checks both
|
||||
/// rather than merely reporting them: `+0x24` must equal the owning class
|
||||
/// entry's own `o_class`, and `+0x2c` must land inside the span.
|
||||
#[test]
|
||||
fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
require_disc!();
|
||||
let data = std::env::var_os("GC_EXTRACTED")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||
PathBuf::from(home).join("Documents/gc-extracted")
|
||||
});
|
||||
if !data.exists() {
|
||||
eprintln!("skipped: unpack the disc first");
|
||||
return;
|
||||
}
|
||||
|
||||
use gc_formats::level_core::ShrubClassEntry;
|
||||
use gc_formats::shrub::{ShrubHeader, SHRUB_CLASS_ENTRY_BYTES};
|
||||
let mut levels = 0usize;
|
||||
let mut spans = 0usize;
|
||||
let mut absent = 0usize;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
let (Ok(core), Ok(index)) = (
|
||||
std::fs::read(dir.join("core_data.bin")),
|
||||
std::fs::read(dir.join("core_index.bin")),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let h = gc_formats::level_core::LevelCoreHeader::parse(&index).expect("core header");
|
||||
let range = h.shrub_classes;
|
||||
for i in 0..range.count.max(0) as usize {
|
||||
let at = range.offset.max(0) as usize + i * SHRUB_CLASS_ENTRY_BYTES;
|
||||
let e = ShrubClassEntry::parse(&index[at..at + SHRUB_CLASS_ENTRY_BYTES])
|
||||
.unwrap_or_else(|e| panic!("level{n}: shrub class {i}: {e}"));
|
||||
match ShrubHeader::region(&core, e.base.offset_in_asset_wad, e.base.o_class) {
|
||||
Ok(Some(s)) => {
|
||||
assert_eq!(
|
||||
s.header.o_class,
|
||||
e.base.o_class.max(0) as u32,
|
||||
"level{n} class {i}"
|
||||
);
|
||||
assert!(!s.data.is_empty(), "level{n} class {i}: empty shrub data");
|
||||
spans += 1;
|
||||
}
|
||||
Ok(None) => absent += 1,
|
||||
Err(err) => panic!("level{n}: shrub class {i}: {err}"),
|
||||
}
|
||||
}
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert_eq!(levels, 27, "expected all 27 levels, got {levels}");
|
||||
assert_eq!(
|
||||
spans + absent,
|
||||
358,
|
||||
"expected 358 shrub classes, got {}",
|
||||
spans + absent
|
||||
);
|
||||
assert!(spans > 340, "only {spans} shrub spans with geometry");
|
||||
eprintln!("shrub: {spans} headers naming their own class, {absent} without geometry");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user