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:
2026-10-12 02:43:28 +02:00
parent bbce492c91
commit 681e7e62ef
5 changed files with 356 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
use gc_formats::level_core::LevelCoreHeader;
fn main() {
let core = std::fs::read("/home/reaper/Documents/gc-extracted/level0/core_data.bin").unwrap();
let idx = std::fs::read("/home/reaper/Documents/gc-extracted/level0/core_index.bin").unwrap();
let h = LevelCoreHeader::parse(&idx).unwrap();
let sky = h.sky as usize;
let end = [
h.collision as usize,
h.moby_classes.offset as usize,
core.len(),
]
.into_iter()
.filter(|&v| v > sky)
.min()
.unwrap_or(core.len());
let b = &core[sky..end];
let mut offs = Vec::new();
for k in 0..64 {
let at = 0x20 + k * 4;
if at + 4 > b.len() {
break;
}
let v = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
if v == 0 || v >= b.len() {
break;
}
offs.push(v);
}
println!(
"RANGE: level0 sky, {} sub-objects from the +0x20 table",
offs.len()
);
for (i, &o) in offs.iter().enumerate() {
let next = offs.get(i + 1).copied().unwrap_or(b.len());
println!(" sub{i}: {o}..{next} = {} bytes", next - o);
let e2 = next.min(b.len());
let seg = &b[o..e2];
for r in 0..3 {
let q = r * 16;
if q + 16 <= seg.len() {
println!(
" +{q:03x}: {}",
seg[q..q + 16]
.iter()
.map(|x| format!("{x:02x}"))
.collect::<Vec<_>>()
.join(" ")
);
}
}
}
}