Tier F - overlay dead-region injection
On a --seru-trade seed every shop in the game has a fourth line: Buy / Sell / Trade / Quit. Pick Trade and a window slides in offering to swap one of your party's learned Seru for a different one at a stated level, rotating as play time passes. Retail has no such screen. It runs on a real PlayStation because its whole code lives in bytes the shop menu already loads and never reads.
At a glance
- Host
- Menu overlay, PROT 0899, load base
0x801CE818; dead region0x801E74E0–0x801E83E0 - Picker edits
- Row clamp 3→4, box height
0x26→0x34, a label-draw detour, a confirm-dispatch detour, a per-frame entry detour - The swap
- Character record Seru id at
+0x13D, level at+0x161; records at0x80084708, stride0x414; count never changes - Offers
- 64-bucket schedule, a pure function of the seed; kernel
legaia_asset::seru_tradeshared with the from-scratch engine - Edit class
- In place, same size, entirely inside 0899 plus a 24-byte engine-facing config blob
- Confidence
- Confirmed - hardware-confirmed render → slide → cursor → confirm → swap
- Oracle
seru_trade_real.rs
Where the shop menu lives
Almost no gameplay code sits in the executable. Menus, battle, field scripts and minigames are overlays paged into the 0x801C0000+ window, and several overlays share the same address range at different times: the field overlay (0897) and the menu overlay (0899) both load at 0x801CE818, so one address is a string in one and a stat aggregator in the other. The static overlay pipeline separates them structurally and verifies 0899's disc bytes against live RAM, so the on-disc entry can be reasoned about as the loaded code.
Three facts about the picker
The Buy / Sell / Quit chooser is what gets extended, so it is what gets reversed. A fourth row needs exactly three load-bearing facts, each read from the overlay's disassembly at its recovered base and checked against the live menu.
| Fact | Where | Edit |
|---|---|---|
| The row count is a literal immediate | addiu a1, zero, 3 at 0x801DB098 | 3 → 4, so the cursor can reach a fourth line |
| The window is a sprite definition with a height byte | +0xA of the def, at 0x801E49E2 | 0x26 → 0x34 (one row of 14 px), or the new row draws outside its box |
| Draw order is text first, box last | renderer FUN_801D4868 | the fourth label must be drawn before the box or it vanishes behind it |
The dead region
| VA | Contents |
|---|---|
0x801E74E0 | trade-screen handler: render, input, slide, swap |
0x801E7B00 | entry-detour stub - per-frame gate on the active flag |
0x801E7B60 | dispatch-detour stub - routes the Trade row to its sub-mode |
0x801E7C20 | row-4 label-draw stub (text first, per the native order) |
0x801E7D20 | "Trade" / "SERU TRADE" / "Trade?" / "Yes" / "No" |
0x801E7D60 | seed-derived offer schedule: 64 buckets × 3 bytes |
0x801E7E20 | trade-active flag |
Splicing it in
0x26→0x34, one detour in the renderer body to the row-4 label stub.0x801DB0C8) sends row 2 into the trade sub-mode and sets the active flag.0x801DAFD4) checks the flag and jumps to the handler - a sub-mode of the shop, no scene warp, no overlay reload.+0x13D, level at +0x161. One-for-one, never a duplicate.Every piece is written into 0899 through patch_prot_entry(899, …), each target guarded as all-zero dead space, so an already-patched or unexpected disc is rejected rather than corrupted.
Deterministic offers
- Each of 64 buckets holds one
(want, give, give_level)triple; the level is rolled on a curve - 1-3 common (70%), 4-6 rare (25%), 7-9 very rare (5%) - so a high-level Seru is a jackpot. - The bucket is
(play_time / period + vendor_offset) & 63. The play counter advances about once per frame, so the period is frame-denominated (~9 minutes) and the full cycle takes ~9.6 hours. vendor_offsetis derived from the armed shop record (stock count + ids + name bytes, mod 64), so every merchant shows its own offer while sharing one on-disc schedule.- The bucket expands to one line per member who owns the wanted Seru, excluding anyone who already owns the give-back. Ids are the player Seru-magic block
0x81..=0x95.
The same kernel feeds the from-scratch engine through a 24-byte SeruTradeConfig blob in the executable (the config variant) - the bridge from the disc patcher to the reimplementation, and inert on real hardware.
How we know
| Claim | Evidence | Reference |
|---|---|---|
0899 is the menu overlay at 0x801CE818 | Static jal base recovery; RAM byte-match of a 0x15E8C-byte clean prefix across six menu-open save states | static-overlay-pipeline |
| Picker clamp, box height, draw order | Renderer FUN_801D4868 and its dispatcher FUN_801DAFD4, disassembled at the recovered base | shop |
| The dead region is unreferenced | Address-reference scan over the based image (no literal, lui/addiu, jal, j or branch reaches it); all-zero in live RAM | address-reference-scan |
Spell list layout +0x13C / +0x13D / +0x161 | Typed save-record accessors; the engine's apply_trade mirrors the same offsets | save-record |
Play counter at 0x80084570 ticks per frame | Runtime capture | memory-map |
| The screen works end to end | Hardware playtest; disc oracle decodes the embedded schedule back to the kernel's offers and checks the rodata gap is untouched | seru_trade_real.rs |
Details: the handler's primitives
The handler uses only the game's own routines: pad poll FUN_8001822C, text FUN_80036888, number formatter FUN_80034B78, window box FUN_8002C69C, animated cursor FUN_8002B994. The slide is a signed offset stepping 0x18 px per frame from -0xF0 to 0 - exactly ten frames. The screen names both sides of the offer in its header and shows a "No <want> available" line when nobody qualifies.
Details: roadmap features on this rung
- Enemy battle assist - a defeated enemy joins as a non-controllable ally actor.
- Seru racing - a race state machine, odds and betting hosted in a minigame overlay's dead space.
- Muscle Dome spectate + betting - watch two NPCs fight and wager beforehand.
- Blood Moon - a periodic overworld event (red tint, stronger enemies, better loot).
Adding a tier-F mod: find a reference-free dead region in the resident host overlay (all-zero in live RAM), keep routine + data under the ceiling with a compile-time disjointness assertion, guard every write as dead space, add a disc-gated oracle.