The technique in one box
Find the routine that reads a stat at runtime, walk back to the table it indexes, recover the stride and the field offset, and check every record against the published game data. Then overwrite only the value column, in place, and re-encode the touched sector. Structural bytes - ids, masks, slot types, progression gates - never move.

At a glance

Target
Fixed-width tables in SCUS_942.54 (always resident) or in a raw overlay's data section (PROT 0898 battle, PROT 0899 menu)
Edit class
In place, same size - no compression, no relocation, no new code
Mods
--steals · --spell-cost · --equip-bonus · --move-power · --element-affinity · item prices · --casino · --starting-level (variant: stamp) · --unused-items (variant: string)
Ground truth
The curated game-data tables - every located table is byte-validated against them before it is trusted
Confidence
Confirmed - each table pinned from its reader and cross-checked field-by-field
Oracles
cross_table_integrity_real.rs plus one disc-round-trip test per feature

How a table is pinned

None of these tables is labelled. Each is located from its consumer - the routine that reads it while the game runs - and validated against the curated tables before a single byte is written.

1
Find the reader
The steal resolver, the cast-MP deduction, the damage kernel, the shop pricer. Most live in overlays, not the executable.
Ghidra + overlay capture
2
Recover the base
MIPS forms a 32-bit address as a lui/addiu pair; scan for the pair, since a plain cross-reference finds nothing.
find_lui_writers.py
3
Read the stride + field
The index multiply gives the stride; the load offset gives the field byte.
disassembly
4
Validate every record
Decode the whole table and compare it to the published walkthrough data - zero mismatches or it is not the table.
gamedata
5
Overwrite the value column
Shuffle or re-roll one field per record; leave ids, masks and gates untouched; re-encode the sector.
DiscPatcher
A same-size column edit on the steal table one record per monster, 2 bytes, 1-based id chance item chance item chance item … base + id*2 +2 +4 highlighted bytes are the only ones the randomizer moves - the chance column stays, so how often a steal lands is unchanged
A tier-A edit rewrites one column of a fixed-stride table. The steal table's field order is chance first, item second - the reverse of the drop fields inside a monster record.

The tables

TableWhereStrideField movedMod
StealSCUS DAT_800778282 Bitem byte at +1; chance at +0 preserved--steals
Spell recordSCUS DAT_800754C812 BMP cost at +3, permuted across costed spells--spell-cost
Equipment bonusSCUS DAT_80074F688 Bstat tuple, permuted within a slot category; equip mask + slot type preserved--equip-bonus
Item recordSCUS DAT_8007436812 Bprice u16 at +2item prices
Move powerPROT 0898 (battle overlay) file 0x2674426 Bpower / behaviour bytes - move-power--move-power
Element affinityPROT 08988×8 cellsdamage-scale percentages permuted (100 = neutral, 0 = immune)--element-affinity
Casino prizesPROT 0899 (menu overlay) file 0x15D008 Bwhole record shuffled, so a prize keeps its coin price and progression gate--casino

Two of the seven live in overlays - the code the game pages into RAM per mode - rather than in the executable. The technique is identical; only the file the write targets differs.

Two variants that ride on the same shape

  • New-game seed stamp (the stamp variant, --starting-level). The executable holds the roster the game seeds at New Game. Overwriting its displayed-level byte, growth-curve rows and the lead's cumulative-XP words is a same-size edit aimed at the boot template instead of a gameplay table - new-game table.
  • String injection (the string variant, --unused-items). Naming an unused item re-points its name-table pointer (a tier-A write) and writes the new string into a reserved zero run - the executable's rodata gap, the contended region the tier E map lays out.

Tier A composes with everything: every edit is same-size and self-contained, so none can disturb another's offsets.

How we know

ClaimEvidenceReference
Steal table base, stride, [chance, item] orderLive steal capture (monster 13 entry at 0x80077842 matches the banner) + byte-exact match against the published steal table, every idsteal-table
Spell MP cost at +3Cast-MP deduction in the battle-action machine FUN_801E295C, state 0x28spell-table
Equipment bonus stride 8Slot walker FUN_801CF650 reads five slots at that strideequipment-table
Item price at +2Verified live against shop prices (War God Band 21000)item-table
Move power / affinity in PROT 0898Damage kernel reads via FUN_801dd0ac, indexed by the actor's queued action bytemove-power, battle-formulas
Casino prize table in PROT 0899Prize exchange debits the coin bank, not gold; four 0x60-byte blocks at VA 0x801E4518randomizer
Details: why a cross-reference query finds nothing

Two properties of the binary make step 2 above necessary. Most game logic lives in RAM overlays loaded at 0x801C0000+, so a table read only by overlay code has zero references in the executable - "no static caller" never means dead. And a MIPS 32-bit address is assembled from two instructions (lui for the high half, addiu for the low half); Ghidra's reference manager does not join them, so a direct cross-reference on the table address returns nothing even when it is read every frame. The base is recovered by scanning for the writer pair instead - Ghidra workflow.

Details: adding a tier-A mod
  1. Locate the table through its consumer (overlay sweep + lui/addiu scan).
  2. Decode the whole table and byte-validate every field against gamedata.
  3. Move only the value column; preserve every structural byte.
  4. Add a disc-gated oracle under crates/patcher/tests/ that re-decodes the patched image and checks the multiset is preserved and every touched sector is EDC/ECC-valid.

See also