At a glance

Stock
Inline in each scene’s MAN script: field-VM op 0x49 sub-0 = [count][item ids][name]
Prices
SCUS_942.54 item table, halfword at record +2; price 0 = never sold
Screens
Menu overlay (PROT 0899) window-descriptor table; no separate shop overlay
Purses
Gold _DAT_8008459C; Point Card _DAT_800845B4; casino coins _DAT_800845A4 - all capped at 9,999,999
Stack cap
99 per item id, enforced by the list ink and the quantity clamp
In the port
engine-menus::shop, engine-screens, legaia_scene_formats::shop_stock
Confidence
Confirmed - stock scan validated disc-wide; every screen traced in the menu overlay
Used by
Shops browser, ROM patcher shop shuffle, inventory

Flow

Merchantfield-VM op 0x49 Buy / Sell / Quitmode panel Buy liststock x item prices Sell listthe bag, half price Who is it for?equipment only Quantitystepper, commits Goldmoves Exitscript resumes back to the list - keep shopping Quit / cancel
Every commit returns to the list so the player keeps shopping; the only way out is Quit, or a cancel, on the mode panel. Cancel is Circle at every level - the mode panel, both lists, the quantity stepper and the who-is-it-for rows each back out one step; Triangle, which retail leaves unread here, is the port’s second cancel. Sell and Quit grey out together when the bag is empty, and a confirm on Sell then buzzes and keeps the mode panel up: an empty bag never reaches the sell list.
  1. Talk to the merchantthe script reaches op 0x49 sub-0; the field VM parks and hands the stock bytes to the menu
  2. Pick a listbuy = the stock, priced from the item table; sell = the bag. Rows grey when unaffordable or at 99
  3. Quantitybuy max = min(gold / price, 99, 99 − held); sell max = held. Equipment asks who it is for and equips on the spot
  4. Commitgold moves, the bag updates, a Point Card accrues 5%
  5. Quitthe op’s tristate flips to Done and the script resumes past the merchant

Where the stock comes from

The MAN is each scene’s script-and-data bundle. Inside it, a merchant is one field-VM instruction carrying a count, a run of item ids and the vendor’s name. The count over-counts: it includes a trailing run of unsellable, price-0 placeholder ids that the on-screen shop skips. The price table therefore doubles as a sellable mask: a record must lead with a priced item, and the unpriced tail is trimmed. Every shop on the disc partitions cleanly this way.

TrapRule that avoids it
Requiring every id to be sellablerejects nearly every shop - most records carry the placeholder tail. Trim, do not reject.
Walking opcodes linearlydesyncs on a dialogue picker’s jump table and silently misses shops (Biron Monastery). Byte-scan for the op signature instead.
Trusting the CDNAME labela #define name N names extraction entry N − 2; resolve the scene through block_for_extraction_index.

Read a town’s stock off a disc with asset shop-stock --prot PROT.DAT --scus SCUS_942.54 --cdname CDNAME.TXT --scene bylon; it prints “decodes 10 ids, sells 7” rather than truncating. The casino prize counter is a different system: its table lives in the menu overlay and debits the coin bank, not gold.

The shop’s own cues. The Buy / Sell / Quit panel is paged by the overlay’s picker, so a pick sounds its bare 0x36; the handler overwrites it twice - Quit sounds the cancel 0x37, and Sell over an empty bag buzzes 0x23. The buy and sell lists confirm with 0x20 and buzz 0x23 on a dim row (a buy the purse cannot cover). Dismissing the Point Card toast sounds 0x20.

Details: the prize counter’s sound cues

The counter session (FUN_801DC1CC) never picks a cue for a plain step: the list kernel FUN_801D688C pushes step 0x21, confirm 0x36 or cancel 0x37 itself, and the session overwrites it where it disagrees. A redeem the coin bank or the 99 cap refuses becomes the buzz 0x23 (0x801DC3F8). A confirm on the Yes / No prompt becomes cue 0, which plays nothing, so No is silent and Yes is heard as the coin jingle 0x25 the commit state pushes on the next frame (0x801DC4C0). In the port, PrizeEvent::cue reaches both hosts on the same channel the shop’s own screens use.

