At a glance

Entry point
main() = FUN_80015E90; it never returns in normal play
Archive directory
The first three sectors of PROT.DAT (6 KB) land at 0x801C70F0; every later load is a table lookup
Mode machine
14 modes, each an INIT / per-frame pair, in a table at 0x8007078C; the current mode is the word _DAT_8007B83C
Overlay window
Slot A at 0x801CE818 holds one of field / battle / menu / cutscene / minigame code at a time
Title screen
Has no mode of its own: it runs under CARD (modes 22/23), the same pair as the pause menu, from code at the tail of PROT 0899
NEW GAME
Writes mode 2 (field) and opens the prologue scene opdeene; name entry comes later, in town01
In the port
engine-session::BootSession runs the per-frame order; engine-mode::mode::ModeSeat holds the mode word; engine-title::title_overlay is the title tick
Confidence
Confirmed - mode table read off the disc; title tick and launch write pinned by live watchpoints

The boot chain

A cold boot is six writes of a literal into the mode word. Each one hands the frame to the next screen, and none of the screens is named in the mode table - which is why the order has to be read off the stores rather than the table's dev labels.

  1. Hardware initGPU, CD, sound, heap; the PROT.DAT directory and the CDNAME name map into RAM
  2. Mode 16 · READ INITone frame: upload the four init.pak logo textures
  3. Mode 17 · READthe publisher logos fade in, hold and fade out
  4. Mode 22 · CARD INITstream PROT 0899 into slot A
  5. Mode 23 · CARDthe title screen, ticking as a spawned actor
  6. Mode 2 · MAIN INITNEW GAME: stream the field overlay 0897, load opdeene
  7. Mode 3 · MAINthe field loop - the game proper

Almost every PlayStation game bundles its files into one archive so the disc can stream them; Legaia’s is PROT.DAT, with a table of contents saying “entry N starts at sector S”. Reading that table is job one. Job two is the asset-type dispatcher: each asset’s size word carries a type byte, and one router hands the bytes to the matching handler (asset-type). Job three is the mode machine below.

The main loop

After hardware init, main() spins forever: run the current mode’s handler; if the handler wrote a different mode, tidy up and switch. A mode’s INIT handler runs for one frame and usually streams an overlay into slot A; its per-frame handler runs the game from then on, through the master frame driver that ticks actors, VMs and draws. Writing the mode word is the only way to change what the game is doing.

main loopreads the mode word mode table14 INIT / run pairs INIT handlerone frame per-frame handlerevery frame overlay slot Astreamed off the disc master frame driveractors, VMs, draw a handler writes a new mode word pause menu skips this
One loop, one table. A mode switch also stops CD audio, flushes the GPU queue and clears held input before the new mode’s INIT runs.

Most gameplay code is not in the executable. The field VM, dialog renderer and battle logic live in overlays streamed into slot A. “Zero callers in SCUS_942.54” never means dead code - it means the caller is in an overlay. Init stages in order: internals.

The game modes

Walking a town, fighting, browsing the pause menu, watching a movie - to the game these are modes, and exactly one is active. Half the table is dev tooling the retail disc never enters; these are the ones a player sees:

ModeDisc nameWhat it really isOverlay
2/3MAINfield and town gameplay, including the three kingdom overworlds0897
12/13MAPDSIPworld-map display - a head swap over the field overlay0981 over 0897
16/17READpublisher logos at boot0895 (init.pak)
18/19GAME OVERgame over0902
20/21BATTLEbattle; setup is in the executable, the overlay comes in via the field0898
22/23CARDthe title screen and the menu: pause menu, save, options, shops0899
24/25OTHERthe five warp minigames, one overlay each0972..0977, 0980
26/27STRFMV movies0970
  • The pause menu runs under CARD, not the field mode, and mode 23 runs no frame driver - so nothing in the world moves while the menu is up.
  • The mode word does not say which minigame. All five share modes 24/25; the sub-id the door warp wrote (op0 − 100) picks the overlay (warp flow).
  • The dev names mislead. CONFIG is the debug menu, READ is the logo screen, CARD is the title and the whole menu. The full 14-row table, with every INIT handler address and the dev modes, is on internals.

Code that pages in: two slots and one index rule

The executable holds the engine - loaders, the renderer core, the sound libraries, the mode table. Most game logic is not in it. Field scripts, battle, the menu, movies and each minigame are separate code images in PROT.DAT, streamed into one of two fixed windows of RAM when a mode needs them. That is why a function with no caller in the executable is not dead: its caller is usually in an image that is not loaded yet.

