The technique in one box
The project's own LZS encoder produces streams the retail decoder accepts. Its output is slightly longer than Sony's, and that is fine: every monster occupies a fixed 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)) == x against the retail decoder
Slot
[u32 decompressed_size][LZS stream][zero pad], fixed 0x14000 bytes per monster
Edit class
In place at the slot level; repack_slot rejects 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.

Re-packing a monster slot, zero-padded back to its fixed stride original size Sony LZS stream zero pad re-packed size project LZS stream (slightly longer) zero pad slot base +0x14000 fixed slot stride, identical in both rows
The longer stream eats into the pad; the slot boundary does not move. Where an asset has no slack - a scene script - the lazy matching has to fit the original footprint exactly; see tier C.

What moves

FieldInsideOffsetMod
Drop item / drop chancedecoded monster record, PROT 0867+0x48 / +0x49 (u8)--drops
HP, MP, ATK, DEF×2, AGL, SPDdecoded monster record, PROT 0867halfwords from +0x0C--monster-stats
Weapon-class arm costequipment section of the player battle files, PROT 0863..0865swing 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

ClaimEvidenceReference
LZS format + ring-buffer semanticsRetail decoder FUN_8001A55C, reversed for the preservation tracklzs
Slot stride 0x14000, record layoutMonster archive parser + the monster-archive CLI listing every monster's statsbattle-data-pack
Arm cost is disc data at +0x74Battle-load copy into DAT_801C9360[char][cmd] by FUN_800557B8arts-command-gauge
Round trip is exactDisc oracles decode every patched slot and compare against the pre-patch multisetcrates/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
  1. Confirm the field's offset in the decompressed record (structural check on the decode).
  2. Edit, recompress with legaia_lzs::compress, pad back to the slot stride, reject on overflow.
  3. Add a disc-gated oracle that re-decodes the patched slot.

See also