Patching a sealed disc
The randomizer rewrites a retail Legaia disc with no source, no SDK, and no extension points - the game ships as a sealed black box and ships only a decoder. Every mod is one of a handful of injection techniques, and they form a ladder: from same-size byte edits, through editing inside compression and rewriting bytecode, up to hand-assembled MIPS hosted in a resident overlay's dead space. This series walks the ladder one technique at a time, reversing first. The mods are the worked examples; the techniques are the point.
Three capabilities the preservation track never needed
The asset-preservation half of the project only ever reads the disc. Writing one back required three new pieces, and every mod in this series stands on them:
- An LZS encoder (
legaia_lzs::compress). Retail ships only the decoderFUN_8001A55C; there was no way to produce a stream it accepts. Greedy LZSS, weaker than Sony's packer but always accepted - see Legaia LZS. - Mode 2/2352 sector write-back (
legaia_iso::write). Overwriting a sector's 2048-byte payload also means recomputing its 4-byte EDC and 276-byte P/Q ECC, or the sector reads as corrupt - see PSX disc geometry. - A disc bridge (
disc::DiscPatcher) tying the editing primitives to the sector write-back through the PROT.DAT TOC.
The editing model: same-size, except when it can't be
Almost every edit overwrites bytes in place and never changes a byte count, so no LBA, PROT TOC, or ISO 9660 directory record ever moves - the patch is a pure byte-overwrite plus EDC/ECC recompute. That works because each target sits in a fixed slot with slack (the monster archive's 0x14000-byte records) or is a fixed-width table field. The exception is when an edit's meaning forces a length change - a scene-transition door carries its destination's name inline, so re-pointing it resizes the record. Those go through the MAN relocation engine, which rebuilds the decompressed script, fixes every disturbed offset, and keeps the recompressed stream inside the asset's on-disc footprint (or skips the scene). The disc's total size never changes either way.
The technique ladder
Six techniques, in ascending order of what they let you change - and of how much reversing they demand before the first byte can move. Each rung has its own page.
GIVE_ITEM splice, a new-game seed stamp, a string injection, an engine-facing config blob) are specializations of a parent tier and are tagged in the catalog below rather than given their own rung.Mod catalog
Every current mod, grouped by the technique it uses, plus the roadmap. This table is generated from site/_content/writeups/disc-patching/mods.toml at build time, so it never drifts: adding a mod is one row there. The Gap column flags the mods that share the executable's reclaimed rodata region (a finite resource - the contention that tier F exists to escape). Edit is in-place (same-size) or relocate (footprint-bounded). Each shipped mod is backed by a disc-gated oracle test.
| Mod | Flag | Target | Edit | Gap | Status | Test |
|---|---|---|---|---|---|---|
| Tier A - Static-table overwrites - Same-size byte edits to a fixed table in SCUS or a raw overlay. No compression, no relocation - but reverse-engineered to the exact field. | ||||||
| Steal items | --steals | SCUS steal table DAT_80077828, item byte at +1 (chance byte untouched) | in-place | - | shipped | steal_patch_real.rs |
| Spell MP costs | --spell-cost | SCUS spell table DAT_800754C8, cost byte at +3 | in-place | - | shipped | spell_cost_real.rs |
| Equipment stat bonuses | --equip-bonus | SCUS equip-bonus table DAT_80074F68, stat tuple +0..+4 (mask/slot bytes kept) | in-place | - | shipped | equip_bonuses_real.rs |
| Special-attack power | --move-power | Battle overlay (PROT 0898) move-power table, power halfword at +0x00 | in-place | - | shipped | move_power_real.rs |
| Element-affinity matrix | --element-affinity | Battle overlay (PROT 0898) 8x8 affinity matrix | in-place | - | shipped | element_affinity_real.rs |
| Item prices | --shops (pricing) | SCUS item table 0x80074368, u16 price at record +2 | in-place | - | shipped | item_price_real.rs |
| Casino prize exchange | --casino | Menu overlay (PROT 0899) raw prize table at file offset 0x15D00 | in-place | - | shipped | disc_patch_real.rs |
| Starting level H | --starting-level | SCUS new-game roster seed: level literal +0x130, growth-curve stats, lead XP +0x0/+0x4 | in-place | - | shipped | starting_level_real.rs |
| Unused-item naming (Seru Bell) I | --unused-items | Seru Bell name string at reserved gap 0x8007AB40 + name-pointer repoint for id 0xFD | in-place | shared gap | shipped | unused_content_real.rs |
| Tier B - Editing inside LZS - The value lives in a compressed slot: decompress, mutate, re-pack (one-step lazy matching, about as tight as Sony's - not tighter), zero-pad back to the fixed slot so nothing downstream moves. | ||||||
| Monster drops | --drops | Monster archive (PROT 867) slot, drop id/chance at +0x48/+0x49 | in-place | - | shipped | monster_drop_real.rs |
| Monster combat stats | --monster-stats | Monster archive (PROT 867) slot, HP/MP/ATK/DEF/INT/SPD halfwords at +0x0C.. | in-place | - | shipped | monster_stats_real.rs |
| Weapon specialty | --weapon-specialty | Player battle files (PROT 0863..0865) swing record, arm-cost byte at +0x74 | in-place | - | shipped | weapon_specialty_real.rs |
| Tier C - Rewriting field-VM bytecode - The data is bytecode - rewrite an opcode's inline operand and recompress the per-scene MAN within its original footprint budget. Needs an opcode-aware walk to find sites safely. | ||||||
| Treasure chests | --chests | Per-scene MAN: GIVE_ITEM op 0x39 operand + 0xC2 display token | in-place | - | shipped | chest_patch_real.rs |
| Town shops | --shops | Per-scene MAN: inline op 0x49 shop-stock item ids | in-place | - | shipped | shop_patch_real.rs |
| Random encounters | --encounters | Per-scene MAN: formation monster-id bytes (count/reserved preserved)incl. encounter-scope + on-by-default solo-strong pass | in-place | - | shipped | encounter_patch_real.rs |
| House doors (intra-town) | --house-doors | Per-scene MAN partition-0: player-warp op 0xA3 0xF8 tile operands | in-place | - | shipped | house_door_patch_real.rs |
| Tier D - Variable-length relocation - When an edit must change a byte count: rebuild the decompressed MAN, fix every internal offset / table / delta, keep it inside the asset's footprint. | ||||||
| Doors (scene transitions) | --doors | Per-scene MAN: 0x3F transition destination name (resizes the record) | relocate | - | shipped | door_patch_real.rs |
| Starting bag (high-capacity) G | --starting-items | Opening-scene MAN: guarded GIVE_ITEM block (0x70 test / 0x39 give / 0x50 set) spliced in | relocate | - | shipped | starting_bag_real.rs |
| Starting items (seed code) H | --starting-items | SCUS new-game seed routine FUN_80034A6C: reclaimed region re-stamped with grant codelow-count path; high-count uses the MAN splice (G) above | in-place | - | shipped | starting_items_patch_real.rs |
| Tier E - Code injection via the rodata gap - Behavior the game can't express as data: detour an existing routine into hand-assembled MIPS parked in a reclaimed all-zero region of the executable. | ||||||
| Bonus equipment drops | --equipment-drops | Detour at reward routine 0x8004F610 -> hand-assembled routine + id table at gap 0x8007AB80 | in-place | shared gap | shipped | equipment_drops_real.rs |
| Run-away EXP | --flee-exp | Detour at battle-action escape teardown 0x801E5A10 -> routine at gap 0x8007AD00 | in-place | shared gap | shipped | flee_exp_real.rs |
| Enemy ally (charm) | --enemy-ally | Detour at battle setup 0x80051990 -> charm routine at gap 0x8007ACA0; + one-word victory-mask widen 0x801E6638 in overlay 0898A per-battle chance to flip an enemy onto your side, riding the game's own Confuse/Charm retarget; gated to multi-enemy fights so scripted solo and tutorial battles can't softlock. Full story on the Tier E page. | in-place | shared gap | shipped | enemy_ally_real.rs |
| Shiny Seru | --shiny-seru | 9 detours across the battle / menu / capture paths; routines + data in four read-watch-verified-dead SCUS regions outside every live table (gap1 0x80077728, arena1 0x8007AE00, arena2 0x8007AFF8, slot6 0x80078A88)A per-battle chance that a capturable enemy spawns shiny (+35% stats, translucent); the Seru you capture from it keeps a permanent +35% damage bonus, stored in a spare per-Seru byte so it survives saving. The Tier E page tells the placement story (the 'zero is not dead' trap that bit this three times). | in-place | shared gap | shipped | shiny_seru_real.rs |
| Tier F - Overlay dead-region injection - Host hand-assembled code inside a resident overlay's reference-free dead space - so a whole new screen or system costs nothing and never competes for the shared rodata gap. | ||||||
| Seru trading vendor J | --seru-trade | Menu overlay (PROT 0899) dead region 0x801E74E0..0x801E83E0: full Trade screen; SCUS config blob 0x8007AF00 drives the engine mirrorconfig blob is the only gap-resident byte; the UI escapes the gap entirely | in-place | - | shipped | seru_trade_real.rs |
| Enemy battle assist | (planned) | Battle overlay: a defeated/random enemy joins as a non-controllable ally actornew battle-actor behavior; likely overlay-hosted code + an extra actor slot | in-place | - | planned | - |
| Seru racing | (planned) | New minigame (horse-racing shape): race state machine, odds, betting payoutnew system; hosted in a minigame overlay's dead space | in-place | - | planned | - |
| Muscle Dome spectate + betting | (planned) | Muscle Dome overlay: watch two NPCs fight, place a pre-match wager, settle payoutextends the existing match SM with a spectator + betting path | in-place | - | planned | - |
| Blood Moon event | (planned) | Periodic overworld event: red tint + stronger enemies + better loot for a window every ~30 minperiodic global timer + encounter/loot modulation + overworld CLUT tint | in-place | - | planned | - |
Tier links activate as each tier's page lands; planned rows are on the roadmap and concentrate at the top of the ladder, where system-level changes live.