At a glance

Crate
crates/patcher (legaia_patcher); disc bridge disc::DiscPatcher, orchestration apply, patch output ppf
Three capabilities
LZS encoder (legaia_lzs::compress), Mode 2/2352 sector write-back with EDC/ECC (legaia_iso::write), PROT-entry → LBA addressing (DiscPatcher)
Editing rule
Same-size in place; only doors and place names resize a record, through MAN relocation
Code hosts
SCUS rodata gap 0x8007AB38 (1028 B), the SCUS arena (652 B in four regions), reference-free dead runs inside overlays 0898 / 0899 / 0958, the DMY.DAT annex
Detour shape
Two words at the hook site become j routine + nop; the routine replays the two displaced instructions and jumps back
Guards
Every hook word must match the US build; every landing zone must be all-zero, outside every known table, and read-watch-verified unreferenced
Confidence
Confirmed: disc oracles pin every byte; the MIPS hooks are additionally verified by emulator playtest (the from-scratch engine cannot execute them)

How a patch reaches the disc

A PlayStation disc is not a folder of files. Almost every value the randomizer changes sits inside a compressed block, on a sector whose checksums must be recomputed, at an address nothing else may move. Three capabilities the preservation track never needed make that editable.

CapabilityModuleWhy it is needed
LZS encoderlegaia_lzs::compressRetail ships only a decoder. Monster records, scene MANs and art records are LZS streams, so editing one means producing a stream the retail decoder accepts - and packing it no larger than the original. LZS →
Sector write-backlegaia_iso::writeOverwriting a sector's 2048-byte payload also means recomputing its 4-byte EDC and 276-byte P/Q ECC, or the drive reads it as corrupt. Disc geometry →
Disc bridgelegaia_patcher::disc::DiscPatcherTurns "byte K of PROT entry N" into physical sector writes through the PROT.DAT table of contents.
edit (entry N, offset K) PROT TOC start_lba[N]*2048 + K ISO 9660 PROT.DAT at prot_lba sector write 2352 B + EDC/ECC legaia_patcher legaia_prot legaia_iso legaia_iso::write
The three hops of DiscPatcher::patch_prot_entry. patch_named_file is the sibling for edits straight into SCUS_942.54; patch_monster_slot wraps the battle_data archive.

The editing rule

  • Same size, in place. No sector address, PROT entry or ISO directory record ever moves, so a patch is a pure byte overwrite plus checksum recompute. The disc image's size never changes.
  • Fixed slots pad; tight streams re-pack. A monster record is re-emitted zero-padded back to its 0x14000-byte slot in the battle_data archive (PROT 867). A scene MAN has no compressed slack, so the encoder's lazy matching has to fit the exact original span; the rare stream that overflows is skipped and reported.
  • Budget from the boundary. A MAN's re-pack budget is its span up to the next descriptor, not its current compressed length - otherwise a second pass over the same scene (encounters, then chests, then shops) would shrink the budget each time.
  • Doors and place names are the exception. A destination carries its scene name inline and a banner name is stored unpadded, so re-pointing or renaming changes a record's length. The MAN relocation engine fixes every offset the resize disturbs and rewrites the descriptor's size word.

The error-correction math is the generic CD-ROM scheme (ECMA-130): EDC is a CRC over reversed polynomial 0xD8018001, ECC is Reed-Solomon P/Q over GF(28) with generator 0x11D, and the header is treated as zero so parity is independent of the sector's address. It embeds no game bytes; the disc-gated ecc_real test reproduces real PROT.DAT sectors bit-for-bit.

The technique ladder

Every feature uses one of six techniques, from "overwrite a byte in a table" up to "host new machine code in unused overlay space". The disc-patching write-ups walk each tier with a worked example.

