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
+23 -1
View File
@@ -237,6 +237,14 @@ pub struct LevelCoreHeader {
pub shrub_textures: ArrayRange,
/// Byte offset of texture pixel data within `core_data`.
pub textures_base: i32,
/// Compressed size of `core_data`'s WAD stream, and its decompressed size.
///
/// These describe the whole asset lump, not a separate container: on level
/// 0 they are 9,738,678 and 16,969,296, exactly the sizes of `core_data`
/// before and after decompression. The moby, tie and shrub class tables
/// name geometry by byte offset into the *decompressed* form.
pub assets_compressed_size: i32,
pub assets_decompressed_size: i32,
}
impl LevelCoreHeader {
@@ -262,6 +270,13 @@ impl LevelCoreHeader {
// 0x50..0x60: particle and effect texture tables, then the banks.
b.skip(0x60 - 0x50, "particle texture tables")?;
let textures_base = b.i32("textures base")?;
// 0x60..0x80: particle/effect banks and defs, sound remap, then the
// ratchet sequence table and the scene view size.
b.skip(0x80 - 0x64, "particle banks")?;
// 0x80: gadget table, then the asset lump's two sizes.
b.skip(0x88 - 0x80, "gadget table")?;
let assets_compressed_size = b.i32("assets compressed size")?;
let assets_decompressed_size = b.i32("assets decompressed size")?;
Ok(LevelCoreHeader {
gs_ram,
tfrags,
@@ -276,6 +291,8 @@ impl LevelCoreHeader {
tie_textures,
shrub_textures,
textures_base,
assets_compressed_size,
assets_decompressed_size,
})
}
}
@@ -286,7 +303,7 @@ mod tests {
/// A header as measured on level 0.
fn header() -> Vec<u8> {
let mut v = vec![0u8; 0x64];
let mut v = vec![0u8; 0x90];
v[0x00..0x04].copy_from_slice(&404i32.to_le_bytes());
v[0x04..0x08].copy_from_slice(&18_704i32.to_le_bytes());
v[0x08..0x0c].copy_from_slice(&0i32.to_le_bytes());
@@ -296,6 +313,8 @@ mod tests {
v[0x30..0x34].copy_from_slice(&93i32.to_le_bytes());
v[0x34..0x38].copy_from_slice(&10_512i32.to_le_bytes());
v[0x60..0x64].copy_from_slice(&3_518_848i32.to_le_bytes());
v[0x88..0x8c].copy_from_slice(&9_738_678i32.to_le_bytes());
v[0x8c..0x90].copy_from_slice(&16_969_296i32.to_le_bytes());
v
}
@@ -321,11 +340,14 @@ mod tests {
}
);
assert_eq!(h.textures_base, 3_518_848);
assert_eq!(h.assets_compressed_size, 9_738_678);
assert_eq!(h.assets_decompressed_size, 16_969_296);
}
#[test]
fn a_short_header_is_an_error() {
assert!(LevelCoreHeader::parse(&[0u8; 0x20]).is_err());
assert!(LevelCoreHeader::parse(&[0u8; 0x8c]).is_err());
}
#[test]