Tier B - editing inside LZS
A seed can give a Gimard different HP, a different drop, and give Vahn a cheaper swing with a weapon he is not built for. None of those values sits in the open: each is inside a compressed record the game inflates at load. Changing one is decompress, edit, recompress, write back - and the game ships no compressor, only a decompressor.
0x14000-byte slot with padding, so the re-packed stream lands in the same slot, zero-padded to the same size. Nothing downstream moves. Decoded output is trusted only after a structural check - a clean decompress proves nothing.
At a glance
- Target
- Values inside an LZS stream - chiefly the monster archive (PROT 0867) and the player battle files (PROT 0863..0865)
- Enabler
legaia_lzs::compress: greedy LZSS with one-step lazy matching;decompress(compress(x)) == xagainst the retail decoder- Slot
[u32 decompressed_size][LZS stream][zero pad], fixed0x14000bytes per monster- Edit class
- In place at the slot level;
repack_slotrejects the rare stream that would overflow - Mods
--drops·--monster-stats·--weapon-specialty- Confidence
- Confirmed - round-trip oracles re-decode every patched slot
- Oracles
monster_drop_real.rs·monster_stats_real.rs·weapon_specialty_real.rs
Why the slot stride is the whole trick
The slack comes from the archive, not from the compressor. Sony's packer is tighter than the project's, so a re-packed stream is usually a little longer than the original - and the fixed slot has enough padding to absorb it. The decoded record length never changes; the slot stride never changes; the edit stays a pure same-size overwrite.
What moves
| Field | Inside | Offset | Mod |
|---|---|---|---|
| Drop item / drop chance | decoded monster record, PROT 0867 | +0x48 / +0x49 (u8) | --drops |
| HP, MP, ATK, DEF×2, AGL, SPD | decoded monster record, PROT 0867 | halfwords from +0x0C | --monster-stats |
| Weapon-class arm cost | equipment section of the player battle files, PROT 0863..0865 | swing record +0x74 | --weapon-specialty |
Weapon specialty is worth a sentence: which weapon class a character favours is not a runtime comparison. It is a per-(character, weapon) disc byte, copied verbatim into the arts command gauge at battle load - which is exactly what makes it a data target. See arts command gauge.
Drops and stats are independent passes over the same archive. Each works from the original decoded record and pads back to the same stride, so order does not matter and neither pass can shift the other's offsets.
How we know
| Claim | Evidence | Reference |
|---|---|---|
| LZS format + ring-buffer semantics | Retail decoder FUN_8001A55C, reversed for the preservation track | lzs |
Slot stride 0x14000, record layout | Monster archive parser + the monster-archive CLI listing every monster's stats | battle-data-pack |
Arm cost is disc data at +0x74 | Battle-load copy into DAT_801C9360[char][cmd] by FUN_800557B8 | arts-command-gauge |
| Round trip is exact | Disc oracles decode every patched slot and compare against the pre-patch multiset | crates/patcher/tests/ |
Details: "decompresses without error" is not a validity signal
The decoder's 4 KB ring buffer initialises to zeros, so most random input decodes to plausible-looking output. A decoded record is trusted only after a magic or structural check - never because decompression succeeded. This shaped how every LZS edit in the patcher is validated, and it is why the extraction tools report "no plausible sections" on a non-container entry instead of emitting garbage.
Details: adding a tier-B mod
- Confirm the field's offset in the decompressed record (structural check on the decode).
- Edit, recompress with
legaia_lzs::compress, pad back to the slot stride, reject on overflow. - Add a disc-gated oracle that re-decodes the patched slot.