Patching a sealed disc
A randomizer seed changes what a chest holds, what a monster drops, where a door leads and what a shop sells - on a retail disc that ships no source, no SDK and no extension points. Every one of those changes is one of seven injection techniques, and the techniques form a ladder: from same-size byte edits, through editing inside compression and rewriting bytecode, up to hand-assembled MIPS hosted in an overlay's dead space. This series walks the ladder one rung per page.
At a glance
- Tool
legaia-patcher(CLI) and the browser ROM patcher - same code, nothing uploaded- Input / output
- Your own USA disc image → a shareable PPF patch (no Sony bytes) and an optional patched copy
- Three foundations
- LZS encoder
legaia_lzs::compress· sector write-backlegaia_iso::write· PROT-entry bridgedisc::DiscPatcher - Edit classes
- in-place (same-size) for tiers A, B, C, E, F; relocate (footprint-bounded) for tier D
- Confidence
- Shipped - every mod has a disc-gated oracle test that re-decodes the patched image
- Full reference
- Randomizer / disc patcher (every flag, every hook)
The technique ladder
Seven techniques in ascending order of what they let you change - and of how much reverse engineering each demands before the first byte can move.
GIVE_ITEM splice, the new-game seed stamp, the string injection, the engine-facing config blob) are specialisations of a parent tier, tagged on their catalog rows rather than given their own rung.Read in order
Later rungs assume the earlier ones' machinery. If you read only one, read tier B - it is where "we can only overwrite bytes in place" stops being a limitation.
| Tier | Page | What it is about | Why a player might care |
|---|---|---|---|
| A | Static-table overwrites | Fixed-width stat tables in the executable - steal items, spell costs, element affinities, prices, casino prizes. | The simplest way anything on the disc changes, and where every randomizer starts. |
| B | Editing inside LZS | Monster records live compressed inside fixed slots; an encoder the retail decoder accepts is what makes them editable. | Why "edit the monster" needed a compressor before it needed an editor. |
| C | Rewriting field-VM bytecode | Chests, shops, encounters and house doors are operands in each scene's script. | How a chest in Rim Elm can hold something it never held. |
| D | Variable-length relocation | A door carries its destination's name inline, so re-pointing it resizes the record. | The door randomizer and the high-capacity starting bag. |
| E | Code injection via the rodata gap | Hand-assembled MIPS detoured into reclaimed space in the executable. | New behaviour, not just new data - and the traps that bite the first time. |
| F | Overlay dead-region injection | A whole new screen hosted in a menu overlay's dead space: the Seru-trading vendor. | Where genuinely new features come from. |
| G | Carrying a RAM patch on disc | A community GameShark mod - the Super Arts Pack by ZetaPhoenix - parked in annex sectors and streamed into free RAM by an injected loader, installed byte-for-byte. | How someone else's cheat-device mod becomes a cold-boot disc patch, credit intact. |
Three capabilities the preservation track never needed
The asset-preservation half of the project only reads the disc. Writing one back needs three pieces, and every mod stands on them.
| Capability | Module | Why |
|---|---|---|
| LZS encoder | legaia_lzs::compress | Retail ships only a decoder. Greedy LZSS, slightly looser than Sony's packer, always accepted - Legaia LZS. |
| Sector write-back | legaia_iso::write | A CD sector carries error-correction fields over its payload; overwrite the payload and they must be recomputed or the sector reads as damaged - disc geometry. |
| Disc bridge | disc::DiscPatcher | Maps a "PROT entry N, offset K" edit to the sector it lives in through the PROT.DAT table of contents. |
The editing model: same-size, except when it can't be
- In place. Each target sits in a fixed slot with slack (a monster's
0x14000-byte slot) or is a fixed-width table field, so the patch is a byte overwrite plus a sector re-encode. No offset anywhere moves. - Relocate. When the edit itself changes a byte count, the MAN relocation engine rebuilds the decompressed script, fixes every disturbed offset, and recompresses into the asset's original footprint - or leaves the scene vanilla.
- Either way the disc's total size never changes, which is what makes a PPF patch a complete description of the edit.
Mod catalog
Every current mod grouped by technique, plus the roadmap. The table is generated from site/_content/writeups/disc-patching/mods.toml at build time. Gap flags mods that share the executable's reclaimed rodata region - a finite resource, and the contention that tier F exists to escape. Edit is in-place (same-size) or relocate (footprint-bounded).
| 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 stamp | --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) string | --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 |
| Enemy difficulty scale | --enemy-stat-scale | Monster archive (PROT 867) slot, the same HP/MP/ATK/DEF/INT/SPD halfwords at +0x0C.., multiplied - one factor for all seven, or per stat | in-place | - | shipped | monster_stats_real.rs |
| Enemy attack count | --enemy-attack-count | Monster archive (PROT 867) slot, each affordable attack entry's AGL-cost byte at entry +0x74, divided by the multiplier so the AGL gauge affords more (or fewer) strikes | in-place | - | shipped | attack_count_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 |
| Delilas party swap structural rebuild | --delilas-party V,N,G | monster archive slots 162-164 + player battle files PROT 863-865 + field pack PROT 0874 + nilboa NPC pack PROT 0639 + battle voice bank PROT 0869 + readef.DAT art slots + XA arts-voice banks + SCUS arts-name table + new-game template names + PROT 0898 element/camera rowsplay as Gi / Lu / Che (any assignment); duels + dome Master legs field Vahn / Noa / Gala; arts shouts follow --delilas-arts-voice (default original; adjusted = pitch/formant re-voice); save-select/card-icon/load-screen portraits + sibling-name dialog follow the mapping; menu portraits / HUD faces stay retail; the cast route yields the SCUS gap to any hard consumer | cross-rig battle-model rebuild both directions (anatomy permutation + rest-pose bake + 4bpp texel re-layout), monster-block renames, template name writes | - | shipped | delilas_party_real + party_swap_real + delilas_cast_stage_real + delilas_cast_remap_real + enemy_anim_mirror_real + nivora_field_real |
| Super Art damage power | --super-art-power | per-strike power run of the Super Arts' replacement rows in the player record[0] art bankcomposes with everything - a damage row claims none of the SCUS arena | same-size byte edits inside the LZS record, recompressed into the retail footprint | - | shipped | super_art_power_real |
| Monster battle skins | legaia-patcher monster-texture | 4bpp texture pages + CLUTs inside a monster's battle_data block (PROT 867)browser: the monster tier of the texture-replacement page | PNG in, bit-exact 4bpp re-layout into the block's own page footprint, block recompressed | - | shipped | monster_texture_real |
| Custom monster models structural rebuild | legaia-patcher monster-model | a monster's whole 3D model (mesh + texture) inside its battle_data blockthe mesh must keep the retail part count - animations pose parts by index, so a same-part-count mesh inherits every retail move | OBJ+PNG out, custom model re-encoded (byte-exact TMD encoder) and re-pooled into the block | - | shipped | monster_model_real |
| 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) splice | --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) stamp | --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 |
| Super Arts on the move list | --show-super-arts | pause-menu Moves list: performed-gated Super/Miracle rows with derived arrow inputs, AP-sorted interleavea Super Art has no AP field but the chain that performs it pays; rows appear once performed | SCUS injection-gap code + the free 16th learned-id slot (+0x75D performed flag); menu overlay renderer hook | shared gap | shipped | super_art_list_real |
| 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 config | --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 HP bars | --enemy-hp-bar | Two-word detour at the damage-popup renderer (PROT 0898, 0x801DF6B8); the routine laid over four routines nothing on the disc references: 0x801F2D54 / 0x801F463C / 0x801DBB2C in the battle overlay and FUN_8005126C in SCUSA red HP gauge over every living monster, drawn every frame from the roster panel's HP label and the AP meter's gauge primitive (red meter + percentage numeral, no blue chrome), fed by the displayed-HP mirror so it steps per hit inside a combo; one row per monster slot along the top of the screen, tracking each monster's screen X through the game's own billboard projector. Hosts are unreferenced code, not zero padding, so 'zero is not dead' does not apply and no arena byte is claimed. | in-place | - | shipped | enemy_hp_bar_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 G - Carrying a RAM patch on disc - A payload that targets free RAM no overlay covers cannot be hosted - it must be streamed: parked in annex sectors and read to its address at load time by an injected stub calling the game's own CD reader. Also the rung where a third-party mod's bytes ride the disc verbatim. | ||||||
| Super Arts Pack (by ZetaPhoenix) | --super-arts-pack | 3764-byte community RAM patch streamed to free RAM 0x801FD000 at battle load; 14 hook words across SCUS + PROT 0898fifteen extra Super Arts, five per character; ZetaPhoenix's block and hook set installed verbatim (the loader stub is the carrier's only added code), incl. his fix for retail's mis-centred art name banner, relocated to arena 1 | annex sectors + in-place word edits | shared gap | shipped | super_arts_pack_real |
Planned rows concentrate at the top of the ladder, where system-level changes live.