The commit is its own frame. Yes only stores the commit state; the next frame reads no pad, grants the prize, debits the coin bank and rebuilds the rows. The port answers Yes with PrizeEvent::Committing and the grant with PrizeEvent::Redeemed on the tick after, and the kernel differential runs the session against FUN_801DC1CC frame by frame.

Up and Down only, and only once the windows land. The list and the Yes / No prompt both step through FUN_801D688C, which reads Up and Down and nothing on the other axis. Each input phase also reads nothing while a window is sliding: the opening slides the exchange windows in, an affordable row's confirm slides the prompt window in, and No, a cancel or the commit slides it out. The port's counter waits the same way (field menu).

Prices, purses and the Point Card

  • Buy price is the item table halfword; there is no per-shop price anywhere.
  • Sell price is half the item table's price times the quantity, rounded down. A zero-priced item shows “Cannot sell”.
  • Stack cap 99. A row greys once the held count reaches 99, and the quantity picker clamps to 99 − held.
  • Point Card (item 0xFE) is a third purse, passive while held. Each purchase credits price / 20 per unit - 5% - before the gold debit; sales never accrue. A one-line toast (window 31) shows the balance after each buy and holds until a confirm or cancel press, which plays cue 0x20 on the way back to the list; without the card the buy returns to the list on the confirm’s own frame.
  • Buying equipment first asks who it is for: a party row checks the equip mask and, on a match, equips immediately and returns the replaced piece to the bag. The picker’s rows are the present party, not the four character records: the cursor gets party-size rows (0x801DB434) and a confirmed row resolves to its character through the party list (0x801DB554), so a lone Noa is row 1 and a member who has not joined has no row. The piece lands in the equip byte its own class names (0x801DB5E8..0x801DB670): body armour in byte 0, head gear in 1, footwear in 4, and a weapon in the wearer’s own weapon byte (2 for Vahn and Gala, 3 for Noa) - the same bytes the pause menu’s Equip screen writes, so a piece is worn in one place whichever screen put it on.
Details: what spends the Point Card

The bank’s only debit is a battle effect arm: entry 14 of the effect jump table takes min(bank, 9999), subtracts it from the bank and applies it as damage to the target - the Point Card strike. No item descriptor on the disc selects arm 14; the battle-action caller passes the actor’s +0x1E8 byte as the selector. The card’s descriptor carries flag bit 0x40, shared only with the summon flutes: that bit is the descriptor’s target side (set = the enemy party), read at 0x801D18E0 in FUN_801D0748 and forked with 0x20 into four target modes - so the five carriers are exactly the enemy-directed consumables.

Screens and row ink

Each shop window is a record in the menu overlay’s window-descriptor table (field menu); the 9-slice frame is caller-drawn and each renderer only paints content from its window origin. Text indents 20 px, lines are 14 px apart.

WindowRectContent
0x2A mode panel(42, 46, 80×38)Buy / Sell / Quit rows; bobbing cursor on the selected row
buy listlist windowcontent-builder list (case 0x0B), priced against gold; the row words are [class][0x800 dim][item id] and the record’s last entries are hoisted to the top
35 quantity(138, 100, 168×50)“Have N”, “How many will you buy?”, then quantity / bound at +34 px; the price appears once, in the running total, whose digit-field width comes from the unit price
39 sell detail(14, 95, 144×53)name, description, price row (5-digit, half price), accessory passive name + description
31 Point Card toast-8-digit balance between a heading and a “point(s)” label; the quantity commit closes the stepper first, so the toast rides over the name, gold, detail and compare windows alone

The last rows come first - for Platinum Card holders. The buy list is built by the SCUS content builder’s case 0x0B, which splits the record at count − 3: entries at or past the split go into the row buffer first, drawn in the teal pen even when the player cannot afford them, and the rest follow. Those last three record slots are walked only when the party carries or wears item 0xFF, the Platinum Card; otherwise the builder stops three entries short. Every shop record reserves the three slots and pads the unused ones with unsellable template ids, so the card’s exclusive strip is 0, 2 or 3 rows wide - and it lines up one-for-one with the * markers the walkthrough tables carry. Those markers were read as “new in this town”, but the same item is starred in more than one town. A retail capture confirms the gate: Retock’s Items Shop builds thirteen rows, the last three first, while the party carries the card, and ten rows in record order once the card is taken out of the bag.

