At a glance

Lives in
The menu overlay (PROT 0899) - the same code chunk as the shop, inn and status screens. There is no save overlay of its own.
Driver
FUN_801DC6B4: a 9-state outer machine that fades in, dispatches one sub-screen from a 33-entry function table, and fades out.
Card I/O
A 5-state machine over Sony's libcd (FUN_801E3294), sequenced by a per-frame ticker; BIOS bu device calls underneath.
Block
One card block, 0x2000 bytes: SC header + icon, then 0x1A18 bytes copied verbatim from RAM 0x80084140; checksum word at +0x1FFC.
Engine
engine-core::save_select (session + card machine), save_subscreen (the screen graph), legaia_save (block layout, round-trip).
Confidence
Confirmed - overlay captures, disassembly, GP0 draw-list pins (how we know).

The flow, screen by screen

Pause menuSave row Port pickSLOT 1 / SLOT 2 Now checkingcard read Block grid5 x 3 previews Confirmoverwrite Yes/No Writechecksum + libcd Returnfade to menu pick a block
Two pick stages in two id spaces: the pills choose a memory-card port, the grid chooses one of that card's fifteen blocks. The card read sits between them, which is why "Now checking" exists at all.
StageWhat the player picksCountRetail anchor
Pill rowa memory-card port2the libcd channel's port (chan = port × 16 + sub_op)
Preview grida save block on that card15 (+ a Return cell)the directory walk; per-slot preview buffer 0x801EF1B8 + N × 0x100

A third id lives in the filename: retail files a save as BASCUS-94254PRO-nn where nn is the list position it was standing on, and lets the BIOS put the file in any free block. The grid index, the port and that number are three different spaces.

Three layers of code

LayerRoutineJob
Outer dispatcherFUN_801DC6B4State 0 decodes the entry context into a sub-screen id and starts the fade; state 2 calls the sub-screen through the table at 0x801E4F40; states 3..5 fade out; 6+ returns.
Sub-screenstable 0x801E4F4033 small step machines sharing one shape: run a display script, wait for it to go idle, take input, write the next screen id. The save flow uses about a third of them; the rest are the pause menu, shop and casino exchange.
Card I/OFUN_801E3294Five states over libcd with a 5-try retry budget; result codes 1 ok, -1 no card, -2 stray event, -3 abort / timeout.

The entry context (_DAT_8007B450) is what a field script parks when it opens the menu; its first byte decides where the save UI opens.

Entry contextOpensMeaning
sentinel 10x02Save from the pause menu
kind 0x010x19Save from a field script's save point - straight to the card driver
kind 0x070x20Casino prize exchange (shares the table; touches no card)
kind 0x0D0x04Post-save "press any button" return
kind 0x000x1AShop mode select (Buy / Sell / Quit)
Save-flow sub-screens
IdFunctionRole
0x00FUN_801DD12CFinal exit: terminal display script, then arms the fade-out delta and outer state 3.
0x01FUN_801D6B20Root command picker - the pause menu's own top level, 7 rows. Row 5 Load opens 0x18 unless the entry context is kind 0x0D; row 6 Save opens 0x19 unless the scene's save-allow byte _DAT_8007B6A8 is zero. Blocked rows buzz (cue 0x23).
0x03FUN_801D6D38Yes / No confirm, default cursor on No; Yes advances to 0x00.
0x04FUN_801DD1B8Post-save "press any button": waits for a held confirm-or-cancel button, returns to 0x01.
0x18FUN_801DAE24Load-card driver: install the card handle, run the display script, call FUN_801DD35C(1, 2) until done, return to 0x01.
0x19FUN_801DAEF4Save-card driver: same four steps with FUN_801DD35C(1, 1). Returns to 0x01, or exits to 0x00 when a field script parked the context.

Op 1 is save and op 2 is load: the row labels (@Load row 5, @Save row 6), the op flag (op 1 erases the file before writing), and the op-2 arm's "Unable to load data." message all agree. Both directions share FUN_801DD35C, whose load branch copies the 0x2000-byte read buffer at 0x801E5120 back over the live window; the save branch composes into 0x801E7120.

