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.

screenwindows (draw order)
top-level pause menu50 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
Statustab 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
Equiptab 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
Optionstab 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).

Top-level pause menu

Three descriptor-table windows. The command list (id 50, FUN_801CFD68) draws seven rows at (WX+0x14, WY + n*0xe) in the retail order Items, Magic, Equip, Status, Options, Load, Save - all CLUT-7 white; the selection is the pointing-hand cursor at (WX, row_y) (FUN_8002b994), and blocked rows gray to CLUT 0 (Load under a dialog-context lock, Save when the save-enabled flag DAT_8007b6a8 is clear). The money / play-time box (id 49, FUN_801D0148) draws the money pictogram (ICO 0x62) at (WX, WY+2) with an 8-digit amount at +0x28, an optional casino-coin row (ICO 0x66, bank 0x800845A4), and the clock row (ICO 0x63) from the 60 Hz tick counter 0x80084570 - hours 3-wide at +0x20, colons at +0x38/+0x50, zero-padded minutes / seconds at +0x40/+0x58. The party info panel (id 51, FUN_801D030C) lays one block per roster member at stride 0x3e: name at +0x10, LV icon + 2-digit level at +0x70/+0x80, HP / MP label sprites (ICO 0x3f/0x40 - the same sheet rects as the status page labels) at +0x28 with 4-digit current/max fields at +0x38/+0x60, and the kind-0x31 AP gauge at (+0x28, +0x29) fed from the persistent AP +0x10E. HP / MP ink runs through per-member health-tier color fns (FUN_800349EC / FUN_80035EA8).

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

rowchoicesconfig word
Battle CameraClose / Normal / Far0x800846C0
Battle Select AttackSelect / Automatic / Command0x800846C4
Battle CommandDirectional Buttons / ✕-glyph " button"0x800846C8
Field MoveWalk / Run0x800846CC
Field HP DisplayImmediate / Gradual / Display Off0x800845C4
SoundStereo / Monaural0x800846BC
Dual Shock (header)--
  Battles / Events / EncountersVibration On / Off0x800845C8 / 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