Compute core_data region bounds once, from successor boundaries

Region spans were being re-derived per format while probing, usually
running from one known offset to the next with the last span running to
core_data.len(). On level 0 that gave the final shrub class a 925,008-byte
span absorbing most of the lump's tail, and every aggregate computed over
it -- record counts, value distributions, "this looks random" -- was
silently wrong. Nothing errored, which is what made it survive two
commits and a correction.

core_layout computes the layout once: each region ends where the next one
begins, class regions start at the lowest non-zero geometry offset in their
table, and a region whose successor is unknown gets end: None rather than a
clamped guess. Zero class entries mean absent, not position zero -- the
same trap occlusion and collision each fell into separately.

The disc test asserts what the module is actually for: that the unknown is
visible. Shrub's end is unresolved on the 24 levels that have a shrub
region, and that is asserted rather than hidden.

It also found that not every level has every region -- level 10 has neither
occlusion nor shrub -- so the test checks ordering against the canonical
sequence instead of demanding an identical set.

Still open: where the shrub region ends. Texture texels cannot bound it;
they sit before moby, not after shrub. See docs/formats/core-data-regions.md.
This commit is contained in:
2026-10-11 03:02:02 +02:00
parent aec462df21
commit cb38b8c279
4 changed files with 396 additions and 0 deletions
+112
View File
@@ -1712,3 +1712,115 @@ fn shrub_headers_name_their_own_class_on_every_level() {
size field {min_size}..{max_size}; {denormals} denormal reserved words"
);
}
/// Region bounds derived from successor boundaries, on every level.
///
/// The point is not that these are correct -- shrub's end is still unknown --
/// but that the unknown is visible. Every region here is closed by the next
/// region's start rather than by `core_data.len()`, so no span silently
/// absorbs the tail of the lump the way the last shrub class was doing.
#[test]
fn core_regions_chain_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::LevelCoreHeader;
use gc_formats::regions::core_layout;
let mut levels = 0usize;
let mut unresolved_shrub = 0usize;
let (mut first, mut last) = (usize::MAX, 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 core_len = std::fs::metadata(dir.join("core_data.bin"))
.map(|m| m.len() as usize)
.unwrap_or(0);
let h = LevelCoreHeader::parse(&index).expect("core header");
let regions = core_layout(&index, &h);
// Regions appear in order, never overlap, and never run past the lump.
for pair in regions.windows(2) {
assert!(
pair[0].start <= pair[1].start,
"level{n}: {} vs {}",
pair[0].name,
pair[1].name
);
if let Some(end) = pair[0].end {
assert!(
end <= pair[1].start,
"level{n}: {} ends after {}",
pair[0].name,
pair[1].name
);
}
}
for r in &regions {
assert!(
r.start < core_len,
"level{n}: {} starts past the lump",
r.name
);
if let Some(end) = r.end {
assert!(end <= core_len, "level{n}: {} ends past the lump", r.name);
}
}
// Regions appear in the canonical order, but not every level has every
// region -- level 10 has no occlusion and no shrub at all.
const ORDER: [&str; 7] = [
"tfrags",
"occlusion",
"sky",
"collision",
"moby",
"tie",
"shrub",
];
let names: Vec<_> = regions.iter().map(|r| r.name).collect();
let ranks: Vec<usize> = names
.iter()
.map(|name| {
ORDER
.iter()
.position(|o| o == name)
.unwrap_or_else(|| panic!("level{n}: unknown region {name}"))
})
.collect();
assert!(
ranks.windows(2).all(|w| w[0] < w[1]),
"level{n}: regions out of order: {names:?}"
);
if let Some(s) = regions.iter().find(|r| r.name == "shrub") {
if s.end.is_none() {
unresolved_shrub += 1;
}
first = first.min(s.start);
last = last.max(s.start);
}
levels += 1;
}
assert_eq!(levels, 27, "expected all 27 levels, got {levels}");
// Known gap, not a bug: the offset ending the shrub region is not in the
// core index and textures sit before moby, so they cannot bound it.
assert!(
unresolved_shrub > 20,
"shrub end should still be unresolved, saw {unresolved_shrub}"
);
eprintln!(
"regions: shrub starts {first}..{last}, end unresolved on {unresolved_shrub}/27 levels"
);
}