At a glance

Window table
52 records at menu-overlay VA 0x801E4738 (PROT 0899 file 0x15F20); parser legaia_asset::menu_windows
Record
0x10 bytes: content id, park edge, window class (2 tab / 3 standard / 4 list), content rect x,y,w,h, renderer VA
Choreography
Windows slide in / out via the window-widget VM; a closed window parks offscreen at its park edge
Data read
The live 0x414-byte character records at 0x80084708 - the same block the save file copies
Engine port
engine-ui draw builders (wgpu-free, shared by native window and browser) over engine-core::pause_screens
Confidence
Confirmed - table byte-matched disc-vs-RAM; layouts pinned by GPU-prim scans of menu captures (how we know)
QuestionShort answerSection
Where do the windows come from?One table row per window: rect + content renderer.Window table
How does a screen get picked?A submenu index through the same sub-screen pointer table the save screen uses.Dispatch
How does "Best Equipment" choose?A bag scan with two surprising ranking laws.Equip
Where are the options stored?Straight into the live config words the audio and pad code read.Options

Window descriptor table

Press the menu button and a set of bordered gold windows slides in from the screen edges. None is hand-placed in code: each screen is a list of window ids, each id a row in the table giving the window's home rectangle and content renderer. The renderers only ever draw inside; the frame - which extends 8 px past the content rect on every side - is drawn by whoever opened the window.

320 x 240 stage - Status screen id 26 party list id 27 pager id 30 summary id 28 - main panel FUN_801D33D8 rect (90,16,218,188)
One screen = one window-id list. Each id resolves through the 52-record table to a content rect and a renderer; the gold frame is drawn 8 px outside the rect by the caller.
ScreenWindows (draw order)
Top-level pause menu50 command list (24,24,104,94); 49 money / play-time box (24,178,104,24); 51 right party panel (144,24,152,180)
Statustab 3; 26 party list (14,38,60,38); 27 "Condition" pager (14,92,60,10); 30 summary (14,134,60,70); 28 main panel (90,16,218,188)
Equiptab 2; 21 party (14,42,80,38); 23 item list (174,22,132,182) (renderer-less); 22 main (14,96,292,108)
Optionstab 4; 48 settings (24,40,256,148); 47 value popup (170, *, 128, *) - y/h stamped per open

Live windows are doubly-linked structs carrying the descriptor id and an animated copy of the rect: on screen exit a window slides to its park edge and sits offscreen. The top-level windows stay parked in every sub-screen capture - which is how the top-level set was pinned without a top-level capture.

Equip screen and "Best Equipment"

Four windows; the main window draws "Best Equipment" plus seven slot rows (pictogram, equipped name), a stat-compare block when previewing, and the item's description panel when a slot row is focused. The interesting part is the candidate scan behind "Best Equipment": it seeds with what the character wears, walks the bag keeping one winner per armament slot, and obeys two ranking laws the screen never reveals:

  • Armour ranks on UDF + LDF only. The INT and SPD bonuses are never read - a pure-INT head accessory or a pure-SPD boot can never displace an incumbent.
  • A weapon's category check dominates its attack. The score is attack + (favored ? 1000 : 0), and an attack byte cannot reach 1000 - so a category-favoured weapon outranks every unfavoured one regardless of raw attack.
  • An empty slot is filled by the first eligible entry; ties keep the incumbent. The whole scan is a trial equip the routine undoes before returning.

An item competes only when its record marks it equipment and its equip-character mask matches the character; the slot it competes for comes from the equipment record's slot bits, permuted weapon-first. Engine port: equip_session::best_equipment_candidates.

Options screen

The settings rows are data-driven too: a display-layout table joined to 8-byte row descriptors, each naming the live config word the row edits. Confirm opens a value popup positioned under the row; a choice commits directly into the config word - edits are already live, backing out reverts nothing.

RowChoicesConfig word
Battle CameraClose / Normal / Far0x800846C0
Battle Select AttackSelect / Automatic / Command0x800846C4
Battle CommandDirectional Buttons / button glyph0x800846C8
Field MoveWalk / Run0x800846CC
Field HP DisplayImmediate / Gradual / Display Off0x800845C4
SoundStereo / Monaural0x800846BC
Dual Shock: Battles / Events / EncountersVibration On / Off0x800845C8 / 0x800845A8 / 0x800845CC

A hidden "Battle Voices" row (0x800845AC) exists in the descriptor list but not in the layout table. Engine port: engine-core::options + engine-ui::options_draws_for; the Sound row drives the mixer's monaural downmix.

Deep dive: popup geometry and the debug flag editor

Popup (window 47): x/w static (170,128); y = settings-window y + 0x16 + Σ advances above the cursor row, h = choices × 13 - 4, flipped up when the bottom would pass 0xB0. Rows advance 14 px, 20 px across the two group separators; labels ink 7 white (ink 5 teal on the indented Dual Shock sub-rows), values ink 6 gold at x+140. Committing Events to Off also zeroes the live rumble state.

The retail debug build's developer menu (overlay 0897) carries a raw EVENT FLAG index/value poke tool - not part of the retail pause menu. Two kernels are ported for completeness: the value step (Up/Down ±0x8, ±0x80 with Triangle, Left/Right ±1, clamped [0, 0xFFF]) and the wrapping list cursor. Engine: engine-core::dev_menu.

List pages: one kernel

Every class-4 list page - the Items and Magic lists, the Equip candidates, shop rows - is paged by one executable-resident kernel. What a player feels as "fixed 12-row pages" is its navigation law:

  • Up / Down move the selection within the page, wrapping at the edges - they never scroll.
  • Left / Right are the only scroll, one whole page at a time.
  • Confirming a disabled (grey) row buzzes; a list parked behind the command window draws every row white.

