diff --git a/docs/design/macros-wram.md b/docs/design/macros-wram.md index 3b55030..5cf5efb 100644 --- a/docs/design/macros-wram.md +++ b/docs/design/macros-wram.md @@ -128,7 +128,7 @@ parked its cursor. All five bytes are contiguous: `wTopMenuItemY` `$cc24`, `wTop | menu | signature | cursor | verified | | --- | --- | --- | --- | | the top-level battle menu | `wTextBoxID` = `$0b`, `wTopMenuItemY` = 14, `wTopMenuItemX` = 9 with watched keys `PAD_RIGHT\|PAD_A` (left column) or 15 with `PAD_LEFT\|PAD_A` (right), `wMaxMenuItem` = 1 (`DisplayBattleMenu`, `engine/battle/core.asm:2081` and `:2114`) | reported 0 FIGHT, 1 PKMN, 2 ITEM, 3 RUN: the game keeps the index *within* the column and `.rightColumn` adds two on selection | ROM (a fresh menu is FIGHT; RIGHT is ITEM; DOWN from there is RUN), trace | -| the move list | `wTopMenuItemY` = 12, `wTopMenuItemX` = 5 (`MoveSelectionMenu`'s regular menu, `:2492`) | the game's list is **one-based** — `wCurrentMenuItem` is `wPlayerMoveListIndex + 1` and `wMaxMenuItem` is the move count plus one — so the accessor reports the 0-based slot, and `None` for an index that names no move | trace | +| the move list | `wTopMenuItemY` = 12, `wTopMenuItemX` = 5 (`MoveSelectionMenu`'s regular menu, `:2492`) **and the box it draws** — section 10, because nothing clears the cursor bytes and `SelectMenuItem` decrements `wCurrentMenuItem` back into range on its way out | the game's list is **one-based** — `wCurrentMenuItem` is `wPlayerMoveListIndex + 1` and `wMaxMenuItem` is the move count plus one — so the accessor reports the 0-based slot, and `None` for an index that names no move | trace, and the press survey of section 10 | | the party list | `wTopMenuItemY` = 1, `wTopMenuItemX` = 0, `wMaxMenuItem` = `wPartyCount - 1`, watched keys `PAD_A\|PAD_B` or `PAD_A` alone (`PartyMenuInit`, `home/pokemon.asm:201`) | 0-based party slot | trace | | **a forced switch** | the party list, in a battle, with `wPartyMenuTypeOrMessageID` = `BATTLE_PARTY_MENU` (`$02`) at `$d07d`. `ChooseNextMon` is the battle path that sets it (`engine/battle/core.asm:1088`, and `:1389` for the "use next mon?" branch); choosing PKMN from the menu sets `NORMAL_PARTY_MENU` (`$00`, `:2316`), which is why the two are distinguishable. `wForcePlayerToChooseMon` (`$d11f`) is the byte `PartyMenuInit` turns into "A only, no way out". | — | trace | @@ -714,3 +714,72 @@ answers that, and the executor's per-step moved check covers the rest), a warp t step onto it, and a script that pushes the fly off a tile (a session ledger answers that). The water half of the tile-pair lists is deliberately absent: it is the list `CheckForJumpingAndTilePairCollisions` uses while surfing, and the palette cannot surf. + +## 10. A menu that is accepting input, against one that is only remembered (2026-09-22, row 50) + +`HandleMenuInput` is shared by every menu in the game (section 2) and so are the five bytes it +parks a cursor in. Section 2's table reads those bytes to say *which* menu is up; it does not say +whether anybody is reading them. The difference is the whole of row 50: `MOVE n` reported `blocked` +**890 times in 1,431 macros** on the cartridge, every one of them on a frame the seam called an +open move list with a placeable cursor. + +**Nothing in the game clears the cursor bytes.** `MoveSelectionMenu` writes `wTopMenuItemY` 12 and +`wTopMenuItemX` 5 once, and the whole of the turn that follows — the text, the animation, the +damage, the enemy's reply — reads them back unchanged. It is the same fact section 7's YES/NO box +rests on ("the cursor bytes survive the box closing"), and the reason the battle's *top-level* menu +never had this problem is that it carries `wTextBoxID` = `$0b` beside its geometry. + +`SelectMenuItem` makes it worse rather than better: on its way out of `HandleMenuInput` it does +`ld a, [wCurrentMenuItem] / dec a / ld [wCurrentMenuItem], a`, turning the menu's one-based index +back into a 0-based move slot. That lands straight back inside the range the accessor reads as a +valid one-based slot, so a turn spent on move 2, 3 or 4 leaves a *placeable* cursor behind it. + +### The accessor + +| state | how | verified | +| --- | --- | --- | +| the move list is **accepting input** | the cursor at `wTopMenuItemY` 12 / `wTopMenuItemX` 5 **and** the figure `MoveSelectionMenu` draws: a `TextBoxBorder` at (4, 12) fourteen wide and four tall, with a horizontal run written over its top-left corner and the `┘` junction written over (10, 12) (`engine/battle/core.asm`, `.regularmenu`). Read whole — both verticals, both horizontal runs, all four corners — because a single frame tile id is an ordinary character. The mimic and relearn menus draw at row 7 and never reach a battle's own turn. | survey (below) | + +`Scene::Battle { own_turn }` follows it: a frame whose move list is not on screen reads +`BattleMenu::None`, which is nobody's turn, which is the between-turns row and its one `NEXT` +(`docs/design/macros.md` 12.10). Nothing else moves — the top-level menu, the party list and the +bag keep the readings they had. + +### The survey + +`examples/scene_probe.rs`, `FLY_PROBE_CATCH=accept`, from the rung-9 forest checkpoint. The +question "is this menu accepting input" is answered by **pressing at it**, not by nominating a +flag: on every battle frame the emulator exports its state, one directional pulse is issued, +`wCurrentMenuItem` is read, and the state goes straight back — `HandleMenuInput` moves the cursor +on UP and DOWN before it even looks at `wMenuWatchedKeys`, so a cursor that moves is a menu running +its input loop. The pulse *releases* the buttons first, because `JoypadLowSensitivity` acts on a +key's edge and a direction the fly is already holding would read as refused for the measurement's +reason rather than the cartridge's. + +| the reading | press refused | press honoured | +| --- | ---: | ---: | +| the cursor bytes alone (what the seam read before row 50) | 2,838 | 264 | +| the cursor bytes **and** the box on screen | **0** | **231** | +| the cursor bytes with no box drawn | 2,838 | 33 | + +So 91.5% of the frames the old reading called an open move list were frames no press reached, and +the reading that survives is exact on the 231 it keeps. (The 33 are frames where the pulse's own +thirty frames were long enough for the cartridge to open something by itself; the pulse is a +measurement and not a claim about one frame.) + +Beside the press, the probe asks **every byte of WRAM and HRAM** whether its values on accepting +frames are disjoint from its values on refusing ones, so a reading is found rather than guessed. +Over the move list, once the box is in the reading, no byte separates the two classes at all — +there is nothing left to separate. Over the whole class before the fix, the only separators were +the HRAM joypad bytes, which is the measurement seeing its own held button. + +### What the same survey found and this section did not fix + +- **The top-level battle menu is already exact**: 413 frames, 0 refused. `wTextBoxID` is why. +- **The bag is the same trap, unfixed and named.** `wListMenuID` = `ITEMLISTMENU` outlives the bag + exactly as the cursor bytes outlive the move list: 449 refused against 36 honoured over the + frames the seam calls an open battle bag. The bag list is drawn in the top half of the screen and + the survey has not yet found the figure that tells it from the frame after it closes, so it is + reported rather than guessed — `docs/design/ladder.md`'s rule. `ITEM` and `THROW BALL` are the + two macros it costs. +- **The party list, likewise**: `PartyMenuInit`'s geometry outlives its list. diff --git a/docs/design/macros.md b/docs/design/macros.md index 81535b7..3c2c559 100644 --- a/docs/design/macros.md +++ b/docs/design/macros.md @@ -1217,6 +1217,51 @@ 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 menu is up while its box is on screen, not while its cursor bytes say so (2026-09-22, row 50) + +The largest thing left inside a battle after 12.17: `MOVE n` reported `blocked` **890 times in +1,431 macros** from the rung-9 forest checkpoint, `MOVE 4` **222 of 224**, and 82% of a fixed run's +frames were battle time with one battle running 30,809 of them. Row 50 called it "the move list +drawn and its cursor placeable but not accepting input". Half of that turned out to be wrong, and +finding out which half is the whole fix. + +- **The cursor bytes outlive the list, so the list was not drawn at all.** `MoveSelectionMenu` + writes `wTopMenuItemY` 12 and `wTopMenuItemX` 5 and **nothing in the game clears them**. That is + the same fact 12.12 rested the YES/NO box on — "the cursor bytes survive the box closing" — and + the reason the *top-level* battle menu never had it is that `wTextBoxID` = `$0b` sits beside its + geometry and is written by somebody else. `SelectMenuItem` then decrements `wCurrentMenuItem` back + into a 0-based move slot on its way out, which lands inside the one-based range the accessor reads + as valid, so a turn spent on move 2, 3 or 4 leaves a *placeable* cursor behind it. Every frame of + the text, the animation, the damage and the enemy's reply read as the fly's own turn on an open + move list. The pad dealt `MOVE 1..4` and `BACK` on all of them, the roll landed on one, and the + cursor step pressed at a list nobody was reading until its budget ran out. That is section 12.2's + trap wearing 12.6's clothes: a macro whose precondition is satisfied where the fly stands. +- **So a menu is up while its box is on screen.** The reading is the figure `MoveSelectionMenu` + draws — a box at (4, 12) fourteen wide, with a horizontal run over its top-left corner and the + `┘` junction over (10, 12) — read whole, exactly as `text_box`'s `waiting` and `yes_no_prompt` + are. `docs/design/macros-wram.md` section 10 has the accessor. +- **It was surveyed by pressing, not by nominating a flag.** `examples/scene_probe.rs`, + `FLY_PROBE_CATCH=accept`: on every battle frame the emulator exports its state, one directional + pulse is issued, `wCurrentMenuItem` is read and the state goes straight back, so every frame has a + ground truth and the run is not perturbed by the measurement. By the cursor bytes alone a press + was honoured on **264 frames of 3,102**; by the cursor bytes and the box on **231 of 231**. Beside + it every byte of WRAM and HRAM was asked whether it separates the two classes, so a reading was + found rather than guessed — nothing separates once the box is in it, which is what "exact" means + here. +- **A frame whose list is not on screen is between turns**, whose pad is the one `NEXT` that + advances text (12.10). No pad gains or loses a button anywhere else: the top-level menu, the party + list, the bag and the forced switch keep exactly the rows 13.1 gives them, and which move the fly + uses is still the fly's. +- **The bag is the same trap and it is named rather than fixed.** `wListMenuID` = `ITEMLISTMENU` + outlives the bag as surely as the cursor bytes outlive the move list: over the frames the seam + calls an open battle bag, the same survey refused **449** presses against 36 honoured. The bag's + list is drawn in the top half of the screen and the survey has not yet found the figure that tells + it from the frame after it closes, so `ITEM` and `THROW BALL` still pay for it and that is + reported. A reading this crate cannot verify does not go in (`docs/design/ladder.md`). + +The decoder, the reward catalog, the adapter version, the roles 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.") @@ -1298,11 +1343,11 @@ observe is not a precondition, it is a guess. | Menu (the bag, an elevator, the party list outside a battle) | CLOSE, CONFIRM, BACK | unchanged | | Unknown (the Pokédex, the trainer card, OPTION, a naming screen, a mid-warp frame) | NEXT, **BACK** | **BACK added** (row 9): B is what leaves the first three, and A leaves none of them | | Battle, own turn, main menu | MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN (whose cursor indices are FIGHT 0, **ITEM 1, PKMN 2**, RUN 3 -- two columns, 12.11) | four move buttons for `ATTACK` (section 14); THROW BALL added, and gated on the species since 12.9; **RUN gated**, below. No `BACK`: the four entries are the answers to this menu. **`NEXT` removed by 12.10** — an A press here confirms FIGHT and reopens the list the move list's `BACK` just closed, and `MOVE 1` is the backstop instead, bound here whatever the battler reads as | -| Battle, own turn, move list | MOVE 1..4, BACK -- or **MOVE 1 alone** | as above, plus **12.11**: `BACK` is dealt here only while `wBattleMon*` reads, because a list that binds no `MOVE n` has a pad whose one button closes the list `MOVE 1` underneath had just opened. With nothing readable the pad is `MOVE 1` and its script confirms where the cursor stands | +| Battle, own turn, move list (**the box on screen**, 12.18) | MOVE 1..4, BACK -- or **MOVE 1 alone** | as above, plus **12.11**: `BACK` is dealt here only while `wBattleMon*` reads, because a list that binds no `MOVE n` has a pad whose one button closes the list `MOVE 1` underneath had just opened. With nothing readable the pad is `MOVE 1` and its script confirms where the cursor stands | | Battle, own turn, party list | SWITCH, BACK | unchanged | | Battle, own turn, the bag | ITEM, THROW BALL, **BACK** | the bag reports a *cursor* (`macros-wram.md` 7.1), and since **12.10** it is the own turn, because a cursor accepting input is one. Its pad is the list's own answers; `NEXT` and `CONFIRM` are both off it, being the same blind A press that *uses* whatever the cursor holds | | Battle, forced switch | SWITCH, NEXT | unchanged (row 8). The one arm that keeps `NEXT` with a cursor up, because it cannot be cancelled and has no `BACK` to undo it | -| Battle, between turns | NEXT | `BACK` was added here for the bag and **taken back out by 12.9**: on a frame of battle text there is no list to leave, and a `BACK` that changes nothing is the trap of section 12.2. Since **12.10** the bag is not on this row at all, so `NEXT` here is only ever the A that advances text | +| Battle, between turns | NEXT | since **12.18** this row is most of a battle, and correctly so: a frame whose move list is remembered rather than drawn lands here. `BACK` was added here for the bag and **taken back out by 12.9**: on a frame of battle text there is no list to leave, and a `BACK` that changes nothing is the trap of section 12.2. Since **12.10** the bag is not on this row at all, so `NEXT` here is only ever the A that advances text | | Shop | BUY POTION, BUY BALL, BUY ANTIDOTE, BUY REPEL, CONFIRM, LEAVE | two purchases to four; CONFIRM added | | PC | **CONFIRM**, LEAVE | **CONFIRM added**: a list the fly opened is one it can answer rather than only close. Depositing and withdrawing are still not in the vocabulary (row 17) | | Title | nothing | unchanged, by contract: the readout's boot variant applies | diff --git a/infra/docs/macros-traps.md b/infra/docs/macros-traps.md index 8043bf7..90a2835 100644 --- a/infra/docs/macros-traps.md +++ b/infra/docs/macros-traps.md @@ -1863,7 +1863,7 @@ same question, and that was the second half of the trap. | 41 | the nurse's conversation is a ring of forty-six A presses that ends where it began, and the dialog pad deals two names for the A press that walks it | standing at a Pokémon Center's counter with a party that is already full -- which is every visit after a heal, and the state a `GO HEAL` errand leaves the fly in | `talk_is_off_the_pad_at_a_nurse_the_party_has_no_use_for`, `the_nurses_prompt_offers_only_the_answer_that_changes_something`, `a_completed_heal_writes_the_nurse_into_the_talked_ledger`, `a_declined_heal_writes_the_nurse_into_the_talked_ledger`, `the_fly_leaves_the_pokemon_center_from_the_rung_ten_checkpoint` (ROM-gated) | **fixed**: `TALK` is off the pad at a nurse the party has no use for; her prompt deals only the answer that changes something; `NEXT` is off any readable YES/NO pad, because an A press there *is* `YES`; and a completed heal or a declined prompt retires her | | 48 | `TALK` is bound by a reach that goes over a counter and recorded by one that does not, so a counter person is never retired | any mart clerk or centre nurse, since the counter reach was added | `a_completed_heal_writes_the_nurse_into_the_talked_ledger` (the ledger entry is the assertion) | **fixed**: the ledger entry comes from `palette::facing_target`, which is `TALK`'s own precondition | | 49 | a YES/NO answer that brings the same prompt straight back | any readable two-option box the answer does not settle | `a_yes_no_box_that_reopens_unchanged_takes_that_answer_off_the_pad`, `a_prompt_that_does_not_come_back_excludes_nothing` | **fixed**: `TargetKey::Answer { at, yes }` in the blocked ledger, same ten-minute window as a walk's target, armed for one hold after the answer. The exclusion narrows a pad and never empties one | -| 50 | `MOVE n` reports `blocked` with the move list drawn and its cursor placeable but not accepting input | every battle | -- | **unchanged from v0.4.3 and v0.4.4, named again**: 222 of 224 `MOVE 4` and 162 of 171 `MOVE 2` in the ROM run below. Row 30b's unplaceable cursor inverted; the honest fix is a WRAM reading of "this list is accepting input" rather than a pad change, and it is the next brief | +| 50 | `MOVE n` reports `blocked` with the move list drawn and its cursor placeable but not accepting input | every battle | `a_move_list_is_the_box_on_screen_and_not_the_cursor_bytes_it_left_behind`, and the `MOVE n` blocked share in `the_battles_turns_advance_from_the_rung_nine_forest_checkpoint` (ROM) | **fixed** (2026-09-22, `docs/design/macros.md` 12.18), and the half of the row that was wrong is where the fix is: the list was **not** drawn. `MoveSelectionMenu`'s cursor bytes are never cleared and `SelectMenuItem` decrements `wCurrentMenuItem` back into the one-based range on its way out, so every frame of a turn's text and animation read as an open list with a placeable cursor. A menu is up while its **box** is on screen -- surveyed by pressing at every battle frame with a rollback pulse, honoured on 264 frames of 3,102 by the cursor bytes alone and on **231 of 231** by the bytes and the box | ### The ROM-gated run, from the live checkpoint