Rules a player runs into

  • Save is overworld-only. The Save row is gated by a per-scene byte seeded from the scene MAN's header bit; across the disc that bit is set only on the three kingdom world maps. A field save comes through a script's save point instead, which opens the card driver directly.
  • "Now checking" ends when the card answers - or after 120 frames. The per-frame poll tests four card event handles in order (last one wins) and forces a timeout status on the 121st poll; timeout and a stray complete event both tear the read down.
  • A foreign save is never offered as free. The directory walk clears every slot to "unreadable", stamps "Legaia save" on each filename matching BASCUS-94254PRO- (JP: BISCPS-10059PRO-), and only then spends the card's reported free-block count marking the rest free. A slot the walk cannot afford to call free stays unreadable.
  • The block is checksummed. The compose path zeroes a 0x2000 buffer, copies 0x1A18 bytes of live state over the front, sums the first 0x7FF words and stores the sum at +0x1FFC; the load path re-sums and routes a mismatch to "Damaged data."

What the block holds

The game-data region is a linear copy of RAM from 0x80084340: any live field lands at block + 0x200 + (addr − 0x80084340).

OffsetSizeField
0x00002SC magic; icon flags, title (Shift-JIS), 16-colour palette and 16x16 icon follow through 0x1FF
0x02000x3C8Display / global header: location name +0, leader name +0x54, scene labels +0x208 / +0x218, gold +0x25C
0x05C80x414 × 4Character records Vahn, Noa, Gala, Terra (RAM 0x80084708)
0x14C00x200Story-flag bitmap (RAM 0x80085600); overlaps Terra's tail
0x18180x90Inventory page, 72 × (id, count) (RAM 0x80085958)
0x1FFC4Additive checksum of words 0..0x7FE

Two stores share the name "story flags" and never sync: the 512-byte bitmap above persists; the field VM's 32-bit scratchpad word at 0x1F800394 (opcodes 0x2E/0x2F/0x30) does not, and is re-seeded from the mode table on mode init. The engine's SaveExt mirrors both independently.

What the screen is drawn from

Every sprite on the Continue / Load screen is pinned to a source texture and palette row by reading the GPU's own draw list out of a capture.

ElementSourceNote
Title art behindPROT 0890 title TIMDrawn dimmed at (33, 6).
"Load" panel chromeSystem-UI sheet at PROT.DAT[0x018E0], CLUT row 29-slice: 14 textured sprites compose the 81x29 panel at (6, 4); 3 gouraud quads tile the marbled interior.
"Load" glyphsThe dialog font, VRAM page (896, 0)Four 14x15 sprites; bright colour is CLUT entry 15.
Pointing-finger cursorSame sheet, CLUT row 7, (152, 64, 16, 16)At (114, 100); 17 px lower for SLOT 2.
SLOT pillsSave-menu TIM at PROT 0899 +0x16908, CLUT 7Baked "SLOT 1" / "SLOT 2" labels.
Panel geometry, slide-ins and the info panel

Messagebox drawer FUN_801E36C4. Its x is a centre, and the box emitter inflates the rect by 8 px on every side: footprint = (cx − w/2 − 10, y − 2, w + 16, h + 16). The header tab (48, 6, 65, 13) predicts (6, 4, 81, 29), the Load panel's pinned rect; the confirm prompt is two panels, a bar at (8, 86, 300, 29) and a Yes/No box at (129, 118, 58, 42).

Slide-in primitive FUN_801E1C1C. 12-bit fixed-point lerp, timer ramps +0x100 per frame to 0x1000 - a 16-frame slide.

ModeElementStart → target
0"Now checking" dialog(416, 112) → (160, 112)
2"Load" tab + active pill(160, 96) → (48, 40)
3Yes/No confirm(160, 344) → (160, 88)
4Card-format dialog(576, 112) → (160, 112)

