macros: NEXT off every own-turn pad, and the bag is the turn its cursor says it is

Live on rung 9 thirty-five minutes after v0.4.2: NEXT 1264 macro starts, BACK
1241, the log alternating NEXT start/done, BACK start/done every hold, on map
51. NEXT on the top-level battle menu is an A press on the cursor, and the
cursor sits on FIGHT, so it opened the move list; BACK on the move list closed
it again. Each button was legitimate where it stood, and the pair undid itself
with nothing else changing, so the turn never resolved.

NEXT is now on no pad with a cursor accepting input. It keeps the between-turns
row alone and the forced switch, which has no BACK beside it to undo it. MOVE 1
is the top-level menu's backstop instead: the question over that menu is 12.8's
"is there a move list to open", FIGHT always opens, and the script there is
confirm FIGHT and stop, which reads no move at all.

The bag is the fly's own turn, because a cursor accepting input is one, and its
pad is the bag's own three answers: ITEM, THROW BALL, BACK. Section 12.10.
This commit is contained in:
acamilo 2026-09-22 06:14:33 +00:00
parent 592c2640a3
commit 05c2552d78
3 changed files with 58 additions and 34 deletions

View file

@ -504,35 +504,44 @@ pub fn scene_set(scene: Scene, state: &mut dyn MacroState) -> Vec<MacroKind> {
// `SWITCH` needs a Pokémon to switch *to* and a forced switch with none leaves a menu the
// game will not let the fly cancel and a pad with nothing on it (row 8).
Scene::Battle { forced_switch: true, .. } => vec![Switch, Next],
// The fly's turn, by which menu of it is accepting input (section 12.6). `NEXT` is on none
// of the two lists: an A press on whatever the cursor happens to be sitting on is what the
// live loop was made of.
// The fly's turn, by which menu of it is accepting input (section 12.6). **`NEXT` is on
// no own-turn pad at all** (section 12.10): it is the A that advances *text*, and on a menu
// that is accepting input the same A press opens or confirms whatever the cursor happens to
// be sitting on, which is never one of the answers to that menu.
//
// Live on rung 9 after v0.4.2, 71 hours in Viridian Forest: `NEXT` 1264 macro starts and
// `BACK` 1241, the log alternating `NEXT start/done, BACK start/done` every hold. `NEXT` on
// the *top-level* menu pressed A on FIGHT and opened the move list; `BACK` on the *move
// list* closed it again; neither spent a turn, and the two of them were half the pad
// between them. Two buttons that undo each other with nothing else changing are section
// 12.2's trap spread over two sub-states of one turn.
Scene::Battle { own_turn: true, .. } => match battle_menu(state) {
BattleMenu::Moves { .. } => vec![Move1, Move2, Move3, Move4, Back],
BattleMenu::Party { .. } => vec![Switch, Back],
// The bag reads as nobody's turn (`state::battle`), so it cannot land here; the arm
// exists because `own_turn` is a boolean and this match is over the menu.
BattleMenu::Bag { .. } => vec![Confirm, Back],
// The bag, which is the fly's turn since 12.10. Its three answers: use the thing the
// cursor is on (`ITEM`), throw the ball (`THROW BALL`), or leave the list (`BACK`).
// `CONFIRM` was an A press on whatever the cursor held, which is the same press
// `NEXT` was and reads nothing; the two scripts navigate the list's own cursor.
BattleMenu::Bag { .. } => vec![Item, ThrowBall, Back],
// `None` cannot land here -- `own_turn` is false for it -- and the arm exists because
// `own_turn` is a boolean and this match is over the menu. `MOVE 1` is what keeps this
// pad from ever being one the fly cannot end the turn from: FIGHT is one of these four
// entries and it always opens (section 12.8), which is the backstop `NEXT` was.
BattleMenu::Main { .. } | BattleMenu::None => {
vec![Move1, Move2, Move3, Move4, Switch, Item, ThrowBall, Run, Next]
vec![Move1, Move2, Move3, Move4, Switch, Item, ThrowBall, Run]
}
},
// 2026-09-16 hotfix (live deadlock on Route 1): battle text between turns waits for a
// press exactly like a dialog. Section 13.1 added `BACK` here for the bag a battle's ITEM
// entry opens, which is none of `BattleMenu`'s three and reads as nobody's turn -- and
// section 12.9 narrows it back to that list.
// press exactly like a dialog.
//
// **`BACK` is a button only where there is a list to leave.** Live on rung 9, 69 hours in
// Viridian Forest: `BACK` was 135 of 183 macro starts since the restart, `RUN blocked,
// BACK start, BACK done` over and over. Between turns there is nothing open to back out
// of, so the B press changes nothing the `NEXT` beside it does not, the macro completes on
// the tile it started on in a handful of frames, and the roll lands on it most holds while
// the turn does not move. That is section 12.2's trap exactly: a macro that completes
// without moving because its precondition is already satisfied where the fly stands.
Scene::Battle { .. } => match battle_menu(state) {
BattleMenu::Bag { .. } => vec![Next, Back],
_ => vec![Next],
},
// **One button, and it is the A that advances text.** Section 13.1 added `BACK` here for
// the bag, because a bag read as nobody's turn; 12.9 narrowed it back to the bag alone;
// 12.10 moves the bag to the own turn where its cursor says it belongs, so nothing is open
// on this row any more and nothing but `NEXT` is on it. A frame that reaches here has no
// cursor accepting input -- text, an animation, a turn resolving -- so there is nothing to
// back out of (`BACK` would be 12.2's trap, the rung-9 loop of 12.9) and nothing for an A
// press to open (`NEXT` here cannot be the press that opened a menu, which is 12.10).
Scene::Battle { .. } => vec![Next],
// Section 13: the shop's buttons are the four purchases, plus the two answers any list has.
Scene::Shop => vec![BuyPotion, BuyBall, BuyAntidote, BuyRepel, Confirm, Leave],
// Section 13.1: `CONFIRM` as well as `LEAVE`, so a PC the fly opened is a list it can
@ -1579,7 +1588,18 @@ pub const fn move_index(kind: MacroKind) -> Option<u8> {
/// one at all -- which nothing in the game reaches -- is the one case that still answers `false`.
pub fn move_slot_bound(state: &mut dyn MacroState, kind: MacroKind) -> bool {
let Some(index) = move_index(kind) else { return false };
let Some(own) = state.battle().and_then(|battle| battle.own) else { return false };
let Some(battle) = state.battle() else { return false };
// Over the **top-level menu** the question is section 12.8's -- "is there a move list to
// open" -- and FIGHT always opens: what is behind it is the cartridge's business, because
// `CheckPlayerHasUsableMoves` prints "has no moves left!" and sets Struggle without opening
// the list. `MOVE 1`'s script over that menu is "confirm FIGHT and stop" and reads no move at
// all, so the button is bound there whatever the seam can make of the battler. That is the
// backstop `NEXT` used to be on this row (section 12.10): the own turn's main menu always has
// a button that ends the turn, and it is never one that merely reopens a list.
if index == 0 && matches!(battle.menu, BattleMenu::Main { .. }) {
return true;
}
let Some(own) = battle.own else { return false };
let holds = |slot: usize| -> Option<&Move> {
own.moves.get(slot).and_then(|entry| entry.as_ref()).filter(|entry| entry.id != 0)
};

View file

@ -41,7 +41,8 @@ pub enum Scene {
Dialog,
/// Start menu or one of its submenus, outside battle.
Menu,
/// In a battle. `own_turn` is the top-level FIGHT/PKMN/ITEM/RUN menu waiting for input;
/// In a battle. `own_turn` is any battle menu waiting for input -- the top-level
/// FIGHT/PKMN/ITEM/RUN one, the move list, the party list or the bag;
/// `forced_switch` is the party list the game opens when the active Pokémon has fainted, which
/// cannot be backed out of.
Battle { own_turn: bool, forced_switch: bool },
@ -234,10 +235,10 @@ pub enum BattleMenu {
/// The bag, opened from a battle's ITEM entry: `wListMenuID` is `ITEMLISTMENU`.
///
/// Not one of the three `docs/design/macros.md` section 12.6 named, and the gap was
/// observable: the bag list is not the top-level menu, so `own_turn` is false and the frame
/// reads as a battle between turns, whose pad is `NEXT` and `BACK` -- and `NEXT` was an A
/// press on whatever the cursor held. What the list needed is a *cursor* the scripts can read,
/// which is what `ITEM` and section 14's `THROW BALL` navigate by (2026-09-17).
/// observable twice over. First the list needed a *cursor* the scripts can read, which is what
/// `ITEM` and section 14's `THROW BALL` navigate by (2026-09-17). Then it needed to be the
/// fly's *turn*: a bag reading as nobody's turn landed on the between-turns row, whose `NEXT`
/// is the A that advances text and on an open bag is the A that uses an item (12.10).
Bag { cursor: u8, count: u8 },
}
@ -245,8 +246,9 @@ pub enum BattleMenu {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Battle {
pub kind: BattleKind,
/// A battle menu is open and waiting for the fly: the top-level one, the move list, or the
/// party list outside a forced switch (`pokemon_red::state::battle`).
/// A battle menu is open and waiting for the fly: the top-level one, the move list, the bag,
/// or the party list outside a forced switch (`pokemon_red::state::battle`). Every frame with a
/// cursor accepting input is one of these, which is section 12.10's invariant.
pub own_turn: bool,
/// The party list is open because the active Pokémon fainted; it cannot be cancelled.
pub forced_switch: bool,

View file

@ -459,11 +459,13 @@ pub fn battle(memory: &mut dyn MemoryReader) -> Option<Battle> {
// frame is between turns, which is what it was before this change.
BattleMenu::Moves { cursor, .. } => cursor.is_some(),
BattleMenu::Party { .. } => !forced_switch,
// The bag is a list the fly opened *during* its turn, but it is not the turn's own menu:
// the pad that belongs to it is the list's two answers, which is what the between-turns
// row deals (`NEXT`, `BACK`). Reporting it as the own turn would put the move buttons on
// a screen they cannot press.
BattleMenu::Bag { .. } => false,
// The bag is a list the fly opened *during* its turn, and it is a menu cursor accepting
// input, so by the rule above it is the fly's turn (2026-09-22, section 12.10). Reading it
// as nobody's turn put it on the between-turns row, whose one button is the `NEXT` that
// advances *text* -- and on an open bag that same A press *uses* whatever the cursor
// happens to be sitting on. The pad that belongs to a bag is the bag's own three answers,
// `ITEM`, `THROW BALL` and `BACK`, which is what `palette::scene_set` deals here now.
BattleMenu::Bag { .. } => true,
BattleMenu::None => false,
};
Some(Battle {