Boot path
What the game does between disc-spin and title screen. Three jobs, in order: read the PROT.DAT directory into RAM, wire up the asset-type dispatcher, and hand control to the title-screen overlay.
How it works
Almost every PSX game has a built-in archive that bundles the small files together so the disc can stream them efficiently. Legaia's archive is PROT.DAT, and it has its own table of contents (TOC) - a list saying "entry N starts at sector S and is L bytes long". Before the game can do anything, it has to read that table into RAM. That's job one.
Job two is wiring up a small asset-type dispatcher. The PROT entries don't all hold the same kind of data - some are textures, some are 3D meshes, some are dialog blobs, some are animation containers. Each entry's first byte is a type tag, and there's a single function that reads that tag and routes the bytes to the right per-format parser. Once the dispatcher's helper tables are populated, every later "load this entry" call works.
Job three: the boot code reaches a top-level game-mode state machine. This is a 28-entry table where each slot is a function pointer for one major mode of the game - boot, title, field, battle, world map, menu, cutscene, etc. The mode pointer changes whenever the game transitions; the per-frame loop just calls "whatever the current mode's handler says". The title-screen overlay is loaded next, and the actor VM inside it starts ticking the splash animation.
One important quirk: the script that drives every running script - the field VM - is not in the main executable. It lives in a RAM overlay loaded from the disc when you enter a town or field map. That's why the boot path here is so short: most of the gameplay code isn't in SCUS_942.54 at all.
TOC loader
- Function
FUN_8003E4E8- Reads
- First three sectors of
PROT.DAT(= 6 KB) - Lands at
0x801C70F0in RAM- Called from
FUN_8003EFE8andFUN_8003F08Cat boot
The on-disc TOC and the in-RAM TOC have different strides - the on-disc-to-in-RAM transformation runs once at boot, but its function hasn't been reversed yet. See the PROT.DAT format spec for the byte layout.
After the TOC is in RAM, two resolvers are usable:
| Resolver | Mode | Used by |
|---|---|---|
FUN_8003E8A8 | Index-based (PROT entry number in) | Streaming loader, dev-build sound branch |
FUN_8003E6BC | Path-based (resolves data\battle\efect.dat, h:\PROT\FIELD\<scene>\…) | Most retail-build code paths |
The path-based opener runs the dev-style filename through the CDNAME.TXT name map to find the index, then delegates to the index-based resolver. It's a thin layer.
Asset-type dispatcher
- Function
FUN_8001F05C- Calling convention
result = FUN_8001F05C(byte *src_data, u32 type_and_size, int param3, int copy_only)- Encoding
type_and_sizepacks the type byte in the high 8 bits, size in the low 24 bits
Every TIM, TMD, MES, ANM, etc. is reached through this one function. The boot path doesn't call it itself - it just makes sure the buffer pointers it writes to are valid. FUN_80020224 (the asset descriptor walker) is one of the dispatcher's two static call sites and is invoked from the town overlay's FUN_801D6704 (MAIN_INIT) at runtime. See Asset loader for the full story.
Game-mode state machine
The 28-mode state machine table at 0x8007078C is the top-level "what is the game doing" dispatcher. Each 24-byte entry is a (name_ptr, …, handler_ptr, param) tuple corresponding to a major mode - boot, title, field, battle, world map, menu, cutscene, and so on. Modes come in init/per-frame pairs (even index = init, odd = per-frame).
Handler map (recovered from the disc)
The whole table is static executable data, so legaia_asset::mode_table reads the index → handler/param/name map straight out of SCUS_942.54 (asset mode-table; disc-gated test). The dev names mislead - this is the ground-truth dispatch:
| Mode | Name (disc) | Init handler | Per-frame handler |
|---|---|---|---|
| 0/1 | CONFIG (dev debug menu) | 0x80025C68 | 0x80025EEC |
| 2/3 | MAIN (field/town) | 0x80025B64 | 0x80025EEC |
| 4/5 | MONSTER TEST | 0x8002611C | 0x80025EEC |
| 6/7 | TMD TEST | 0x801CF730 | 0x80025EEC |
| 8/9 | EFECT TEST | 0x80025E68 | 0x80025EEC |
| 10/11 | TEST | 0x8002B97C | 0x80025EEC |
| 12/13 | MAPDSIP (world-map display) | 0x80025DA0 | 0x80025F2C |
| 14/15 | MAP TEST | 0x8002B904 | 0x80025EEC |
| 16/17 | READ | 0x8002612C | 0x80025EEC |
| 18/19 | GAME OVER | 0x80025B30 | 0x80025EEC |
| 20/21 | BATTLE | 0x800565D8 | 0x80025EEC |
| 22/23 | CARD (menu / memory card) | 0x8002574C | 0x80025F74 |
| 24/25 | OTHER | 0x80025980 | 0x80025EEC |
| 26/27 | STR (FMV) | 0x80025FB4 | 0x80025EEC |
Three structural facts: (1) field/town gameplay is modes 2/3 MAIN (game_mode 0x03), not the world map - the "MAPDSIP" pair (12/13, misspelled on the disc) is the world-map display mode, whose per-frame handler routes the world-map render tick. The mode-12 init FUN_80025DA0 is a transient sub-overlay swap (same save/restore shape as the mode-24 minigame warp): it saves the field overlay's slot-A head (0x801CE818, 0x4000 bytes), loads PROT 981 over it, and calls its init 0x801CF4AC (file +0xC94 - so base 0x801CE818 is pinned by the call target), restoring 0897's head on exit. PROT 981 seeds the scratchpad display list 0x1F800314 and runs a 21-state display SM that reads the still-resident 0897 body; the world-map controller proper (FUN_801E76D4) stays in 0897. (2) 12 of the 14 per-frame ("MODE") handlers share one generic per-frame tick 0x80025EEC; only Mode 13 (world map) and Mode 23 (menu / memory card) carry their own - so the per-frame half is mostly one shared handler parameterised by +0x14. (3) The in-field pause menu runs under the CARD pair (mode 23), not field mode 3: every menu-open capture in the save library (equipment / status / options, field and town) holds game_mode = 0x17 - the "CARD" dev label covers the whole menu / memory-card overlay surface (the field menu carries the Save flow).
Each table entry also carries an i16 next-mode field at +0x0A: -1 = the mode manages its own transitions, 0 = fall back to mode 0 on completion (the only two retail values). The word at +0x08 reads 0xFFFF0000 on self-managed modes - that is the -1 over a zero low half, not a sentinel constant. legaia_asset::mode_table::ModeEntry::next_mode decodes it, and the engine's transcribed table is cross-checked field-by-field against the executable by the disc-gated mode_table_reconcile test.
Mode-init → overlay loader census
The init handlers stream their overlays through the two parallel loaders FUN_8003EBE4/FUN_8003EC70, both calling FUN_8003E8A8(param + 0x381) - in extraction index space that is entry param + 0x37F, two below the raw + 0x381 (the resolver indexes the raw in-RAM PROT.DAT head; see PROT § In-RAM TOC). The verified mode → entry map: mode 0 CONFIG → 0971 (the dev debug-menu overlay - an earlier + 0x381-arithmetic reading placed it at 973 and took the slot-machine text in 973's over-read tail for its content; 973 itself is the 1-sector OTHER2 dev module, and the casino slot machine is 975); mode 2 MAIN → 0897 (field overlay; the earlier “loads 899” was the same off-by-2 - 0899 is the menu overlay, which mode 22 loads); mode 8 EFECT TEST → 0979; mode 12 MAPDSIP → 0981; mode 18 GAME OVER → 0902; mode 22 CARD → 0899; mode 24 OTHER → 0972..0977, 0980 (the minigame door-warp sub-id slots, see script-VM § 0x3E WARP); mode 26 STR → 0970 (cutscene). The census is exhaustive for static SCUS_942.54: a full-image scan of both loaders' jal sites finds 16 callsites - the computed params cover the battle special-attack, summon-stager (id - 0x79 → extraction 903..913) and battle-stage bands, plus the slot-B default pair FUN_80025BA0 (param 5 or 6 → extraction 0900/0901, the summon-render pair). No site can produce param 0 or 1, so extraction entries 0895/0896 are unreachable from any static loader call; the “mode-24 loads PROT 0896” association is refuted (0896's bytes appear nowhere in RAM across the entry window).
The script VM that drives every running script is not in SCUS_942.54. It lives in RAM overlays at 0x801C0000+, loaded on demand:
| VM | Driver | Lives in |
|---|---|---|
| Actor / sprite VM | FUN_801D6628 | Title-screen overlay |
| Field / event VM | FUN_801DE840 | Town / field overlay |
| Effect VM cluster | FUN_801DE914 / 801DFDF8 / 801E0088 | Battle overlay |
Publisher-logo boot screens (PROT 0895 init.pak)
PROT entry 0895 (filename label bat_back_dat) is the boot-time init.pak bundle. The label is the +2 filename shift, not dev mislabeling: in the define map, raw 897 (= extraction 0895) is the first xxx_dat slot, while bat_back_dat truly names the battle side-band summon.dat/readef.DAT files at extraction 893/894. The bundle contains four uncompressed PSX TIMs that the boot sequence uploads to VRAM and fades through before the title screen appears:
| Offset | Logo | Mode | Decoded size |
|---|---|---|---|
+0x021c4 | PROKION | 8 bpp | 176×256 |
+0x0d3e4 | Contrail | 8 bpp | 184×256 |
+0x18e04 | SCEA Presents | 4 bpp | 256×128 |
+0x1ce44 | WARNING | 4 bpp | 256×256 |
A typed parser at legaia_asset::init_pak returns slice pointers + decoded VRAM rects for each logo; a disc-gated test pins the on-disc layout. The asset viewer's "Boot publisher logos" panel and the legaia-engine play-window --boot-ui binary both consume this parser to drive a 30 / 90 / 30 fade-in / hold / fade-out animation per logo (see legaia_engine_core::publisher_logos).
Strip-grid unfolding
Two of the four TIMs (PROKION, SCEA) are vertically-packed sprite atlases - the decoded bitmap stacks several smaller strips that retail unfolds into a horizontal/grid layout via multiple GPU quads. Blitting the whole TIM as one quad shows the packed layout (PROKION as PROK over KION, SCEA as four wrapped text rows) rather than the on-screen logo. The per-logo grid lives in publisher_logos::STRIP_GRID:
| Logo | Grid (cols, rows) | Source strip | Unfolded |
|---|---|---|---|
| PROKION | (2, 1) | 176×128 | 352×128 (PROK ☉ KION) |
| Contrail | (1, 1) | 184×256 | 184×256 (no slicing) |
| SCEA | (2, 2) | 256×32 | 512×64 (Sony Computer Entertainment America / Presents) |
| WARNING | (1, 1) | 256×256 | 256×256 (no slicing) |
Source strips are stored column-major in the bitmap; the output grid is row-major, so source strip s = c × rows + r lands at output cell (col c, row r). PROKION's two halves combine into PROK ☉ KION (the green hemispheres in each half complete a single sun when adjacent); SCEA's four 32-row strips read top-line Sony Computer Entertainment America + bottom-line Presents.
The exact on-screen layout retail uses still has to be RE'd from the unlocated title-overlay tick body - the STRIP_GRID constants are hypothesis-fit-to-visible-content, not pinned to specific GPU draw commands.
There is no single title.pak bundle entry. The h:\prot\field\title\title.pak string in init.pak's pool is only a debug-print referent; SCUS does not contain the literal string title.pak anywhere. The dev-tree title.pak content is split across two retail PROT entries, both confirmed by the init.pak fingerprint method against the title_screen_new_game save state: the title wordmark TIM is PROT 0888/0890 (sound_data2; parsed by legaia_asset::title_pak, the big-logo RAM TIM at 0x80170DF8 fingerprint-matches it), and the options / config-menu bundle is PROT 0899 (xxx_dat). Entry 0899's indexed payload opens with the config-menu string pool - Display Off / Gradual / Immediate / Field HP Display / Encounters / Vibration / Dual Shock / Voices / Battle Camera / Monaural / Stereo / Sound - followed by the small config-screen TIMs (CLUTs byte-matched at 0899 offsets 0x169DC and 0x1F91C+). The title-overlay code lives in the unindexed gap immediately after entry 0899. (The extraction filename labels here are the +2 numbering shift - always cross-validate against the loader-call constant or the file's magic bytes.) So 0888 = title image, 0899 = options/config bundle + trailing overlay code.
Pre-init_data system-UI gap (menu-glyph atlas + boot cursors)
A separate 236 KB / 118-sector unindexed region sits between the TOC and the first indexed entry (init_data at LBA 121). The TOC ends at PROT.DAT offset 0x1800 (3 sectors); the first indexed payload starts at 0x3C800 (sector 121). The gap is a packed bundle of system-UI TIMs - boot-time cursors, the menu-glyph small-caps font retail samples for the title menu rows ("NEW GAME" / "CONTINUE" / "OPTIONS"), and a handful of ornamental sprite strips. All TIMs are 4bpp + CLUT and target the bottom-right corner of PSX VRAM (fb_x >= 640).
| PROT.DAT offset | TIM dims | VRAM target | Purpose |
|---|---|---|---|
0x01858 | tiny | (896,256) | boot cursor variant |
0x018E0 | 256×192 | (896,256) | large UI sprite sheet |
0x07F40 | 256×256 | (896,0) | dialog-font / large bitmap sheet |
0x10178 | 256×32 | (896,448) | AP / status-icon sprite sheet |
0x11218 | 256×256 | (960,256) | menu-glyph small-caps font (NEW GAME / CONTINUE / …) |
0x19438 | 240×24 | (960,400) | UI sprite strip |
0x1B80C | 256×256 | (640,0) | system sprite sheet |
Menu-glyph atlas
The TIM at PROT.DAT[0x11218…0x11218 + 33312] (256×256 @ 4bpp + 16×16 CLUT bank) is the small-caps font retail samples for the title-menu rows and the same shape elsewhere in the menu UI. Confirmed by pinning the in-RAM copy at vaddr 0x80106478 (sstate8, live title-menu state) against PROT.DAT - byte-equal modulo the runtime CLUT relocation. The atlas does NOT appear in any extracted PROT entry; it's strictly in this pre-init_data gap.
- Alphabet row: atlas
y=224…238, 26 cells × 8 px wide on an 8 px pitch starting atx=8. - Digits row: atlas
y=209…220, 10 cells with the same pitch. - Non-glyph debug content (a
<DEMO>row, dev string,FONT CLUTpalette-bar) is present but ignored by the engine.
CLUT row 0 renders the alphabet in solid red with magenta highlights; retail switches CLUT rows per context to read white / gold / dim. The clean-room engine sidesteps the CLUT-switching logic by decoding once to a stencil (pixel-index 0 → transparent, indices 1…15 → opaque white) and applying a SpriteDraw::color tint at draw time. The atlas builder lives in crates/asset/src/menu_glyph_atlas.rs (source pin + per-character rect helper) and crates/engine-core/src/menu_glyph_atlas.rs (RGBA8 stencil decode).
Extraction reads PROT.DAT directly via the new ProtIndex::prot_dat_raw_bytes(byte_offset, len) accessor; the per-entry entry_bytes / entry_bytes_extended paths walk TOC indices and never visit this gap.
Note on title-screen "NEW GAME" / "CONTINUE": The title menu rows are NOT rendered from this atlas - retail samples a pre-rendered band at y=227..237 inside the title TIM itself (PROT 0888 / 0889 / 0890; see TITLE_BAND_MENU_NEW_GAME / TITLE_BAND_MENU_CONTINUE in legaia_asset::title_pak). The band carries both strings packed into a single 128×10 strip; the engine emits two SpriteDraws sampling the left half (x=0..65) and right half (x=65..127). Selection is colour-coded: bright/white for the cursor row, dim/gray otherwise - there's no arrow cursor mark in retail.
Title-overlay source on disc
The title-overlay code (function FUN_801DD35C at 0x801DD35C, the captured overlay_title.bin 256-KiB window) lives in an unindexed 60-sector gap inside PROT.DAT between TOC entries 899 and 900. The per-entry extractor's size formula stops at each TOC entry's claimed end, so the gap bytes never landed in extracted/PROT/ until the extractor was extended with the max(indexed, footprint) rule. The gap is now surfaced as the trailing portion of extracted/PROT/0899_xxx_dat.BIN.
| Range (PROT.DAT) | Sectors | Bytes | Contents |
|---|---|---|---|
0x5C3D800..0x5C44800 | 47227..47241 | 28 672 | PROT 899 indexed payload (field/town/menu overlay - loaded by mode-2 field init; carries the in-game options submenu strings) |
0x5C44800..0x5C62800 | 47241..47301 | 122 880 | Unindexed gap = title overlay code |
0x5C62800..0x5C67800 | 47301..47311 | 20 480 | PROT 900 indexed payload |
The title-tick body (FUN_801DD35C) source is at PROT.DAT offset 0x5C4C344 (gap-relative +0x7B44, sector +15 within the gap).
How the load happens
The SCUS boot sequence issues a multi-sector ReadN starting at PROT 899's LBA (47227) and reads ~74 sectors of contiguous on-disc data - crossing PROT 899's TOC-claimed end (47241) into the unindexed gap. The CD-DMA primitive (FUN_8005D9A0) breaks the read into 5 sequential DMA bursts, each landing into a different point in the overlay window (capture: scripts/pcsx-redux/autorun_title_overlay_writer_hunt.lua with LEGAIA_NO_SSTATE=1). PCSX-Redux Write breakpoints catch DMA writes from CD-DMA-channel-3.
Why the TOC misses it
The historical per-entry size formula size_sectors = toc[p+5] - toc[p+3] + 4 gives 14 sectors for PROT 899, but the on-disc contiguous range to PROT 900's LBA is 74 sectors. The formula appears to describe an "indexed" subset of each entry's disc footprint, with trailing unindexed bytes carrying overlay code the SCUS loader reads via an explicit larger sector count. The extractor now honors max(indexed_size_sectors, footprint_sectors) - see PROT TOC.
Title-screen overlay state
The title-screen overlay loads into 0x801E0000+ during the boot sequence and keeps its mode state in a struct at 0x801EF018. Known fields:
| Offset | Width | Field |
|---|---|---|
+0x154 | u32 | Title-attract idle countdown (_DAT_801EF16C). Initialized to 0x8000; decremented per-frame by _DAT_1F800393 (the global per-frame scalar - same byte used by World::tick_move_vms_with_delta); underflow writes the master game-mode index to 0x1A (= STR FMV mode 26) and zeroes the FMV id at _DAT_8007BA78 → MV1.STR. See cutscene. |
+0x158 | u32 | Title-overlay frame counter (_DAT_801EF170). Incremented unconditionally every tick. |
Initial values come from a SCUS-side bulk-initializer at FUN_8005DA40 (called via 0x8005C2D4) that walks a pointer table at _DAT_800795B4 and writes initial values into multiple overlay BSS regions in one pass. The countdown's 0x8000 sentinel is set during this init pass, before the overlay's tick function starts running. The same initializer writes other addresses sharing a …116C low-half offset, suggesting _DAT_800795B4 is a list of struct bases the init pass walks with a common per-struct displacement.
Tick function
The per-frame tick function is FUN_801DD35C (entry 0x801DD35C, 12 104 bytes / 3 026 instructions, in the title overlay at 0x801C0000+, not in SCUS). Pinned via a PCSX-Redux watchpoint on the countdown - the BP captured pc=0x801DDCCC on the exact sw v0, -0xe94(a0) instruction that writes the decremented value back. Full disassembly + decompile in ghidra/scripts/funcs/overlay_title_801ddccc.txt; capture pipeline in scripts/pcsx-redux/autorun_countdown_trigger.lua (slot-8 save state default; outputs RAM + screenshot + regs to captures/boot_walk/overlay_title.bin*).
Decrement sequence (around 0x801DDCB0..0x801DDCCC):
lui a0, 0x801f
lui v1, 0x1f80
lbu v1, 0x393(v1) ; v1 = *_DAT_1F800393 (per-frame scalar)
lw v0, -0xe94(a0) ; v0 = *0x801EF16C (countdown, u32)
nop
subu v0, v0, v1 ; v0 -= scalar
bgez v0, 0x801dfc3c ; if signed >= 0, branch to "still counting"
_sw v0, -0xe94(a0) ; <-- captured pc: store decremented value
The "still counting" path branches to 0x801DFC3C (the normal per-frame attract loop: rendering, input, cursor logic). The "underflow" path falls through past 0x801DDCCC into a block that prepares draw primitives via 0x80058490 and writes the master game-mode index _DAT_8007B83C = 0x1A, zeroing _DAT_8007BA78 (FMV id slot) → MV1.STR.
Sub-mode dispatcher
The first ~250 instructions of FUN_801DD35C fan out via a 25-entry jump table at 0x801CF244 keyed on state[+0x204]. The JT, the state-struct field offsets, and the observed state[+0x204] = N transitions are pinned in legaia_engine_vm::title_overlay. Four modes are semantically labelled (Init, Idle, AttractIdle, AttractDelay); the other 21 carry Phase0xNN placeholders with traced-transition docstrings.
Standout pin: Phase06 writes _DAT_8007B83C = 0x02 at 0x801DFC00 - the title-screen → main-game master-mode transition, exported as MASTER_GAME_MODE_FIELD_LAUNCH + PHASE06_LAUNCH_GAME_PC. Engine-side observers can detect the launch by watching that single write site.
This sub-mode SM is the front-end title-menu + memory-card manager + new-game/continue launcher - not an opening-narration/name-entry sequence. The full C-decomp (a switch over DAT_801f0204, cases 0x00..0x18) is all title-menu / memory-card UI: the strings are card/save messages (Now checking MEMORY CARD, Do you wish to format?, Load/Save successful, …) and FUN_801E3EE0/FUN_801E36C4 are the centered card-message text+box drawers. Sub-mode 0x10 is the menu (2-option cursor _DAT_8007B820; idle-timeout → master-mode 0x1A = attract/opening FMV); 0x15 is the card-check poll (Now checking / An error occurred + retry). NEW GAME and CONTINUE both funnel menu → fade (0x16) → init_game (0x06) → master-mode 2 (field); init_game writes the opening scene id opdeene (the prologue cutscene, CDNAME/PROT #748) into the active-scene-name buffer (0x8007050C) - verified live (opdeene at the intro save, town01 at the Rim Elm saves), so s_opdeene is a scene id, not a name. There is no narration and no name-entry in this front-end SM - the engine's “menu → new-game → field” jump is faithful to retail's front-end. The opening narration and naming happen downstream of the field launch. The captured opening order (save-state corpus new_game_cutscene_intro_a / rim_elm_zoom_intro / vahn_walks_out / name_input_ui) is: an engine-rendered 3D cutscene (low-poly actors around the Genesis tree with subtitle text, “It was the Seru.” - not an STR FMV) → an establishing shot of Rim Elm → Vahn outside his house → the name-entry menu overlay (“Select your name.”, character grid, default Vahn) → free roam. The data-init (gold/flags/stats) + town01 scene run after the field launch; the narration + naming are field/event/menu-overlay steps still to be traced.
New Game boot chain (title → field)
Confirming NEW GAME is what crosses from the title overlay into the 28-mode table. The menu handler reads the live cursor (state[+0x1FC]), and on L1|Cross (pad & 0x44) stashes the chosen row at state[+0x200] and advances to sub-mode 0x14; NEW GAME is row 0 (a non-zero row, CONTINUE, routes to the save/card load path). The row-0 sub-phases reach the launch write above:
- Mode 2 (field INIT).
_DAT_8007B83C = 2→ the dispatcher runsFUN_80025B64, which loads the field/town/menu overlay (FUN_8003EBE4(2), PROT 899) and calls the per-scene initializerFUN_801D6704. - Field scene init.
FUN_801D6704reads the resident map id and loads geometry + MAN (FUN_8003AEB0) + camera + fog + BGM through the field loaderFUN_8001F7C0, allocates the game-mode work buffer, then writes_DAT_8007B83C = 3. - Mode 3 (field RUN). The field per-frame loop ("MAIN MODE",
game_mode 0x03) takes over. So modes 2/3 are the field/town gameplay INIT/RUN pair - not an options screen, despite the dev label and older notes.
The mode-transition control flow is mirrored in legaia_engine_vm::title_overlay (MASTER_GAME_MODE_FIELD_RUN, FIELD_SCENE_INIT_PC, MENU_INDEX_NEW_GAME) and World::begin_new_game in engine-core. The fresh-state seed a new game establishes is the new-game data-init FUN_80034A6C (reached via the boot mode initializer FUN_8001DCF8):
- Gold → party gold
_DAT_8008459Cis set to a hardcoded 500 (a constant, not a template field). Mirror:NEW_GAME_STARTING_GOLD. - Story flags → the routine zeroes a ~
0x200-byte story-flag region, so a New Game starts with all flags clear. - Party stats → it calls
FUN_800560B4, which expands theSCUS_942.54starting-party template into the live records (stride0x414), copying each member's default name (Vahn). This default is what the downstream name-entry screen (“Select your name.”, save-statename_input_ui) pre-fills and lets the player overwrite - that screen fires in the field/event flow after the opening, not here. - Opening scene → the front-end launcher (
init_game), not this data seed, sets the opening scene: NEW GAME entersopdeene(the prologue cutscene) first, which hands off totown01(Rim Elm).
FUN_801D6704 is generic field entry and reads this seeded state from globals.
opdeene → town01 handoff (scene-change packet)
The opening cutscene scene opdeene advances to town01 (Rim Elm) through a name-based scene-change packet, not the map-id door-warp (0x3E / FUN_80025980). Confirmed empirically: the door-warp handler backs up the active scene name into 0x8007BAE8, but that buffer is empty in the town01 opening saves, so the door-warp never fired here.
FUN_8001FD44(name_ptr) is the scene-change-packet API: it copies the target name into 0x8007050C, syncs it to the active buffer 0x80084548 (FUN_8001D7F8), and stages the load (gated retail-vs-debug on _DAT_8007B8C2; s_ERR_CHANGE_PACKET guards re-entry). The per-frame field/cutscene controller FUN_801D1344 issues the opening handoff as a one-shot, flag- and pad-gated block:
if (_DAT_8007b868 == 0 && (_DAT_1f800394 & 0x4000000) && (_DAT_8007b850 & 0x100)) {
FUN_801d58f0(2, 0, 0xffffff, 0, 0x3c, -1); // fade out
_DAT_80073ef4 = 0xec0; _DAT_80073ef8 = 0x2dc0; // town01 entry coords
_DAT_1f800394 &= 0xfbffffff; // clear bit 0x4000000 (fire-once)
func_0x8001fd44(s_town01_801ce82c, 3); // next scene = "town01"
}
The target name "town01" is the overlay literal at 0x801CE82C - which is why scanning opdeene’s per-scene data (MAN + event scripts) finds no scene-name string. The pad bit _DAT_8007B850 & 0x100 is the player’s confirm-to-continue after the “It was the Seru.” prologue. The executable’s default scene name is also town01: FUN_8001D424 loads the dev file initmap.txt into 0x8007050C; init_game overrides it with opdeene, and this handoff sets it back.
Trigger flag (_DAT_1F800394 & 0x4000000, bit 26). Set by the field VM’s generic scratchpad-bit opcode GFLAG_SET (op 0x2E, operand 0x1A) - the FUN_801DE840 dispatcher runs _DAT_1f800394 |= 1 << (idx & 0x1f), and idx = 0x1A is bit 26. The only GFLAG_SET 26 in opdeene’s decoded MAN is not in the partition-1 per-actor/scene-entry scripts; it lives in the last record of the MAN’s third record partition (partition index 2, count 19; record start at MAN file offset 0xA47, the 2E 1A at 0xA5E) - a cutscene-timeline script where GFLAG_SET 26 is immediately followed by a Camera Configure op and an actor MoveTo, i.e. the prologue’s closing camera/actor staging arms the handoff just before returning control. (Earlier notes guessing a 0x4C MenuCtrl sub-op in record 0 are falsified - record 0 has no 0x2E/0x2F byte at all.)
Clean-room port. World::take_prologue_handoff mirrors the FUN_801D1344 gate: while the active scene is opdeene and the trigger bit (PROLOGUE_HANDOFF_FLAG = 1 << 26 in World::story_flags, the engine’s _DAT_1F800394 mirror) is set, a confirm press clears the bit (fire-once) and returns town01. On the Some(target) the host runs BootSession::enter_field_live(target).
The arm is data-driven: rather than blindly raising the bit on entry, SceneHost::enter_field_scene walks opdeene’s own MAN cutscene-timeline partition for the real GFLAG_SET 26 write and arms only when it is present (World::arm_prologue_handoff_from_man, built on man_field_scripts::walk_partition_gflag_sites). The engine confirms the arming op exists in the disc bytecode and sets exactly the bit the executed op would, so a cutscene scene that never issues that write can never produce a false hand-off. The disc-gated test opdeene_prologue_arm.rs pins the GFLAG_SET 26 at the partition-2 record-18 offset 0xA5E and asserts town01 carries no such arm.
The opening narration plays from this same timeline record: its inline subtitle pages (0x1F/0x00-framed ASCII decoded by legaia_asset::cutscene_text) are installed on entry and walked one page at a time by the CutsceneNarration presenter, which gates the hand-off so the narration finishes before the town01 confirm fires. What remains of the prologue tail is ticking the timeline's camera + actor MoveTos frame-by-frame (so GFLAG_SET 26 fires by execution and the 3D staging plays alongside the subtitles). The field-VM op that auto-opens the name-entry overlay is now pinned (see below).
Name-entry overlay
The “Select your name.” screen (default Vahn) runs after the field launches, as a menu overlay invoked during the town01 opening (master mode 0x03, captured in the name_input_ui save state) - part of the field/dialog overlay at 0x801C0000, not a title sub-mode. The field-VM op that opens it is pinned: op 0x49 STATE_RESUME sub-op 3 at town01 partition-2 record 3 (P2[3]) body offset 0x02c6 (49 03 00), in the opening cutscene timeline. After the establishing camera sweep and Vahn's walk-out the script suspends on this STATE_RESUME and op49_invoke_setup (func_0x80020de0(0x8007065c, _DAT_8007c34c)) hands off to the overlay. Confirmed by executing P2[3] through the engine field VM and correlating against this save: _DAT_8007B450 (the op-0x49 state slot) holds 0x800EB297 = the 0x49 op's RAM address + 1 (record body 0x02b0 loads at RAM 0x800EB280, byte-identical), so the field script is parked precisely at this op while name entry is up. This is executed in-engine: the new-game hand-off installs P2[3] as a spawned cutscene timeline (World::install_town01_opening_timeline), which plays the establishing camera + Vahn's walk-out (stepping past the conditional-wait parks the engine doesn't model) then opens the overlay at op 0x49 via the op-49 host hooks (op49_invoke_setup → open_name_entry) and freezes until a name commits. Regression: town01_opening_timeline_trace.rs + town01_opening_name_entry_wiring.rs. The character grid lives at 0x801F29F0: a flat ASCII table, 6 rows × 17 bytes (ABCDE|abcde|12345 … Z␣␣␣␣|z␣␣␣␣|:;()~), three column-groups of 5 with | (0x7C) separators. The cursor (0x8007BB88) is a linear index over a 7-row × 17-col navigation space (0..0x77 = 119, wrapped modulo 0x77): cells 0..0x66 are the glyph rows, 0x66..0x77 the control row. The live name buffer is at 0x801F2A6C; the committed name lands in the character record at internal offset +0x2A7 (record base 0x80084708 + n*0x414; save-block offset +0x86F for slot 0); the prompts (“Select your name.”, “Is this name okay?”, …) are at 0x801CF698+.
Two functions carry it. FUN_801E6B34 renders the grid (skipping | / space) via glyph drawer FUN_80036888, plus the current name, the blinking caret (the Vahn_ underscore) and the box frames. FUN_801F03F0 is the state machine: substate at struct+0x54, 5-entry jump table at 0x801CF71C - init (0x801F0444) → interactive (0x801F0480) → three confirm handlers (0x801F095C/09C0/097C). The interactive handler applies d-pad deltas -0x11/+0x11/+1/-1, wraps modulo 0x77, and skips separators; confirm on a glyph appends it (width-capped at 0x39=57 px), and the control row tiles the sentinel bytes 00 00 | 66×6 | 64×6 | 65×3 → Backspace (0x66) / Space (0x64) / End (0x65, gated on a non-empty name → the Yes/No confirm, whose Yes commits the name into the record's name field at +0x2A7, save-block +0x86F).
Clean-room engine port. The whole SM is ported as a standalone overlay in legaia_engine_core::name_entry (NameEntry + NameEntryState + Control), driven on the world by World::open_name_entry / step_name_entry (committing into World::party_names) and rendered through legaia_engine_render::name_entry_draws_for. In legaia-engine play-window, the NEW GAME flow opens the prompt automatically at the opdeene → town01 opening hand-off (take_prologue_handoff fires only on the new-game opening, so it never re-prompts on a normal town01 visit), seeded with the template default Vahn - and it is correctly ordered after the opening narration, which now plays in-engine and gates the hand-off (see the prologue narration above). The remaining open thread is the precise field-VM op that opens the overlay mid-establishing-sequence; a dev N key also opens it for testing.
Sprite-emit helpers
The title-tick body reaches into three SCUS-side helpers to emit GPU primitives. All three are ported clean-room in legaia_engine_vm::title_prim:
FUN_80058298(ClearImagerect-fill queue, 37 instructions) →exec_clear_image(host, rect, r, g, b).FUN_80058490(MoveImageVRAM-copy queue, 49 instructions) →exec_move_image(host, src, dst_x, dst_y)with early-out on zero extent.FUN_800198E0(sprite-descriptor dispatcher, 146 instructions) →exec_sprite_descriptor(host, &SpriteDescriptor)with full tag-0x11+ complex variant routing (alpha-OR pre-pass underflags & 8, four width-divisor variants fromflags & 3).
SpriteDescriptor { tag, flags, rect, pixel_data_ptr } + Rect12 { x, y, w, h } capture the wire shapes. The PrimHost trait abstracts the four engine-side callbacks. Overlay-side helpers (FUN_801E1C1C / FUN_801E373C / FUN_801E3EE0 / FUN_801E36C4, shared across menu / battle / shop / save UI overlays) are deferred to their own port.
Title TIM source pinned
The main title-screen art (Legend of Legaia wordmark, orb, "PRESS START BUTTON", "NEW GAME" / "CONTINUE" menu, copyrights) is sourced from PROT 0888 (CDNAME label sound_data2; the multi-bank sound-data cluster carries title art in the trailing pool past the audio payload). Multi-bank duplicates live in PROT 0889 and 0890:
| PROT entry | File offset | Size | Format |
|---|---|---|---|
0888_sound_data2.BIN | 0x1AA28 | 66 080 bytes | 256×256 8bpp + 256-colour CLUT (PRIMARY) |
0889_sound_data2.BIN | 0x19A28 | 66 080 bytes | same content (multi-bank duplicate) |
0890_sound_data2.BIN | 0x14228 | 66 080 bytes | same content (multi-bank duplicate) |
The 256×256 image is a sprite sheet that bundles every text band the title screen could draw - retail composes the screen by sampling specific sub-rects rather than blitting the full quad. The bands, top to bottom in source-y:
Source rect (x, y, w, h) | Content | Drawn when |
|---|---|---|
(0, 17, 256, 124) | Orb + "Legend of Legaia" wordmark | every post-fade phase |
(96, 151, 64, 10) | <DEMO> | never - demo-build leftover |
(60, 178, 196, 16) | "PRESS START BUTTON" prompt | PressStart phase only |
(4, 195, 244, 14) | "TM of Sony…" copyright | every post-fade phase |
(8, 209, 234, 14) | "© 1998,1999…" copyright | every post-fade phase |
(0, 226, 256, 11) | small "NEW GAME CONTINUE" footer | replaced by larger font glyphs |
The <DEMO> band is a residual from a development demo build that retail simply never samples - verified by capturing main RAM at the live title screen (sstate8, sub-mode 0x10 AttractIdle) and confirming the in-RAM TIM bytes byte-match PROT 0888 while the live framebuffer omits the band. The small "NEW GAME CONTINUE" footer is similarly never drawn; retail renders the menu labels using the dialog-font glyph atlas instead (which is why the on-screen "NEW GAME / CONTINUE" letters are visibly larger than the embedded footer text).
Pinned by capturing main RAM at the live title screen, TIM-magic-scanning the dump, and byte-grepping the in-RAM copy (at vaddr 0x80170DF8) against the extracted PROT corpus. Reusable scanner at scripts/asset-investigation/scan_tims_and_match_prot.py. Typed parser at legaia_asset::title_pak::extract_title_tim, with band-rect constants TITLE_BAND_WORDMARK / TITLE_BAND_PRESS_START / TITLE_BAND_TM_COPYRIGHT / TITLE_BAND_C_COPYRIGHT (plus TITLE_BAND_DEMO for reference) pinning the sub-rects above; an engine-side RGBA decoder lives at legaia_engine_core::title_screen_atlas::build_atlas_from_prot_888. The play-window --boot-ui binary uploads the atlas and emits one SpriteDraw per active band each frame (title_screen_sprite_draws in legaia-engine), gating the press-start band on phase. The font-rendered "PRESS START" overlay is suppressed via the atlas_present flag on title_draws_for so the TIM band isn't duplicated.
Two more title-related TIMs live embedded inside the title overlay's data segment (the trailing-gap portion of PROT 899) at the addresses the tick body's FUN_800198E0 calls reference: 0x801E5120 → extracted/PROT/0899_xxx_dat.BIN @ 0x16908 (save-menu UI atlas) and 0x801EE120 → 0x1F908 (animated PSX memcard icon strip, 14 frames). The mismatch between the debug-print path h:\prot\field\title\title.pak in init.pak's string pool and the actual on-disc location (PROT 0888) is a dev-tree-vs-disc split; the 0895_bat_back_dat-style label confusions are the separate +2 filename-numbering shift. Either way: always cross-validate against the loader-call constant or the file's magic bytes.
A town/field subsystem uses a separate format-string pool at 0x80011079..0x80011109 (" town ", "mode %d", " baria mode ", " walking set", "end of mes works set", "open port.dat", "nt_group_table %x"). These print at retail-build runtime but have no LUI+ADDIU caller resident until the town/field overlay is loaded - i.e. the "mode 17 / mode 16" runtime printfs are town-subsystem mode transitions, not the master 28-mode state-machine index.
Debug flags
Two RAM bytes that several subsystems branch on at boot time and beyond:
| Address | Role |
|---|---|
_DAT_8007B8C2 | Dev/retail build toggle. Sound init, the field loader, the monster-bank loader, the save-card path, the scene-change packet, and the title overlay all carry an "if dev" branch keyed on this byte. |
_DAT_8007B98F | Separate debug-mode flag (NA build offset; JP retail uses 0x07D51F, an 0x1B90 build-shift). |
The exhaustive corpus sweep (2661 files) returns zero writes to _DAT_8007B8C2 and zero references (read or write) to _DAT_8007B98F. _DAT_8007B8C2 is BSS-resident (zero = retail at boot) and is only mutated via external POKE - the TCRF GameShark codes that flip it to dev mode are the only known writers. _DAT_8007B98F is live-confirmed: writing 0x8007B98F = 1 via RAM poke in a stable field scene brings up the debug menu on SELECT+△ in the NA retail build; the consumer is overlay-resident and outside the captured corpus. The earlier "stripped at link time / inert" conclusion is falsified.