Bottom info panel FUN_801E08D8. Slides vertically from 394 to 138 once "Now checking" retracts; draws the slot's kingdom, play time and up to three party columns 96 px apart (portrait, name, LV, HP, MP; HP/MP go yellow at half and red at a quarter). The view mode per cell comes from FUN_801E3F74: readable save, foreign block, or free block. Its fourth arm, a "Return" caption for cell 0xF, is dead code — the grid cursor is clamped to col 0..4 / row 0..2, so the cell index tops out at 14 and nothing ever asks for 15. Slot data is read from 0x801EF1B8 + N × 0x100 (kingdom name +0, seconds +0x24, party count +0x28, names +0x54 + i × 0xC).

The port

  • SaveSelectSession models the phases; the host's SaveRack decides the id space - CardPorts (both shipped hosts) or a flat Blocks list for headless drivers. The browser mounts the player's own card images; the native shell mounts its save directory as port 1.
  • CardIoMachine, card_status_poll, classify_card_directory and save_block_checksum are line-for-line ports of the retail routines above; the write / format sequencer FUN_801E13B8 is ported too, as card_flow::CardWriteMachine, and classed replaced-by rather than unwired - the port’s one card backend patches block bytes synchronously, so there is no asynchronous BIOS beat for it to sequence.
  • legaia_save::SaveFile (LGSF) is the engine's own container; write_into_retail_sc_block patches a real block in place and restamps the checksum, so an edited retail save still loads.
  • Chrome and the slide animations are rebuilt from the pinned geometry in a canonical 320x240 stage (save_menu_atlas, save_select_chrome_draws_for).

How we know

Function / dataAddressWhat it provesDump
Outer dispatcherFUN_801DC6B49-state machine on _DAT_8007B43C; entry-context decode; fade constants 0xF2 / 0x79.overlay_menu_801dc6b4.txt
Root pickerFUN_801D6B20Row 5 Load / row 6 Save gates; the @Load / @Save string pointers at 0x801CEA00 / 0x801CEA08.menu overlay dump
Card driversFUN_801DAE24 / FUN_801DAEF4Identical four-step machines; op 2 = load, op 1 = save.overlay_menu_801daef4.txt
Load / save mainFUN_801DD35COp flag 0x801F0200; erase-before-write on op 1; checksum compare at 0x801DF888.menu overlay dump
libcd machineFUN_801E3294Five states, retry budget DAT_801E4FC4, result codes; status strings NOT_CARD / card_sts.menu overlay dump
Status pollFUN_801E3900Four TestEvent handles, last wins; 120-frame backstop with the increment in the delay slot.menu overlay dump
Directory walkFUN_801E120815-entry table at 0x801F32A8, 16-byte strncmp against the regional prefix; class-then-budget order.menu overlay dump
ChecksumFUN_801E38D8Sums 0x7FF words; compose buffer 0x801E7120, read buffer 0x801E5120.menu overlay dump
List navigatorFUN_801D688CPacked cursor (low 12 bits index, high nibble flags); cues 0x36 / 0x37 / 0x21.overlay_save_ui_select_801d688c.txt
Draw-list pinsPCSX-Redux state, GP0 scanSprite sources, CLUT rows, panel rects; VRAM dump + PROT.DAT signature grep.scan_panel_prims.py, scan_textured_quads.py
Block layoutreal .mcr saves vs RAM dumpsThe 0x200 + (addr − 0x80084340) mapping.crates/save tests
History: readings this page replaced

The card-driver pair reads naturally the other way round (0x18 before 0x19, save before load); three independent signs put load on 0x18. Several table slots once carried save labels - "slot confirm", "card-full screen", "auto-save" - and are the shop's mode picker, the shop's buy list and the casino prize exchange. The card filename separator is a hyphen, not an underscore. The "menu-glyph atlas" pin for the Load text was wrong; the glyphs are the dialog font. See do-not-re-walk.

Full write-up, including the debug character editor and sub-screen 0x15: docs/subsystems/save-screen.md.

See also