From 549df6c32300cb1bb1a18e3b0f8f6139ecab3a46 Mon Sep 17 00:00:00 2001 From: acamilo Date: Tue, 22 Sep 2026 22:29:07 +0000 Subject: [PATCH] docs: row 55, and the two claims about a mart the survey narrowed `macros.md` section 12.18 is the review; `macros-wram.md` section 7.1 corrects the address table, where `wListMenuID` was said to be cleared by every text display and an item's position in the stock was said to be its cursor index; `macros-traps.md` carries row 55, its two arms and its ROM run, and two new residuals -- `wListScrollOffset` is not a pinned address, and a mart that has never drawn a buy list reads `Unknown` rather than `Shop`. --- docs/design/macros-wram.md | 39 +++++++++++++++++++++- docs/design/macros.md | 42 +++++++++++++++++++++++ infra/docs/macros-traps.md | 68 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/docs/design/macros-wram.md b/docs/design/macros-wram.md index 1798322..1fc69ea 100644 --- a/docs/design/macros-wram.md +++ b/docs/design/macros-wram.md @@ -528,9 +528,46 @@ change, 648 bytes, checked on the WSL box. | state | symbol | address | encoding | verified | | --- | --- | ---: | --- | --- | -| the open mart's stock | `wItemList` | `$cf7b` | `ds 16`. `LoadItemList` (`home/text_script.asm:156`) copies the clerk's `script_mart` list out of its text script the moment the counter opens: **a count byte** (the macro's `_NARG`), then the item ids, then `$ff`. `DisplayPokemartDialogue_` points the buy list's `wListPointer` at the same buffer for `PRICEDITEMLISTMENU`, so **an item's position in this list is its cursor index** in the buy menu — which is what lets a purchase be navigated by reading the cursor rather than by counting presses. The terminator wins over the count, as it does for the bag, and the sixteen-byte buffer bounds both. | ROM (Viridian's counter reads POKE BALL, ANTIDOTE, PARLYZ HEAL, BURN HEAL, in that order, and no Potion), trace | +| the open mart's stock | `wItemList` | `$cf7b` | `ds 16`. `LoadItemList` (`home/text_script.asm:156`) copies the clerk's `script_mart` list out of its text script the moment the counter opens: **a count byte** (the macro's `_NARG`), then the item ids, then `$ff`. `DisplayPokemartDialogue_` points the buy list's `wListPointer` at the same buffer for `PRICEDITEMLISTMENU`, so an item's position in this list is its cursor index in the buy menu **for the first three entries only** — see the correction below. | ROM (Viridian's counter reads POKE BALL, ANTIDOTE, PARLYZ HEAL, BURN HEAL, in that order, and no Potion), trace | | the tileset's counter tiles | `wTilesetTalkingOverTiles` | `$d532` | three tile ids from the tileset header (`data/tilesets/tileset_headers.asm`), `$ff` for a tileset with fewer. Mart and Pokecenter are both `$18 $19 $1e`; the overworld and an ordinary house have none. `IsSpriteOrSignInFrontOfPlayer`'s `.extendRangeOverCounter` branch (`home/overworld.asm:1115`) walks exactly this list and doubles the talking range from `$10` to `$20` pixels — one tile to two — when the tile in front of the player is one of them. | ROM (the Viridian mart's column 1 and the centre's (3, 2) read as counters and the floor either side does not), trace | +### 7.1 Correction, 2026-09-22 (row 55): the list byte and the cursor index both say less than this + +Two claims in the table above were surveyed again from the live checkpoint the stream looped in +(`examples/scene_probe.rs`, `FLY_PROBE_CATCH=shop`, in the Pewter mart), and both are narrower +than they were written. + +**`wListMenuID` says the counter is open, not which of its screens is up.** Section 2's row for it +says it is "zeroed by `DisplayTextIDInit` at the start of every text display, so a stale value +cannot outlive one". That holds for text the overworld displays and not for the mart's own: the +clerk's "Here you are! Thank you!" is printed from inside `DisplayPokemartDialogue_`, which never +calls the routine that clears it. Measured: **every frame of a mart visit read `PRICEDITEMLISTMENU` +`$02`** — the counter menu and each of the clerk's text boxes included — and on the counter menu +`wTextBoxID` reads `MONEY_BOX` `$0d` rather than `BUY_SELL_QUIT_MENU` `$15`, because the money box +is the last template drawn. So `ShopScreen::BuySellQuit` and `Selling` were unreachable in a mart +that had ever drawn a buy list, and the frame the stream sat on — a dialogue box waiting for a +press, with a two-option box's leftover cursor bytes (`wTopMenuItemY` 8, `wTopMenuItemX` 15, +`wMaxMenuItem` 1, `wMenuWatchedKeys` `$03`) — read as "the priced buy list is open". + +The screen is now read from the figure the game draws, the construction [`text_box`]'s `waiting` +test and [`yes_no_prompt`] already use: the full-width box drawn and waiting is the clerk +(`ShopScreen::Talking`, new), the item window drawn is the buy list, and the item window blank is +the counter menu. The item window's own figure is its first name cell, screen (6, 4), which held +`$7f` on the counter menu and `P` of `POKE BALL` on every frame the list was drawn. + +**The buy list scrolls, so a position in `wItemList` is a cursor index only for the first three +entries.** Walked one DOWN pulse at a time on the live list: the cursor went `0, 1, 2` and then +**stopped moving while the window scrolled under it** — the fourth drawn row is a look-ahead the +cursor never occupies. The absolute position of the item under the cursor is that index plus +`wListScrollOffset`, which is **not** in the reviewed address list and cannot be pinned without the +disassembly `gen_symbols.py` reads (the same refusal `$cfc5` met in section 9). Viridian's +four-item counter hid it: POKE BALL is 0 and ANTIDOTE is 1, both in reach. Pewter's seven-item +counter did not: its ANTIDOTE is **3**. + +So the macros carry `MART_CURSOR_ROWS = 3` and a purchase past it is not on the pad +(`infra/docs/macros-traps.md` row 55). Pinning `wListScrollOffset` would widen that, and it is a +residual rather than a guess. + **Why the second one is load-bearing.** A mart clerk is `object_event 0, 5, SPRITE_CLERK` behind a counter running down column 1; a Pokémon Center nurse is `object_event 3, 1, SPRITE_NURSE` behind the counter tile at (3, 2). **None of the four tiles around either of them is standable** — they are diff --git a/docs/design/macros.md b/docs/design/macros.md index 81535b7..ad7adf8 100644 --- a/docs/design/macros.md +++ b/docs/design/macros.md @@ -1217,6 +1217,48 @@ walk. Both were measured from the same rung-10 checkpoint, with Nothing here changes which button the fly presses. The decoder, the reward catalog, the adapter version and the compatibility string are untouched. +### 12.18 A mart's counter is four screens, and one of them is the clerk talking (2026-09-22, row 55) + +Minutes after v0.4.7 went live the watchdog flagged the narrowest loop yet: map `0x38`, scene +`shop`, `sequence: [BUY ANTIDOTE], period 1, repeats 747, distinctMacros 1` over ten brain +minutes, with `BUY ANTIDOTE start` then `BUY ANTIDOTE blocked` every 0.8 s and **nothing else +starting at all**. Surveyed from the live checkpoint with a new probe mode +(`FLY_PROBE_CATCH=shop` in `examples/scene_probe.rs`); the bytes are in +`docs/design/macros-wram.md` section 7.1. + +- **`wListMenuID` says the counter is open, not which of its screens is up.** The mart prints its + own text from inside `DisplayPokemartDialogue_`, which never calls the routine that clears that + byte, so it holds `PRICEDITEMLISTMENU` for the **whole visit**. The frame the stream sat on was + the clerk's "Here you are! Thank you!" box waiting for a press, and the seam called it the buy + list; the cursor bytes on it belong to a two-option box (`max` 1). So which screen is up is now + read from the figure the game draws -- the construction the dialogue box's `waiting` test and the + YES/NO prompt already use. The clerk talking is `ShopScreen::Talking`, it reports **no listing at + all**, and no purchase starts on it: a scene whose menu is not open offers what actually opens it, + which here is `CONFIRM` and `LEAVE`, and both are already on the shop's pad. +- **A mart's buy list scrolls, so an item's position in the stock is its cursor index only for the + first three entries.** Walked one pulse at a time on the live list, the cursor went `0, 1, 2` and + then stopped moving while the window scrolled under it. The offset that names the scrolled + position is not a pinned address, so the fourth item of a counter and after have no index this + seam can aim at. Pewter's counter carries seven items and its ANTIDOTE is the **fourth**: + `BUY ANTIDOTE` aimed a cursor step at index 3 in a list reporting a max of 1 and returned + `Blocked` **on its own first frame, having pressed nothing** -- three of three attempts, zero + frames. A macro that cannot run is not on the pad, so the four purchases are bound on the rows + the cursor can reach, and Viridian's four-item counter is why this went unseen: its ANTIDOTE is + index 1. +- **A purchase has no target, so a blocked purchase records nothing.** Section 12.1's ledger is + keyed by what a walk set out for and a press sets out for nothing, so the button was dealt again + on the very next hold, for ever -- row 6's shape in a scene with no walk in it. The fix is the + precondition rather than a new ledger: the question the ledger would have answered is a fact about + the counter, and the pad can ask it before the fly presses. + +The loop was also **the fly's own choice landing on the one button it could afford**: the wallet +read 104, so of Pewter's stock only the Antidote was under it, and `BUY ANTIDOTE` was the only +purchase bound. Nothing about the choice changes here. What changes is that the button is not +offered, and the two presses that leave a counter are. + +Nothing here changes which button the fly presses. The decoder, the reward catalog, the adapter +version and the compatibility string are untouched. + ## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a ## priority to visit the shop at least once per area; make shop macros item purchases. same ## for the Pokécenter. heal should be a macro.") diff --git a/infra/docs/macros-traps.md b/infra/docs/macros-traps.md index 8043bf7..344143d 100644 --- a/infra/docs/macros-traps.md +++ b/infra/docs/macros-traps.md @@ -1552,9 +1552,22 @@ arm fought fifty thousand frames of battle *and* walked twenty thousand frames o Raw reports: `hunt-before-20260922T0459.md` and `hunt-after-20260922T0459.md` in the coordination state's `runs/` directory. +| 55 | `BUY ` is dealt on a frame where no list is accepting input, and aimed at a stock index the counter's cursor cannot reach: it gives up on its own first frame, presses nothing, records nothing, and is dealt again next hold | the Pewter mart, ten brain minutes after v0.4.7: map `0x38`, scene `shop`, `loop.json` `sequence: [BUY ANTIDOTE], period 1, repeats 747, distinctMacros 1`, `BUY ANTIDOTE start` / `BUY ANTIDOTE blocked` every 0.8 s with **nothing else starting**; money 104, so the Antidote was the only purchase money allowed | `the_clerks_text_box_is_not_the_marts_buy_list`, `the_clerks_own_text_box_deals_no_purchase_and_reports_no_list`, `a_purchase_past_the_cursors_reach_is_not_on_the_pad`, `a_purchase_out_of_reach_by_the_time_it_starts_is_refused_without_a_press`, and the ROM run below | **fixed**, two facts, both surveyed with `FLY_PROBE_CATCH=shop`. `wListMenuID` says the **counter is open**, not which screen is up -- the mart prints its own text without clearing it, so every frame of a visit read "the priced buy list", including the clerk's "Here you are! Thank you!" box whose leftover cursor bytes are a two-option box's (`max` 1); the screen is now read from the figure the game draws and the clerk is `ShopScreen::Talking`, which reports no listing and deals no purchase. And the buy list **scrolls**: the cursor walks rows 0, 1, 2 and then the window moves under it, so only the first three of a counter's stock have an index this seam can aim at, and Pewter's ANTIDOTE is its fourth. `docs/design/macros.md` section 12.18, `docs/design/macros-wram.md` section 7.1 | ### Residuals, named rather than worked around +- **`wListScrollOffset` is not a pinned address** (row 55), so a mart's fourth item and after have + no cursor index this seam can name and their `BUY` buttons are off the pad -- Pewter's ANTIDOTE, + BURN HEAL, AWAKENING and PARLYZ HEAL among them. Pinning it is the same survey `$cfc5` is waiting + on in `docs/design/macros-wram.md` section 9: `gen_symbols.py` refuses a hand-written address and + the checkout it reads is not on this box. +- **A mart that has never drawn a buy list reads `Unknown`, not `Shop`** (measured beside row 55). + On a first visit `wListMenuID` is 0 and `wTextBoxID` on the counter menu is `MONEY_BOX` `$0d` + rather than `BUY_SELL_QUIT_MENU` `$15` -- the money box is the last template drawn -- so + `state::shop` answers `None` until the fly has opened the list once. Named rather than worked: + widening the test to `MONEY_BOX` would let the Game Corner's prize counter read as a mart, and + what that costs has not been surveyed. + - **`NEXT` on a move list whose cursor the seam cannot place is now the largest source of it** -- 142 of the after arm's 248 `NEXT` starts, over 8,687 frames. That frame is *correctly* not the fly's turn (row 30b: `MoveSelectionMenu`'s coordinates appear before the engine has copied the @@ -2156,3 +2169,58 @@ skipped cleanly without `FLY_ROM` and the checkpoint): - `infra/tests/lint.sh`: all checks passed, de-PII guard included. - `--print-compatibility`: **648 bytes, sha256 `0d9bfde7...707fa`** -- byte-identical to v0.4.1 through v0.4.5. Decoder, reward catalog, adapter version and roles untouched. + +## Row 55: the two arms, and the ROM run (2026-09-22, v0.4.7) + +One checkpoint, the mart the stream was standing in. Same seed, same ground, `main` at `5512900` +against this branch, twenty brain minutes each. + +| measure | before (`main`) | after | +| --- | ---: | ---: | +| distinct (map, tile) | **1** | **62** | +| windows flagged | 73 / 73 | 72 / 73 | +| macros started | 1,494 | 1,136 | +| macros **blocked** | **1,494 — every one of them** | 82 | +| `BUY ANTIDOTE` starts | **1,494, mean 0 frames, mean net 0.0 tiles** | **0** | +| frames in scene `shop` | **71,673 of 71,673** | **225** (longest run 171) | +| the repeated sequence | `BUY ANTIDOTE` x149 to x150, every window | `NEXT` x14 to x19 | +| the fly leaves the mart | **never** | **at 0.19 brain minutes** | +| frames in `battle` | 0 | 66,727 (longest run 17,722, from minute 0.58) | +| wall clock | 4,210 s | 2,384 s | + +**The before arm is the live loop whole, with the real brain in it**: the fly started 1,494 macros +in twenty brain minutes, **every single one of them `BUY ANTIDOTE`, every single one `blocked`, +every one at a mean of nought frames** — so it pressed no button at all for the whole run and stood +on one tile. That is the sharpest reproduction any row here has had, and it is what a macro that +gives up before its first press looks like from outside. + +**The after arm walks out and then fights.** The shop scene is 225 frames of 71,673 instead of all +of them, `CONFIRM` and `LEAVE` are the two macros that end it, and the fly is on the overworld at +0.19 brain minutes, takes an item six tiles away, walks three routes at a mean net of 9.0 tiles +(max 19) and is in a battle from 0.58. Sixty-two times the ground. + +**The flagged-window count barely moves (73 → 72), and this says so rather than smoothing it.** +What the windows are flagging after the fix is **row 50**: 66,727 of the 71,673 frames are one long +battle, `MOVE 1`–`MOVE 4` are 74 of the 82 remaining `blocked` finishes at a mean of ~230 frames +each, and the repeated sequence is `NEXT`/`BACK` in a battle. The hunt's tile rule cannot tell a +long fight from a stall — `docs/design/macros.md` section 15's own measurement, and the third branch +in a row to run into it. The loop this row is about is gone; the one underneath it is somebody +else's brief. + +**ROM-gated, from the same checkpoint** +(`services/flysim/crates/flysim/tests/rom_macros_mode.rs`, skipped cleanly without `FLY_ROM` and +the checkpoint): + +- `the_fly_leaves_the_pewter_mart_counter_from_the_rung_eleven_checkpoint` — route `[56, 2, 14]` + over 20.1 brain minutes: out of the mart, across Pewter City and on to Route 3. `BUY ANTIDOTE` + **0** starts, `LEAVE` 1, `CONFIRM` 1, empty shop pads **0**, and the longest chain of one macro + reporting `blocked` in the shop with the map, the tile, the wallet and the scene unchanged is + **0**, against a bound of three. The same chain in the *battle* scene is **47** (`MOVE 2`), + printed beside it and asserted on by nobody here: it is row 50's. + +### Gates + +- `cargo test --workspace` with `FLY_ROM` and `FLY_DATASET` set. +- `cargo clippy --all-targets`. +- `infra/tests/lint.sh`, de-PII guard included. +- `flysim --print-compatibility`, byte-identical to this branch's base.