Shop
Talk to a merchant, pick Buy or Sell, choose an item and a quantity, confirm, and the gold moves. The surprise is where the data lives: a merchant’s stock list is not a table anywhere on the disc - it sits inline in the town’s own event script - while every price comes from one item table in the main executable. Only the screens are shared code, hosted by the same menu overlay that draws the pause and save screens.
At a glance
- Stock
- Inline in each scene’s MAN script: field-VM op
0x49sub-0 =[count][item ids][name] - Prices
SCUS_942.54item 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
- Engine
engine-core::shop(ShopSession, quantity / recipient sessions),shop_catalog,legaia_asset::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
0x49 sub-0; the field VM suspends and hands the stock bytes to the menufield VM
2Mode panelBuy / Sell / Quit; Sell and Quit grey out when the bag is emptymenu overlay
3Listbuy list = the stock ids priced from the item table; sell list = the bag; rows grey when unaffordable or at 99menu overlay
4Quantity + confirmbuy max = min(gold / price, 99, 99 − held); sell max = held; equipment asks who it is for and equips on the spotmenu overlay
5Commitgold moves, items go through the inventory add / consume helpers, the Point Card accrues 5%inventory helpers
6Quitthe op-0x49 tristate flips Armed → Done and the script resumes past the merchantfield VM
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.
| Trap | Rule that avoids it |
|---|---|
| Requiring every id to be sellable | rejects nearly every shop - most records carry the placeholder tail. Trim, do not reject. |
| Walking opcodes linearly | desyncs on a dialogue picker’s jump table and silently misses shops (Biron Monastery). Byte-scan for the op signature instead. |
| Trusting the CDNAME label | a #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.
Prices, purses and the Point Card
- Buy price is the item table halfword; there is no per-shop price anywhere.
- Sell price is exactly half the buy price. 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 creditsprice / 20per unit - 5% - before the gold debit; sales never accrue. A one-line toast (window 31) shows the balance after each buy. - 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.
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.
| Window | Rect | Content |
|---|---|---|
0x2A mode panel | (42, 46, 80×38) | Buy / Sell / Quit rows; bobbing cursor on the selected row |
| buy list | list window | content-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 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-core::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.
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.
Engine port
| Retail piece | Engine mirror |
|---|---|
| stock record + prices | legaia_asset::shop_stock scan → shop_catalog → ShopInventory on World::shops.scene_shops |
op-0x49 trigger | FieldHost::op49_menu_request → World::try_arm_field_shop / finish_field_shop; non-shop payloads (inn, save prompts) fail validation and arm nothing |
| screen routing | engine-vm::menu (commit_route on Cross, back_route on Triangle); ShopExit auto-advances after the fade |
| commit | ShopSession::try_buy / try_sell; grant kernel World::buy_from_shop refuses a buy past 99 |
| Point Card | World::minigames.point_card bank, credit_point_card; toast held on MenuRuntime::point_card_toast; kept out of buy_from_shop so the randomizer oracles stay retail-shaped |
| recipient picker | BuyRecipientSession, live behind MenuRuntime::retail_equipment_buy (browser play page enables it) |
| ink + panels | shop_stock_row_ink, shop_root_command_rows, shop_sell_detail_panel → engine-ui::shop_draws_for; the two quantity windows are engine-ui’s own painters |
Gaps: the quantity screen is retail’s stepper on both hosts now — one number under the pad, committing with no Yes/No screen — so the shop no longer reaches ShopConfirm at all; what is left is the sell-list scroll fix-up, which repairs a paged list the engine does not run, and the literal item restore amounts, which stay curated because they are overlay-resident rather than in the descriptor table.
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| buy quantity + commit | FUN_801DB7F4 | max clamp literals 0x63; Point Card price / 20 before the gold debit | overlay_menu_801db7f4.txt |
| recipient picker | FUN_801DB380 | equip-mask check, equip-on-buy, same accrual; reads price at item record +2 | - |
| sell quantity | FUN_801DBD94 | credits (price × qty) >> 1; scroll fix-up | - |
| buy-list confirm route | FUN_801DB21C | affordability refusal + item-kind switch | - |
| mode panel | FUN_801D4868 | row geometry, empty-bag dim, cursor word bits | overlay_shop_save_801d4868.txt |
| picker input | FUN_801DAFD4 | row clamp li a1,0x3 at 0x801DB098; Quit at 0x801DB0D0 | - |
| quantity panel | FUN_801D5510 | window 35 layout; digit-field width from unit price | - |
| sell detail panel | FUN_801D5AE8 | window 39 layout; passive index re-derived per draw | - |
| shop buy-list row | FUN_80030628 case 0x0B | row greys at sltiu v0,v0,0x63 (0x80030F0C); reads gold vs item price | funcs/80030628.txt |
| Point Card toast | FUN_801DCE20 | window 31 renderer | - |
| Point Card strike | jump table 0x80014FA0 entry 14 (0x8004209C) | the bank’s only debit | - |
| casino prize list | FUN_801D5DE0 | window 44; indexes 0x801E4518, gates on the coin bank - not the shop | overlay_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.