The shape of the engine

Legaia is a fairly typical late-90s PSX RPG engine, but with a twist: most of the gameplay logic doesn't live in the main executable. SCUS_942.54 contains the boot path, low-level I/O, the asset dispatcher, the move-table VM, the motion VM, and the audio / renderer libraries — about half the runtime. The other half lives in RAM overlays that the disc loads on demand: title screen, town, battle, menu. Two of the runtime VMs (the field/event VM and the effect VM) live in those overlays.

The list below groups subsystems by what layer of the engine they sit at.

Bootstrap and asset plumbing

The five runtime VMs

Legaia's runtime is driven by five independent VMs that all talk to one shared actor model. Four are clean bytecode dispatchers (actor / move-table / motion / field-event); the fifth (effect) is a per-slot state machine — different shape, same architectural role. They serve different layers of the engine and were almost certainly written by different members of the dev team.

Per-domain runtime

World map
Overworld traversal mode. Controller FUN_801E76D4 handles the debug top-view toggle, top-view camera scroll/azimuth/zoom, and normal-walk per-frame update. Entity tick SM (FUN_801DA51C, 5 states) drives encounter and location-entry sequences.
Save screen
Save-slot selection and write flow. Lives in the menu overlay (not a separate overlay). Outer dispatcher FUN_801DC6B4: 9-state machine, entry-context pointer table at 0x801E4F40, slot-select via actor VM, save-block existence scan at DAT_80084140.
Shop
Buy / sell / quantity / confirm flow. Lives in the menu overlay. ShopSession tracks pending item, quantity, and buy/sell direction. Sell price = half buy price (min 1). Per-scene item tables pending overlay trace.
Inn
HP / MP restore flow. InnSession { cost } gates the stay on affordability, then restores all active party members to full HP/MP. Per-scene costs pending overlay trace.
Level-up
Post-battle XP distribution and stat gain. XP table from SCUS_942.54 0x8007123C (98 u16 increments, L1→2 = 50). LevelUpBanner renders for 180 frames via level_up_draws_for(). Per-character HP/MP growth curves pending level-up overlay capture.
Cutscene (STR mode)
PSX STR video decode + XA audio playback. Game modes 26/27 (StrInit / StrMode). MDEC decoder: VLC → IDCT → BT.601 YCbCr→RGBA. Clean-room port in crates/mdec.
Battle
Battle scene loader, the per-action state machine, character record layout, the per-frame stat aggregator, range / line-of-sight, the weapon-trail builder, status-effect ticker, AP / Spirit gauge, equipment-aware stat aggregator, and item-effect catalog.
Battle action FSM
The 47-state machine inside FUN_801E295C — the layer between “player picked Attack” and “HP has been deducted.” Two-level dispatch: action category (party byte) plus execution phase (ctx byte).
Battle formulas
Damage / MP-cost / accuracy / RNG arithmetic. Selector dispatch in FUN_800402F4; spirit damage is hard-coded; MP cost is ability-bit modified; engine-vm port lives in battle_formulas.rs.
Audio
Sound-driver path strings, three SCUS consumers, the SsAPI sequencer cluster (statically linked PsyQ libsnd), and the libspu DMA engine that moves bytes into SPU RAM.
Renderer
The Legaia TMD renderer (60 GTE ops with a per-mode descriptor table). The engine port emulates PSX VRAM in a compute-friendly format so multi-page meshes render correctly.

The clean-room port

