Parse the level core class tables: moby, tie and shrub
These were ruled out for want of an entry size. The size is knowable exactly, because each table ends precisely where the next begins: moby 238 entries at 192, 32 bytes: 192 + 238*32 = 7808 = tie's tie 74 entries at 7808, 32 bytes: 7808 + 74*32 = 10176 = shrub's shrub 7 entries at 10176, 48 bytes: 10176 + 7*48 = 10512 = textures That closes on 27 of 27 levels, which is what makes it evidence rather than a coincidence: any wrong entry size breaks the chain immediately. It is the same "does this hold on every level" discriminator the project already uses for the container pair table. Across the disc: 5,796 moby, 1,864 tie and 358 shrub classes, plus the 1,595 tfrag textures. Class entries are 32 bytes -- asset offset, class number, two unknown words and 16 bytes of texture slots -- and a shrub entry adds 16 bytes of billboard slots for 48. The header now decodes through to the texture tables rather than skipping 0x18..0x30 wholesale, which also recovers the moby, tie and shrub texture ranges. Two more pairs sit between shrub textures and the textures base offset. Two things worth recording. ClassEntry::parse read its 16 texture bytes before its four i32s, because the struct literal that uses them comes after the loop that advances the cursor -- so every entry was parsed backwards. Struct literal field order does not reorder the reads that already happened. And every shrub class on level 0 carries all-zero billboard slots, so the dimensions are optional rather than something worth asserting on. This is the third instance of the same failure shape. The gameplay extent was "an unidentified compression" because it was parsed compressed; the tfrag record framing was "unrecoverable" because the entry size was never known. Both looked like properties of the format and were properties of the question. Also records that point lights are a runtime feature rather than missing work: every one of the 18,102 patches has point_lights == 0xffff and the eight-slot bank is filled by gameplay, so there is nothing for them to light in a static level render. And that PCSX2 is not installed, so the comparison loop cannot be built yet -- which is why lighting magnitudes stay unverified.
This commit is contained in:
@@ -970,3 +970,125 @@ fn tfrag_lod_parents_resolve_for_every_entry() {
|
||||
{lod01} LOD-01 and {lod0} LOD-0, all resolved"
|
||||
);
|
||||
}
|
||||
|
||||
/// The level core index's class tables.
|
||||
///
|
||||
/// These are what put the environment on top of the terrain: tie instances,
|
||||
/// moby actors and shrub foliage. They were ruled out earlier for want of an
|
||||
/// entry size, which turned out to be knowable exactly.
|
||||
///
|
||||
/// The check is the chain: each table must end precisely where the next one
|
||||
/// begins, moby (32 bytes/entry) into tie (32) into shrub (48) into the tfrag
|
||||
/// texture table. That closes on all 27 levels and would not close if any
|
||||
/// entry size were wrong, so it is the evidence rather than a coincidence.
|
||||
#[test]
|
||||
fn level_core_class_tables_tile_the_index() {
|
||||
require_disc!();
|
||||
gc_formats::iso9660::IsoImage::open(std::path::Path::new(
|
||||
&std::env::var("GC_DISC").expect("GC_DISC is set by require_disc!"),
|
||||
))
|
||||
.expect("ISO should open");
|
||||
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 as lc;
|
||||
let mut levels = 0usize;
|
||||
let (mut moby, mut tie, mut shrub, mut textures) = (0usize, 0usize, 0usize, 0usize);
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
let Ok(index) = std::fs::read(dir.join("core_index.bin")) else {
|
||||
continue;
|
||||
};
|
||||
let h = lc::LevelCoreHeader::parse(&index).expect("core header");
|
||||
|
||||
let end = |r: lc::ArrayRange, size: usize| r.offset as usize + r.count as usize * size;
|
||||
assert_eq!(
|
||||
end(h.moby_classes, lc::MOBY_CLASS_SIZE),
|
||||
h.tie_classes.offset as usize,
|
||||
"level{n}: moby -> tie"
|
||||
);
|
||||
assert_eq!(
|
||||
end(h.tie_classes, lc::TIE_CLASS_SIZE),
|
||||
h.shrub_classes.offset as usize,
|
||||
"level{n}: tie -> shrub"
|
||||
);
|
||||
assert_eq!(
|
||||
end(h.shrub_classes, lc::SHRUB_CLASS_SIZE),
|
||||
h.tfrag_textures.offset as usize,
|
||||
"level{n}: shrub -> textures"
|
||||
);
|
||||
|
||||
// Every entry parses, and its asset offset lands inside the level.
|
||||
if n == 0 {
|
||||
eprintln!(
|
||||
"hdr moby={:?} tie={:?} shrub={:?} tex={:?} base={}",
|
||||
h.moby_classes, h.tie_classes, h.shrub_classes, h.tfrag_textures, h.textures_base
|
||||
);
|
||||
eprintln!("index[192..200] = {:?}", &index[192..200]);
|
||||
}
|
||||
let moby_v = lc::class_table(
|
||||
&index,
|
||||
h.moby_classes,
|
||||
lc::MOBY_CLASS_SIZE,
|
||||
lc::ClassEntry::parse,
|
||||
)
|
||||
.expect("moby classes");
|
||||
let tie_v = lc::class_table(
|
||||
&index,
|
||||
h.tie_classes,
|
||||
lc::TIE_CLASS_SIZE,
|
||||
lc::ClassEntry::parse,
|
||||
)
|
||||
.expect("tie classes");
|
||||
let shrub_v = lc::class_table(
|
||||
&index,
|
||||
h.shrub_classes,
|
||||
lc::SHRUB_CLASS_SIZE,
|
||||
lc::ShrubClassEntry::parse,
|
||||
)
|
||||
.expect("shrub classes");
|
||||
let tex_v = lc::texture_table(&index, h.tfrag_textures).expect("tfrag textures");
|
||||
|
||||
assert_eq!(moby_v.len(), h.moby_classes.count as usize);
|
||||
for (i, c) in moby_v.iter().chain(tie_v.iter()).enumerate() {
|
||||
assert!(
|
||||
c.offset_in_asset_wad >= 0,
|
||||
"level{n} entry {i}: asset offset {}",
|
||||
c.offset_in_asset_wad
|
||||
);
|
||||
}
|
||||
// Billboard slots are optional: every shrub class on level 0 carries
|
||||
// them all zero, so only their sign is meaningful here.
|
||||
for s in &shrub_v {
|
||||
assert!(s.base.offset_in_asset_wad >= 0);
|
||||
assert!(
|
||||
s.palette_offset >= -1 && s.texture_offset >= -1,
|
||||
"level{n}: shrub billboard offsets below the -1 sentinel"
|
||||
);
|
||||
}
|
||||
moby += moby_v.len();
|
||||
tie += tie_v.len();
|
||||
shrub += shrub_v.len();
|
||||
textures += tex_v.len();
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert_eq!(levels, 27, "expected all 27 levels, got {levels}");
|
||||
assert!(
|
||||
moby + tie + shrub > 1000,
|
||||
"only {moby}+{tie}+{shrub} class entries"
|
||||
);
|
||||
eprintln!(
|
||||
"level core index: {moby} moby, {tie} tie, {shrub} shrub classes, \
|
||||
{textures} tfrag textures across {levels} levels"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user