//! Region bounds for a level's `core_data`. //! //! Spec: `docs/formats/core-data-regions.md`. //! //! This exists because of a specific failure. Region spans were being derived //! per-format during probing, usually by running from one known offset to the //! next, and usually with the last span running to `core_data.len()`. On level //! 0 that made the final shrub class a 925,008-byte span absorbing most of the //! lump's tail. Aggregates computed over that -- record counts, value //! distributions -- were silently wrong, and nothing errored. //! //! So bounds are computed once, here, from successor boundaries, and every //! caller uses these rather than inventing their own. Where a bound is not yet //! known it is `None`, not a guess: [`Region::end`] being `None` means //! "unresolved", and [`core_layout`] refuses to close a region whose successor //! it cannot find. //! //! Measured on the disc: //! //! | region | start | end | //! |------------|--------------|-------------------------| //! | tfrags | 0 | occlusion | //! | occlusion | occlusion | sky | //! | sky | sky | collision | //! | collision | collision | first moby class | //! | moby | first moby | first tie class | //! | tie | first tie | first shrub class, | //! | | | else the chunk container | //! | shrub | first shrub | the chunk container | //! | chunks | the chunk | lump length − 0x38A10 | //! | | container | | //! //! The chunk container's start is located by signature rather than read from //! the core index: the class table that precedes it carries no successor //! offset, so the boundary is found in `core_data` itself -- see //! [`crate::chunks::chunk_region_start`]. Every level has one: on the 24 levels //! with shrub it sits exactly two bytes after the last shrub body, and on the //! three levels without shrub it closes tie instead. //! //! Texture texels do **not** bound anything: they sit before moby, not after //! shrub. On level 0 the texel range is 3539328 to 5326208 while the last //! shrub class starts at 16044288. use crate::chunks::chunk_region_start; use crate::level_core::{ArrayRange, LevelCoreHeader}; /// Bytes at the end of `core_data` that are identical across all 27 levels. /// /// Measured by comparing the levels byte-for-byte from the end: the last /// 231,952 bytes (0x38A10) of every level's lump are the same. Level-specific /// content therefore ends at `len - 231952`, which is a real bound -- unlike /// running a last region out to `core_data.len()`. /// /// This is not yet explained. It may be fixed data, or padding from a /// fixed-size decompression buffer. Either way it is constant, so it bounds /// the tail region without having to guess what is in it. pub const CORE_DATA_TRAILER_BYTES: usize = 0x38A10; /// A byte range within `core_data`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Region { pub name: &'static str, pub start: usize, /// Exclusive end, or `None` when the successor boundary is not known. pub end: Option, } impl Region { pub fn len(&self) -> Option { self.end.map(|e| e.saturating_sub(self.start)) } pub fn is_empty(&self) -> bool { self.len() == Some(0) } } /// The lowest geometry offset in a class table, ignoring absent entries. /// /// Absent entries are zero, and treating zero as an offset would drag the /// region's start to zero -- the same "zero means absent, not position" trap /// that the occlusion and collision headers both fell into. pub fn first_geometry(index: &[u8], range: ArrayRange, stride: usize) -> Option { let count = range.count.max(0) as usize; let base = range.offset.max(0) as usize; let mut best: Option = None; for i in 0..count { let at = base + i * stride; let chunk = index.get(at..at + 4)?; let off = i32::from_le_bytes(chunk.try_into().expect("4 bytes")); if off > 0 { best = Some(best.map_or(off as usize, |b: usize| b.min(off as usize))); } } best } /// The highest geometry offset in a class table, ignoring absent entries. /// /// This is the *last* class in the region, which is where the search for the /// successor region starts. Starting there rather than at the region's head /// matters: the search is for the first chunk signature after this offset, so /// beginning it inside the region's own class bodies would risk matching one. pub fn last_geometry(index: &[u8], range: ArrayRange, stride: usize) -> Option { let count = range.count.max(0) as usize; let base = range.offset.max(0) as usize; let mut best: Option = None; for i in 0..count { let at = base + i * stride; let chunk = index.get(at..at + 4)?; let off = i32::from_le_bytes(chunk.try_into().expect("4 bytes")); if off > 0 { best = Some(best.map_or(off as usize, |b: usize| b.max(off as usize))); } } best } /// Compute the region's bounds for one level. /// /// `core` is the level's `core_data` lump. It is needed for one thing only: the /// class regions end at the chunk container, whose start is located by /// signature in `core` because the class table that precedes it carries no /// successor offset. pub fn core_layout(index: &[u8], core: &[u8], h: &LevelCoreHeader) -> Vec { let at = |v: i32| -> Option { (v > 0).then_some(v as usize) }; let first_moby = first_geometry(index, h.moby_classes, 0x20); let first_tie = first_geometry(index, h.tie_classes, 0x20); let last_tie = last_geometry(index, h.tie_classes, 0x20); let first_shrub = first_geometry(index, h.shrub_classes, 0x30); let last_shrub = last_geometry(index, h.shrub_classes, 0x30); // Each region ends where the next one begins. `chained` keeps `None` // propagating rather than silently closing a region against a gap. fn chained(name: &'static str, start: Option, next: Option) -> Option { let start = start?; Some(Region { name, start, end: next.filter(|&n| n > start), }) } // The chunk container follows the last of the shrub and tie classes. // Searching from the last *class* rather than from the region start keeps // the signature search out of the class bodies themselves. let container = match (last_shrub, last_tie) { (Some(shrub), _) => chunk_region_start(core, shrub), (None, Some(tie)) => chunk_region_start(core, tie), (None, None) => None, }; // The final region is bounded by the constant trailer rather than by // `core_data.len()`, which would let it absorb the trailer and whatever // tail follows the last named region. let tail_end = core .len() .checked_sub(CORE_DATA_TRAILER_BYTES) .filter(|&e| e > 0); let regions = [ chained("tfrags", Some(0), at(h.occlusion)), chained("occlusion", at(h.occlusion), at(h.sky)), chained("sky", at(h.sky), at(h.collision)), chained("collision", at(h.collision), first_moby), // A region with no successor region ends at the chunk container, which // is what follows the last class on every level. chained("moby", first_moby, first_tie.or(container)), chained("tie", first_tie, first_shrub.or(container)), chained("shrub", first_shrub, container), chained("chunks", container, tail_end), ]; regions.into_iter().flatten().collect() } #[cfg(test)] mod tests { use super::*; /// One index buffer carrying both the header fields and the class tables, /// so a test cannot accidentally pair a header with unrelated class data. fn index(occlusion: i32, sky: i32, collision: i32, classes: &[(usize, &[i32])]) -> Vec { let mut v = vec![0u8; 4096]; v[0x0c..0x10].copy_from_slice(&occlusion.to_le_bytes()); v[0x10..0x14].copy_from_slice(&sky.to_le_bytes()); v[0x14..0x18].copy_from_slice(&collision.to_le_bytes()); let bases = [0x18usize, 0x20, 0x28]; for (slot, (offset, vals)) in classes.iter().enumerate() { v[bases[slot]..bases[slot] + 4].copy_from_slice(&(vals.len() as i32).to_le_bytes()); v[bases[slot] + 4..bases[slot] + 8].copy_from_slice(&(*offset as i32).to_le_bytes()); for (i, g) in vals.iter().enumerate() { let at = offset + i * 0x20; v[at..at + 4].copy_from_slice(&g.to_le_bytes()); } } v } fn layout(v: &[u8]) -> Vec { let h = crate::level_core::LevelCoreHeader::parse(v).unwrap(); core_layout(v, &core(v), &h) } /// A zeroed lump, optionally carrying a real chunk-container signature just /// past the last class -- the layout locates the container in `core`, so a /// core of plain zeros locates nothing. fn core(v: &[u8]) -> Vec { let h = crate::level_core::LevelCoreHeader::parse(v).unwrap(); let mut c = vec![0u8; 1 << 24]; let last_class = [ last_geometry(v, h.tie_classes, 0x20), last_geometry(v, h.shrub_classes, 0x30), ] .into_iter() .flatten() .max(); if let Some(last) = last_class { plant_chunk(&mut c, last + 100); } c } /// The marker pair plus a chunk signature at `at`, as the disc carries it. fn plant_chunk(core: &mut [u8], at: usize) { core[at - 2..at].copy_from_slice(&crate::chunks::CHUNK_REGION_MARKER); core[at + 14] = 4; core[at + 15] = 0xff; for k in 0..4 { let word = (crate::chunks::CHUNK_TAG as u32) << 24; let at = at + crate::chunks::CHUNK_HEADER_BYTES + crate::chunks::CHUNK_GAP_BYTES + k * 4; core[at..at + 4].copy_from_slice(&word.to_le_bytes()); } } #[test] fn regions_chain_and_do_not_overlap() { let v = index( 2_117_888, 2_192_576, 2_615_232, &[(200, &[3000]), (400, &[4000]), (600, &[5000])], ); let regions = layout(&v); let names: Vec<_> = regions.iter().map(|r| r.name).collect(); assert_eq!( names, [ "tfrags", "occlusion", "sky", "collision", "moby", "tie", "shrub", "chunks" ] ); assert_eq!(regions[0].start, 0); assert_eq!(regions[1].start, 2_117_888); assert_eq!(regions[1].end, Some(2_192_576)); assert_eq!(regions[4].start, 3000); assert_eq!(regions[4].end, Some(4000)); for pair in regions.windows(2) { if let Some(end) = pair[0].end { assert!( end <= pair[1].start, "{:?} ends after {:?} starts", pair[0], pair[1] ); } } } #[test] fn a_zero_class_offset_is_absent_not_position_zero() { let v = index( 100, 200, 300, &[(200, &[0, 3000]), (400, &[4000]), (600, &[5000])], ); let moby = layout(&v).into_iter().find(|r| r.name == "moby").unwrap(); assert_eq!(moby.start, 3000); } #[test] fn the_class_regions_end_at_the_chunk_container() { let core_len = 1 << 24; let v = index( 100, 200, 300, &[(200, &[1000]), (400, &[2000]), (600, &[3000])], ); let regions = layout(&v); // The container sits 100 bytes past the last shrub class, which is // where the search for it starts. let container = 3000 + 100; let shrub = regions.iter().find(|r| r.name == "shrub").unwrap(); assert_eq!(shrub.start, 3000); assert_eq!(shrub.end, Some(container)); let chunks = regions.iter().find(|r| r.name == "chunks").unwrap(); assert_eq!(chunks.start, container); // Not core_data.len(): the trailer is constant across levels and does // not belong to any region. assert_eq!(chunks.end, Some(core_len - CORE_DATA_TRAILER_BYTES)); assert!(chunks.end.unwrap() < core_len); // Tie still ends at shrub, not at the container. let tie = regions.iter().find(|r| r.name == "tie").unwrap(); assert_eq!(tie.end, Some(3000)); } #[test] fn tie_ends_at_the_container_when_there_is_no_shrub() { // Third class table absent: tie is the last class region, so it is tie // the container closes. let v = index(100, 200, 300, &[(200, &[1000]), (400, &[2000])]); let h = crate::level_core::LevelCoreHeader::parse(&v).unwrap(); let core = core(&v); let container = 2000 + 100; let regions = core_layout(&v, &core, &h); let tie = regions.iter().find(|r| r.name == "tie").unwrap(); assert_eq!(tie.end, Some(container)); assert!(regions.iter().all(|r| r.name != "shrub")); let chunks = regions.iter().find(|r| r.name == "chunks").unwrap(); assert_eq!(chunks.start, container); } #[test] fn an_unlocated_container_leaves_the_class_end_unresolved() { // A core of plain zeros carries no signature, so shrub cannot be // closed -- the end stays visible rather than clamped to the lump's // tail, and the chunk region does not exist at all. let v = index( 100, 200, 300, &[(200, &[1000]), (400, &[2000]), (600, &[3000])], ); let h = crate::level_core::LevelCoreHeader::parse(&v).unwrap(); let regions = core_layout(&v, &[0u8; 4 << 20], &h); let shrub = regions.iter().find(|r| r.name == "shrub").unwrap(); assert_eq!(shrub.end, None); assert!(regions.iter().all(|r| r.name != "chunks")); } #[test] fn a_lump_shorter_than_the_trailer_leaves_the_end_unresolved() { let v = index( 100, 200, 300, &[(200, &[1000]), (400, &[2000]), (600, &[3000])], ); let h = crate::level_core::LevelCoreHeader::parse(&v).unwrap(); // Shorter than the trailer, so the container starts but cannot end. let mut core = vec![0u8; CORE_DATA_TRAILER_BYTES - 1]; plant_chunk(&mut core, 3000 + 100); let regions = core_layout(&v, &core, &h); let shrub = regions.iter().find(|r| r.name == "shrub").unwrap(); assert_eq!(shrub.end, Some(3100)); let chunks = regions.iter().find(|r| r.name == "chunks").unwrap(); assert_eq!(chunks.end, None, "must not clamp a backwards bound"); } #[test] fn an_absent_region_is_omitted_entirely() { let v = index(0, 0, 0, &[]); let regions = layout(&v); // Only tfrags can be placed at all, and only with an unknown end. assert_eq!(regions.len(), 1, "{regions:?}"); assert_eq!(regions[0].name, "tfrags"); assert_eq!(regions[0].start, 0); assert_eq!(regions[0].end, None); } #[test] fn a_gap_does_not_close_a_region() { // Sky starts before occlusion ends: nonsense, so neither may be closed // against the other. let v = index( 500, 400, 600, &[(200, &[3000]), (400, &[4000]), (600, &[5000])], ); let regions = layout(&v); // occlusion at 500 cannot be closed against sky at 400, so it stays // open rather than being clamped into a backwards range. let occ = regions.iter().find(|r| r.name == "occlusion").unwrap(); assert_eq!(occ.start, 500); assert_eq!(occ.end, None); assert_eq!(occ.len(), None); } }