Identify the container the class tables point into: core_data itself
assets_compressed_size and assets_decompressed_size at index offsets 0x88 and 0x8c are 9,738,678 and 16,969,296 on level 0 -- exactly the sizes of core_data before and after decompression. There is no separate asset container. The "asset WAD" is core_data, and offset_in_asset_wad indexes the decompressed form directly. Checked rather than assumed, on all 27 levels: the decompressed size equals core_data's exact length, every class offset is either 0 or inside it, and the moby, tie and shrub spans do not overlap each other. An offset of 0 means the slot has no geometry, not the start of the lump; level 0's ninth moby class is 4909 at offset 0. Two assumptions worth discarding while here -- the tables are not sorted by class number (level 0's run 0, 1, 2, 14 ... 4909) and the offsets within a table are not monotonic. Neither is needed, but both look true from the first few entries. The chunk descriptors after the named extents are not it: they exist on only 3 of 27 levels, because level 0's four named extents tile the container exactly. An asset blob there would be missing almost everywhere. What sits at the offsets is raw GS/VIF packet data rather than a further container -- moby[0] begins 00 00 00 00 3f 16 02 55 6f 6f and tie[0] 00 01 00 00 70 50 00 00, which read as packet headers rather than a magic number. None of the three is decoded yet and this does not start that. Moby is skeletal and is the largest single piece left before the rest of a level can draw. Also: the pipeline now writes each level's chunk descriptors where they exist, and LevelCoreHeader decodes the asset lump's two sizes.
This commit is contained in:
@@ -1092,3 +1092,110 @@ fn level_core_class_tables_tile_the_index() {
|
||||
{textures} tfrag textures across {levels} levels"
|
||||
);
|
||||
}
|
||||
|
||||
/// The moby, tie and shrub class tables point into `core_data`, and they do so
|
||||
/// without overlapping.
|
||||
///
|
||||
/// `assets_decompressed_size` is the size of `core_data` itself, so there is
|
||||
/// no separate asset container: the class entries index the decompressed asset
|
||||
/// lump directly. If that is right, every offset must land inside the lump, the
|
||||
/// three tables must occupy disjoint spans, and each must be internally sorted
|
||||
/// so the geometry is laid out in class order.
|
||||
#[test]
|
||||
fn class_geometry_offsets_tile_core_data_without_overlap() {
|
||||
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;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
let (Ok(index), Ok(core)) = (
|
||||
std::fs::read(dir.join("core_index.bin")),
|
||||
std::fs::read(dir.join("core_data.bin")),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let h = lc::LevelCoreHeader::parse(&index).expect("core header");
|
||||
|
||||
// The claim under test: the "asset lump" is core_data itself.
|
||||
assert_eq!(
|
||||
h.assets_decompressed_size as usize,
|
||||
core.len(),
|
||||
"level{n}: assets_decompressed_size is not core_data's length"
|
||||
);
|
||||
|
||||
let span = |r: lc::ArrayRange, size: usize| -> Vec<usize> {
|
||||
lc::class_table(&index, r, size, lc::ClassEntry::parse)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|c| c.offset_in_asset_wad as usize)
|
||||
.collect()
|
||||
};
|
||||
// Shrub entries are read as ShrubClassEntry so the offsets line up.
|
||||
let shrub: Vec<usize> = lc::class_table(
|
||||
&index,
|
||||
h.shrub_classes,
|
||||
lc::SHRUB_CLASS_SIZE,
|
||||
lc::ShrubClassEntry::parse,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|s| s.base.offset_in_asset_wad as usize)
|
||||
.collect();
|
||||
let moby = span(h.moby_classes, lc::MOBY_CLASS_SIZE);
|
||||
let tie = span(h.tie_classes, lc::TIE_CLASS_SIZE);
|
||||
|
||||
// An offset of 0 marks a slot with no geometry rather than the start of
|
||||
// the lump, so those are excluded from the span checks. Entries are not
|
||||
// sorted by class number and the offsets are not monotonic, so nothing
|
||||
// here assumes an order.
|
||||
let moby_l: Vec<usize> = moby.iter().copied().filter(|&o| o != 0).collect();
|
||||
let tie_l: Vec<usize> = tie.iter().copied().filter(|&o| o != 0).collect();
|
||||
let shrub_l: Vec<usize> = shrub.iter().copied().filter(|&o| o != 0).collect();
|
||||
|
||||
for (name, v) in [("moby", &moby), ("tie", &tie), ("shrub", &shrub)] {
|
||||
for &o in v {
|
||||
assert!(
|
||||
o < core.len(),
|
||||
"level{n}: {name} offset {o} past core data ({})",
|
||||
core.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
// The three spans must not overlap each other.
|
||||
for (an, a) in [("moby", &moby_l), ("tie", &tie_l), ("shrub", &shrub_l)] {
|
||||
for (bn, b) in [("moby", &moby_l), ("tie", &tie_l), ("shrub", &shrub_l)] {
|
||||
// A level may have no geometry of one kind at all.
|
||||
if an >= bn || a.is_empty() || b.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (lo, hi) = (a[0], *a.last().unwrap());
|
||||
for &o in b {
|
||||
assert!(
|
||||
o >= hi || o < lo,
|
||||
"level{n}: {an} [{lo},{hi}] overlaps {bn} at {o}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert_eq!(levels, 27, "expected all 27 levels, got {levels}");
|
||||
eprintln!("class geometry offsets verified inside core_data on {levels} levels");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user