TierTechniqueWhat changesFeatures
AStatic tableBytes in a SCUS_942.54 data table, same sizeSteals, spell costs, equip bonuses and masks, casino prizes, item prices, Seru Bell name
BLZS slotDecompress, mutate, recompress, pad back into a fixed slotDrops, monster stats, arts power and combos, difficulty scale, custom monster models
CScript operandAn operand byte inside a scene MAN's field-VM script, same size, MAN re-packedChests, shops, encounters, house doors, inn costs, Earth Egg threshold
DMAN relocationA record grows or shrinks; every dependent offset is fixedDoors, place names, starting-bag overflow, Delilas Challenge script, custom-item narration
ECode in the rodata gapA hand-assembled routine in preserved SCUS padding, reached by a two-word detourEquipment drops, run-away EXP, enemy ally, Seru Bell string
FOverlay dead region / arenaCode in an overlay's reference-free zero run, or in the verified-dead SCUS arenaShiny Seru, Seru trading, Super Arts list, Super Arts Pack, arts AP override, Delilas Challenge, Delilas party swap, custom items

Code hooks: where injected code lives

A detour overwrites two instructions at a hook site with j routine + nop. The routine does its work, replays the two displaced instructions, and jumps back to the word after the site. On the PSX all resident RAM is executable, so a routine written into a dead data region runs when jumped to. Every feature below guards on the hook words matching the US build and refuses a differently-laid-out image rather than corrupting it.

FeatureHook site(s)Host imageCode lives atWhat the routine does
Equipment drops0x8004F610 in the reward routine, right after the normal drop grantSCUSrodata gap 0x8007AB80 + id tableRolls rand() % 100 < chance, picks a random id from an embedded equipment table, calls the inventory-add helper once per battle
Run-away EXP0x801E5A10, the successful-escape teardown (state 0x66)PROT 0898 → SCUSrodata gap 0x8007AD00Sums the formation's EXP, scales to the percentage, banks it into every member's cumulative-XP cell (cap 9,999,999) without calling the level processor
Enemy ally0x80051990 after battle setup's monster loop; one word at 0x801E6638SCUS + PROT 0898rodata gap 0x8007ACA0Rolls the chance and ORs the AI-delegated flag 0x380 into the frontmost enemy; the overlay word widens the victory mask so the ally counts as down
Shiny SeruNine detours: setup, capture, grant, grant-shift, damage, menu, battle-menu, summon-fade, captionSCUS + 0898 + 0899the four arena regionsBoosts a capturable enemy ×135/100, marks it, carries the mark through capture into a per-slot shiny byte at record +0x788, and applies +35% on every cast
Seru tradingTwo picker edits in the shop menuPROT 08990899's own dead run (~3.8 KB), from 0x801E7FB0Adds a Trade row and hosts the whole trade screen - render, slide, cursor, confirm, swap - plus the seed-derived 64-bucket offer table. Touches no SCUS bytes
Show Super Arts (battle list)Count read 0x800343C4, id read 0x80034450, applier arm 0x801EFBCC, pager 0x801D3748SCUS + PROT 0898all four arena regions (613 B)Synthesises a 20-byte arts-table record in dead space and enters the list renderer's hit arm with it, so retail draws the row; the pager is replaced in place
Show Super Arts (Moves page)Count reads 0x801D4454 / 0x801D4440, scan entry 0x801D4480, cursor bound 0x801DA64CPROT 0899dead runs 0x801E7FB0.. and 0x801E65F4..The same merge for the pause menu's panel renderer; composes with everything because it uses no SCUS bytes
Super Arts PackSix applier retargets, three jumps, battle-load stub at 0x80055DBCSCUS + PROT 089856 B at arena 1 head; the 4 KB block in the DMY.DAT annex, streamed to 0x801FD000 at battle loadDoubles the applier's per-character stride so its find/replace tables point at the pack's wider tables; the stub reads the block with the game's own synchronous reader and flushes the I-cache
Arts AP overrideThree detours in the party arts queue-builderPROT 0898 → SCUSarena regions + a 4×32 i8 config tableReplaces the computed per-command cost with a per-(character, row) value, or turns it into a grant; rewrites the menu AP byte alongside
Delilas Challenge (dome)Seed detour 0x801CEBCC, payout 0x801D1118, display 0x801D125C, settle 0x801D114CPROT 0977 → SCUSarena 1 (seed routine + relocated hub template); grant cave 0x800352EC; display override over FUN_800260DCAdds course 3 to the arena's descriptor table, pays 5000 for it, and grants the reward item(s) on a cleared settle
Delilas party swap (cast route)Queue hook in the arts matcher; strike-loop fetch 0x801E374C; label gate 0x801E43D0; thirteen module damage retargetsSCUS + 0898 + 0958/0959/0960rodata gap tail, arena 1 / arena 2 / slot 6, PROT 958's dead party-wipe bodyConverts a matched signature combo into a capture-class cast, deferring when the starter is not at the queue base; halves the Hyper admission tier
Jewel fixThirteen jal words across PROT 944 / 952 / 953 / 958 / 959 / 960cast modulesnone (word edits)Retargets the resist-bypassing damage wrapper to the guard-respecting one, so boss signature casts respect Jewels and guards
Approach-softlock fixNine words at 0x801E3568 in the state-0x19 range pollPROT 0898none (in-place rewrite)Replaces a redundant facing recompute with a guard that bounces a monster whose Move clip died back to state 0x14, which re-stages the approach
Starting bag / level40 bytes at 0x80034B04 in the new-game data-initSCUSnone (the seed code itself)Rewrites the code that builds the opening inventory (one packed halfword store per slot, cap 7) and stamps the party level; overflow items are spliced into town01's entry script as silent give-item ops
Seru Bell nameName pointer of item 0xFDSCUSrodata gap 0x8007AB40Writes the string into loader-preserved padding and repoints one name pointer
Spirit / enemy-damage APThe accrual tails in 0898PROT 0898none (same-size word rewrites)Changes the AP constants; a negative value also installs a signed floor - see the signed tail

