Tier E overview
Tier E of the A–F ladder - the first code-injection tier. The next rung, tier F, exists precisely to escape this tier's one limitation: the gap is finite and shared. New here? Start at the series index.

Mechanism at a glance

Why code
the change can't be a data edit: a second drop the one slot can't hold; an XP grant the flee teardown never reaches; a charmed enemy no static table can flag; a persistent per-Seru bonus retail saves have no field for
Host
a reclaimed all-zero run of SCUS_942.54 rodata (always resident), holding the hand-assembled routine - and, when the gaps fill up, additional reference-free SCUS runs. Each candidate must be verified all-zero and constant across runtime states and outside every live table, and - the part a static check can't prove - read-watch-verified unreferenced (shiny Seru spreads across four such regions)
Hooks
bonus drop - reward routine 0x8004F610 → gap 0x8007AB80; flee-EXP - escape teardown 0x801E5A10 (battle overlay) → gap 0x8007AD00; enemy ally - battle setup 0x80051990 → gap 0x8007ACA0 (+ a one-word victory-check widen in the battle overlay); shiny Seru - nine detours across the battle / menu / capture paths → four read-watch-verified-dead SCUS regions, with the persistent flag in a spare byte array in the character record (not the spell-level byte)
Detour shape
overwrite two instructions with j routine + nop; the routine runs the work, replays the displaced instructions, jumps back to the return address
Edit class
in-place / same-size; consumes reclaimed rodata / table padding
Oracles
equipment_drops_real.rs · flee_exp_real.rs · enemy_ally_real.rs · shiny_seru_real.rs

The reversing: a gap to host it, a seam to enter it

Code injection needs two finds, and both are reversing problems before they're engineering ones.

A reclaimable host. The executable carries runs of zero bytes in its rodata segment that nothing references - padding between sections. One such ~1 KB run (around 0x8007AB380x8007AF40) is the routines' home. Reclaiming it safely means confirming it is genuinely unreferenced and genuinely zero, and then guarding every write as all-zero so an already-patched or atypical image is rejected, not corrupted.

A seam at the right moment. The detour has to land in control flow at exactly the point where the new behavior belongs, with no live register clobbered across the jump. Finding it means tracing the relevant routine in Ghidra to the precise instruction pair to displace, then preserving those two instructions inside the routine so the original semantics survive. The detour itself is the standard idiom - j to the routine, do the work, replay the displaced pair, j back - but it only works because the hook site and the displaced instructions were pinned exactly.

Worked examples

Bonus equipment drops - the slot can't hold two

A monster record has exactly one drop slot, so “also drop a rare piece of gear” cannot be a data edit - there is nowhere to put the second item. The detour goes into the battle-end reward routine (FUN_8004E568) at 0x8004F610, displacing two instructions; the gap routine at 0x8007AB80 rolls the per-battle chance, picks an equipment id from a small table appended right after it in the gap, grants it on top of the normal drop, replays the displaced pair, and returns to 0x8004F618. The normal drop is never disturbed.

Run-away EXP - the path that grants nothing

The battle-action escape teardown (the state-machine FUN_801E295C, state 0x66) tears the fight down without ever reaching an experience grant, so rewarding a successful flee needs code. The detour sits at 0x801E5A10 - inside the battle overlay - and routes to a routine in the always-resident SCUS gap at 0x8007AD00 that banks a configured percentage of the fled formation's EXP into the party. The split is deliberate: the hook lives in the overlay (reloaded each battle), but the routine lives in SCUS so it survives every overlay swap.

Enemy ally - charm an enemy instead of adding one

“Have an enemy fight for you” sounds like adding a party member, but retail battles are hard-wired to three party slots (the setup loop FUN_800513F0 is bounded < 3, and party meshes / HUD only exist for slots 0–2), so a genuine fourth combatant isn't possible. The trick is to reuse a state the game already has: Confuse/Charm. The action SM flips an actor's target to the opposite side when its +0x16E & 0x380 “AI-delegated” bits are set - the mechanism behind a confused party member attacking allies. Set the same bits on an enemy at battle setup and that enemy attacks the other enemies. So the gap routine is tiny: it just rolls the per-battle chance and OR-s 0x380 into the frontmost enemy's flags. The detour to it sits at 0x80051990 (in SCUS, right after the monster-setup loop, so the actor table is populated) and the routine lives at 0x8007ACA0 - the free window between the bonus-drop and flee-EXP routines.

It needs one companion edit outside the gap. The monster-wipe victory check counts a monster as “down” only if it is dead or non-targetable (andi v0,v0,0x4 at 0x801E6638 in the battle overlay); a charmed-but-alive enemy would otherwise be one you still had to defeat. Widening that mask to 0x384 (a one-instruction edit) makes a 0x380-charmed enemy also count as down, so the battle ends once the real enemies fall. On a solo-enemy boss the lone enemy simply turns on itself.

Shiny Seru - a persistent bonus with nowhere to store it

A “shiny” capturable enemy is two problems at once. The easy half: at battle setup, roll the per-battle chance and, if the frontmost enemy is one the player can capture, multiply its stat block ×135/100 and stamp a free per-actor byte as a shiny marker. There's no reliable “is a Seru” field on retail, so the capturable set is built at patch time from the disc's own monster names - every enemy named after a player Seru-magic - baked into an allowlist bitmap the setup routine indexes by id.

