Save Screen
Everything between choosing "Save" and bytes landing on the memory card: the SLOT 1 / SLOT 2 pills, the "Now checking. Do not remove MEMORY CARD" beat, the fifteen-block preview grid, the overwrite prompt and the card write. The saved block is a straight copy of the live game state in RAM with an additive checksum stamped on the end - which is why a card editor only needs the layout, not a cipher. Fully ported
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; BIOSbudevice calls underneath. - Block
- One card block,
0x2000bytes:SCheader + icon, then0x1A18bytes copied verbatim from RAM0x80084140; 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
| Stage | What the player picks | Count | Retail anchor |
|---|---|---|---|
| Pill row | a memory-card port | 2 | the libcd channel's port (chan = port × 16 + sub_op) |
| Preview grid | a save block on that card | 15 (+ 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
| Layer | Routine | Job |
|---|---|---|
| Outer dispatcher | FUN_801DC6B4 | State 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-screens | table 0x801E4F40 | 33 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/O | FUN_801E3294 | Five 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 context | Opens | Meaning |
|---|---|---|
sentinel 1 | 0x02 | Save from the pause menu |
kind 0x01 | 0x19 | Save from a field script's save point - straight to the card driver |
kind 0x07 | 0x20 | Casino prize exchange (shares the table; touches no card) |
kind 0x0D | 0x04 | Post-save "press any button" return |
kind 0x00 | 0x1A | Shop mode select (Buy / Sell / Quit) |
Save-flow sub-screens
| Id | Function | Role |
|---|---|---|
0x00 | FUN_801DD12C | Final exit: terminal display script, then arms the fade-out delta and outer state 3. |
0x01 | FUN_801D6B20 | Root 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). |
0x03 | FUN_801D6D38 | Yes / No confirm, default cursor on No; Yes advances to 0x00. |
0x04 | FUN_801DD1B8 | Post-save "press any button": waits for a held confirm-or-cancel button, returns to 0x01. |
0x18 | FUN_801DAE24 | Load-card driver: install the card handle, run the display script, call FUN_801DD35C(1, 2) until done, return to 0x01. |
0x19 | FUN_801DAEF4 | Save-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
0x2000buffer, copies0x1A18bytes of live state over the front, sums the first0x7FFwords 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).
| Offset | Size | Field |
|---|---|---|
0x0000 | 2 | SC magic; icon flags, title (Shift-JIS), 16-colour palette and 16x16 icon follow through 0x1FF |
0x0200 | 0x3C8 | Display / global header: location name +0, leader name +0x54, scene labels +0x208 / +0x218, gold +0x25C |
0x05C8 | 0x414 × 4 | Character records Vahn, Noa, Gala, Terra (RAM 0x80084708) |
0x14C0 | 0x200 | Story-flag bitmap (RAM 0x80085600); overlaps Terra's tail |
0x1818 | 0x90 | Inventory page, 72 × (id, count) (RAM 0x80085958) |
0x1FFC | 4 | Additive 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.
| Element | Source | Note |
|---|---|---|
| Title art behind | PROT 0890 title TIM | Drawn dimmed at (33, 6). |
| "Load" panel chrome | System-UI sheet at PROT.DAT[0x018E0], CLUT row 2 | 9-slice: 14 textured sprites compose the 81x29 panel at (6, 4); 3 gouraud quads tile the marbled interior. |
| "Load" glyphs | The dialog font, VRAM page (896, 0) | Four 14x15 sprites; bright colour is CLUT entry 15. |
| Pointing-finger cursor | Same sheet, CLUT row 7, (152, 64, 16, 16) | At (114, 100); 17 px lower for SLOT 2. |
| SLOT pills | Save-menu TIM at PROT 0899 +0x16908, CLUT 7 | Baked "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.
| Mode | Element | Start → target |
|---|---|---|
| 0 | "Now checking" dialog | (416, 112) → (160, 112) |
| 2 | "Load" tab + active pill | (160, 96) → (48, 40) |
| 3 | Yes/No confirm | (160, 344) → (160, 88) |
| 4 | Card-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
SaveSelectSessionmodels the phases; the host'sSaveRackdecides the id space -CardPorts(both shipped hosts) or a flatBlockslist 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_directoryandsave_block_checksumare line-for-line ports of the retail routines above; the write / format sequencerFUN_801E13B8is ported too, ascard_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_blockpatches 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 / data | Address | What it proves | Dump |
|---|---|---|---|
| Outer dispatcher | FUN_801DC6B4 | 9-state machine on _DAT_8007B43C; entry-context decode; fade constants 0xF2 / 0x79. | overlay_menu_801dc6b4.txt |
| Root picker | FUN_801D6B20 | Row 5 Load / row 6 Save gates; the @Load / @Save string pointers at 0x801CEA00 / 0x801CEA08. | menu overlay dump |
| Card drivers | FUN_801DAE24 / FUN_801DAEF4 | Identical four-step machines; op 2 = load, op 1 = save. | overlay_menu_801daef4.txt |
| Load / save main | FUN_801DD35C | Op flag 0x801F0200; erase-before-write on op 1; checksum compare at 0x801DF888. | menu overlay dump |
| libcd machine | FUN_801E3294 | Five states, retry budget DAT_801E4FC4, result codes; status strings NOT_CARD / card_sts. | menu overlay dump |
| Status poll | FUN_801E3900 | Four TestEvent handles, last wins; 120-frame backstop with the increment in the delay slot. | menu overlay dump |
| Directory walk | FUN_801E1208 | 15-entry table at 0x801F32A8, 16-byte strncmp against the regional prefix; class-then-budget order. | menu overlay dump |
| Checksum | FUN_801E38D8 | Sums 0x7FF words; compose buffer 0x801E7120, read buffer 0x801E5120. | menu overlay dump |
| List navigator | FUN_801D688C | Packed cursor (low 12 bits index, high nibble flags); cues 0x36 / 0x37 / 0x21. | overlay_save_ui_select_801d688c.txt |
| Draw-list pins | PCSX-Redux state, GP0 scan | Sprite sources, CLUT rows, panel rects; VRAM dump + PROT.DAT signature grep. | scan_panel_prims.py, scan_textured_quads.py |
| Block layout | real .mcr saves vs RAM dumps | The 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.