Legaia LZS compression Confirmed
Reverse-engineered byte-for-byte from FUN_8001A55C. Implementation: crates/lzs/src/lib.rs.
Overview
A standard LZSS variant with three Legaia-specific choices:
- 4096-byte sliding ring buffer, initialised to zero.
- Initial write position 0xFEE (3054).
- Control byte: 8 bits, LSB-first.
Pseudocode
let mut window = [0u8; 4096];
let mut window_pos = 0xFEE;
let mut control = 0u32;
while !done {
if (control & 0x100) == 0 {
control = (input[src] as u32) | 0xFF00;
src += 1;
}
if (control & 1) != 0 {
// LITERAL: copy 1 byte
let v = input[src]; src += 1;
out.push(v);
window[window_pos] = v;
window_pos = (window_pos + 1) & 0xFFF;
} else {
// BACK-REF: 2 bytes encode (12-bit absolute window position, 4-bit length-3)
let b0 = input[src] as u32;
let b1 = input[src + 1] as u32;
src += 2;
let base = b0 | ((b1 & 0xF0) << 4);
let len = (b1 & 0x0F) + 3;
for n in 0..len {
let v = window[(base + n as u32) as usize & 0xFFF];
out.push(v);
window[window_pos] = v;
window_pos = (window_pos + 1) & 0xFFF;
}
}
control >>= 1;
}
The 0xFF00 mask above is the trick that lets the control register tell the decoder when to refill: every shift right pulls a 1 bit into bit 8, and after 8 shifts bit 8 reaches the test position and triggers a refill from input[src].
Container format
crates/lzs::parse_container handles the multi-section player.lzs-style wrapper used by some PROT entries - a length-prefixed array of independently-compressed sections concatenated together.
Where LZS is consumed
The asset-type dispatcher (FUN_8001F05C) calls the LZS path when its copy_only flag is zero. See asset type dispatcher. Standalone-shaped LZS containers (with the descriptor-pair walker in asset-descriptor.md) are also recognised by crates/lzs.