Parse the sky block: 149 sub-objects with a bounding sphere, and correct the count field
Range measured: all 27 levels, each sky region from the core index's sky offset to the next region. Method: read the 32-bit offset table at +0x20, bounded by staying inside the region, and parse each sub-object it names. Result: 149 sub-objects across 27 levels. Each opens with a count, then a bounding sphere of four floats at +0x10, then a texture offset at +0x20 and dimensions at +0x24/+0x26: +000: 26 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 +010: e0 8d b2 c1 | 9f d6 f6 3d | ef fc fb be | 1c 61 4e 41 +020: 90 99 05 00 | 1d 00 | 1d 00 | 00 00 e8 00 ... Count 1 to 38, width 3 to 81, radius 1.95 to 14.56, and the radius is positive on 149 of 149 -- the same sphere shape and the same reasoning as the shrub class header's. Correction to an earlier claim: the word at +0x0c is not the sub-object count. It reads 5 on level 0 where the table holds 6 entries, 3 on level 1 against 4, 7 on level 3 against 8, and 196617 on level 2 against 4. Across the disc offsets.len() == count+1 holds on 10 of 27 levels. The field is reported as SkyHeader::field_0c and nothing depends on it; the offset table enumerates the sub-objects and is self-limiting. That earlier claim came from level 0 alone, where 5 against 6 looked like a consistent off-by-one. It is not an off-by-one -- it is a field that happens to be small on most levels and is not a count at all. gc_formats::sky parses the header and the sub-objects. The gated test asserts the sphere is finite, the radius positive where positive at all, the dimensions non-zero, and the sub-object list non-empty on every level.
This commit is contained in:
@@ -2364,3 +2364,106 @@ fn constant_positions(records: &[&[u8]]) -> usize {
|
||||
.filter(|&i| records.iter().all(|r| r[i] == first[i]))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// The sky block: the sub-object table at `+0x20` enumerates the sub-objects on
|
||||
/// every level, and each carries a bounding sphere and texture dimensions.
|
||||
///
|
||||
/// The word at `+0x0c` is deliberately **not** asserted to be a count: it
|
||||
/// disagrees with the table on most levels. See `gc_formats::sky`.
|
||||
#[test]
|
||||
fn sky_sub_objects_parse_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;
|
||||
}
|
||||
|
||||
let mut levels = 0usize;
|
||||
let mut total = 0usize;
|
||||
let mut radii = (f32::MAX, f32::MIN);
|
||||
let mut counts = (u32::MAX, 0u32);
|
||||
let mut dims = (u16::MAX, 0u16);
|
||||
let mut nonzero_radius = 0usize;
|
||||
let mut reserved_nonzero = 0usize;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
let core = match std::fs::read(dir.join("core_data.bin")) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let index = match std::fs::read(dir.join("core_index.bin")) {
|
||||
Ok(i) => i,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let hdr = match gc_formats::level_core::LevelCoreHeader::parse(&index) {
|
||||
Ok(h) => h,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let sky = hdr.sky as usize;
|
||||
if sky == 0 || sky >= core.len() {
|
||||
continue;
|
||||
}
|
||||
let end = [
|
||||
hdr.collision as usize,
|
||||
hdr.moby_classes.offset as usize,
|
||||
core.len(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|&v| v > sky)
|
||||
.min()
|
||||
.unwrap_or(core.len());
|
||||
let region = &core[sky..end.min(core.len())];
|
||||
|
||||
let parsed = gc_formats::sky::Sky::parse(region).expect("sky should parse");
|
||||
assert!(
|
||||
!parsed.sub_objects.is_empty(),
|
||||
"level{n}: sky region has no sub-objects"
|
||||
);
|
||||
levels += 1;
|
||||
total += parsed.sub_objects.len();
|
||||
|
||||
for s in &parsed.sub_objects {
|
||||
if s.reserved != [0u8; 12] {
|
||||
reserved_nonzero += 1;
|
||||
}
|
||||
assert!(s.count > 0, "level{n}: sky sub-object count is {}", s.count);
|
||||
assert!(
|
||||
s.width > 0 && s.height > 0,
|
||||
"level{n}: sky texture dims are {}x{}",
|
||||
s.width,
|
||||
s.height
|
||||
);
|
||||
assert!(
|
||||
s.sphere.iter().all(|f| f.is_finite()),
|
||||
"level{n}: sky sphere is not finite: {:?}",
|
||||
s.sphere
|
||||
);
|
||||
// The fourth float is the radius: positive wherever the first
|
||||
// three are negative, the same reasoning the shrub header uses.
|
||||
if s.sphere[3] > 0.0 {
|
||||
nonzero_radius += 1;
|
||||
radii.0 = radii.0.min(s.sphere[3]);
|
||||
radii.1 = radii.1.max(s.sphere[3]);
|
||||
}
|
||||
counts.0 = counts.0.min(s.count);
|
||||
counts.1 = counts.1.max(s.count);
|
||||
dims.0 = dims.0.min(s.width);
|
||||
dims.1 = dims.1.max(s.width);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(levels >= 20, "only {levels} levels had a sky region");
|
||||
assert!(total >= 100, "only {total} sky sub-objects parsed");
|
||||
eprintln!(
|
||||
"sky: {total} sub-objects over {levels} levels; \
|
||||
count {}..={}, width {}..={}, radius {:.2}..={:.2} on {nonzero_radius}",
|
||||
counts.0, counts.1, dims.0, dims.1, radii.0, radii.1
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user