diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index ff674ec..54ee900 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -6710,3 +6710,111 @@ fn tie_tail_offsets_are_not_more_structured_than_control() { {mc:.5}: they may select real content after all" ); } + +/// `moby_classes.offset` is a load-bearing constant, proved by its neighbours. +/// +/// The immediate `0x00c0` appears in **394 of 2,323** level-0 functions of at +/// least 128 bytes. Every value immediately adjacent to it appears in 1 to 14. +/// +/// That is the control 27891ce lacked. `0xc0` is small, and small immediates are +/// common, so its raw frequency proved nothing on its own. The neighbours share +/// its magnitude exactly and share none of its frequency, which is what makes the +/// 394 mean something rather than being an artifact of the value being short. +/// +/// Boundaries: level 0 only; function extents from the decomp's functions.tsv, +/// where "level 0" means the function's first listed place is level 00; size floor +/// of 128 bytes, since 59957a0 recorded 20-byte "functions" that are mis-bound +/// extents. An immediate alone does not prove the chunk is read -- it proves the +/// constant is used, and that is the claim being made. +#[test] +fn moby_classes_offset_is_referenced_far_more_than_its_neighbours() { + require_disc!(); + let data = std::env::var_os("GC_EXTRACTED") + .map(PathBuf::from) + .unwrap_or_else(|| { + PathBuf::from(std::env::var("HOME").unwrap()).join("Documents/gc-extracted") + }); + let dcomp = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../vendor/rac2-gc-decomp/config/levels/functions.tsv"); + if !data.exists() || !dcomp.exists() { + eprintln!("skipped"); + return; + } + + let txt = std::fs::read_to_string(dcomp).unwrap(); + let mut funcs: Vec<(u32, u32)> = Vec::new(); + for line in txt.lines() { + if line.starts_with('#') || line.trim().is_empty() { + continue; + } + let f: Vec<&str> = line.split('\t').collect(); + if f.len() < 6 { + continue; + } + let size: u32 = match f[2].parse() { + Ok(v) => v, + Err(_) => continue, + }; + for pl in f[5].split(',') { + let mut it = pl.split(':'); + if it.next() == Some("00") { + if let Some(a) = it.next().and_then(|x| u32::from_str_radix(x, 16).ok()) { + funcs.push((a, size)); + break; + } + } + } + } + funcs.sort_unstable(); + + let idx = std::fs::read(data.join("level0/core_index.bin")).unwrap(); + let ov = std::fs::read(data.join("level0/overlay.bin")).unwrap(); + let h = gc_formats::level_core::LevelCoreHeader::parse(&idx).unwrap(); + let secs = gc_formats::overlay_reloc::parse_overlay_sections(&ov).unwrap(); + let text = &secs[6].data; + let dest = secs[6].dest; + let has_imm = |op: u8| matches!(op, 0x09 | 0x0d | 0x0f | 0x0c | 0x05 | 0x06); + const FLOOR: u32 = 128; + + let mut sets: Vec> = Vec::new(); + for (start, size) in &funcs { + if *size < FLOOR { + continue; + } + let off = (*start).wrapping_sub(dest) as usize; + if off >= text.len() { + continue; + } + let n = (*size as usize).min(text.len() - off) & !3; + let mut set = std::collections::BTreeSet::new(); + for w in text[off..off + n].chunks_exact(4) { + let i = u32::from_le_bytes([w[0], w[1], w[2], w[3]]); + if has_imm((i >> 26) as u8) { + set.insert((i & 0xffff) as u16); + } + } + sets.push(set); + } + let hit = |v: u16| sets.iter().filter(|s| s.contains(&v)).count(); + + let base = h.moby_classes.offset as u16; + assert!(sets.len() > 2_000, "only {} functions kept", sets.len()); + let self_hits = hit(base); + let mut neighbour_max = 0usize; + let mut detail = Vec::new(); + for d in -8i32..=8i32 { + if d == 0 { + continue; + } + let v = (base as i32 + d) as u16; + let c = hit(v); + neighbour_max = neighbour_max.max(c); + detail.push(format!("{v:#06x}:{c}")); + } + eprintln!("moby_classes {base:#06x} -> {self_hits} functions; neighbours {detail:?}"); + assert!(self_hits > 100, "only {self_hits} functions reference it"); + assert!( + self_hits > neighbour_max * 10, + "only {self_hits} vs neighbours up to {neighbour_max}: no longer special" + ); +} diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index a6a19a8..5566639 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -2003,3 +2003,50 @@ where this should resume. Note the one caution this measurement earns: a 20-byte "function" containing the immediate is not a real candidate, it is a function whose extent ends just after some other function's instruction. Sizes need a floor before the set is used. + + +## moby_classes.offset is a load-bearing constant + +First positive result of the loader hunt, and it turns entirely on the control. + +Across 2,323 level-0 functions of at least 128 bytes (extents from the decomp's +`functions.tsv`), the immediate `0x00c0` -- `moby_classes.offset` -- appears in + + **394 functions** + +and every value immediately adjacent to it appears in 1 to 14: + + 0xb8:8 0xb9:2 0xba:2 0xbb:2 0xbc:3 0xbd:5 0xbe:5 0xbf:3 + 0xc1:3 0xc2:3 0xc3:2 0xc4:1 0xc5:2 0xc6:3 0xc7:1 0xc8:14 + +That is the control 27891ce said was missing. `0xc0` is small and small +immediates are common, so its raw frequency proved nothing -- 17,699 occurrences +across the program, with `0x0001` appearing 200,784 times. The neighbours have +the *same magnitude* and none of the frequency, which is what makes 394 mean +something instead of being an artifact of the value being short. + +Against the five other level-0 chunk offsets used as a second control, all of +which are larger and so would be biased in the *other* direction: + + shrub_classes 0x27c0 1 tfrag_textures 0x2910 2 + moby_textures 0x2ee0 2 tie_textures 0x3960 0 + shrub_textures 0x3e20 2 + +So the chunk offsets the loader would use are referenced about once each, and +`moby_classes` is referenced 394 times. The program uses this constant heavily. + +**What this does not say.** An immediate appearing in a function is not proof the +chunk is *read* there -- it is proof the constant is used, which is the claim +being asserted and all the test asserts. It does not say which of the 394 is the +loader, or that the loader is among them. + +**Also measured, and negative:** zero functions contain both the `moby_classes` +and `tie_classes` offsets, so there is no single function that indexes both chunk +tables by immediate. The moby loader, if it exists in this form, does not reach +the tie table the same way. + +**Next step, now concrete:** the 394 is a real shortlist. Take the ones whose +immediates also include chunk offsets that *do* look like class-table references, +or the largest ones -- the 34,316-byte outlier is the most promising single +target -- and disassemble those. That is bounded work, unlike the four probes +that produced nothing.