Field Menu - Windows + Status Panel
How the pause menu is built - the screen you open in the field to check Status, change Equipment, or flip Options. It turns out to be data-driven: a 52-entry window-descriptor table on the disc places every screen's bordered windows and names each one's content-renderer function, and the renderers draw window contents only - the gold 9-slice frame (corner/edge/fill tiles stretched to any size) is emitted by the caller. This page pins the table, every screen's layout to the pixel, and the clean-room port that redraws it all.
Window descriptor table
The menu code lives in the menu overlay (a chunk of code the game streams into RAM on demand, at base 0x801CE818); the FUN_80xxxxxx names below are RAM addresses in our disassembler trace of it. The centrepiece content renderer is FUN_801D33D8, which draws one party member's status / party panel - the page the Status, Magic, Moves, and Skills tabs open. Every position it emits is an offset from the caller-supplied window origin (WX = *(i16*)(a0+0xa), WY = *(i16*)(a0+0xc)), resolved through the descriptor table.
Every pause-menu window comes from a 52-entry table in the menu overlay's data segment at VA 0x801E4738 (PROT 0899 file offset 0x15F20; parser legaia_asset::menu_windows). Each 0x10-byte record holds a content id (u8 - the SCUS content-builder dispatch selector, non-zero on the renderer-less list windows), a park-edge class (u8 - which screen edge the closed window slides toward), a window class word (u16: 2 = title tab, 3 = standard, 4 = list page), the content rect x, y, w, h (4 × i16 - the a0+0xa..+0x10 rect the content renderer receives), and a content-renderer VA (u32; 0 = content-builder-driven list window). The drawn frame extends past the content rect by 8 px on every side (the RAM GPU-prim scan of the menu_status_town capture places each window's corner tiles at content - 8). Byte-matched between the disc entry and the resident overlay across the six catalogued menu-open save states; only two fields differ at runtime.
| screen | windows (draw order) |
|---|---|
| top-level pause menu | 50 command list (24,24,104,94) → FUN_801CFD68; 49 money/play-time box (24,178,104,24) → FUN_801D0148; 51 right party panel (144,24,152,180) → FUN_801D030C |
| Status | tab 3; 26 party list (14,38,60,38) → FUN_801D2094; 27 "Condition" pager (14,92,60,10) → FUN_801D30A4; 30 summary (14,134,60,70) → FUN_801D31EC; 28 main panel (90,16,218,188) → FUN_801D33D8 |
| Equip | tab 2; 21 party (14,42,80,38) → FUN_801D2094; 23 item list (174,22,132,182) (renderer-less; lower span occluded by 22); 22 main (14,96,292,108) → FUN_801D21C0 |
| Options | tab 4; 48 settings (24,40,256,148) → FUN_801DCEF0; 47 value popup (170, *, 128, *) → FUN_801D2B44 (y/h stamped per open) |
Live windows are 0x5C-stride doubly-linked structs (descriptor id at +0x8, animated rect at +0xa): on screen exit a window slides to the nearest screen edge and parks offscreen. The top-level windows 49/50/51 stay parked in every sub-screen capture - which is how the top-level set (including the money/play-time corner box) was pinned without a top-level capture.
Tab banner + status satellites
The class-2 title-tab windows (ids 0..=4) draw no gold 9-slice frame: their entire chrome is the carved brown plaque - a left cap (208,64,8,20) at (WX-8, WY-4), a 16x20 body tile (192,64) repeated across the content width, and a right cap (216,64,8,20) at (WX+w, WY-4), all CLUT row 12 of the system-UI sheet (a CLUT is a palette stored as a row of video-memory pixels; the same sprite sheet re-colours per row). Pinned by a RAM prim scan over the menu_status_town capture. The tab renderer (FUN_801DCAD8 for Status) draws only the label at the content origin, staged text CLUT 7.
The Status screen's satellites: the party list (FUN_801D2094) puts each name at WX+6 on a 0x0e pitch, always CLUT 7, and marks the highlighted row with the 16x16 pointing-hand sprite at WX-0xc (animated-cursor primitive FUN_8002b994, kind 0 of the sprite table at 0x80073d18, UV (152,64) CLUT row 7). The "Condition" pager (FUN_801D30A4) draws its label at WX+6 flanked by the solid triangles - kind 2 (UV (168,8)) at WX-0x10 and kind 3 (UV (168,40)) at WX+0x3A, both at WY-2. The summary window (FUN_801D31EC) draws the name, the LV icon at (+0x1c,+0xf) with the 2-digit level at (+0x2c,+0xd), and "ATR:" with the per-character element icon at +0x20: a 0xCE-token string (0x801E4720 + char*4) resolving through the 0x80074050 aux table to ICO codes 0x94/0x96/0x95 - 28x12 sprites in the system-UI extension strip TIM at PROT.DAT[0x10178] (sheet V 192..224), decoded with the VRAM row-500 palettes of the sibling TIM at PROT.DAT[0x10028].
Plumbing
The highlighted member index is uVar1 = *(u8*)(0x80084598 + (DAT_801e46c4 & 0xfff)); its live record is 0x80084708 + uVar1*0x414 (the 0x414-stride party record array). s8 is a running Y cursor advanced down the page (+0x13 after the header, then +0x2f / +0x2b / +0x38 between status sub-blocks); s7 resets to WX or WX+0x10 per block.
Status page layout
Offsets relative to (WX, WY). Header row at Y=0: name (+8), "LV" label (+0x50), LV value (+0x60) from record +0x130. HP row at Y+0x13 and MP row at Y+0x20 each place current / max / base at X+0x30 / +0x58 / +0x84 (HP = record +0x106/+0x104/+0x11c, MP = +0x10a/+0x108/+0x11e). AP gauge at (X+0x40, Y+0x2d): four 1:1 sprites from the system-UI sheet (CLUT row 4 — arrow cap with the red "AP" chip, trough, value box, right tip), plus the content renderer FUN_8002c0b0, which fills the meter with two untextured gouraud quads spanning x+0x1B .. x+0x1B + value/2 (dark-red (80,20,10) → gold (C0,A0,40) → dark-red vertical gradient, drawn over the trough) and prints the value from 6x6 digit records (tens at +0x50, ones at +0x56; a dedicated "100" glyph at full charge). Six growth stats in a 3x2 grid at rows Y+0x42 / +0x4f / +0x5c (left col label X+0, right col label X+0x74). Seven equipment slots start at Y+0x6d (icon X+0, name X+0x10; slots 4..6 in a right column at X+0x6a/+0x7a). Experience at Y+0xa5, Next Level at Y+0xb2 (values at X+0x78).
Equip screen
Four descriptor-table windows (draw order: tab 2, party 21, item-list 23, main 22 — the main window's opaque interior occludes the item-list window's lower span). The tab renderer FUN_801DCA94 draws the "Equip" string at the tab origin (the carved banner behind it is caller art). The party window FUN_801D2094 (shared with the status screen's party list) draws each present member's name at X+6 on 0xE-pitch rows, with the pointing-hand cursor at X-0xC on the focused row.
The main window FUN_801D21C0 draws "Best Equipment" at (X+0x10, Y), then 7 slot rows at Y+0xE*(i+1): hand cursor at X, 12x12 slot pictogram at X+0x10 (the fixed DAT_801E43F4 code array — fist / helmet / armor / boot / 3x Goods ring), equipped item name at X+0x20 (id from record[0x196 + slot_off], name via the item table 0x8007436C). With the cursor on "Best Equipment", a preview pass draws per-armament change arrows at X+0x8E with candidate class icon + name at X+0xA8/+0xB8, and a stat-compare block at rows Y+0x48/+0x55/+0x62: label X+0xA0, current 3-digit value X+0xC8, up/down arrow X+0xE4 and preview value X+0xF0 when it differs. With the cursor on a slot row, the item's description panel draws at (X+0x94, Y+0xC) over two shade boxes. The id-23 item-list window is renderer-less (frame-only container).
Best Equipment: how the candidates are picked
FUN_801CF88C seeds the four-slot candidate array with what the character already wears, then walks the bag keeping one winner per armament slot. An id competes only when its item record +0 is 1 (equipment) and its equipment record +6 character mask shares a bit with the per-character mask byte. The slot it competes for is the equipment record's +7 bits permuted: raw (bits & 0x60) >> 5 is body / head / weapon / footwear, and the candidate array is indexed weapon-first, so the mapping is [2, 1, 0, 3].
Two ranking laws the screen itself never reveals. Armour ranks on UDF + LDF only (equipment record +2 plus +3): the INT and SPD bonuses are never read, so a pure-INT head accessory or a pure-SPD boot can never displace an incumbent. And a weapon's category check dominates its attack: the weapon score is equip[+1] + FUN_801DD0C0(char, id, 1), and that check returns a flat 1000 or 0 - 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 and ties keep the incumbent. Everything around the scan is a trial equip the routine undoes before it returns. Engine port: equip_session::best_equipment_candidates.
Options screen
The settings rows are data-driven: the id-48 renderer FUN_801DCEF0 wraps the row renderer FUN_801D2910, which walks a 10-entry display layout ([u16 row_id, u16 advance] at 0x801E4404; advance 14 px, 20 px on the two group separators) joined to 8-byte row-descriptor nodes at 0x801E44B8 ([config_word_ptr][value_count][label_ink][row_id][string_index]). A row's value string is strings[string_index + value + 1] in the shared pointer table at 0x801E442C. Cursor arrow at content x-10, labels at x+8 (ink 7 white; ink 5 teal on the indented Dual Shock sub-rows), values at x+140 (ink 6 gold).
| row | choices | config word |
|---|---|---|
| Battle Camera | Close / Normal / Far | 0x800846C0 |
| Battle Select Attack | Select / Automatic / Command | 0x800846C4 |
| Battle Command | Directional Buttons / ✕-glyph " button" | 0x800846C8 |
| Field Move | Walk / Run | 0x800846CC |
| Field HP Display | Immediate / Gradual / Display Off | 0x800845C4 |
| Sound | Stereo / Monaural | 0x800846BC |
| Dual Shock (header) | - | - |
| Battles / Events / Encounters | Vibration On / Off | 0x800845C8 / 0x800845A8 / 0x800845CC |
Interaction (FUN_801DA9F8): Up/Down skip valueless rows; Cross opens the value popup (window id 47 - x/w static (170,128), y/h stamped per open: y = id-48 y + 0x16 + Σ advances above the cursor row, h = choices×13 - 4, flipped up when the bottom would pass 0xB0); Cross inside commits the choice directly into the config word (committing Events to Off also zeroes the live rumble state), Circle backs out - edits are already live, nothing reverts. 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 audio mixer's monaural downmix.
Dev-menu EVENT FLAG editor (debug build only). The retail debug build's developer menu lives in overlay 0897 (the warp-applier + flag-editor toolset), not in the retail pause menu; its EVENT FLAG editor is a raw index/value poke tool. Two value-adjust kernels are ported for completeness: the value step FUN_801dbd04 (Up/Down move by 0x8, or 0x80 while Triangle is held; Left/Right by ±1; clamped to [0, 0xFFF]) and the list cursor FUN_801db8f4 / FUN_801db8b4 (prev/next, each wrapping across the 'X' end sentinel of the stride-0xA flag table). Both read the packed pad words from FUN_8001822C, not the raw-BIOS pad layout. Engine: engine-core::dev_menu (EventFlagEditor); the row render stays with the UI crates.
The kind-4 list kernel (SCUS FUN_80032A44)
Every class-4 list page (the Items / Magic lists, the Equip candidate list, shop rows) is paged by one SCUS-resident kernel, FUN_80032A44, run per frame for each live list window whose id byte is in the allowlist at 0x80073E1C. The list node (built by the allocator FUN_80030104: count*2 + 0x2A bytes) holds the scroll top, visible-row count, row count, selection, and per-row u16 entries packing [class nibble][0x800 disabled][0x400 alt-ink][12-bit payload] - the menu overlay rebuilds the entries (FUN_80030628), the kernel only reads them. The overlay talks to the kernel through a small global protocol: _DAT_8007BB94 = mode/result (0 idle, 1 browsing, 2 row confirmed, 3 cancelled, 4 parked behind the command window), _DAT_8007BB88 = the selected row's payload (the bag-slot index on item lists), _DAT_8007BB9C = its class nibble, _DAT_8007BB90 / _DAT_8007BB98 / _DAT_8007BBA0 = scroll top / selected row / row count - with every phase step gated on the window-slide latch _DAT_8007BB80 == 0.
Navigation: Up/Down move the selection within the page (wrapping at the page edges) and never scroll; Left/Right are the only scroll, one page at a time - which is why the lists read as fixed 12-row pages. Confirming a disabled row buzzes. Row ink stages CLUT-7 white, dropping to CLUT-0 grey on the row's 0x800 bit - unless the list is parked (mode 4), which forces every row white; that is the whole white-page-then-grey focus behaviour of the captures. The per-row draw switches on the class nibble: bag rows with 8-px digit-cell counts, equip-candidate rows with a slot pictogram, accessory-passive rows, shop rows with 5-digit prices, plain name rows. The header's "PAGE" small-cap tag is UI-icon sprite 0x76 (atlas UV (80,136), 24x8, the teal ink) with gold digit sprites 0x7A..0x83 and the slash 0x79; the blink-gated page triangles are ICO 0x27/0x28, the right one resolving to the capture-pinned (WX+0x84, WY+0x53) on the live list rect. Engine port: engine-core::pause_screens::list_kernel_navigate (page-local wrap + page flip); the row / header draws stay in engine-ui::pause_lists at the pinned pens.
Items screen: Use / Throw Out / Arrange
The Items command window's three rows are fully traced state machines. The command SM FUN_801D7C00 (submenu 5) routes Use to submenu 6, Throw Out to submenu 7, and runs Arrange in place via the sort kernel FUN_801D64A8: it inverts the overlay's display-order table at 0x801E4A88 into an id → rank map and selection-sorts the bag slot pairs by rank, emptied slots sinking behind the occupied run (engine engine-core::menu_arrange). Every confirm first re-runs the bag scan and buzzes on an empty bag.
Use (FUN_801D7E50): a pick dispatches on the item's effect class (item record +1 into the 0x800752C0 item-effect table): class 0x80 → Door of Light, 0x81 → Door of Wind, 0x82 → Incense; anything else → the all-party apply (FUN_801D7FF8) when the effect's flag bit 0x20 is set, else the single-target apply (FUN_801D8308). All five routes are traced; engine kernel engine-core::pause_screens::use_route_for_effect. The two apply routes open window 14, the party target panel, and apply through the SCUS item-effect applier FUN_800402F4 + ability-bit rebuild FUN_80042558, consuming one bag copy per use; the single-target route re-validates each confirm through FUN_8003FB10 (failure buzzes) and opens a notify window when the applier reports a result.
Party target panel (window id 14, rect (174,28,132,176), renderer FUN_801D0520): one block per roster member - name / LV header, then HP / MP or stat rows switched by the preview word derived by FUN_801D6A54. Mode 0 draws plain cur / max pairs through the health-tier inks; the non-zero modes are the permanent-stat Water previews - effective ( base ) maxima for Life / Magic Water, single ATK / SPD / INT stat rows (via the equip-stat aggregator FUN_801CF650) for Power / Swift / Wisdom Water, both defence rows for Guardian Water. The earlier "HP-restore preview" reading of mode 1 is superseded: restore items use the plain mode-0 panel. The hand cursor decodes DAT_801E46C4 (hide bit, all-row bit for the all-party hand, static-sprite bit). Engine port: engine-ui::target_panel_draws_for / target_panel_sprites_for, fed by engine-core::pause_screens::target_panel_model.
The class-item routes: Door of Light (FUN_801D8A58) is a Yes/No confirm defaulting to Yes that consumes one id-0x88 Door of Light and exits the whole menu with the field-side dungeon-escape code (_DAT_8007B43C = 4). Door of Wind (FUN_801D8B90) opens window 11 - a kernel-driven destination list - and a pick reads the 6-byte quick-travel placement record at 0x80073A98 + slot*6 (parser legaia_asset::worldmap_menu), consumes one 0x89 and exits with code 5, the world-map warp. Incense (FUN_801D8D94) confirms, consumes one 0x8A, applies the encounter suppression through the standard applier, and stays on the Items screen. Engine: engine-core::pause_screens::SpecialUseSession.
Throw Out (FUN_801D8734): re-points the live list window to descriptor 16, and a pick opens the confirm window (id 9, renderer FUN_801D1B20 - item name + count, "You are about to" / "Throw out?", Yes/No seeded to No). Yes zeroes both bytes of the selected bag-slot pair - the whole stack is discarded, no compaction - with a scroll fix-up when the last row is deleted. Engine: items_screen_draws_for + items_throw_confirm_draws_for over engine-core::pause_screens::PauseItemsSession, with discards reaching the world bag through field_menu_dispatch::apply_inventory_outcome.
Draw primitives + CLUT staging
Three shared primitives render everything: FUN_80036888 (proportional string; MES control tokens 0x7c/0xce/0xcf), FUN_8002c488 (one UI-icon sprite, UV/CLUT from a 12-byte-stride table at 0x800732a4), and FUN_80034b78 (decimal digits vs the powers-of-ten table at 0x80073dcc). The palette-staging global DAT_8007b454 (in-primitive CLUT = index + 0x7f86) is read only by the string primitive; icon and number primitives carry their own CLUT, so a write before an icon/number draw is really staging the next string's palette. Values seen: 7 (text), 5 (separators), 6 (magic header / skill labels), 9 (moves header), 4 (skill passives), 1 (command label), 0 (unselected rows). Each staged index selects a 16-colour CLUT at VRAM (16*(6+index), 510); the main ink is palette entry 15 — 7 = white (206,206,206), 5 = teal (66,222,222), 6 = gold (231,173,0), 9 = orange (222,90,0), 2 = red (231,33,0).
Record fields consumed
Into the 0x414-stride live record: experience +0x0, next-level threshold +0x4, HP +0x104/+0x106/+0x11c, MP +0x108/+0x10a/+0x11e, HP-bar +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 named here lives in legaia-engine-ui, not legaia-engine-render. engine-render re-exports the whole crate (pub use legaia_engine_ui::*), so an engine-render:: path still compiles — but the code is in engine-ui, and that split is the point: engine-ui is the renderer-agnostic, wgpu-free leaf, which is what lets the browser play page build these same menus without linking wgpu.
The clean-room engine parses the window-descriptor table from the user's disc at boot (legaia_asset::menu_windows) and frames each screen's window set with the reusable 9-slice primitive engine-ui::menu_window_chrome_draws_for, placed on the shared 320x240 boot-UI stage via engine-ui::scale_stage_text_draws. The status main panel renders through engine-ui::status_screen_draws_for at the byte-pinned offsets, hung off the id-28 content origin; satellites through status_satellite_draws_for; the top-level list / money box / party panel through field_menu_draws_for + field_menu_info_draws_for. The status page's icon primitives are ported from the 0x800732a4 UI-icon table records verbatim and pixel-verified against the golden menu captures: the LV / HP / MP labels (CLUT row 1), the AP gauge with its gouraud meter fill and value digits (CLUT row 4, via engine-ui::status_icon_sprites_for; the value feeds from the persistent record +0x10E AP), and the seven equipment-slot pictograms (CLUT row 8; the menu overlay's fixed DAT_801e43f4 code array — fist / helmet / armor / boot / Goods ring). The satellites are sprite-ported at the traced offsets (status_satellite_icon_sprites_for: pointing hand, pager triangles, summary LV + ATR element icons), the title tabs wear the carved plaque via the shared engine-ui::tab_banner_draws, number fields sit on the retail fixed 8-px digit cells, and the parenthesised base/growth groups use the retail teal ink (66,222,222) with body text in the CLUT-7 white (206,206,206). The Equip screen renders its retail four-window set through equip_screen_draws_for + equip_screen_sprites_for at the traced FUN_801D21C0 / FUN_801D2094 offsets, and the top-level menu renders the traced row / money-box / party-panel content (field_menu_draws_for + field_menu_info_draws_for + field_menu_icon_sprites_for, with the AP gauge shared through ap_gauge_sprites). The HP / MP number fields ink through the ported health-tier fns (engine-ui::menu_hp_ink / menu_mp_ink, from FUN_800349EC / FUN_80035EA8: red at 0 HP, orange at ≤ max/4, gold at ≤ max/2, white above — ink RGBs decoded from the string-CLUT rows at VRAM row 510). The Items and Magic screens render their retail four-window sets too (items_screen_draws_for / magic_screen_draws_for + their sprite builders: command window, 12-row list pages with the PAGE header and page-turn arrows, info windows with the disc-parsed item / spell descriptions, the id-17 renderer's extra widget box, and the caster window's MP cur / max through the tier ink), fed from engine-core::pause_screens with real bag counts and the executable's text tables — on both the native window and the browser play page. The Use flow's target pick renders the retail window-14 party panel (target_panel_draws_for / target_panel_sprites_for over pause_screens::target_panel_model, with the Water previews), and the Throw Out / Arrange routes run their traced state machines through PauseItemsSession. The two special Use routes that raise a confirm window — Door of Light (window 10, FUN_801D1DAC) and Incense (window 12, FUN_801D1F10) — open from the Use list and draw on both hosts through engine-ui::confirm_prompt_draws over SpecialUseSession. Still engine-styled: the Equip item-picker rows. Still unrouted: Door of Wind, whose submenu opens a destination list rather than a prompt.
See also: save screen · new-game table · spell table · item table