//! A structure dumper for reverse engineering. //! //! This exists because of a specific, repeated failure. Probing these formats //! by unpacking raw offsets out of `core_index.bin` got `collision` read as //! `sky` on all 27 levels, produced a page of implausible collision headers, //! and led to "fixing" a stride bug that did not exist. The core index //! interleaves single-word region offsets with count/offset **pairs**, so every //! field after a pair shifts if you miscount it. //! //! Everything here goes through the same `gc_formats` types the rest of the //! project uses, so a field name printed here is the field name the parser //! uses. Reading a structure by hand in a shell remains possible; this just //! stops it being the default. use std::path::Path; use anyhow::{Context, Result}; /// Byte size of a moby or tie class entry. pub const CLASS_ENTRY_BYTES: usize = 0x20; /// Byte size of a shrub class entry, which carries billboard fields the other /// two do not. pub const SHRUB_CLASS_ENTRY_BYTES: usize = 0x30; /// One level's core header plus its three class tables. pub struct LevelProbe { pub level: u32, pub core_len: usize, pub index_len: usize, /// Rendered field-by-field dump of the core header. pub header: Vec<(String, String)>, /// Region bounds, computed from successor boundaries. Shown next to the /// header so a probe never re-derives them inline. pub regions: Vec, /// The three class tables, in the order the header lists them. pub classes: Vec, } /// One class table, with its entries already sliced at the right stride. pub struct ClassTable { pub name: &'static str, pub stride: usize, pub entries: Vec, } /// One class table row. #[derive(Clone, Copy)] pub struct ClassRow { pub index: usize, pub geometry_offset: i32, pub o_class: i32, pub unknown_8: i32, pub unknown_c: i32, /// The entry's raw bytes, so a probe can look at what the struct drops. pub raw: [u8; SHRUB_CLASS_ENTRY_BYTES], } impl ClassRow { /// Whether this row's geometry offset lands inside `core_data`. pub fn in_core(&self, core_len: usize) -> bool { self.geometry_offset > 0 && (self.geometry_offset as usize) < core_len } } /// Read a level's core index and dump its header and class tables. pub fn probe_level(dir: &Path, level: u32) -> Result { let level_dir = dir.join(format!("level{level}")); let core = std::fs::read(level_dir.join("core_data.bin")) .with_context(|| format!("reading {}", level_dir.join("core_data.bin").display()))?; let index = std::fs::read(level_dir.join("core_index.bin")) .with_context(|| format!("reading {}", level_dir.join("core_index.bin").display()))?; let h = gc_formats::level_core::LevelCoreHeader::parse(&index) .context("parsing the core header")?; let range = |v: i32| { if v < 0 { "(absent)".to_string() } else { v.to_string() } }; let mut header: Vec<(String, String)> = Vec::new(); header.push(("gs_ram count".into(), h.gs_ram.count.to_string())); header.push(("gs_ram offset".into(), h.gs_ram.offset.to_string())); for (name, v) in [ ("tfrags", h.tfrags), ("occlusion", h.occlusion), ("sky", h.sky), ("collision", h.collision), ("textures_base", h.textures_base), ("assets compressed", h.assets_compressed_size), ("assets decompressed", h.assets_decompressed_size), ] { header.push((name.into(), range(v))); } let regions = gc_formats::regions::core_layout(&index, &core, &h); let mut classes = Vec::new(); for (name, r, stride) in [ ("moby", h.moby_classes, CLASS_ENTRY_BYTES), ("tie", h.tie_classes, CLASS_ENTRY_BYTES), ("shrub", h.shrub_classes, SHRUB_CLASS_ENTRY_BYTES), ] { classes.push(ClassTable { name, stride, entries: read_class_table(&index, r, stride)?, }); } Ok(LevelProbe { level, core_len: core.len(), index_len: index.len(), header, regions, classes, }) } fn read_class_table( index: &[u8], r: gc_formats::level_core::ArrayRange, stride: usize, ) -> Result> { let base = r.offset.max(0) as usize; let count = r.count.max(0) as usize; let mut out = Vec::with_capacity(count); for i in 0..count { let at = base + i * stride; let mut raw = [0u8; SHRUB_CLASS_ENTRY_BYTES]; let Some(chunk) = index.get(at..at + stride) else { break; }; raw[..stride].copy_from_slice(chunk); out.push(ClassRow { index: i, geometry_offset: gc_formats::level_core::ClassEntry::parse(chunk) .map(|e| e.offset_in_asset_wad) .unwrap_or(0), o_class: gc_formats::level_core::ClassEntry::parse(chunk) .map(|e| e.o_class) .unwrap_or(0), unknown_8: gc_formats::level_core::ClassEntry::parse(chunk) .map(|e| e.unknown_8) .unwrap_or(0), unknown_c: gc_formats::level_core::ClassEntry::parse(chunk) .map(|e| e.unknown_c) .unwrap_or(0), raw, }); } Ok(out) } #[cfg(test)] mod tests { use super::*; #[test] fn class_rows_report_whether_geometry_is_in_core() { let r = ClassRow { index: 0, geometry_offset: 100, o_class: 1, unknown_8: 0, unknown_c: 0, raw: [0; SHRUB_CLASS_ENTRY_BYTES], }; assert!(r.in_core(200)); assert!(!r.in_core(50)); assert!(!ClassRow { geometry_offset: 0, ..r } .in_core(200)); } #[test] fn a_table_past_the_index_stops_rather_than_panicking() { let index = vec![0u8; 16]; let r = gc_formats::level_core::ArrayRange { count: 100, offset: 8, }; let rows = read_class_table(&index, r, CLASS_ENTRY_BYTES).unwrap(); assert!(rows.len() < 100, "should stop at the end of the index"); } }