What this solves

Almost everything on the disc lives inside PROT.DAT, one archive of 1233 numbered entries, and most entries are containers within containers: compressed streams, texture packs, mesh bundles, sound banks. Nothing on the disc is a file you can open directly. The extraction tools walk that whole chain so you never have to. Use them to browse the art, models and audio; to feed the engine, the asset viewer and the site's 3D pages (all read the extracted/ tree); or to drive one stage while reverse-engineering.

At a glance

Input
A raw Mode2/2352 .bin dump, or a .cue resolved to its BINARY track.
Output
./extracted/ (gitignored): PROT entries, streaming sub-assets, PNG / OBJ / WAV exports, TIM catalogs, dialog font.
Driver
legaia-extract (crates/extract) runs every stage; each stage is also its own binary.
Stages
verify → disc → PROT → categorize → streaming extract → PNG → CD-XA demux → TIM catalogs → dialog font.
Consumers
legaia-engine, asset-viewer, the web viewer, Ghidra (mounts extracted/ read-only).
Tests
Disc-gated on LEGAIA_DISC_BIN; skip and pass without it.

Quick start: the one-command pipeline

./legaia-extract "/path/to/Legend of Legaia (USA).bin" --out extracted
disc imageISO9660 walk PROT.DAT1233 entries classifyby magic bytes LZS decodemagic-checked sub-assetschunk by chunk exportsPNG OBJ WAV disc-extract prot-extract asset categorize lzs-decode asset extract tim tmd vab xa CD-XA audio is demuxed straight from the raw disc, bypassing the walk
Each stage is a standalone binary under target/release/; legaia-extract chains them.
FlagSkips
--skip-verifySHA verification of the disc's key files.
--skip-pngStreaming-container TIM → PNG (a handful of files; the bulk inventory is the catalogs).
--skip-xaThe CD-XA audio demux (voice lines, streamed audio).
--skip-catalogprot_tim_catalog.tsv + prot_tim_deep_catalog.tsv.
--skip-fontThe font/ dialog-font artifacts.

What lands in extracted/

PathContents
PROT.DAT CDNAME.TXT SCUS_942.54raw archive, entry name map, executable
PROT/####_<name>.BINone file per entry - its whole sector footprint - plus categorize.json
streaming/####_<name>/chunk##_<TYPE>/TMD chunks split into numbered meshes, TIM lists (+ PNG), opaque blobs (MAN, MES, MOVE, VDF)
prot_tim_catalog.tsv prot_tim_deep_catalog.tsvflat TIM inventory; TIMs inside LZS-compressed sections
font/dialog-font atlas, width table, metadata
XA/raw Form-1 dumps - truncated, not listenable
XA_WAV/XAn_fileN_chM.wavcorrectly paced per-channel voice / streamed audio

Getting all the textures

The PNG step covers only the few TIMs the streaming containers carry. For the full set, export the catalogued hits and convert them - the two commands the pipeline's summary prints:

asset tim-scan extracted/PROT --out extracted/tim_scan
tim convert-dir extracted/tim_scan
Details: why voice audio is demuxed from the raw disc

The ISO walk writes *.XA as Form-1 files, which truncates every Form-2 audio sector (2324 → 2048 bytes) and collapses a file's multiplexed channels into one shuffled stream. The CD-XA step instead reads raw 2352-byte sectors and writes one WAV per (file_no, ch_no), at the rate and stereo mode each sector's subheader declares. The NA corpus is 34 files / 316 channels, all 4-bit 37.8 kHz; the decoder is bit-exact.

Per-stage tools

