Playing and viewing
The project's engine is a from-scratch rewrite of the game that reads your own disc: point legaia-engine at the image and it boots Rim Elm, plays the music, runs the NPC scripts, rolls encounters and fights battles. asset-viewer is its museum-mode sibling for browsing one texture, mesh or sound bank at a time. Both ship in the release archive; commands use the bare ./tool form (source builds live at target/release/).
At a glance
--scene, --battle, --party.Where the data comes from
Every subcommand accepts the disc directly via --disc - no extraction step required to play. Without --disc, tools read an extracted/ tree (--extracted-root, default extracted, resolved against the current directory) produced by legaia-extract (getting started).
1 · Boot the engine
./legaia-engine play-window --disc "/path/to/Legend of Legaia (USA).bin"This boots the scene town01 (Rim Elm) straight off the disc: field rendering, BGM, NPC scripts, dialog, and the full gameplay loop - walking rolls the scene's own random encounters, a battle opens the command menu, and victory returns you to the field with XP, gold and drops.
Keys
Arrows are the D-pad; the rest of the desktop defaults:
| Key | Pad button |
|---|---|
| Z | Cross |
| S | Circle |
| A | Triangle |
| X | Square |
| Q / W | L1 / R1 |
| 1 / 2 | L2 / R2 |
| Enter, Space | Start |
| RShift | Select |
Esc quits. The browser play page ships a different default layout (X is Circle there, C / V carry Triangle / Square, and WASD doubles the arrows), so a key that works in the tab is not necessarily the same button in the window. config show prints the desktop table, including any rebinding.
Window toggles
- Left-mouse drag orbits the camera; T cycles the camera-distance preset.
- R toggles precise free-angle movement (an opt-in enhancement - retail-style movement is the default).
- I toggles dynamic lighting (also opt-in;
--dynamic-lightingstarts with it on). - F1 shows the engine's diagnostic text rows (off by default); F2 mutes audio; F3 swaps the field camera for the wide debug orbit.
- F4 toggles the camera-occlusion fade (on by default: walls between the camera and your character dissolve to a dither so you can always see yourself;
--no-occlusion-fadestarts with it off).
Window toggles and pad bindings cannot collide: every toggle lives on an F-key or a letter outside the bindable vocabulary (the table above plus C, V, D, E), so no rebinding can silently delete a button.
Flags
--boot-uistarts at the title screen → save-select flow instead of jumping into the scene.--no-live-loopstops random encounters from rolling, leaving field VM + locomotion only - the mode to use when you are inspecting a scene rather than playing it. A battle the engine is already in still resolves.--no-player-battleauto-attacks each party turn instead of opening the command menu.
Towns have no random encounters, by design. Rim Elm is one of them, and so are most scenes with a shop in them: their encounter regions carry a zero trigger rate, exactly as on the retail disc. The window says so in the corner (no random encounters in this scene) so a quiet walk does not look like a broken engine - step onto the overworld, or --scene map03, to fight.
2 · Pick a scene, or a fight
./legaia-engine list-scenes --disc "/path/to/disc.bin"./legaia-engine play-window --disc "/path/to/disc.bin" --scene town04list-scenes prints every scene name the game's file map exposes with the PROT entry range each covers - the same names feed --scene, and a range start is where that scene's files land in extracted/PROT/.
Getting into a battle on purpose
Waiting for a step roll is fine for playing and useless for checking something on the battle screen: the retail counter takes several hundred steps to drain. --battle names the fight instead:
./legaia-engine play-window --disc "$DISC" --scene map03 --battle first./legaia-engine play-window --disc "$DISC" --scene rikuroa --battle 17first is the scene's lowest formation row that carries monsters; a number is a specific row of the scene's own MAN encounter table (rikuroa row 17 = the lone Caruban boss) - the same id space the region roll produces, so --battle 12 fights what walking into that region would have. Unregistered rows are refused with the scene's row list logged.
The fight is armed, not injected: the row goes into the encounter session's transition state machine exactly as a roll does, so the intro transition, the BGM swap and the battle load are the ordinary ones. It also means the fight opens a beat after boot rather than on frame 0 - allow ~40 ticks when pairing it with a capture. --battle turns the live loop on even under --no-live-loop, because the loop is what drains the transition.
./legaia-engine play-window --disc "$DISC" --scene rikuroa --battle 17 --seed-party --screenshot /tmp/battle.png --screenshot-tick 200Choosing the party
--seed-party alone seeds retail's New Game roster, which is Vahn alone - correct for the early game, and not enough for a three-member fight. Name the members you want and each one is seeded from its own row of the executable's starting-party template, so it arrives with real level-1 stats and its own name:
./legaia-engine play-window --disc "$DISC" --scene rikuroa --battle 17 --seed-party --party vahn,noa,galaThe list is in battle order (vahn/noa/gala/terra, or roster indices) and caps at the three on-screen positions. A member whose record already carries stats - a loaded save - is never overwritten.
3 · Play the FMVs
./legaia-engine play-str MOV/MV1.STR --disc "/path/to/disc.bin"With --disc, the argument is the movie's path inside the disc image and the interleaved XA audio track plays in sync (the video clock is driven off the audio cursor). Without --disc it plays a raw extracted .STR file (video only). To dump frames as PNGs instead, use mdec (extracting assets).
4 · Start a minigame from a live scene
Every ported minigame is a mode suspend on the running world, not a separate program: the field scene stays loaded underneath and comes back when you leave, so you start one from wherever you happen to be standing.
In play-window each is one key, and the same key leaves again:
| Key | Minigame |
|---|---|
| L | Fishing |
| K | Noa's dance |
| O | Casino slot machine |
| B | Baka Fighter |
| M | Muscle Dome |
Each loads that minigame's overlay off the disc and installs a session, so the rules, tables and scoring all come from the disc rather than from hardcoded numbers.
The browser play page carries the fishing entry as a Fish here button under the canvas. Cross casts and reels, Square reels harder, Cross recasts once a catch resolves - the same pad the field controller reads. Points bank into the world's persistent pool, so they survive leaving and re-entering, and the prize-exchange rows come off the same overlay with retail availability gating.
One HUD caveat that looks like a bug and is not: the fishing sprite page is the one asset in that chain nobody has decoded, so both hosts draw the HUD's digit and caption rows from the dialog font and skip its glyphs. The captions are engine-side English placeholders at the retail pen positions, so a long placeholder can overlap the count beside it.
5 · What you hear
Both hosts decode their audio from the disc you supply - no samples ship with the engine or the site. Music is a SEQ played through the from-scratch SPU against the scene's own sound bank; sound effects come from the executable's descriptor table plus the resident program bank, and both hosts key them into the same SPU the music uses, so a cue shares the voice pool exactly as it does on hardware.
Before judging a sound, know its provenance: a cue is either disc (the id is retail's, fired where retail fires it) or site (retail's id there is unpinned, so the port reuses the closest one), and the browser page reports the split per event. The footstep is the clearest example - its timing is the retail cadence, while the cue id it fires is the port's pick.
6 · Saves and config live next to you
The engine resolves its files against the current directory: key bindings in legaia-input.toml, options (camera preset, movement mode) in legaia-options.toml, and save slots under saves/. Run from the same directory each time - or pass explicit paths where supported (--save-dir, config set --config-file).
Rebind keys with config:
./legaia-engine config show./legaia-engine config set --binding Space=Cross./legaia-engine config set --binding Enter=StartKEY=BUTTON uses friendly key names (Z, Up, Enter, RShift) and PSX pad button names (Cross, Circle, Start, L1).
7 · Record and replay a session
./legaia-engine record --disc "/path/to/disc.bin" --out session.toml./legaia-engine replay --input session.tomlrecord is play-window plus input capture into a small j-replay-v1 TOML file. The file is checkpointed to disk about once a second and finalized on window close (Esc), so an interrupted session still yields a valid file up to the last checkpoint. replay runs it back headless and deterministic - the same file always produces a bit-identical engine trace, and it needs no disc at all.
legaia-engine --help lists many more subcommands; the COMMAND GROUPS footer separates the player-facing ones above from the development diagnostics (parity oracles, synthetic state drivers) you can ignore.
8 · Browse assets interactively
asset-viewer reads the extracted/ tree (there is no --disc here - run legaia-extract first). The field and dialog demos additionally need the dialog font at extracted/font/, which the pipeline writes by default (or font-extract --disc rebuilds).
./asset-viewer prot extracted/PROT.DAT --cdname extracted/CDNAME.TXT./asset-viewer tim extracted/tim_scan/<entry>/<tim-file>./asset-viewer tmd extracted/tmd_scan./asset-viewer vab extracted/PROT/<entry>.BIN --sample 0./asset-viewer field town01Those are, in order: the archive browser, one texture, a walk through every mesh in a directory, playing one sample from a sound bank, and the playable field demo. The scene name is a positional argument here, not a --scene flag (field, world and dialog all take their subject that way).
In the PROT browser: N / P = next/prev entry, PgDn / PgUp = jump 10, Esc = quit; each entry's format is auto-detected and the first viewable sub-asset is shown. tmd pointed at a directory walks every mesh with the same keys. The tim subcommand also takes extracted/PROT.DAT itself with --offset/--clut for the system-UI textures that live outside any TOC entry.
9 · Read the game's scripts
Every scene - who says what, which door leads where, what a chest holds - is driven by the field/event VM's bytecode. Its disassembler is a release binary too:
./field-disasm scan-prot --prot extracted/PROT.DAT./field-disasm file <extracted-script-body>scan-prot sweeps the archive for event scripts and FMV triggers; file walks one raw script linearly. For a specific scene's per-scene scripts (LZS-compressed inside the scene's MAN sub-asset), the engine has the direct path:
./legaia-engine man-scripts --scene town01 --disc "/path/to/disc.bin"10 · Run the browser version locally
The same engine runs in a browser as the static site's play page, sharing the simulation kernels with the native window. That build is not in the clone - site/wasm/ is generated output. Build it once:
scripts/ci/build-wasm.shpython3 -m http.server -d siteThe first takes around nine minutes cold and needs wasm-pack; then open /play.html. Nothing is uploaded: the disc image you pick stays in the tab.
Rebuild after changing anything the page compiles - which is most of the workspace, not just crates/web-viewer. To check whether the bundle you built still matches your sources:
python3 scripts/ci/check-wasm-freshness.pyWorth running before concluding a change did or didn't work in the browser: a stale bundle looks exactly like a fix that had no effect. Background: tooling/shipped-bundle-freshness.md.