Read a moby handler: the chain works, with two caveats and a 110 discrepancy
Followed the chain through -- dispatch table to handler address to disassembly --
and read level 0's class 2 handler at 0x00380a88:
0x00380a84: jal 0x2ee0d0
0x00380a88: .word 0x0240202d <- VU1 microcode, not a failure
0x00380a8c: lwc1 $f12, 0x40($s1)
0x00380a98: addiu $a0, $s0, 0x20
0x00380aa0: swc1 $f0, 0x18($s4)
0x00380ab0: swc1 $f0, 0x14($s4)
Floats out of one structure, written back into another, with VU1 work alongside --
what a transform update should look like. The chain from an asset observation to the
game's own code is now complete and usable.
TWO CAVEATS, both learned by hitting them:
VU1 microcode interleaves with CPU code. 0x0240202d is a COP2 opcode, not an
undecodable word. A tool that counts .word lines as failures is counting the wrong
thing -- a trap I walked into earlier today with the region walkers, now recorded
so it is not repeated.
Handlers are not all independent functions. Level 0's class 2 is at 0x380a88 and
class 0x0E at 0x380a90 -- EIGHT BYTES apart -- and 72 of the 5,788 records sit
within 16 bytes of the one before them. Consecutive classes share one routine with
per-class entry points inside it, so reading "a handler" gives a fragment. That
matters downstream: a class's behaviour is not one function, and assuming
otherwise would misattribute shared code.
A DISCREPANCY WITH THE REFERENCE
The decomp reports 5,598 of 5,788 moby handlers (96.7%) preceded by a jr ra within
three words. Measured here, the same way, over the same 5,788 records: 5,488
(94.8%). Same population, same test, 110 handlers different.
Both figures support the premise, so nothing turns on it, and I am recording it
rather than picking the number that suits. The reference measured on 2026-10-02
against its own pinned overlay set, so a method or version difference is as
plausible as a data difference and there is no basis here for saying which. The
test asserts the premise (>90%) and deliberately NOT either exact figure.
Asserted in moby_handlers_are_overwhelmingly_function_entries.
264 tests.
This commit is contained in:
@@ -5984,3 +5984,101 @@ fn camera_and_sound_dispatch_tables_decode_at_their_documented_widths() {
|
||||
assert_eq!(out[1].1, 6, "distinct sound id count moved");
|
||||
assert_eq!(out[1].2, 0x07, "the largest sound id moved");
|
||||
}
|
||||
|
||||
/// The dispatch handlers' addresses are almost all function entries -- measured
|
||||
/// independently, and not quite matching the reference figure.
|
||||
///
|
||||
/// `docs/MOBY-DISPATCH-TABLES.md` reports 5,598 of 5,788 moby handlers (96.7%)
|
||||
/// preceded by a `jr ra` within the three words before them. Measured here the
|
||||
/// same way, over the same 5,788 records: **5,488 (94.8%)**.
|
||||
///
|
||||
/// Same population, same test, 110 handlers different. Both confirm the premise
|
||||
/// that these addresses are function entries rather than arbitrary words, and the
|
||||
/// residual is recorded as a discrepancy rather than explained away -- the
|
||||
/// reference measured on 2026-10-02 against its own pinned overlay set, so a
|
||||
/// difference in method or version is as likely as a difference in the data.
|
||||
///
|
||||
/// Two things worth knowing before reading a handler. First, the code interleaves
|
||||
/// VU1 microcode with CPU instructions, so a disassembly window runs `.word
|
||||
/// 0x0240202d` lines that are COP2 opcodes rather than failures. Second, the
|
||||
/// handlers are **not all independent functions**: level 0's class 2 and class
|
||||
/// `0x0E` are 8 bytes apart (0x380a88 and 0x380a90), so consecutive classes share
|
||||
/// one routine with per-class entry points inside it. Reading a handler gives you
|
||||
/// a fragment, not a whole function.
|
||||
#[test]
|
||||
fn moby_handlers_are_overwhelmingly_function_entries() {
|
||||
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(|_| ".".to_string());
|
||||
PathBuf::from(home).join("Documents/gc-extracted")
|
||||
});
|
||||
if !data.exists() {
|
||||
eprintln!("skipped: unpack the disc first");
|
||||
return;
|
||||
}
|
||||
|
||||
use gc_formats::overlay_reloc::parse_overlay_sections;
|
||||
|
||||
const JR_RA: u32 = 0x03E0_0008;
|
||||
let (mut total, mut with_jr) = (0usize, 0usize);
|
||||
let mut gaps: Vec<u32> = Vec::new();
|
||||
for lv in 0..27usize {
|
||||
let Ok(ov) = std::fs::read(data.join(format!("level{lv}/overlay.bin"))) else {
|
||||
continue;
|
||||
};
|
||||
let secs = parse_overlay_sections(&ov).expect("sections");
|
||||
let text = &secs[6];
|
||||
let mut last: Option<u32> = None;
|
||||
for r in 0..secs[3].data.len() / 12 {
|
||||
let off = r * 12 + 4;
|
||||
let h = u32::from_le_bytes(secs[3].data[off..off + 4].try_into().unwrap());
|
||||
if h == u32::MAX {
|
||||
break;
|
||||
}
|
||||
// how close is this handler to the previous one?
|
||||
if let Some(p) = last {
|
||||
if h > p && (h - p) < 16 {
|
||||
gaps.push(h - p);
|
||||
}
|
||||
}
|
||||
last = Some(h);
|
||||
let Some(rel) = h.checked_sub(text.dest) else {
|
||||
continue;
|
||||
};
|
||||
let rel = rel as usize;
|
||||
let Some(slice) = text.data.get(rel.saturating_sub(12)..rel) else {
|
||||
continue;
|
||||
};
|
||||
let jr = slice
|
||||
.chunks_exact(4)
|
||||
.rev()
|
||||
.take(3)
|
||||
.any(|w| u32::from_le_bytes([w[0], w[1], w[2], w[3]]) == JR_RA);
|
||||
total += 1;
|
||||
if jr {
|
||||
with_jr += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pct = 100.0 * with_jr as f64 / total as f64;
|
||||
eprintln!(
|
||||
"moby handlers: {with_jr} of {total} ({pct:.1}%) preceded by a jr ra within 3 \
|
||||
words -- the reference reports 5598 of 5788 (96.7%); {n} of those records sit \
|
||||
within 16 bytes of the one before",
|
||||
n = gaps.len()
|
||||
);
|
||||
assert_eq!(total, 5788, "handler record count moved");
|
||||
// the premise, asserted loosely because the reference's own figure differs
|
||||
assert!(
|
||||
pct > 90.0,
|
||||
"handlers stopped looking like function entries: {pct:.1}%"
|
||||
);
|
||||
assert!(
|
||||
!gaps.is_empty(),
|
||||
"no two consecutive handlers are close; they stopped sharing routines"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user