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:
2026-10-11 02:03:31 +02:00
parent e1105543e4
commit 4c4a78d402
6 changed files with 213 additions and 6 deletions
+24
View File
@@ -2496,6 +2496,9 @@ e1baca5f8178da95e7dc496305a36f984be542aa 15 hud/0780.bin
acef5e387c8051229eecf70a64fd6daa077b34b3 518656 level0/gs_ram.bin
d7045d6c1256a9ffa241acca76e3761c34da3341 2751268 level0/overlay.bin
7ec9566aeae513fe272bc97899a4dbe15a2a74ff 1503232 level0/sound_bank.bin
6d98a8d7b65d6c743c23c27863c9c7dbf3fc4493 3110912 level1/chunk0.bin
e01bcf80d072da3298d854fb095f13cd6834fca6 237568 level1/chunk1.bin
62b48a3295f5d0cb8094a63ec77c2f514b4c5077 321536 level1/chunk3.bin
6b1a358c6eb6039d0df51fce061b58c274e8c99c 19023392 level1/core_data.bin
96b33cef1ac3c9bab5283ccf11bd86b0aa20ebcb 12165629 level1/core_data.wad
7cae5c7c2258f391ef72a8503183e36747214fbd 34016 level1/core_index.bin
@@ -2512,6 +2515,9 @@ fe9e5169820bc9272aa5fcfbbabfc74e7b12d1ce 12498944 level10/data.bin
4036bc105fa429b97a725ad40bd4051168c886ad 367872 level10/gs_ram.bin
76fa348a8764cbe97c8caf5a892105007c2bf4a2 2870708 level10/overlay.bin
d3e48237b03649578f6962e36f1bc197c3a8af9f 1306624 level10/sound_bank.bin
74ef643e332327bc142155c5570be38ecfe18d10 1988608 level11/chunk0.bin
62eab219f89f2c1374e29dd302e8f86f69882e6e 1521664 level11/chunk1.bin
ab08381b884f6585818780791a27fd8f44366e76 704512 level11/chunk3.bin
da253d8b4dc4f8fc482880c54cd832c53bfd4aa0 20717584 level11/core_data.bin
a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
0e0d5289451a3bffa376992d0b6a288421a7a55d 36000 level11/core_index.bin
@@ -2577,6 +2583,11 @@ d0f931677cb6589a564937c3d15c13344b923969 15501312 level18/data.bin
7f6a823c4b47fdfb58e29237b680d03e946329db 571136 level18/gs_ram.bin
e5370c97b9a2ccdb00678e57f9e18042eab6735d 2937576 level18/overlay.bin
b26483cc29ecb8b6d2c71919ff7cd56b09522bc1 1492992 level18/sound_bank.bin
11dbeeb1847ff09621d132adb9ab0a149fc65a5a 3252224 level19/chunk0.bin
0e05525617f49895b26da1521b8d2ceb9f0c52a2 2742272 level19/chunk1.bin
f9110d5cb18a0fbd45a760dc4d926a87db099b7c 1017856 level19/chunk2.bin
c739bdb3ea348b169b63114f2b13149e647070e0 92160 level19/chunk3.bin
77cbf1d1847c0846c13a23983e664f9546feddf8 92160 level19/chunk4.bin
0918d824b72b892a24f46108bbc60f6e875236f6 20233504 level19/core_data.bin
b79df8e074db7d3c75bc2b3b9c2a4de61e5dd9cd 11797811 level19/core_data.wad
b0ea377a39a616dbd2b15224a9996925c7b28f4d 31536 level19/core_index.bin
@@ -2586,6 +2597,9 @@ d5061a551eb354ddf7952c4bfd62767c6e074c3f 497920 level19/gs_ram.bin
877ca7fb8e80d810d6c37fb993795aa10b5b1033 47104 level19/occlusion.bin
428ea51a157b35cdf831f2a6908647d8ffe6a91b 2821856 level19/overlay.bin
8298f799c394542e912e41ff3321ee55c70e0c8f 1570816 level19/sound_bank.bin
e6c6de0fafb808093d6e3f2282e86b3fc623d7cb 2486272 level2/chunk0.bin
84bb6b19112861026a24d84cbf83946e2bf31892 321536 level2/chunk1.bin
6cb42e4727ce5a55cf6e5a9147a32149a2d15891 438272 level2/chunk3.bin
6d56189ad74f04d366116b736ab12888154de833 20209408 level2/core_data.bin
f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
73411362c1cfa8faec733705d93e953f1381ef30 33808 level2/core_index.bin
@@ -2595,6 +2609,11 @@ f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
01c7d4c7b0ecb44a91694ffde796ff941057191a 28672 level2/occlusion.bin
d7182afdf4a986c9c781a9658373281bfc37df22 2928424 level2/overlay.bin
bce56b3b899d72c5d89f612bd70447f066c10757 1118208 level2/sound_bank.bin
015dfaf782ade5bdd3815a66b4c381a93ec7cc6d 1462272 level20/chunk0.bin
93481eb3afd7edfe679c878a74d5ec627196cf86 1878016 level20/chunk1.bin
1867fb80ce6d3b592c687e712e9782cd5a43dd6f 565248 level20/chunk2.bin
b81d361a7c7136d92990d720f6f22fbbee9b0d89 342016 level20/chunk3.bin
1baaf377e209e353b2fbb80d408bbc52a67f6b5f 342016 level20/chunk4.bin
daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
7e7203ac3a2dbf578546573a34fa6dce4c336d35 10449039 level20/core_data.wad
b12e8599d211addc85cbe138e58157ade380a575 37360 level20/core_index.bin
@@ -2658,6 +2677,9 @@ eb8366046b5643cc54d42d800c06bf66ccd4ef36 15157248 level3/data.bin
ed1f77cd35e2572adc51f4f43de030f31b8c6ed9 675840 level3/gs_ram.bin
6fb6a7e7a64afd2f1e897f23640866f28154aa1d 2820776 level3/overlay.bin
d986825dfc2bed713eb9699504536346658aaa3f 1705984 level3/sound_bank.bin
2778532b95fe6a545004f5a922977cac656519e7 3436544 level4/chunk0.bin
59aead3118f5be41ab3ab545e2476e7fed6842f1 2426880 level4/chunk1.bin
482ac30a9958e484708f9b82e2784a62a0cf753e 303104 level4/chunk3.bin
f8c0aa3778b5bcb1995e516045b28ca6ac35502f 21107088 level4/core_data.bin
ec75f324670188af581c4fceaa4cb285353f95b8 12171908 level4/core_data.wad
cc4997acbdc0a6ac85d427227c04e6af6e34cd46 38080 level4/core_index.bin
@@ -2683,6 +2705,7 @@ d5874f71309fb750d37a08977653a4fe1b48672a 2617984 level6/gameplay.bin
c404dfa08cab9603e5deeeed9778e4d29b755d88 604160 level6/gs_ram.bin
4d981fd750196894f96f0a24437bc76210a1663a 3086372 level6/overlay.bin
82fcce964396eb508768933d092a557bc1b05c89 1669120 level6/sound_bank.bin
ef8e0a3a46dcb8f29595332ff9545b654130b194 3280896 level7/chunk0.bin
6692290d78706a2b92cba541264ddea9a764e2f2 18209392 level7/core_data.bin
f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
79e3957d149ce3b00f713eb1757fec89d7db86c4 32736 level7/core_index.bin
@@ -2692,6 +2715,7 @@ cc78412e868725caa8e9736653749e4b7eeacba4 2147328 level7/gameplay.bin
e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
82f26617c99f502324fb556006f8f859fda1b5f1 2733840 level7/overlay.bin
8e07c803b709100023c3b44c8842eec9b8f49898 1556480 level7/sound_bank.bin
78e0c64ccb11e29c9bdbfb9c9283a754867d6b5c 868352 level8/chunk0.bin
264a306c3a83b9137d9753b282e9c773cd386713 21086224 level8/core_data.bin
0a96094edd63dc4dd940894b007f85b21874cfeb 9700660 level8/core_data.wad
8d1b6c3a05e9b2140c056df3e538ed537b9d3a33 30624 level8/core_index.bin
1 3e93b99123a5611ed8cd197437a341daa7ad140c 264449 IOPRP255.IMG
2496 acef5e387c8051229eecf70a64fd6daa077b34b3 518656 level0/gs_ram.bin
2497 d7045d6c1256a9ffa241acca76e3761c34da3341 2751268 level0/overlay.bin
2498 7ec9566aeae513fe272bc97899a4dbe15a2a74ff 1503232 level0/sound_bank.bin
2499 6d98a8d7b65d6c743c23c27863c9c7dbf3fc4493 3110912 level1/chunk0.bin
2500 e01bcf80d072da3298d854fb095f13cd6834fca6 237568 level1/chunk1.bin
2501 62b48a3295f5d0cb8094a63ec77c2f514b4c5077 321536 level1/chunk3.bin
2502 6b1a358c6eb6039d0df51fce061b58c274e8c99c 19023392 level1/core_data.bin
2503 96b33cef1ac3c9bab5283ccf11bd86b0aa20ebcb 12165629 level1/core_data.wad
2504 7cae5c7c2258f391ef72a8503183e36747214fbd 34016 level1/core_index.bin
2515 4036bc105fa429b97a725ad40bd4051168c886ad 367872 level10/gs_ram.bin
2516 76fa348a8764cbe97c8caf5a892105007c2bf4a2 2870708 level10/overlay.bin
2517 d3e48237b03649578f6962e36f1bc197c3a8af9f 1306624 level10/sound_bank.bin
2518 74ef643e332327bc142155c5570be38ecfe18d10 1988608 level11/chunk0.bin
2519 62eab219f89f2c1374e29dd302e8f86f69882e6e 1521664 level11/chunk1.bin
2520 ab08381b884f6585818780791a27fd8f44366e76 704512 level11/chunk3.bin
2521 da253d8b4dc4f8fc482880c54cd832c53bfd4aa0 20717584 level11/core_data.bin
2522 a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
2523 0e0d5289451a3bffa376992d0b6a288421a7a55d 36000 level11/core_index.bin
2583 7f6a823c4b47fdfb58e29237b680d03e946329db 571136 level18/gs_ram.bin
2584 e5370c97b9a2ccdb00678e57f9e18042eab6735d 2937576 level18/overlay.bin
2585 b26483cc29ecb8b6d2c71919ff7cd56b09522bc1 1492992 level18/sound_bank.bin
2586 11dbeeb1847ff09621d132adb9ab0a149fc65a5a 3252224 level19/chunk0.bin
2587 0e05525617f49895b26da1521b8d2ceb9f0c52a2 2742272 level19/chunk1.bin
2588 f9110d5cb18a0fbd45a760dc4d926a87db099b7c 1017856 level19/chunk2.bin
2589 c739bdb3ea348b169b63114f2b13149e647070e0 92160 level19/chunk3.bin
2590 77cbf1d1847c0846c13a23983e664f9546feddf8 92160 level19/chunk4.bin
2591 0918d824b72b892a24f46108bbc60f6e875236f6 20233504 level19/core_data.bin
2592 b79df8e074db7d3c75bc2b3b9c2a4de61e5dd9cd 11797811 level19/core_data.wad
2593 b0ea377a39a616dbd2b15224a9996925c7b28f4d 31536 level19/core_index.bin
2597 877ca7fb8e80d810d6c37fb993795aa10b5b1033 47104 level19/occlusion.bin
2598 428ea51a157b35cdf831f2a6908647d8ffe6a91b 2821856 level19/overlay.bin
2599 8298f799c394542e912e41ff3321ee55c70e0c8f 1570816 level19/sound_bank.bin
2600 e6c6de0fafb808093d6e3f2282e86b3fc623d7cb 2486272 level2/chunk0.bin
2601 84bb6b19112861026a24d84cbf83946e2bf31892 321536 level2/chunk1.bin
2602 6cb42e4727ce5a55cf6e5a9147a32149a2d15891 438272 level2/chunk3.bin
2603 6d56189ad74f04d366116b736ab12888154de833 20209408 level2/core_data.bin
2604 f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
2605 73411362c1cfa8faec733705d93e953f1381ef30 33808 level2/core_index.bin
2609 01c7d4c7b0ecb44a91694ffde796ff941057191a 28672 level2/occlusion.bin
2610 d7182afdf4a986c9c781a9658373281bfc37df22 2928424 level2/overlay.bin
2611 bce56b3b899d72c5d89f612bd70447f066c10757 1118208 level2/sound_bank.bin
2612 015dfaf782ade5bdd3815a66b4c381a93ec7cc6d 1462272 level20/chunk0.bin
2613 93481eb3afd7edfe679c878a74d5ec627196cf86 1878016 level20/chunk1.bin
2614 1867fb80ce6d3b592c687e712e9782cd5a43dd6f 565248 level20/chunk2.bin
2615 b81d361a7c7136d92990d720f6f22fbbee9b0d89 342016 level20/chunk3.bin
2616 1baaf377e209e353b2fbb80d408bbc52a67f6b5f 342016 level20/chunk4.bin
2617 daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
2618 7e7203ac3a2dbf578546573a34fa6dce4c336d35 10449039 level20/core_data.wad
2619 b12e8599d211addc85cbe138e58157ade380a575 37360 level20/core_index.bin
2677 ed1f77cd35e2572adc51f4f43de030f31b8c6ed9 675840 level3/gs_ram.bin
2678 6fb6a7e7a64afd2f1e897f23640866f28154aa1d 2820776 level3/overlay.bin
2679 d986825dfc2bed713eb9699504536346658aaa3f 1705984 level3/sound_bank.bin
2680 2778532b95fe6a545004f5a922977cac656519e7 3436544 level4/chunk0.bin
2681 59aead3118f5be41ab3ab545e2476e7fed6842f1 2426880 level4/chunk1.bin
2682 482ac30a9958e484708f9b82e2784a62a0cf753e 303104 level4/chunk3.bin
2683 f8c0aa3778b5bcb1995e516045b28ca6ac35502f 21107088 level4/core_data.bin
2684 ec75f324670188af581c4fceaa4cb285353f95b8 12171908 level4/core_data.wad
2685 cc4997acbdc0a6ac85d427227c04e6af6e34cd46 38080 level4/core_index.bin
2705 c404dfa08cab9603e5deeeed9778e4d29b755d88 604160 level6/gs_ram.bin
2706 4d981fd750196894f96f0a24437bc76210a1663a 3086372 level6/overlay.bin
2707 82fcce964396eb508768933d092a557bc1b05c89 1669120 level6/sound_bank.bin
2708 ef8e0a3a46dcb8f29595332ff9545b654130b194 3280896 level7/chunk0.bin
2709 6692290d78706a2b92cba541264ddea9a764e2f2 18209392 level7/core_data.bin
2710 f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
2711 79e3957d149ce3b00f713eb1757fec89d7db86c4 32736 level7/core_index.bin
2715 e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
2716 82f26617c99f502324fb556006f8f859fda1b5f1 2733840 level7/overlay.bin
2717 8e07c803b709100023c3b44c8842eec9b8f49898 1556480 level7/sound_bank.bin
2718 78e0c64ccb11e29c9bdbfb9c9283a754867d6b5c 868352 level8/chunk0.bin
2719 264a306c3a83b9137d9753b282e9c773cd386713 21086224 level8/core_data.bin
2720 0a96094edd63dc4dd940894b007f85b21874cfeb 9700660 level8/core_data.wad
2721 8d1b6c3a05e9b2140c056df3e538ed537b9d3a33 30624 level8/core_index.bin
+20
View File
@@ -137,6 +137,26 @@ pub fn unpack(iso_path: &Path, out_dir: &Path) -> Result<Vec<Extracted>, String>
}
parts += 1;
}
// Chunk descriptors follow the named extents. One of them
// is the level's asset WAD, which the moby/tie/shrub class
// tables point into by byte offset.
for (i, range) in header.present_chunks() {
let Some(offset) = range.byte_offset() else {
continue;
};
let len = range.byte_len() as usize;
if offset + len as u64 > raw.len() as u64 {
continue;
}
write_file(
out_dir,
&format!("{stem}/chunk{i}.bin"),
&raw[offset as usize..offset as usize + len],
&mut written,
);
parts += 1;
}
// The `data` extent is itself a container of byte ranges.
// Split out the geometry directory and the geometry, so the
// latter is usable without re-deriving the layout later.
+107
View File
@@ -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");
}