//! The level container header (`LEVEL.WAD`). //! //! Spec: `docs/formats/level-wad.md`. //! //! Every one of the 27 level containers begins with the same 0x60-byte header, //! which names five extents and turns the generic pair table into named, //! individually parseable assets: //! //! ```text //! offset size field //! 0x00 4 header size, 0x60 on every level //! 0x04 4 zero on every level; a field the game fills in at runtime //! 0x08 4 level id //! 0x0c 4 reverb configuration index //! 0x10 8 data (geometry: tfrag, tie, shrub, moby, sky, ...) //! 0x18 8 sound_bank //! 0x20 8 gameplay //! 0x28 8 occlusion //! 0x30 48 chunk descriptors (3 x 16) //! ``` //! //! `data` is by far the largest and is where the level's visible content lives. //! `gameplay` carries the per-class dispatch tables; `occlusion` is tiny, 12 //! sectors on level 0. //! //! **Sizes here are sector counts, not byte counts.** This differs from the //! generic pair table in [`crate::global_wad`], whose sizes are bytes. Both //! appear on the disc and reading one as the other produces extents that look //! plausible and are wrong by a factor of 2048. Verified: on all 27 levels the //! four named extents tile the container in order with no gap and no overlap, //! which only holds if the size field is a sector count. use crate::buf::{Buf, Error, ErrorKind, Result}; use crate::global_wad::SectorRange; /// Header size, identical on all 27 levels. pub const LEVEL_HEADER_SIZE: usize = 0x60; /// A parsed level container header. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LevelHeader { pub header_size: u32, /// Always zero on disc across all 27 levels. The game writes it at load /// time, so it is read but must not be trusted as the container size; use /// the ISO entry's length for that. pub runtime_sector: u32, /// The level's own identifier. /// /// Equal to the container index on 26 of 27 levels. `LEVEL21.WAD` reports /// **30**, so the two are not interchangeable: this field is what the game /// calls a level, and the file order is not authoritative. Some level was /// removed from the disc and its number reused by nothing, leaving a gap. pub id: i32, /// Index into the game's reverb configuration. pub reverb: i32, pub data: SectorRange, pub sound_bank: SectorRange, pub gameplay: SectorRange, pub occlusion: SectorRange, /// Three 16-byte chunk descriptors. These follow the named extents and claim /// the remainder of the container on most levels; they are zero on others. pub chunks: [SectorRange; 6], } /// A named extent from a level header. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LevelPart { Data, SoundBank, Gameplay, Occlusion, } impl LevelPart { pub fn name(self) -> &'static str { match self { LevelPart::Data => "data", LevelPart::SoundBank => "sound_bank", LevelPart::Gameplay => "gameplay", LevelPart::Occlusion => "occlusion", } } } impl LevelHeader { /// Parse a level container header from the start of the file. pub fn parse(bytes: &[u8]) -> Result { let mut b = Buf::new(bytes); let header_size = b.u32("level header size")?; if header_size as usize != LEVEL_HEADER_SIZE { return Err(Error { offset: 0, context: "level container header", kind: ErrorKind::Invalid("header size is not 0x60"), }); } let runtime_sector = b.u32("level runtime sector")?; let id = b.i32("level id")?; let reverb = b.i32("reverb index")?; let range = |b: &mut Buf<'_>, what: &'static str| -> Result { let offset = b.i32(what)?; let size = b.i32(what)?; Ok(SectorRange { offset, size }) }; let data = range(&mut b, "data extent")?; let sound_bank = range(&mut b, "sound_bank extent")?; let gameplay = range(&mut b, "gameplay extent")?; let occlusion = range(&mut b, "occlusion extent")?; // 0x30..0x60: six u32s forming three {offset, size} descriptors. let mut chunks = [SectorRange { offset: 0, size: 0 }; 6]; for slot in chunks.iter_mut() { slot.offset = b.i32("chunk descriptor")?; slot.size = b.i32("chunk descriptor")?; } Ok(LevelHeader { header_size, runtime_sector, id, reverb, data, sound_bank, gameplay, occlusion, chunks, }) } /// The extent for a named part. pub fn part(&self, which: LevelPart) -> SectorRange { match which { LevelPart::Data => self.data, LevelPart::SoundBank => self.sound_bank, LevelPart::Gameplay => self.gameplay, LevelPart::Occlusion => self.occlusion, } } /// The named extents in the order they appear in the file. /// /// The order on the disc is sound bank, data, gameplay, occlusion — not the /// order the fields appear in the header. pub fn ordered_extents(&self) -> Vec<(LevelPart, SectorRange)> { [ (LevelPart::SoundBank, self.sound_bank), (LevelPart::Data, self.data), (LevelPart::Gameplay, self.gameplay), (LevelPart::Occlusion, self.occlusion), ] .into_iter() .filter(|(_, r)| r.is_present()) .collect() } /// Every extent that points at real data, in file order. pub fn present_parts(&self) -> Vec<(LevelPart, SectorRange)> { self.ordered_extents() } /// Chunk descriptors that claim real data, as `(index, extent)`. pub fn present_chunks(&self) -> Vec<(usize, SectorRange)> { self.chunks .iter() .copied() .enumerate() .filter(|(_, r)| r.is_present()) .collect() } /// Total sectors claimed by the named extents and the chunks. /// /// Cross-check against the container's length in sectors. Measured across /// all 27 levels: 21 land at `length` (the extents start at sector 1, after /// the header, and the last one ends one sector past the file end, which is /// normal sector rounding), and 6 leave 134 to 689 sectors of trailing data /// past the last named extent. /// /// So the value is always `>= length - 1`, and that is the invariant worth /// asserting. It is not always equal to `length`, and treating it as such /// would truncate the tail of a level. pub fn payload_sectors(&self) -> i32 { let named: i32 = self.ordered_extents().iter().map(|(_, r)| r.size).sum(); let chunks: i32 = self.present_chunks().iter().map(|(_, r)| r.size).sum(); named + chunks } } #[cfg(test)] mod tests { use super::*; /// A header matching LEVEL0.WAD on the retail disc. fn level0_header() -> Vec { let mut v = vec![0u8; 0x60]; v[0x00..0x04].copy_from_slice(&0x60u32.to_le_bytes()); // The runtime sector field is zero on every retail level. v[0x04..0x08].copy_from_slice(&0u32.to_le_bytes()); v[0x08..0x0c].copy_from_slice(&0i32.to_le_bytes()); v[0x0c..0x10].copy_from_slice(&3i32.to_le_bytes()); v[0x10..0x14].copy_from_slice(&735i32.to_le_bytes()); v[0x14..0x18].copy_from_slice(&6484i32.to_le_bytes()); v[0x18..0x1c].copy_from_slice(&1i32.to_le_bytes()); v[0x1c..0x20].copy_from_slice(&734i32.to_le_bytes()); v[0x20..0x24].copy_from_slice(&7219i32.to_le_bytes()); v[0x24..0x28].copy_from_slice(&322i32.to_le_bytes()); v[0x28..0x2c].copy_from_slice(&7541i32.to_le_bytes()); v[0x2c..0x30].copy_from_slice(&12i32.to_le_bytes()); v } #[test] fn parses_a_retail_level_header() { let h = LevelHeader::parse(&level0_header()).unwrap(); assert_eq!(h.header_size, 0x60); assert_eq!(h.runtime_sector, 0); assert_eq!(h.id, 0); assert_eq!(h.reverb, 3); assert_eq!( h.data, SectorRange { offset: 735, size: 6484 } ); assert_eq!( h.sound_bank, SectorRange { offset: 1, size: 734 } ); assert_eq!( h.gameplay, SectorRange { offset: 7219, size: 322 } ); assert_eq!( h.occlusion, SectorRange { offset: 7541, size: 12 } ); } #[test] fn extents_tile_the_container_in_order() { let h = LevelHeader::parse(&level0_header()).unwrap(); // Each extent's end must be the next extent's start, in file order. This // is the check that holds on every level rather than only the sample in // front of us. let parts = h.ordered_extents(); for w in parts.windows(2) { assert_eq!( w[0].1.offset + w[0].1.size, w[1].1.offset, "extent gap or overlap between {:?} and {:?}", w[0].1, w[1].1 ); } // Level 0's container is 7552 sectors; its extents end one sector past // that, which is why `payload_sectors` is compared with `>=` below // rather than for equality. assert!(h.payload_sectors() >= 7552 - 1); } #[test] fn payload_sectors_sums_the_named_extents() { let h = LevelHeader::parse(&level0_header()).unwrap(); // Sizes are sector counts: 734 + 6484 + 322 + 12. assert_eq!(h.payload_sectors(), 7552); } #[test] fn chunk_descriptors_add_to_the_remainder() { let mut v = level0_header(); // Level 1 declares two chunks after its named extents. v[0x30..0x34].copy_from_slice(&8741i32.to_le_bytes()); v[0x34..0x38].copy_from_slice(&1519i32.to_le_bytes()); v[0x38..0x3c].copy_from_slice(&10260i32.to_le_bytes()); v[0x3c..0x40].copy_from_slice(&116i32.to_le_bytes()); let h = LevelHeader::parse(&v).unwrap(); assert_eq!(h.present_chunks().len(), 2); // The named extents still total 7552; the two chunks add 1635. assert_eq!(h.payload_sectors(), 7552 + 1519 + 116); } #[test] fn present_parts_skips_empty_extents() { let mut v = level0_header(); v[0x28..0x30].copy_from_slice(&[0u8; 8]); let h = LevelHeader::parse(&v).unwrap(); let names: Vec<&str> = h.present_parts().iter().map(|(p, _)| p.name()).collect(); assert_eq!(names, vec!["sound_bank", "data", "gameplay"]); } #[test] fn rejects_a_wrong_header_size() { let mut v = level0_header(); v[0x00..0x04].copy_from_slice(&0x68u32.to_le_bytes()); let err = LevelHeader::parse(&v).unwrap_err(); assert!(matches!(err.kind, ErrorKind::Invalid(_)), "got {err:?}"); } #[test] fn rejects_a_truncated_header() { // Correct declared size, but the buffer stops early. let mut v = vec![0u8; 0x20]; v[0x00..0x04].copy_from_slice(&0x60u32.to_le_bytes()); let err = LevelHeader::parse(&v).unwrap_err(); assert!( matches!(err.kind, ErrorKind::OutOfBounds { .. }), "got {err:?}" ); } #[test] fn extent_order_is_sound_bank_then_data_not_header_order() { let h = LevelHeader::parse(&level0_header()).unwrap(); let order: Vec<&str> = h.ordered_extents().iter().map(|(p, _)| p.name()).collect(); assert_eq!(order, vec!["sound_bank", "data", "gameplay", "occlusion"]); } #[test] fn chunk_descriptors_are_read() { let mut v = level0_header(); v[0x30..0x34].copy_from_slice(&11i32.to_le_bytes()); v[0x34..0x38].copy_from_slice(&22i32.to_le_bytes()); let h = LevelHeader::parse(&v).unwrap(); assert_eq!( h.chunks[0], SectorRange { offset: 11, size: 22 } ); // The rest stay zero. assert_eq!(h.chunks[1], SectorRange { offset: 0, size: 0 }); } }