Boot UI sessions
engine-core::title::TitleSession drives the FadeIn → PressStart → MainMenu → Done loop with a no-save fallback path; engine-core::save_select::SaveSelectSession covers the slot-list UI with Browsing / ConfirmLoad / ConfirmOverwrite / ConfirmDelete phases. Both are renderer-agnostic and emit typed event streams; legaia-engine title + save-select drive them headlessly with scripted input.
Encounter system
engine-core::encounter: EncounterTable holds the per-scene rows + trigger rate + safe zones; EncounterTracker rolls per-step against the table with rate-bias hooks for accessory effects; EncounterSession brackets the transition with Idle → Transition → Triggered → Battling → Grace phases so engines render the camera-shake / fade chain.
Battle target picker
engine-core::target_picker::TargetPickerSession drives the post-action target cursor. Parameterised on a TargetKind (SingleEnemy / SingleAlly / SingleAllyOrSelf / DeadAlly / AnyAlly / AllEnemies / AllAllies / Self_) with skip-self / dead-only logic baked in; sweep targets resolve immediately, single-target picks walk valid candidates with cursor-wrap.
Equipment catalog
engine-core::equipment::EquipmentCatalog ships ~30 vanilla entries covering weapons (character-locked: Vahn-only swords, Noa-only knuckles, Gala-only quarterstaves), helmets, body armor, hand guards, boots, rings, and accessories. Per-entry ItemModifier with stat deltas + ability-bit packing; to_modifier_table hands the resolved table to compute_battle_stats.
Seru capture & spell learning
engine-core::seru_learning: SeruRegistry maps Seru ids to spell ids + capture-point grants; SeruCaptureLog tracks per-character per-Seru running totals with a learnable_mask filter; record_capture is the pure resolver that fires LearnEvents when a character crosses the threshold; SeruCaptureSession drives the post-capture banner sequence ("Captured: Spark!" → "Vahn learned Spark!").
Tactical Arts chain editor
engine-core::tactical_arts_editor: ChainLibrary stores up to 8 saved chains per character (3..=7-byte sequences, retail-faithful caps). ChainEditor drives the menu-side editor — Browsing → Editing → Naming → Done — emitting typed events for each direction push, pop, and commit. Engines feed picks back to BattleRunner::push_chained_art at battle start.
Save format LGSF v2
crates/save/src/ext.rs: backward-compatible v1 prelude plus an extension block (magic LGX2) carrying play-time, the active-party slot list, per-character CharSaveExt rows (learned-arts mask, learned-spell list, per-Seru capture totals, four active-chain quick slots), and the cross-character saved-chain library.
Dialog renderer pipeline
engine-render::dialog_box_draws_for takes the dialog panel's typed glyph stream + a DialogBoxLayout (origin / size / padding / line height / column count) and emits TextDraws with per-glyph CLUT tinting via dialog_clut_color, greedy width-based wrap, and newline support; dialog_panel_draws_for is the engine-core compatibility shim.
Field (pause) menu
engine-core::field_menu::FieldMenuSession: seven-row pause panel (Items / Equip / Spells / Arts / Status / Save / Config) with a FieldMenuRowMask for per-row enable/disable; Cross enters a sub-session, the shell pushes the matching tracker, and resume(close) hands control back. Wired into play-window so Start in field opens the menu.
Field-menu sub-session dispatcher
engine-core::field_menu_dispatch::FieldMenuSubsession wires every FieldMenuRow selection to its real session: InventoryUseSession, EquipSession, SpellMenuSession, ChainEditor, StatusScreenSession, SaveSelectSession, OptionsSession. build(row, world, options, slots, library, catalog, table) constructs the matching session from world state; tick_pad_edge(pressed) routes edge-triggered pad input. Apply helpers (apply_equip_outcome, apply_inventory_outcome, apply_spell_outcome, apply_arts_outcome) drain finished sub-sessions back into World. play-window's BootUiState::FieldMenu uses this — Cross on a row pushes the right session, control returns to FieldMenuSession::resume when the sub finishes.
Status screen
engine-core::status_screen::StatusScreenSession: per-character snapshot (level / XP / HP / MP / AP / six base stats / equipment list / element ranks). L1/R1 cycle through party members; the StatusSnapshot view is plain data so engines populate from the live record without coupling to the renderer.
Field spell menu
engine-core::spell_menu::SpellMenuSession: out-of-battle casting flow (CharSelect → SpellSelect → TargetSelect). is_field_usable filters out battle-only effects (Damage / Capture / Escape / Buff); dead-target / Revive constraints are validated; resolution flows through the existing spells::cast_spell resolver and surfaces a typed SpellMenuOutcome.
Options & key rebind
engine-core::options::OptionsSession: typed OptionsState (BGM / SFX volume 0..=10, message speed 1..=8, vibration on/off, audio stereo/mono) with in-place editing. engine-core::key_rebind::KeyRebindSession drives the per-button rebind table; pressing any key in AwaitingKey swaps the binding into input::Mapping while clearing any conflicting key so two keys never report the same pad button.
Game-over panel
engine-core::game_over::GameOverSession: three-row Continue/Retry/Quit panel with a fade-in counter; Continue dimmed when no save data is present. Engines invoke this on a BattleSession::Defeat outcome and route Continue back through SaveSelectSession.
Built-in fallback font
legaia_font::Font::placeholder ships a hand-rolled 5×7 ASCII bitmap font in legaia_font::builtin so HUD text renders legibly even before font-extract populates the real dialog atlas. Printable-ASCII range plus a hollow-box unknown glyph; non-ASCII bytes still distinguish from missing glyphs.