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:
@@ -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 ®ions {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user