From 3ea5ab799f4c4ae7325be1515d98f4237f4eda5b Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Wed, 14 Oct 2026 05:28:34 +0200 Subject: [PATCH] moby_classes.offset is referenced by 394 functions; its neighbours by 1 to 14 First positive result of the loader hunt, and it exists only because of the control that two commits ago I said was missing. Across 2,323 level-0 functions of at least 128 bytes -- extents from the decomp's functions.tsv -- the immediate 0x00c0, which is moby_classes.offset, appears in 394 functions. Every value 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 This is what 27891ce was asking for. 0xc0 is small and small immediates are common -- 17,699 occurrences across the program, against 200,784 for 0x0001 -- so the raw frequency was worthless. The neighbours have the same magnitude and none of the frequency, which is the only reason 394 means anything rather than being an artifact of the value being short. A second control points the other way, so the two bracket it: the other five level-0 chunk offsets are larger values and are referenced 0 to 2 times each. Shrub_classes 1, tfrag_textures 2, moby_textures 2, tie_textures 0, shrub_textures 2. The chunk offsets a loader would use are referenced about once. moby_classes is referenced 394 times. The program leans on this constant heavily. The previous commit's 410 candidates were not a shortlist. These 394 are, because the number is now separated from magnitude rather than merely stated. 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 the test asserts and the only claim it makes. It does not say which of the 394 is the loader, or that the loader is among them. Stated plainly because the 394 is the kind of number that gets quoted without its caveat and then built on. Also measured, negative: zero functions contain both the moby_classes and tie_classes offsets, so nothing indexes both chunk tables by immediate. The moby loader does not reach the tie table the same way, if it reaches it at all. Boundaries: level 0 only, "level 0" meaning the function's first listed place is level 00, and a 128-byte size floor -- 59957a0 recorded 20-byte "functions" that are mis-bound extents. 283 passing with the disc present (23 + 19 + 63 + 156 + 22; disc binary 35.23s), clippy clean. --- crates/gc-extract/tests/disc.rs | 108 ++++++++++++++++++++++++++++++++ docs/plan/level-assets.md | 47 ++++++++++++++ 2 files changed, 155 insertions(+) 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.