The dead-space map

"Dead" is a runtime property, not a file-byte one: a region qualifies only when it is all-zero in the clean image, constant across states, outside every known table, and read-watch-verified unreferenced on a live PCSX-Redux session. These are the regions that pass.

RegionSpanSizeNatureTenants
Rodata gap0x8007AB38..0x8007AF3C1028 BSCUS rodata padding the loader preserves (flanking constants survive file → RAM)Seru Bell string +0x08; equipment-drop routine + table 0x8007AB80..0x8007ACA0; enemy-ally 0x8007ACA0; flee-EXP 0x8007AD00. All coexist
SCUS_GAP0x80077728..0x80077828256 BArena; bounded above by the steal tableArena claimants (exclusive)
ARENA10x8007AE00..0x8007AF00256 BArena; the SsAPI I/O table begins exactly at 0x8007AF00Arena claimants (exclusive)
ARENA20x8007AFF8..0x8007B04072 BArena; a pocket between two SsAPI tablesArena claimants (exclusive)
SLOT60x80078A88..0x80078ACC68 BArena; padding between the 0x80078xxx tablesArena claimants (exclusive)
0899 dead runs0x801E7FB0.., 0x801E65F4..~3.8 KBReference-free zero runs inside the menu overlay image; read/write-watched across a full pause-menu tourSeru trading; Super Arts Moves page (past trading's highest blob)
0958 wipe bodyinside PROT 0958a few wordsThe module's dead party-wipe bodyParty-swap cast stubs
Free RAM0x801FD000..0x801FE0004 KBNot on the disc: all-zero in every state, no overlay covers it, deepest stack use 1388 B above itSuper Arts Pack block, streamed from the DMY.DAT annex at battle load

The injected-code arena budget

Every hand-assembled SCUS-resident feature that needs more than the rodata gap offers lands in the same four verified-dead regions, and they add up to 652 bytes. The limit is arithmetic, not policy: a sweep of every zero run in the executable outside the known live tables finds only 53 further candidate bytes.

RegionSizeShiny Seru usesShow Super Arts uses
SCUS_GAP256 B246 B253 B (routine B, scratch record, glyph buffer)
ARENA1256 B252 B256 B (routine F, packed arrows)
ARENA272 B64 B44 B (routine A)
SLOT668 B56 B60 B (fifteen Super Art records)
Total652 B618 B613 B

Shiny Seru leaves 34 bytes in fragments of 4, 5, 8 and 12; nothing else fits beside it. So the arena claimants are mutually exclusive, enforced by the CLI and the browser patcher:

ClaimantNeedsConflict rule
--shiny-seru618 B across all fourYields to the Delilas Challenge with a note; refused alongside the others
--show-super-arts613 B across all fourRefused alongside any other claimant (it also detours the same applier the Pack retargets)
--super-arts-pack56 B at the head of ARENA1Refused alongside any other claimant
--arts-ap-grant / --arts-ap-costRoutines + config table in the arenaHard conflict with everything, including the Challenge
--delilas-challengeARENA1 (seed routine + relocated hub template)Takes precedence over shiny Seru - the presets that enable both install the Challenge
Delilas party swap (cast route)Rodata-gap tail + ARENA1 / ARENA2 / SLOT6Resolves silently: when another claimant owns the gap the swap still applies, but that slot keeps the art-side signature; the summary says so

The Super Arts list payload cannot move into the battle overlay: the arts list is a shared window widget whose renderer has one reference on the disc, from a dispatcher the field and menu overlays call 64 times between them, so it runs when PROT 0898 is not resident. A Super Art damage row claims no arena and composes with everything. Growing PROT 0898 to cover the Pack's block was rejected because it would zero 0x801F7018..0x801FD000 - persistent effect and world state the field overlay leaves there - on every battle load.

Three traps in the MIPS injection features

Each has bitten a shipped hook at least once; each is guarded now.

TrapWhat goes wrongThe rule
R3000 load-delay slotA just-loaded register is not ready for the very next instruction. A routine that reads one in the instruction after its load reads the stale value; the shiny-Seru boost loop cascades into garbage without itA filler instruction (or a reorder) between every load and its first use; never split a mult/mflo. The in-crate mips_sim interpreter is load-delay-accurate and runs the stubs in unit tests
"Zero is not dead"A run of zero bytes is reusable only if no code reads it. Live indexed tables are full of zero padding the game still indexesRegions must be all-zero in the clean image, constant across states, outside SCUS_TABLE_RANGES / OVERLAY_TABLE_RANGES (the structural assert_not_in_tables guard), and read-watched on a live battle that uses an item, plays a victory pose and casts a summon
Unaligned entryThe j encoding drops the target's low two bits, and zero-run scans return run starts that are frequently unaligned - so a routine placed at one jumps 2-3 bytes into garbageEvery routine VA is rounded up to a word boundary; place refuses an unaligned routine. Only byte-addressed data may sit unaligned
The three times "zero is not dead" bit
Region that looked deadReaderSymptom
Victory mouth-override table 0x80077E80 (rows 0x800781B0..)FUN_8004C7B4, the victory face animatorRoutine bytes read as facial keyframes: a corrupted mouth on the victory pose
Move-power table 0x801F4F5C, records 4..8The move-power lookupSix move ids (0x07, 0x12..0x15, 0x19) read routine bytes as power records: garbage damage
SsAPI sound tables 0x800794F0 (and the former arenas at 0x8007075C / 0x80079340)FUN_8005D0B8, the item-use sound engineUsing a Healing Leaf indexed straight into the bitmap; the sound-synced item banner never dismissed - the Tetsu-tutorial freeze

The same lesson shaped two data choices: the shiny flag lives in a parallel per-slot byte array at record +0x1C0 rather than the spell-level byte's free 0x80 bit (a shared level-up/display function reads that byte unmasked and indexed a table out of bounds), and the Seru Bell string sits in rodata padding rather than the data segment's trailing zero-fill (that fill is .sbss scratch the game overwrites every frame).

The signed accrual tail

The Spirit and enemy-damage AP sliders accept negative values, which invert the knob: Spirit then spends AP, or being hit drains it. The gauge is a u16 and every retail site that grows it clamps only against the 100 ceiling, so a negative constant alone would read back as +200-odd and pin the gauge at 100. The consumer has to honour the sign as well.

  • Damage site: the ceiling words are dead once the site only subtracts, so a bgez floor fits in them.
  • Spirit site: its add/clamp tail is shared with the +8 every non-Spirit action grants, so the ceiling must survive beside the new floor. They fit because a negative setting makes the AP-Boost-1 accessory arm dead, and the head of that arm hosts the relocated over-100 clamp. Consequence: while a slider is negative, the "spirit gain up" accessory is inert.
  • No arena involved: every word is a same-size rewrite inside PROT 0898; the two sliders compose with each other and with everything else, and the retail value restores the stock bytes exactly.
The rewritten Spirit tail
801e5e78  bgez v1,0x801e5e84
801e5e7c  _slti v0,v1,0x65         ; delay slot: ceiling test
801e5e80  move v1,zero             ; floor at 0
801e5e84  beq  v0,zero,0x801e5e40  ; over 100 -> the relocated clamp
801e5e88  _sh  v1,0x170(s3)        ; delay slot: the store

Both boost arms read the staged accrual unsigned, which is what makes the AP-Boost-1 arm dead under a negative setting. The negative modes are statically verified only: the disc oracles prove which words land where and that the hand-assembled branches resolve to these instructions; a live battle playtest has not been run.

Crate modules

One machinery, three families on top: the randomizer, the translation packs and the manual edits. Each module is one row.

Module map
ModuleRole
rngVersion-stable SplitMix64; the first output for seed 0 is pinned by a test.
items / equipmentValid item-id pool from the SCUS item-name table; equipment ids recovered by matching curated names against the disc's own table (~150 ids, no Sony bytes).
drops / monsterDrop-table planner (shuffle preserves the multiset); slot re-pack decompress → mutate → recompress → pad to 0x14000.
encounter / kingdomFormation shuffle per scene, kingdom or world; the kingdom partition of the scene namespace comes from CDNAME anchored on the overworld maps.
chest / shop / door / house_doorOpcode-aware MAN walks for give-item (0x39), vendor (0x49), scene-change (0x3F) and cross-context move sites; doors go through man_edit.
casino / steal / spell_cost / equip_*Same-size static-table edits in the menu overlay (casino, raw file offset 0x15D00) and SCUS_942.54.
monster_stats / move_power / element_affinityBattle tuning: per-stat column redistribution over PROT 867 with tutorial enemies and bosses pinned; move-power halfwords and the 8×8 affinity matrix in PROT 0898.
starting_items / starting_bag / starting_levelRewrite the new-game seed code; splice overflow gives into town01; stamp level and recompute growth templates for Vahn / Noa / Gala.
bonus_drop / flee_exp / enemy_ally / shiny_seruThe SCUS-resident MIPS hooks of the table above.
seru_overlay / super_art_list / super_art_menu / super_arts_pack / arts_ap_grantThe overlay- and arena-resident hooks.
delilas_dome / delilas_challenge / delilas_cast / custom_items / nivora_fieldThe Delilas features: dome course injection, koin1 script edit, cast route, custom item set, field-form mesh swap.
jewel_fix / approach_fixThe two seedless retail-bug fixes.
unused / item_name / texture / modelCut-content ids, the Seru Bell name, PNG → TIM texture swaps, custom monster models.
mips_simIn-crate MIPS interpreter (load-delay-accurate) the hook stubs execute under in unit tests.
disc / apply / ppfThe disc bridge; the orchestration every CLI verb drives, each returning an apply report; PPF 3.0 diff / write / apply.

Tests

Three layers. Disc-gated tests read LEGAIA_DISC_BIN; with it unset they skip and pass.

LayerWhereWhat it proves
Disc-free unit testscrates/lzs, crates/iso, crates/patcherThe encoder round-trips; sector encoding is idempotent and address-independent; the planner is seed-deterministic; hook stubs behave under mips_sim; a synthetic disc round-trips through the disc → ISO → PROT chain.
Disc round-trip oraclescrates/patcher/tests/*_real.rsPatch a feature onto a scratch copy of a real disc, re-decode off the patched image, assert the multiset or invariant is preserved, every touched sector is EDC/ECC-valid, neighbours are untouched, and a fixed seed is byte-deterministic. For code hooks: the stock words match, the detour and routine decode as the hand-assembled bytes, and the build guard refuses an unknown layout.
Runtime oraclescrates/engine-core/tests/*_randomizer_runtime_e2e.rsDrive the from-scratch engine's own grant / spawn / warp kernel over the patched bytes - chest, drop, encounter, steal, door - so a patch a savestate would mask (the scene or archive already resident in RAM) is observed. The engine cannot execute injected MIPS, so hook features are covered by the disc oracle plus an emulator playtest instead.
Per-feature oracle inventory
OracleAsserts
ecc_real, lzs_compress_roundtrip_realReal PROT.DAT sectors re-encode bit-for-bit; real monster records and container sections round-trip and compress.
disc_patch_real, rando_cli_realOne drop patch re-decodes with neighbours untouched; a full-archive shuffle diffs into a PPF that reproduces the patched image.
encounter_patch_realCounts and id multiset preserved per scene, ids in-pool; every scripted or boss formation (Tetsu 0x4F among them) byte-identical.
chest_patch_real, shop_*, casino_*Give-item site offsets unchanged, item multisets preserved; a targeted keikoku chest carries the new id in both the give operand and every announcement token.
steal_patch_realSteal-item multiset preserved, every chance byte untouched.
door_*, house_door_*Destination multisets preserved, names valid, sectors valid, image size unchanged; a census test pins per-scene entry/exit counts against a live-captured anchor.
equipment_drops_real, flee_exp_real, enemy_ally_real, shiny_seru_realEach hook site holds its stock words; detour + routine + tables decode as planned; edits are surgical and compose in the shared gap; every landing region is dead and outside every live table.
seru_trade_realEvery piece lands in 0899, the schedule round-trips to the kernel's offers, the SCUS gap is untouched.
super_art_list_real, super_art_power_real, super_arts_pack_real, arts_ap_grant_realArena accounting intact; the Pack's runtime oracle executes the patched retail applier over a live queue and checks all fifteen added chains fire.
jewel_fix_real, approach_fix_realBaseline words stock, exactly the planned words change, module windows contain no other module's bytes, re-application is refused or a no-op.
delilas_*_real, nivora_field_real, party_swap_realThe dome, cast, mirror and field-form halves of the Delilas features.
unused_content_real, monster_stats_*, move_power_*, element_affinity_*, spell_cost_*, equip_*, weapon_specialty_*The remaining table and slot edits, each with a baseline pass so the oracle is non-vacuous.

No-Sony-bytes hygiene

  • The crate never embeds, commits or redistributes game bytes. Injected routines are the project's own code; item and equipment ids come from the user's disc at patch time.
  • A patched .bin contains Sony data and is never committed. The distribution form is a patcher plus seed, or the PPF the CLI emits - a PPF carries only the deltas between the user's original disc and the patched one and is meaningless without the original.
  • Disc-gated tests skip without LEGAIA_DISC_BIN; CI runs without disc data.

How we know

FunctionAddressWhat it provesDump
LZS decoderFUN_8001A55CThe stream format the encoder must produce; 4 KB ring initialised to zerosghidra/scripts/funcs/8001a55c.txt
PROT entry sizeFUN_8003E68CAn entry's size is the sector gap to the next entry - the re-pack budgetfuncs/8003e68c.txt
Battle reward routineFUN_8004E568, join 0x8004F610Spoils tally once per battle; the equipment-drop hook sitefuncs/8004e568.txt
Battle setupFUN_800513F0, 0x80051990 / 0x80051A20Actor table populated after the monster loop; the enemy-ally and shiny-Seru setup sitesfuncs/800513f0.txt
Battle-action SMFUN_801E295C, state 0x66 at 0x801E5A10Successful-escape teardown runs once; the flee-EXP hook sitefuncs/overlay_battle_action_801e295c.txt
Arts list rendererFUN_80034358, one caller 0x8003238CDraws rows from a 20-byte record found by a linear scan - the record the Super Arts list synthesisesfuncs/80034358.txt
Super-Art applierFUN_801EF9E4Find/replace tables addressed by a per-character stride in t5 - the Pack's retargetfuncs/overlay_battle_action_801ef9e4.txt
Party arts queue-builderFUN_801EED1CAP cost is computed (multiplier × commands), not stored - why the AP override needs a hookfuncs/overlay_battle_action_801eed1c.txt
New-game data-initFUN_80034A6C, 0x80034B04The opening bag is code-built; both callers memset the block, so the zero-loop is reclaimablefuncs/80034a6c.txt
Synchronous CD readerFUN_8005E4D4The read primitive the Pack's battle-load stub callsfuncs/8005e4d4.txt
Damage wrappersFUN_801DD6B4 / FUN_801DD4B0The resist-bypassing and guard-respecting wrappers the jewel fix swapsfuncs/overlay_battle_action_801dd6b4.txt
Read-watch sessionsPCSX-Redux Lua probesEvery arena region unreferenced across item use, victory pose and a summon cast; the 0899 dead runs untouched across a pause-menu tourPCSX-Redux automation

See also