BinaryStageKey commandsFormat
disc-extractdisc → filesextract DISC.bin extracted/disc
prot-extractPROT.DAT → entriesextract, list, locate, retail-namesPROT, CDNAME
lzs-decodedecompressraw --size N, containerLZS
assetclassify + sub-assetscategorize, extract, stream, describe, tim-scan, tmd-scan, tim-catalog, tim-deep-catalogasset type
timtexture → PNGinfo, convert [--clut N | --all-cluts], convert-dirTIM
tmdmesh → OBJinfo, dump-obj, validate-primsTMD
vabsound bank → WAVlist, extract [--wav]VAB
xastreamed audio → WAVdemux-disc-all, demux-disc --lba --size, info, convertXA
mesdialog containersinfo, disasm, jsonMES
mdtmove tablesclassify, records, slotsMDT
font-extractdialog font--disc <bin-or-PROT.DAT>dialog font
  • Entry size is the sector gap. Each PROT/*.BIN holds exactly its entry's sectors, so the files tile the archive with no gaps or overlaps. prot-extract locate maps any offset to the entry that owns it.
  • LZS "decodes without error" proves nothing. The ring buffer starts zeroed, so random input decodes to plausible bytes. Magic-check the output.
  • Prefer xa demux-disc-all over convert: it takes rate and channel mode from each sector instead of guessing.
  • Legaia TMDs are a custom variant (magic 0x80000002); a stock PSX TMD tool will not read them.
Details: prot-extract subcommands and the two TIM catalogs
prot-extract extract extracted/PROT.DAT extracted/PROT/ --cdname extracted/CDNAME.TXT
prot-extract list   extracted/PROT.DAT --cdname extracted/CDNAME.TXT
prot-extract locate extracted/PROT.DAT 0x17855 --in-entry 866 --cdname extracted/CDNAME.TXT
prot-extract retail-names extracted/CDNAME.TXT
  • extract also writes manifest.json and an unpacked tim/<entry>/ dir per TIM-pack entry.
  • list prints each size next to decl_span, the superseded over-reading expression, flagging ovr where it overshoots; locate re-keys coordinates measured under it.
  • retail-names shows what the retail loader reads out of CDNAME.TXT. Its #define numbers are raw TOC indices, shifted +2 from the extraction filenames.
  • --clamp-footprint is a deprecated no-op.

asset tim-catalog scans the whole PROT.DAT image, strict-validates each TIM and maps it to its owning entry + offset (or the unindexed system-UI gap); it recovers the same set an independent reference decoder reports, and --rollup prints the count + digest a disc-gated test pins. asset tim-deep-catalog decompresses every entry and catalogs the TIMs inside each section, keyed (entry, section, offset); a hit is admitted only when it strict-parses and decodes to RGBA.

Details: full asset and xa command lists
asset categorize     <DIR> [--out categorize.json]    # per-class breakdown
asset extract        <file> --out <out_dir>           # streaming chunks -> files
asset stream         <file>                           # walk DATA_FIELD chunks, no extraction
asset describe       <file>                           # container TOC
asset effect-bundle  <file>
asset tmd-scan       <DIR>                            # byte-search for TMD magic
asset tim-scan       <DIR>                            # byte-search for TIM magic (lenient)
asset tim-catalog    <PROT.DAT> [--out f.tsv|f.json]  # strict flat catalog
asset tim-deep-catalog <PROT.DAT> [--out f.tsv|f.json] # TIMs inside LZS sections

xa demux-disc-all <disc.bin> --out extracted/XA_WAV   # every .XA -> per-channel WAV
xa demux-disc <disc.bin> --lba L --size S --out <dir>  # one .XA by LBA/size
xa info    <file.xa> [--channels stereo] [--sample-rate 37800]
xa convert <file.xa> [-o out.wav]                       # single Form-1 dump (guesses rate)

Browsing the result

Once extracted, browse interactively with the native asset-viewer (the browser equivalent is the web viewer).

CommandShows
asset-viewer prot extracted/PROT.DAT --cdname extracted/CDNAME.TXTPROT entry browser, format auto-detected per entry
asset-viewer tim <file.tim>one texture; --offset 0x… --clut N reaches TIMs in the unindexed gap of PROT.DAT
asset-viewer tmd <file.tmd | dir>a mesh, or a directory to cycle (N / P / PgDn / PgUp); --bundle battle pairs the TIMs for correct CLUTs
asset-viewer vab <entry.BIN> --offset 0xN --sample None sound-bank sample

How we know

The pipeline is pinned by disc-gated tests that run only when LEGAIA_DISC_BIN points at a valid .bin; without it every one skips and passes, so CI needs no Sony data. Do not change that gating.

LEGAIA_DISC_BIN="/path/to/Legend of Legaia (USA).bin" cargo test --workspace
TestWhat it proves
crates/iso/tests/disc_pipeline.rsDisc walk, file count, key-file SHA-256s.
crates/iso/tests/ecc_real.rsSector write-back keeps EDC/ECC valid.
crates/prot/tests/archive_tiling_real.rsEntry footprints tile PROT.DAT exactly - the sector-gap size matches retail's own span routine (FUN_8003E68C).
crates/extract/tests/validation_suite.rsFull pipeline: entry count, sub-asset totals, TIM round-trip, catalog digests.

Find every disc-gated test with grep -rl LEGAIA_DISC_BIN crates/*/tests.

See also