Row ink is last-rule-wins. Three tests run in order and each overwrites the last: a full stack greys the row; the record’s “owned / restricted” marker re-inks it to the accent pen even so; an unaffordable price greys it again even so. On the mode panel, an empty bag dims Sell - and Quit with it, because nothing restores the ink between the two draws.

Details: cursor word bits and the window choreography

The picker cursor word (DAT_801E46BC): low 12 bits = selected row; bit 0x1000 = blink phase; bit 0x2000 = parked presentation (every row drawn, keyed to blink); bit 0x4000 = cursor suppressed. The stock list re-runs the same decode against _DAT_8007BB98; shared kernel engine-menus::shop::shop_cursor_mode.

Opening and closing slide the windows through widget scripts: the open script (DAT_801E4E38) brings in vendor name 0x21, mode panel 0x2A, gold 0x20, 0x28 and 0x22; the Sell transition (DAT_801E4E54) slides away 0x28 / 0x2A / 0x22 and keeps the gold and name plates. The randomizer’s Seru-trading vendor patches exactly these seams to grow the panel to four rows.

Leaving runs the same machinery backwards. Quit, or a cancel on the mode panel, selects sub-screen 0 (FUN_801DD12C): its first frame hands the widget VM the close-all script 0x801E4A78, every window slides back to its park edge, and only once the widget VM is idle does the fade start. The exit draws no text of its own - the next line is the merchant’s script resuming past op 0x49. The port’s ShopExit beat is that sub-screen: the windows keep their content while they slide out, and the session is dropped on the way out of the beat.

The opening slides start on the tick after the fade lands, so the first window frame is already one step in from the park edge. Retail then holds black for about fifty frames while the overlay loads; the port does not reproduce that wait.

Every equipment bonus record on the disc carries the 0x40 no-passive sentinel, so the sell panel’s passive lines always come from the item-effect table arm.

Window text and the compare panels

No shop label is computed. Every string a window prints sits in the menu overlay’s own string pool and is loaded by address by that window’s renderer. The port reads the same strings out of your disc’s PROT 0899 at boot (engine-menus::shop::ShopLabels); a session without the overlay draws those windows with no label text rather than with invented text.

WindowRendererWhat it prints
42 pickerFUN_801D4868the three mode rows
41 party compareFUN_801D4C28per-member stat block, or one of two notes
35 buy quantityFUN_801D5510held count, prompt
36 recipientFUN_801D56FCbag row, then one row per present member
37 sell quantityFUN_801D5944prompt
39 sell detailFUN_801D5AE8price, or the unsellable note
31 Point Card toastFUN_801DCE20heading, balance, unit

Three of the labels carry the string primitive’s two-byte name escapes, expanded at draw time (FUN_80036888): each recipient row opens with 0xC1 n, character n’s name, so a row reads as that character’s own sentence; the toast heading opens with 0xC2 0xFE, the Point Card’s item name. An escape’s operand can be 0, so a plain C-string read stops inside the label - the port keeps escapes whole and resolves them off the live roster.

The party compare (window 41)

One block per present party member, walked through the party list; a member who has not joined has no block. Each block takes the first rule that applies:

Already equipped?8 equip bytes vs item Not equipment?item record +0 != 1 Mask excludes?equip record +6 Trial equipwrite, recompute, restore no no no Equipped notenothing else Current statsATK / UDF / LDF, no arrows Cannot-equip notecharacters 0..=2 tested Current, arrow, trialarrow only where it changes yes yes yes
The trial equip writes the staged piece into the slot byte its class names, recomputes the stats (FUN_801CF650 around FUN_80042558) and restores the byte. The class is not the byte index: the weapon goes to a per-character slot, and head, body and legs map through a small remap table.