The interesting half is persistence: the +35% damage bonus has to survive the capture and every save, forever - and retail keeps a captured Seru as nothing but an (id, level) pair, with no spare field. An early design hid the flag in the unused high bit of the level byte; it worked until one shared routine (FUN_800402f4, the spell level-up + display) read the level unmasked, saw 0x81 instead of 1, and rendered the “grew to level N” box blank. The fix was to stop overloading a live field: the flag moved into a parallel per-Seru byte array in a reclaimed run of the character record, with a small grant-shift hook that mirrors the game's insert-at-front spell-list shift onto the array so each Seru keeps its flag. Clean level byte, no masking anywhere, and the patch got smaller. The lesson is the tier's own caution sign: a free bit in a live field is free only if you control every reader - and on a sealed disc you usually don't.

“Zero is not dead” - the trap that bit three times

Every routine and datum above needs a home the game will never disturb, and the obvious test is “find a run of zero bytes.” That test is wrong - and it was wrong three times in a row before the right one was found. A zero run is safe to squat in only if no instruction reads it. A region can be solid zero in the ROM and still be read at runtime, because it's 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 your bytes.

A zero run that passes assert_zero but is still read at runtime as table padding Looks dead - assert_zero passes entry 0 entry 1 entry 2 our routine / bitmap (all-zero in ROM) live indexed table - real entries past the table's real end index by id reads our bytes as data -> garbage Actually dead - read-watch verified our routine / data no instruction ever reads it
The bytes are genuinely zero, so assert_zero is satisfied - but the index arrow doesn't care. The only home that's truly safe is one no instruction reads at runtime.

The assert_zero guard passed all three times, because the bytes really were zero. What it couldn't see was the index arrow. Three different tables reached past their entries into the injected bytes:

  • The victory mouth-override table (0x80077E80). The post-battle face animator read our routine as facial keyframes - a character's mouth rendered garbled during the victory pose.
  • The battle-overlay move-power table (0x801F4F5C). Six attack ids whose records landed in the squatted run read our code as power and trail-texpage values - wrong damage, garbage effect textures.
  • The SsAPI sound tables (0x80079xxx). The item-use sound engine indexes a table at 0x800794F0; a few entries past its end it walked into our capturable-Seru bitmap, read our bytes as sound parameters, and the item's sound never reported “done” - so the sound-synced item banner waited forever and the tutorial fight froze the instant you used a Healing Leaf.

The first two were caught by a structural guard (assert_not_in_tables) that refuses any region overlapping a catalogued table even when it reads as zero. But a guard is only as complete as the map of the binary - the third table simply wasn't on the list yet, which is why the fix was confidently described as “impossible to recur” and then recurred. The authoritative test isn't “is this in a table I know about?” - it's “does any instruction read this at runtime?” The final homes were chosen by putting a read-watchpoint on each candidate in a live emulator and exercising the worst cases - an item use, a victory pose, a summon cast - keeping only the runs nothing ever touched. Shiny's nine routines now live in four such read-watch-verified regions, entirely clear of the contended rodata gap below, so the feature composes with the rest of tier E. The clean-room engine pays none of this: there the same feature is just a HashSet of (character, spell) and a ×1.35 in the damage kernel. The gap-code version is what that idea costs on a sealed disc - including the cost of being wrong, three times, about which bytes are free.

The contended resource

The three routines, the unused-item name string, the engine config blob, and an overlay-loader stub all live in the same ~1 KB run. Each tenant guards its slot as all-zero, so they compose - but the region is finite, and that ceiling is exactly why a whole interactive screen goes somewhere else.

Occupants of the shared SCUS rodata gap Shared SCUS rodata gap - ~1 KB, guarded all-zero (0x8007AB38 – 0x8007AF40) 0x8007AB40 Seru Bell name string 10 bytes · variant I (--unused-items) data 0x8007AB80 Bonus-equipment routine + id table ~128 bytes · Tier E (--equipment-drops) code 0x8007ACA0 Enemy-ally charm routine ~76 bytes · Tier E (--enemy-ally) code 0x8007AD00 Run-away-EXP routine ~237 bytes · Tier E (--flee-exp) code 0x8007AE00 Overlay-loader stub groundwork code 0x8007AF00 Seru-trade config blob 24 bytes · variant J (--seru-trade) → clean-room engine data
Six tenants now share the gap (the enemy-ally routine took the last comfortable window, between the bonus-drop and flee-EXP routines). Anything more - or anything screen-sized like a new vendor UI - won't fit, which is the entire reason tier F hosts its code in a resident overlay's dead region instead of here.

Mods using this technique

  • Bonus equipment drops (--equipment-drops) - an additive drop the one record slot can't encode.
  • Run-away EXP (--flee-exp) - a reward the escape path never grants on its own.
  • Enemy ally (charm) (--enemy-ally) - flips an existing enemy onto your side by setting its Confuse/Charm bits at battle setup; no static table flags a charmed enemy. Pairs the gap routine with a one-word victory-check widen in the battle overlay.
  • Shiny Seru (--shiny-seru) - a capturable enemy spawns with +35% stats and grants a permanent +35% damage bonus once captured; the persistent flag lives in a spare per-Seru byte, and all nine routines sit in read-watch-verified-dead regions (see the trap above).

Adding a tier-E mod: confirm a reclaimable all-zero gap (and that it fits your routine), find a hook site at the right control-flow moment, preserve the displaced instructions, guard every write as dead space, and add a disc-gated oracle. If your routine is screen-sized, you've outgrown the gap - go to tier F.

See also