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.
In the port
engine-menus::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: Saveoverworld only Save pointa field script Pause: Loadfrom the menu Title: Continuecard manager Port pickSLOT 1 / SLOT 2 Now checkingread, 120-frame cap Block grid15 blocks, 5 x 3 ConfirmYes / No, default No Compose blockcopy live RAM + checksum Card writeerase, then write Read blockre-sum the checksum Restore live RAMresume the saved scene Damaged datasum mismatch saving loading
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. Saving and loading share the whole front half, and one routine runs both directions.
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."
  • A damaged save is refused after the read, not on the grid. The grid classifies a block by its directory filename, so a save with a bad sum still shows its cell and preview; only the checksum verify behind a Load turns it away, with a box that waits for a button.
  • Both directions ask first. Confirming a block raises a Yes / No box opening on No: a Load asks to load, a Save asks to save on a free block and to overwrite on anything else. A Yes then holds the screen through the write or read under its own panel - a sliding box with the block number and a progress tube - and a result line, and the panel parks on black. A load holds its panel for at least 128 sixtieth-second units even when the card answers sooner; a save waits on the card alone.
  • A cell that takes no confirm still answers. Only a readable Legaia save (on a Load) or a Legaia save or free block (on a Save) raises the Yes / No; any other cell takes the press without a prompt but sounds the confirm cue 0x20. The card screen has no buzz at all.
  • The card screen waits for the pause menu to leave. The Load and Save drivers open with the close-all display script, so the card screen starts only after the pause menu's windows have slid off - twelve frames of the window-slide lock, which the port's Load and Save rows wait out too.
  • Continue belongs to the title, not the pause menu. The title's own front-end state machine runs it: confirming its second row fades into the card manager, which walks the port pick, the read and the grid and ends in a launch fade that writes the game mode on its load route. No field runs underneath, so nothing that blocks the pause menu - a conversation, a lock, a script's save point holding the Load row - can stand in its way. Both play hosts run it as one shared Continue screen, ContinueScreen, over the same Load screen; backing out returns to the title card.
  • The options come back with the save. Field Move, Select Attack, Battle Command, Battle Camera, Field HP Display, Sound and the vibration rows sit in the live-state window the block copies, so loading a save made with Run on resumes running. In the port both play hosts adopt them on load, except a row the session overrides (a command-line or capture pin), whose override stands.
  • The records count every battle and every escape. Two words of the live-state window, so of the save, hold the records screen's "No. of Battles" and "No. of Escapes": the battle-scene setup adds one to the first each fight, and the Run roll adds one to the second on a successful flee - but not when the special-battle word's 0x100 bit is up, so a forced flee is no escape on the records. New Game zeroes both. The port counts them in the same two places and carries them in its own save files too.
  • A blank card takes the first save. A formatted card with nothing on it answers the directory read like any other card. The pause menu reaches the screen through a short wipe: the field darkens before any window exists and comes back after the menu closes.

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, the scene label +0x208 and a second, lower-cased copy of it at +0x218, play clock +0x230 (RAM 0x80084570, 60 ticks a second), gold +0x25C
0x05C80x414 × 4Character records Vahn, Noa, Gala, Terra (RAM 0x80084708)
0x14C00x358Story-flag window (RAM 0x80085600..0x80085958), including the 0x200-byte system-flag bank from 0x80085758; overlaps Terra's tail
0x18180x200The item bag, 256 × (id, count) (RAM 0x80085958); ends the live-state copy at 0x1A18
0x1A180x5E4Unread tail: zero on a retail card; a card the engine writes keeps its LGXE extension here (party composition, chain library), which retail never reads
0x1FFC4Additive checksum of words 0..0x7FE

Two stores share the name "story flags" and never sync: the 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 second label is not the previous scene. Every name-based scene change copies the label buffer into it only after the resolver has already overwritten that buffer with the destination, so every retail block holds the active scene twice. The port seats it on scene entry the same way. The live-state copy also carries the minigame purses - fishing points, the dance hall best and the dance free play's song counter among them - which the port lifts from a retail block and writes back.

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)

The slot grid slides in from the right and lands as 32x32 cells, row 0 at (98, 28), pitch (40, 20). Each cell's slide start is formed from the previous cell's slot, because the renderer reassigns its slot register only after forming the base - so the parked cells 0 and 1 start one column apart and the last cell's right edge lands where the capture puts it.

Bottom info panel FUN_801E08D8. Slides vertically from 394 to 138 once "Now checking" retracts; draws the slot's location, play time and up to three party columns 96 px apart (face, name, LV, HP, MP as 8x12 numeral cells; a stat pair goes gold at half and orange at a quarter, with no K.O. tint). 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, a verbatim copy of the block's 0x200..0x300 save summary (location +0, play clock in 60 Hz frames +0x24, party count +0x28, names +0x54 + i × 0xC), never the character records. A grid cell is the save number the filename carries, not the card block, and each cell shows that number's own save-icon face.

Text slots, two live and four dead. Six 0x80-byte text slots sit at 0x801EED24. Slot 0 is the play-time line, its digits poked in place by FUN_801DD35C, and slot 3 a two-digit counter FUN_801DE234 pokes the same way. The other four - a load-failure line, a wrong-game line, a not-available line and a not-used line - are named by nothing on the disc, in any address form, so they are dead text: the failure messages the screen really shows come from the overlay's message pool.

In the port

engine-menus::save_select models the screen and the card machine; the host decides what a “port” is - the browser mounts the player's own card images, and the native window mounts its own card, a raw 128 KiB memcard1.mcr in the save directory, read and written through the same kernels (port 2 is a card image given on the command line). A directory with no card yet gets a freshly formatted one, seeded once from any engine-format slot files an older build left there; the first save creates the save folder itself, writes the card beside the old one and renames it over, and a card file that no longer reads is kept aside as memcard1.unreadable.mcr rather than left in the way. On the browser page the saves on its own card show in the save bar, so a save made at a save point is there to resume after a reload. Every save on both play hosts goes through this screen - a gate checks that no host writes save bytes around it - and the load confirm (opening on No, with retail's pointing hand on the row), the overwrite question, the write and read panels and the damaged-block refusal all follow retail. The directory walk, the status poll and the checksum are line-for-line ports of the routines above, and legaia_save patches a real retail block in place and restamps its checksum, so an edited card save still loads on a PlayStation. The options screen's nine words travel the same way: the lift reads them, a load applies them to the world and then to the host's own options (a session-pinned row keeps its override), and every save writes the live ones back. The play clock travels both ways: a retail block lifts as whole seconds and an engine-written block stores seconds × 60 back, so its time prints on retail's own screen. The clock itself counts the way retail’s does: retail adds the vsyncs each game frame took, so the port counts one per game tick on both play hosts, under the pause menu and the title as well as on the field, and a paused or backgrounded game counts nothing. A disc-gated test runs every save on the library memory cards through Load, play, Save and Load again on the shared kernels and requires the two readings to agree field for field and land in the same scene and track. The engine's own richer save container (LGSF) is described on the engine page.

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_panel_rects.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