The rise and fall arrows are not font characters but cells of the menu-glyph page, the row right after the numerals (FUN_8003C1F8, glyphs 4 and 5, in the rise and fall inks). The port attaches those cells to the font with the numerals, so every separator glyph - the compare arrows here and on the Equip screen, the quantity windows’ slash - draws from the same page.

The recipient rows and the item detail line

  • Recipient (window 36). The bag row first, then one row per present member. A member the item’s mask excludes keeps the row in the dim ink; the hand still lands on it, and a confirm there buzzes.
  • Item detail (window 34). The item name and held count, then the executable’s detail printer (FUN_800337B0): an accessory with a passive prints the passive’s description; anything else prints its description. Equipment without one prints a slot template naming who can wear it, followed by one or two label / value pairs taken from the equipment record’s own bonus bytes - ATK for a weapon, UDF and INT for headgear, UDF and LDF for armour, ATK and LDF for footwear.

All of this is capture-confirmed on Retock’s Arms Shop and Items Shop. The shops are interiors of the same scene reached only through their doors: a position poke straight into a shop leaves its room inactive, its merchant neither drawn nor talkable, so the probe pokes the door tile instead and talks to the merchant from there.

In the port

The stock scan is legaia_scene_formats::shop_stock; the session, quantity stepper, recipient picker, Point Card bank and row-ink rules live in engine-menus::shop; engine-menu-runtime’s MenuRuntime runs the session over a live world; and the screens - every window above, the prize exchange and the coin counter - are composed once in engine-screens and drawn by both play hosts. The grant kernel refuses a buy past 99, as the retail clamp does. Gold and the bag move when the quantity stepper confirms - retail has no Yes / No between the number and the transaction - and the session’s standalone buy / sell kernels pay the same sale credit as the stepper, (price × qty) >> 1.

A merchant’s record reaches op 0x49 through a talk, so what parks on the op is the inline dialogue runner, not the field script. The runner holds the conversation on the op while the shop is armed and takes the Done edge on the frame after the screen closes, which is what clears the arm; a talk that ended at the op instead would leave a stale Done edge, and the next shop talk would spend it and open nothing. The casino prize counter parks the same way: retail’s op 0x49 ignores the sub-op while the park word _DAT_8007B450 is up, taking the Done arm on 1 and otherwise returning the unchanged PC (0x801E097C), so every screen the op opens holds its record until it closes.

The literal restore amounts of consumables stay curated, because they live in overlay code rather than in the item-effect table.

How we know

FunctionAddressWhat it provesDump
buy quantity + commitFUN_801DB7F4max clamp literals 0x63; Point Card price / 20 before the gold debitoverlay_menu_801db7f4.txt
recipient pickerFUN_801DB380equip-mask check, equip-on-buy, same accrual; reads price at item record +2-
sell quantityFUN_801DBD94credits (price × qty) >> 1; scroll fix-up-
buy-list confirm routeFUN_801DB21Caffordability refusal + item-kind switch-
mode panelFUN_801D4868row geometry, empty-bag dim, cursor word bitsoverlay_shop_save_801d4868.txt
picker inputFUN_801DAFD4row clamp li a1,0x3 at 0x801DB098; Quit at 0x801DB0D0-
quantity panelFUN_801D5510window 35 layout; digit-field width from unit price-
sell detail panelFUN_801D5AE8window 39 layout; passive index re-derived per draw-
shop buy-list rowFUN_80030628 case 0x0Brow greys at sltiu v0,v0,0x63 (0x80030F0C); reads gold vs item pricefuncs/80030628.txt
Point Card toastFUN_801DCE20window 31 renderer-
Point Card strikejump table 0x80014FA0 entry 14 (0x8004209C)the bank’s only debit-
casino prize listFUN_801D5DE0window 44; indexes 0x801E4518, gates on the coin bank - not the shopoverlay_shop_save_801d5de0.txt
History: the casino list filed as the shop list

The 8-row, 14 px layout of FUN_801D5DE0 was once filed as the shop buy list on the strength of its dump filename, overlay_shop_save.bin. That name identifies the image the routine came from, which carries casino code too; the routine itself never reads the gold purse. The shop’s own row layout is the content builder’s (case 0x0B), traced separately above.

See also