executable always resident mode table and handlers two overlay loaders archive directory at 0x801C70F0 renderer core, sound libraries loader(param) reads entry param + 0x37F slot A at 0x801CE818 - one image at a time field 0897 · battle 0898 · menu and title 0899 · movies 0970 battle intro 0979 · minigames 0972..0977, 0980 · boot logos 0895 map display 0981 swaps over the field image's head and back slot B at 0x801F69D8 - a helper beside slot A field and world-map render libraries 0900 / 0901 battle cast and summon modules 0903..0966, one per spell
Entry numbers are extraction indices. Both slots are fixed addresses, so two images that would overlap can never be resident together - and the same address means different code in different modes.
  • One index rule. Both loaders add 0x381 to their parameter and index the in-RAM directory, which counts two words ahead of the extraction numbering - so the entry loaded is parameter + 0x37F. Parameter 2 is the field overlay 0897, 3 is battle 0898, 4 is the menu 0899.
  • The census is closed. A scan of the whole executable for both loaders finds sixteen call sites, with constant parameters for the fixed images and computed ones for the bands: a minigame's warp sub-id, a spell's cast module, a battle stage.
  • Parameter 0 has one producer: main() itself, loading the boot logo image before the mode loop starts. Parameter 1 has none, so entry 0896 is unreachable from any loader call.
  • Three init handlers load nothing. The battle mode's setup lives in the executable; the logo mode calls straight into the image main() already loaded; and one dev mode writes mode 0 and bounces back to the debug menu it came from.
  • A wipe is not the game-over image. A party wipe routes to the title's CONTINUE flow. The game-over overlay 0902 has an init handler and no retail path that stores its mode.
  • Swaps save what they cover. The minigame and map-display modes back up the scene name or the field image's first 0x4000 bytes before loading over them, and restore on the way out - which is how a minigame returns you to the room you left.

The practical consequence for tracing: an address in the overlay window names a routine only together with the image that is resident. Per-mode handler addresses and what each loads: internals; how an image is identified and extracted from the disc: static overlay pipeline.

The title screen

The title screen is a small program of its own: an attract countdown that eventually plays the opening movie, a Press-Start wait, a two-row NEW GAME / CONTINUE menu, the memory-card manager, and the launch. It runs as a spawned actor under the CARD pair, whose INIT loads PROT 0899 - which is also why the title shares code with the save screen. The tick (FUN_801DD35C) is a 25-way sub-mode switch.

  • Attract. A countdown starting at 0x8000 drains each frame; when it underflows the game switches to movie mode and plays MV1.STR (cutscene), then comes back to the title. Input re-arms the countdown only while more than 0x10 frames are left; in the last few frames the input block is skipped, so a held button cannot hold the movie off.
  • NEW GAME fades out, writes mode 2 and the scene id opdeene. CONTINUE detours through the card-load path first and then writes the same mode 2.
  • No arrow cursor. The two menu rows are a band of the title sheet; the selection is shown bright vs dim.
  • The menu opens on NEW GAME on the US build, saves or no saves. Startup raises the row to CONTINUE only when its card scan finds a save, and that scan compares every filename against a BISCUS-94254PRO- prefix no US save carries (they are BASCUS-), so the count stays zero. The row is never reset afterwards: a later title - after a party wipe, or a backed-out Continue - opens on whichever row it last held. The port does the same on both hosts.

State struct, countdown instruction and the packed pad-mask layout the tick reads: internals.

Where the title overlay lives on disc

The title code is the tail of PROT entry 0899 - the same entry whose head is the options-menu bundle - streamed raw and uncompressed straight into the overlay window: 74 sectors from LBA 47227, in five DMA bursts that all land inside the entry. The same tail carries two embedded TIMs, the save-menu UI atlas and the memory-card icon strip. Sector ranges and burst offsets: internals.

What NEW GAME launches

Press NEW GAME and you do not get a name-entry screen. You get a scripted opening - the creation-myth crawl, the Seru intro, the Mist story, a fly-in over Rim Elm - and only once you stand in Rim Elm does “Select your name” appear. Every leg is an ordinary field scene chained by its own script, not a movie.

