diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 51a8f74..cb7507c 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -6621,3 +6621,94 @@ fn tie_class_tail_offsets_point_past_the_tie_packets() { "{classes} offsets fall inside the tie packets" ); } + +/// Control for `tie_class_tail_offsets_point_past_the_tie_packets`. +/// +/// The bytes at 0x1c and 0x34 are structured -- but **not more structured than +/// core_data anywhere else**, so the offsets select nothing distinctive. +#[test] +fn tie_tail_offsets_are_not_more_structured_than_control() { + 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; + + fn adjacent_equal(b: &[u8]) -> f64 { + let eq = b.windows(2).filter(|w| w[0] == w[1]).count(); + eq as f64 / (b.len() - 1) as f64 + } + let mut n = 0usize; + let mut at_1c = 0.0f64; + let mut at_34 = 0.0f64; + let mut ctl = 0.0f64; + + for lv in 0..27usize { + let (Ok(idx), Ok(core)) = ( + std::fs::read(data.join(format!("level{lv}/core_index.bin"))), + std::fs::read(data.join(format!("level{lv}/core_data.bin"))), + ) else { + continue; + }; + let t = LevelCoreHeader::parse(&idx).expect("hdr").tie_classes; + for c in 0..t.count as usize { + let o = t.offset as usize + c * 0x20; + let Ok(e) = ClassEntry::parse(&idx[o..o + 0x20]) else { + continue; + }; + let base = e.offset_in_asset_wad as usize; + if base == 0 || base + 0x48 > core.len() { + continue; + } + if TieClassHeader::parse(&core[base..base + 0x48]).is_err() { + continue; + } + let word = |k: usize| -> usize { + let at = base + 0x1c + k * 4; + u32::from_le_bytes(core[at..at + 4].try_into().unwrap()) as usize + }; + let (o1, o3) = (word(0), word(6)); + if o1 + 64 > core.len() || o3 + 64 > core.len() || core.len() < 64 { + continue; + } + let ctl_off = o1.wrapping_add(0x2000) % (core.len() - 64); + n += 1; + at_1c += adjacent_equal(&core[o1..o1 + 64]); + at_34 += adjacent_equal(&core[o3..o3 + 64]); + ctl += adjacent_equal(&core[ctl_off..ctl_off + 64]); + } + } + + assert!(n > 1_800, "only {n} classes read"); + let (m1, m3, mc) = (at_1c / n as f64, at_34 / n as f64, ctl / n as f64); + eprintln!( + "adjacent-equal rate: 0x1c {m1:.5} | 0x34 {m3:.5} | control {mc:.5} | random {:.5}", + 1.0 / 256.0 + ); + + // Far from random: this region is structured data, not noise. + assert!(mc > 20.0 / 256.0, "control fell to the random baseline"); + // But the two offsets are NOT *more* structured than an arbitrary one. + // One-sided on purpose: the claim being guarded is "these offsets select + // distinctive content", and a gap in the other direction does not support + // it. Both come out slightly BELOW control (0x1269 and 0.1211 against + // 0.1411), which is why a symmetric tolerance was the wrong shape -- it + // failed on a 0.0200 gap while asserting something the data does not say. + // + // The margin is 0.03, comfortably above the observed 0.014 and 0.020, so + // this still fails if either offset ever becomes markedly more structured + // than its neighbourhood -- which is the thing worth catching. + assert!( + m1 < mc + 0.03 && m3 < mc + 0.03, + "0x1c {m1:.5} / 0x34 {m3:.5} are now MORE structured than control \ + {mc:.5}: they may select real content after all" + ); +} diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index 95ab69e..6531adb 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -1882,8 +1882,30 @@ specific is exactly the kind of finding rule 4 exists to refuse -- none of these three fragments has been shown to beat a control, and they were picked because they looked like something. -What is established is structural: two base-relative offsets, always in range, -always past the tie packets, pointing at repeated structured records rather than -DMA tags. That is consistent with the earlier finding that the packet's own -control region is not a tag stream -- the game's per-class draw state is stored -elsewhere and referenced, not inlined. +## The control, and a retraction + +The last paragraph of the previous section implied the two offsets select +*distinctive* content. They do not. + +Measured adjacent-equal-byte rate over 64 bytes, 1,864 classes: + + 0x1c 0.12688 + 0x34 0.12106 + control 0.14109 (the same offset + 0x2000) + random 0.00391 + +All three are ~30x above the random baseline, so the region really is structured +data and not noise -- that part stands. But **0x1c and 0x34 are no more +structured than an arbitrary offset in the same blob**, and 0x34 is slightly +*less*. So the hex fragments in the previous section were three hand-picked +examples from a structured region, and "they look like shader programs with +small-integer parameters" said something about this part of core_data, not about +what these fields address. + +**Retracted:** the suggestion that the offset targets are identifiable content. +Still established, and independent of all of the above: the offsets are +base-relative, non-zero, in range on 1,864 of 1,864 classes, and always land past +the class's own tie packet blocks. What they address remains unknown, and the +earlier finding that the packet's control region is not a tag stream still stands +-- it was measured by uniformity within the packet, not by comparison to +somewhere else in core_data.