Why a call-graph is not enough

A call-graph tool looks for jal, the MIPS call instruction. Legaia reaches a great deal of its code some other way, and each of those ways is invisible to that search:

  • Through a table. A menu screen's content renderer, a sub-screen dispatcher, an actor template's per-frame tick — the address sits in memory as a plain 32-bit word and something loads and jumps through it. Nothing on the disc ever names it in an instruction.
  • Through an address built in two halves. A 32-bit MIPS address does not fit in one instruction, so the assembler emits a pair: load the top half, add the bottom. Ghidra's cross-reference index does not join those pairs back together, so asking it "who uses this address" returns nothing even for one used everywhere.

And the mirror question has its own trap. An address with no caller of any kind might not be a function at all: it can be a label inside somebody else's function, reached by a relative branch that carries no copy of its target. Ghidra invents a fake function entry at each of those, so a worklist can fill up with addresses that were never separate routines.

The sweep looks for all five forms in one pass — literal word, split-address pair, jal, j, and relative branch — over the main executable, all 31 recovered overlay images, and the raw bytes of all 1233 archive entries. That is what turns "I did not find a caller" into "no reference exists".

What a hit means, and where it is

Reporting a hit as a memory address requires knowing where its containing file gets loaded, and the three cases are kept apart because conflating them has previously put an impossible address into this project's own notes.

ImageWhere its load address comes fromA hit is reported as
The executableits own headerfile offset and the one address it can be
Overlay imagesa committed per-overlay mapfile offset and address, named by image
Every other archive entrynone — streamed datafile offset only

The middle row is the one that bites, because many overlays load at the same address. The same address is a different word in each of them, so the image is as much of the answer as the offset is. The bottom row is why streamed entries carry no address at all: inventing one would be exactly the defect the first row exists to avoid.

Each word hit also carries a rough verdict — dispatch table, template field, or a word that merely happens to fall inside an instruction stream — drawn from what the surrounding bytes look like. The counts behind the verdict are printed alongside it, so it can be second-guessed rather than taken on faith.

A branch cannot cross images

Four of the five forms carry a copy of the address they reach, so a hit is evidence wherever it turns up: a call in one overlay can reach a routine in another whenever both are loaded. A branch is different. It encodes a distance rather than a destination, so it can only reach code in its own image — which makes a branch hit evidence about that image's copy of the address and about nothing else.

Since overlays share load addresses, every one of them has a byte at every address, and this is not a corner case. Two routines on this project's own inert-code list read as referenced for exactly this reason and are not: an FMV-overlay synchronisation helper collects a branch from the field overlay, whose bytes at that address are the tail of an unrelated routine, and a field-overlay list helper collects one from the battle overlay. Neither branch can reach the routine it appears to point at.

Naming the image that holds the routine makes the check mechanical: branch hits from any other overlay are then marked and counted separately, and when they are all a target has, the tool says so outright.

The three outcomes, and what each one means for the work

  • A table or template word. The consumer is whatever dispatches through that record. The stalled thread now has a next step, and the format documentation gains a citation that points at real bytes.
  • Branch sites but no call sites. The address is a label inside a larger function, not an entry point. The worklist row was never work.
  • Nothing, anywhere. The routine is real but the game never reaches it — linked in because the linker keeps whole object files, superseded by a sibling that shipped in its place. This is the outcome worth naming, because it looks identical to unfinished work and is the opposite of it.

That last outcome changes what should be done about a row. Reimplementing a routine the original game never runs cannot make anything work: there is nothing to connect it to, in the reimplementation or in the original, so it becomes a permanent entry on a to-do list nobody can ever complete. Those addresses are recorded as a documented negative instead, in their own category on the port catalog's exclusion list, with the scan's own limits stated so a later pass can reopen one if the evidence changes.

Controls

A tool is only worth its negatives if its positives are checked, so it is run against known answers before a "nothing found" is believed: a routine reached solely through a template word, one reached by ordinary calls in six images, an address known to be an interior label, and a documented pointer table recovered from its single use site. The first of those doubles as a self-check with an exit status, so a broken run fails loudly rather than reporting silence.

Limits

  • Compressed entries. Archive entries are scanned as raw bytes, so a reference inside a compressed one is invisible. Overlay code is stored uncompressed, so this does not cover the images callers actually live in.
  • Addresses built in more than two steps. The pair search wants one instruction for each half. An address assembled another way, or reached as "table base plus index", is not a pair — which is why a negative about one record needs the table's base address checked too before it means anything.
  • The verdict is triage. Classifying a hit as a dispatch table says its neighbours look like function entries, not that the game indexes them. Reading the disassembly at the hit is still the last word.

See also