opdeenecreation-myth crawl opstatiSeru intro opurudthe Mist story map01fly-in + title card town01name entry confirm press: skip straight to town01 (armed once opdeene's timeline sets flag bit 26)
Five field scenes, each one's script ending in a scene change to the next. The skip is a one-shot: it clears its own flag bit as it fires.
  • The fresh-state seed. Gold is set to a hard-coded 500, a story-flag region is zeroed, and the executable’s starting-party template is expanded into the live 0x414-byte character records, default names included.
  • The intro skip. Once opdeene’s timeline raises a scratchpad flag bit, a confirm press fades out, sets the town01 entry coordinates and issues a name-based scene change to town01. The target name is a literal in the field overlay, which is why opdeene’s own data holds no town01 string.
  • Name entry opens from town01’s opening timeline (field-VM op 0x49 sub-op 3): a 6×17 letter grid plus a Backspace / Default / End row, and a Yes/No commit. The cancel button (Circle) takes the last letter. A name holds at most nine letters and 57 pixels. The name lands in the character record at +0x2A7.
  • How the grid moves. A sideways step wraps inside its own row and walks past the | separators; a step down into the control row keeps its column, so the way back up lands on the letter it came from. Start jumps the hand to End. End accepts any name with a length, a name of blanks included, and the Yes/No prompt that follows opens on No, tests confirm before Up/Down and has no cancel. Yes stores the name as typed, trailing blanks and all.

Skip conditions, the scene-change packet and the name-entry grid’s addresses: internals.

Boot-time art

Three separate sources feed the boot screens.

  • Publisher logos - PROT 0895 init.pak. Four uncompressed TIMs (PROKION, Contrail, “SCEA Presents”, an epilepsy warning) played by the bundle’s own sequencer in the order SCEA, Contrail, PROKION. The fade is not alpha: the quads are opaque and the sequencer scales their vertex colour, so the PSX texture blend texel × colour / 128 darkens them to black. The warning is uploaded and never drawn - its sprite descriptor is the one no emit call passes. PROKION does not fade out: it holds at full while an additive grey quad over the screen rises a step a frame to white, and the white background carries into the loading frames. The port draws that wash on both hosts and is checked against the retail sequencer frame by frame - logo, level, wash grey and the tail's white.
  • Title wordmark - PROT 0890. One 256×256 sheet holds the orb, wordmark, Press Start prompt, copyright lines and the NEW GAME / CONTINUE footer; retail samples it in bands, and a demo-build label on the sheet is never drawn.
  • System-UI textures - raw TOC entry 0, init_data. The 118 sectors between the TOC and extraction entry 0 are two TOC entries the extraction index skips: init_data (the menu font, the boot cursor and several sprite sheets, which init.pak loads and uploads) and gameover_data (the GAME OVER screen's art).

Per-logo offsets, quad rectangles and pacing; the wordmark's source rectangles; the two head entries' TIMs and loaders: internals. The play page and the native window both run the logo stage ahead of the title card.

Debug flags

Two RAM values decide whether the game behaves like the shipped disc or a dev station. On real hardware both read “retail”; a cheat device can flip them, which is how the debug menu on the retail disc becomes reachable.

AddressRoleRetail value
_DAT_8007B8C2dev / retail loader selector, read as a halfword at 40 sites; != 0 takes the PROT-index path, == 0 opens a host path that ends in break 0x1031, written once by main()
_DAT_8007B98Cdebug-mode word tested by the input dispatcher and ~14 field-overlay gates; poking its top byte _DAT_8007B98F to 1 makes every gate read active, and SELECT+△ opens the debug menu0
History: why both flags were once read backwards

A sweep for absolute lui+offset references saw no writer for the loader flag, because the one write is gp-relative, and concluded it booted at 0; the executable’s header carries b_size = 0, so no BSS is cleared either. The debug byte returned zero byte-granular references because consumers read the word. Both are under do-not-re-walk; the gp-relative blind spot is why the address reference scan has a gp-relative sibling sweep.

Not every debug-menu entry works once it is reached. On a retail console three of them fail:

  • TMD TEST (mode 6) stores its two draw buffers at the fixed addresses 0x80400040 and 0x80419040 instead of taking them from the heap. A 2 MB console has no RAM there. The addresses wrap onto the BIOS area and the game's own code, so the console crashes. The branch is the beq at 0x8001F6D8; replacing it with a nop should send the mode down the heap path, but that has not been tested.
  • TEST (mode 10) has an empty start-up handler, so it hangs.
  • READ (mode 16) jumps into the boot overlay init.pak, which is no longer loaded once the debug menu has replaced it.

The mode table these entries come from is on internals. Found by Stann0x.

See also