The menu overlay builds each row as a packed u16 - class nibble, disabled bit, alt-ink bit, 12-bit payload - and the kernel only reads them; a small global protocol carries mode, selection and scroll between the two. The per-row draw switches on the class nibble: bag rows with counts, equip candidates with slot pictograms, shop rows with prices, plain names. Engine port: engine-core::pause_screens::list_kernel_navigate + engine-ui::pause_lists.

Items screen: Use / Throw Out / Arrange

  • Use dispatches on the item's effect class from the item-effect table: three class items get their own routes - Door of Light (confirm, defaulting Yes; consumes one and exits the menu with the dungeon-escape code), Door of Wind (a destination list over the 6-byte quick-travel records; exits with the world-map warp), Incense (confirm, applies encounter suppression, stays on the screen). Everything else opens the party target panel - all-party when the effect's flag says so, single-target otherwise - and applies through the executable's item-effect applier, consuming one bag copy per use.
  • Throw Out re-points the list and confirms (Yes/No seeded to No); Yes zeroes both bytes of the bag-slot pair - the whole stack is discarded, with no compaction.
  • Arrange inverts the overlay's display-order table into an id → rank map and selection-sorts the bag pairs; emptied slots sink behind the occupied run.

The target panel (window 14) draws one block per member; for the permanent-stat Waters it previews the changed maxima or stat rows rather than plain cur / max pairs. Engine: PauseItemsSession, SpecialUseSession, target_panel_model + their engine-ui draw builders.

History: the mode-1 target panel

An earlier reading called the target panel's mode 1 an "HP-restore preview". Restore items use the plain mode-0 panel; the non-zero modes belong to the permanent-stat Waters (Life / Magic / Power / Swift / Wisdom / Guardian).

Record fields consumed

Into the 0x414-stride live record: experience +0x0, next-level threshold +0x4, HP +0x104/+0x106/+0x11C, MP +0x108/+0x10A/+0x11E, persistent AP +0x10E, growth stats +0x122..+0x12C, displayed level +0x130, spells +0x13C/+0x13D/+0x161, equipped ids +0x196.., name +0x2A7. External tables: item names 0x8007436C, spell names 0x800754D0, equipment stats 0x80074F68, accessory passives 0x8007625C, arts 0x80075EC4.

Engine port

Every draw builder lives in legaia-engine-ui - the renderer-agnostic, wgpu-free leaf - which is what lets the browser play page build these same menus without linking wgpu (engine-render re-exports the crate). The engine parses the window table off the user's disc at boot, frames each screen with the reusable 9-slice chrome, and renders:

  • The status main panel and satellites at the byte-pinned offsets, icon sprites ported verbatim from the UI-icon table and pixel-verified against golden menu captures - LV / HP / MP labels, the AP gauge with its gouraud meter fill, the equipment-slot pictograms, tab plaques, element badges.
  • The top-level menu (command rows, money box, party panel) and the Equip screen's four-window set at the traced offsets, HP / MP numbers through the ported health-tier inks (red at 0, orange ≤ max/4, gold ≤ max/2, white above).
  • The Items and Magic screens with real bag counts and the executable's text tables - 12-row pages, PAGE header, page-turn arrows, description windows - on both hosts; the Use flow's target panel with the Water previews; Throw Out / Arrange through their traced state machines; the Door of Light and Incense confirm windows.
  • Engine-styled rather than pinned: the Equip item-picker rows. Unrouted: Door of Wind's destination list.
Deep dive: shared draw primitives and CLUT staging

Three primitives render everything: the proportional string (FUN_80036888, MES control tokens 0x7C/0xCE/0xCF), the UI-icon sprite (FUN_8002C488, UV/CLUT from the 12-byte-stride table at 0x800732A4), and decimal digits (FUN_80034B78). The palette-staging global DAT_8007B454 is read only by the string primitive - icons and numbers carry their own CLUT, so a write before an icon draw is really staging the next string's palette. Staged index N selects the 16-colour CLUT at VRAM (16×(6+N), 510); main ink is entry 15 - 7 white (206,206,206), 5 teal (66,222,222), 6 gold (231,173,0), 9 orange, 2 red.

How we know

Function / dataAddressWhat it proves
Window table0x801E4738 (PROT 0899 0x15F20)52 records, byte-matched disc-vs-resident across six catalogued menu-open save states; only two fields differ at runtime.
Status panel rendererFUN_801D33D8Every position an offset from the caller-supplied window origin; the submenu fold.
SatellitesFUN_801D2094 / FUN_801D30A4 / FUN_801D31ECParty list, pager, summary layouts and sprites.
Top-level windowsFUN_801CFD68 / FUN_801D0148 / FUN_801D030CCommand rows, money / clock box, party panel.
Equip screenFUN_801D21C0 / FUN_801CF88CSlot rows, stat compare; the Best Equipment scan and both ranking laws.
OptionsFUN_801DCEF0 / FUN_801D2910 / FUN_801DA9F8Layout + row-descriptor tables at 0x801E4404 / 0x801E44B8; live config-word commits.
List kernelFUN_80032A44Page-local navigation, packed row entries, the parked-white rule.
Items flowsFUN_801D7C00 / FUN_801D7E50 / FUN_801D8734 / FUN_801D64A8Use routing, Throw Out stack discard, Arrange sort.
Pixel pinsGPU-prim scansFrame = content − 8 px; plaque tiles; cursor sprites; AP-gauge quads - from the menu_status_town capture family.

The full doc also pins the live window structs, every submenu's state machine, the Items / Magic screens in full, the prize-exchange screen, the equip stat-compare panels and the overlay-identity notes: docs/subsystems/field-menu.md. There is no inn screen in this overlay - an inn stay is field-VM script, see inn.

See also