Tier E - code injection via the rodata gap
A monster drops a second piece of gear. Running away banks a slice of the EXP. An enemy turns on its own side. A captured Seru keeps a permanent +35% for the rest of the game. None of those is a value that exists on the disc to be changed - they are behaviour, and behaviour needs instructions. This tier parks hand-assembled MIPS in bytes the executable never uses and detours the game into it.
At a glance
- Host
- A 1028-byte all-zero run in
SCUS_942.54rodata at0x8007AB38(always resident), plus four smaller read-watch-verified dead regions totalling 652 bytes - Detour
- Two instructions at the hook site become
j routine+nop; the routine replays them and jumps back - Edit class
- In place, same size; consumes reclaimed rodata - a finite, shared resource
- Mods
--equipment-drops·--flee-exp·--enemy-ally·--shiny-seru- Guards
- Hook-site words must match the USA build; every target must be all-zero; no target may overlap a catalogued table; routine VAs must be word-aligned
- Confidence
- Confirmed - byte/disassembly oracles plus emulator playtests; no engine runtime oracle (the from-scratch engine cannot execute injected MIPS)
- Oracles
equipment_drops_real.rs·flee_exp_real.rs·enemy_ally_real.rs·shiny_seru_real.rs
The detour
The four hooks
| Mod | Hook site | Routine | What it does |
|---|---|---|---|
--equipment-drops | 0x8004F610 in the reward tally FUN_8004E568 (SCUS) | 0x8007AB80 + id table | Rolls a per-battle chance; grants one random equipment id through the game's own add-item helper, on top of the normal drop. |
--flee-exp | 0x801E5A10, escape teardown state 0x66 of FUN_801E295C (battle overlay, PROT 0898) | 0x8007AD00 | Sums the fled formation's EXP, scales by --flee-exp-pct, banks it into every member's cumulative-XP cell. |
--enemy-ally | 0x80051990, after the monster-setup loop (SCUS) | 0x8007ACA0 | Rolls a chance and sets the Confuse/Charm bits (0x380 in +0x16E) on the frontmost enemy, so it attacks its own side. One companion edit widens the victory check's mask to 0x384 so a charmed enemy counts as down. |
--shiny-seru | nine detours across battle setup, capture, damage and the menus | four verified-dead regions (below) | A capturable enemy spawns with stats ×1.35; once captured, its per-Seru flag grants a permanent +35% damage. |
Two design points are worth the read. Flee-EXP's hook is in the battle overlay (reloaded each battle) but its routine is in the executable, so it survives every overlay swap. Enemy ally never adds a fourth combatant - retail is hard-wired to three party slots - it reuses the state a confused party member already has.
"Zero is not dead"
The obvious test for a home - "find a run of zero bytes" - is wrong. A region can be solid zero in the file and still be read at runtime, because it is the unused padding of a table the game indexes by id: ask for an entry past the real end and the reader walks straight into the injected bytes.
The authoritative test is not "is this in a table I know about?" but "does any instruction read this at runtime?" The final homes are chosen by arming a read-watchpoint on each candidate under PCSX-Redux and exercising the worst cases - an item use, a victory pose, a summon cast - and keeping only the runs nothing touches. The structural table-overlap guard stays as a first filter; the watchpoint is the proof.
The contended resource
Every code-hook feature and two data blobs share one 1028-byte run, and four small verified-dead regions hold the rest. Each tenant guards its slot as all-zero, so they compose - but the space is finite, and that ceiling is why a whole interactive screen goes elsewhere (tier F).
| VA | Tenant | Size | Kind |
|---|---|---|---|
0x8007AB40 | Seru Bell name string (--unused-items, the string variant) | 10 B | data |
0x8007AB80 | bonus-equipment routine + id table (--equipment-drops) | ~128 B | code |
0x8007ACA0 | enemy-ally charm routine (--enemy-ally) | ~76 B | code |
0x8007AD00 | run-away-EXP routine (--flee-exp) | ~237 B | code |
0x8007AE00 | arena 1: shiny Seru / Super-Art menu / arts-AP hooks (mutually exclusive) | 256 B | code |
0x8007AF00 | Seru-trade config blob (--seru-trade, the config variant) - engine-facing, inert on hardware | 24 B | data |
0x80077728 | gap 1: shiny-Seru setup, capture, bitmap, banner string | 256 B | code + data |
0x8007AFF8 | arena 2: a dead pocket between two sound tables | 72 B | code |
0x80078A88 | slot 6: padding between the 0x80078xxx tables | 68 B | code |
Shiny Seru alone occupies 618 of the 652 bytes across the four verified-dead regions, which is why it is mutually exclusive with the features that need the same arenas.
How we know
| Claim | Evidence | Reference |
|---|---|---|
| Reward tally runs once per battle | FUN_8004E568 gates on actor+0x6CE == 0; normal drop granted through FUN_800421D4 at 0x8004F608 | level-up |
Escape teardown is state 0x66 | Battle-action machine FUN_801E295C; a failed run goes 0x65 → 0x50 instead | battle-action |
Cumulative XP at record +0x5C8 | Written by the tally, read by the level processor FUN_801E9504 | save-record |
Charm bits 0x380 flip a target's side | Action-machine target selection on actor+0x16E; victory check andi v0,v0,0x4 at 0x801E6638 | battle-action |
| Party is hard-wired to three slots | Setup loop FUN_800513F0 bounded < 3; meshes and HUD exist for slots 0-2 only | battle |
| The four dead regions are unread | PCSX-Redux read-watchpoints across item use, victory pose and summon cast | pcsx-redux-automation |
| Hand-assembled bytes are what the oracle expects | Disc oracles decode the detour + routine + table and check every touched sector | randomizer |
History: the three tables that read the injected bytes
Three earlier homes passed the all-zero check and were still read at runtime as table padding: the victory mouth-override table at 0x80077E80 (a garbled mouth during the victory pose), the battle-overlay move-power table at 0x801F4F5C (six attack ids read code as power and texture values), and the SsAPI sound tables around 0x800794F0 (an item-use sound never reported done, so the tutorial fight froze on a Healing Leaf). The first two were caught by the table-overlap guard; the third table was not yet catalogued. Read-watch verification replaced "is it in a known table" as the acceptance test.
History: the shiny flag in the level byte
Retail keeps a captured Seru as an (id, level) pair with no spare field. An early design hid the shiny flag in the unused high bit of the level byte; one shared routine (FUN_800402F4, spell level-up + display) read the level unmasked and rendered the "grew to level N" box blank. The flag moved to a parallel per-Seru byte array in a reclaimed run of the character record, with a small hook mirroring the game's insert-at-front spell-list shift. A free bit in a live field is free only if you control every reader - and on a sealed disc you usually do not.
Details: the R3000 traps
- Load-delay slot. A value loaded by
lwis not usable in the very next instruction; a replayed displaced pair that depended on that spacing must keep it. - Jump alignment.
jdrops the target's low two bits, so a routine placed at an unaligned zero-run start jumps into the middle of a word. Every routine VA is rounded up to a word boundary; only byte-addressed data may sit unaligned. - Capturable set at patch time. No retail field says "this enemy is a Seru", so the shiny allowlist is built from the disc's own monster names - every enemy named after a player Seru-magic - and baked into a bitmap the setup routine indexes by id.
The from-scratch engine pays none of this: there the same feature is a set of (character, spell) pairs and a ×1.35 in the damage kernel.
Details: adding a tier-E mod
- Find a hook site at the right control-flow moment; record the two words it displaces.
- Choose a home: all-zero, outside every catalogued table, read-watch-verified, word-aligned, and sized for the routine.
- Replay the displaced pair inside the routine; guard every write as dead space.
- Add a disc-gated oracle. If the routine is screen-sized, it has outgrown the gap - go to tier F.