diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 11cee75..29f30e5 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -4535,6 +4535,83 @@ fn tie_vertex_layout_is_selected_by_i16_3_high_byte() { /// 2. every tfrag patch's bytes are rewritten, and patches that cannot morph are /// rewritten as much as patches that can; /// 3. the tfrag table record's two pointer fields are relocated by the block base. +/// Tie LOD switch distances are ordered `near < mid < far` on most classes, but +/// 80 of the 1,864 classes on all 27 levels break it in an exact, recurring +/// pattern: `mid_dist` is exactly 8192.0 and `far_dist` is exactly 8190.0 in +/// every one. +/// +/// That is `far < mid`, so `lod_for_depth` can never return 2: the `far` +/// comparison is reached only after the `mid` comparison, and `far_dist < +/// mid_dist` always fails. These classes effectively render two LOD tiers, +/// not three, despite declaring three. +/// +/// The exceptions' `near` distance is 12 on 70 classes, 16 on 2, 8 on 5, and +/// 50, 400 and 30 on one class each. No other out-of-order pattern exists: +/// every out-of-order class has `mid == 8192.0` and `far == 8190.0`. +#[test] +fn tie_lod_distances_break_ordering_exactly_80_classes_at_mid_8192_far_8190() { + require_disc!(); + + let data = std::env::var_os("GC_EXTRACTED") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home).join("Documents/gc-extracted") + }); + if !data.exists() { + eprintln!("skipped: unpack the disc first"); + return; + } + + use gc_formats::level_core::{ClassEntry, LevelCoreHeader}; + use gc_formats::tie::TieClassHeader; + + let mut total = 0usize; + let mut bad = 0usize; + let mut exact = 0usize; + let mut near = std::collections::BTreeMap::new(); + + for lv in 0..27usize { + let idx = std::fs::read(data.join(format!("level{lv}/core_index.bin"))).expect("idx"); + let core = std::fs::read(data.join(format!("level{lv}/core_data.bin"))).expect("core"); + let h = LevelCoreHeader::parse(&idx).expect("hdr").tie_classes; + for c in 0..h.count as usize { + let o = h.offset as usize + c * 0x20; + let e = ClassEntry::parse(&idx[o..o + 0x20]).expect("entry"); + let base = e.offset_in_asset_wad as usize; + if base == 0 || base + 0x48 > core.len() { + continue; + } + let Ok(hdr) = TieClassHeader::parse(&core[base..base + 0x48]) else { + continue; + }; + total += 1; + let mid = hdr.mid_dist; + let far = hdr.far_dist; + if mid >= far { + bad += 1; + if mid == 8192.0 && far == 8190.0 { + exact += 1; + } + *near.entry(hdr.near_dist as u32).or_insert(0) += 1; + } + } + } + + eprintln!( + "tie LOD ordering: {total} classes, {bad} out of order ({bad} * 100.0 / {total:.2} = {:.2}%), of which {exact} are mid==8192.0 && far==8190.0", + bad as f64 * 100.0 / total as f64 + ); + for (&k, &v) in &near { + eprintln!(" near_dist={k} x{v}"); + } + + assert_eq!(total, 1864, "tie class count changed: {total}"); + assert_eq!(bad, 80, "out-of-order classes moved to {bad}"); + assert_eq!(exact, 80, "only {exact} of {bad} are mid=8192.0 & far=8190.0"); + assert_eq!(near.len(), 6, "near-distance histogram changed: {near:?}"); +} + fn ram_image() -> Option> { let p = std::env::var_os("GC_SSTATE")?; gc_trace::Savestate::open(std::path::Path::new(&p)) diff --git a/crates/gc-formats/src/tie.rs b/crates/gc-formats/src/tie.rs index 0373d0d..9f5afcf 100644 --- a/crates/gc-formats/src/tie.rs +++ b/crates/gc-formats/src/tie.rs @@ -18,12 +18,16 @@ //! the remainder would repeat the pattern this project keeps running into: a //! field order that produces plausible output and is wrong. //! -//! Evidence for what is decoded, over 320 classes across five levels: +//! Evidence for what is decoded, over 1,864 classes on all 27 levels: //! -//! * `o_class` at `0x44` equals the core index's class number: **320 of 320**. +//! * `o_class` at `0x44` equals the core index's class number: **1,864 of 1,864**. //! A coincidence is not a plausible explanation for that. -//! * the LOD distances are ordered `0 < near < mid < far`: 315 of 320. The -//! five exceptions are not yet explained and are worth chasing. +//! * the LOD distances are ordered `0 < near < mid < far` on most classes, but +//! **80 of 1,864** break it in an exact pattern: `mid_dist` is exactly +//! `8192.0` and `far_dist` is exactly `8190.0` in every one. Those classes +//! have `far < mid`, so `lod_for_depth` can never return 2 (the `far` test is +//! reached only after the `mid` test). Effectively two LOD tiers, not three. +//! See `docs/plan/level-assets.md` for the test. use crate::buf::{Buf, Error, ErrorKind, Result}; use crate::level_core::ClassEntry;