macros: Red's battle menu is two columns, so ITEM is under FIGHT and PKMN beside it

THROW BALL was 63 starts and 63 blocked on v0.4.3 and SWITCH 15 of them, and the
reason is neither macro's own script. The screen reads FIGHT PKMN over ITEM RUN
and the game's index does not: wCurrentMenuItem is the row inside the column the
cursor is in, and selection adds two for the right column, so the order is
FIGHT, ITEM, PKMN, RUN. battle_entry had PKMN 1 and ITEM 2 -- the row-major
reading of the picture -- so every macro that meant to open the bag opened the
party list and every macro that meant to open the party list opened the bag.

Surveyed on the cartridge from the rung-9 checkpoint: a THROW BALL aiming at 2
walked the cursor to wTopMenuItemX 15 / wCurrentMenuItem 0, pressed A, and the
party list opened -- wTopMenuItemY 1, wTopMenuItemX 0, wListMenuID $02 -- with
the game writing wCurrentMenuItem 2 on the frame after the press.

The fake had the same mistake in its own geometry, so it could not have caught
it: its two-by-two moved row-major. It is column-major now, like the cartridge.
This commit is contained in:
acamilo 2026-09-22 08:50:40 +00:00
parent 80325c8d57
commit 57d9bdff5a
3 changed files with 69 additions and 9 deletions

View file

@ -107,8 +107,24 @@ pub const CHEAPEST_PURCHASE: u32 = {
/// 1 PKMN, 2 ITEM, 3 RUN".
pub mod battle_entry {
pub const FIGHT: u8 = 0;
pub const PKMN: u8 = 1;
pub const ITEM: u8 = 2;
/// `$01`. **The left column's second row, not the right column's first.**
///
/// Red draws the battle menu as `FIGHT PKMN` over `ITEM RUN`, which reads as two rows -- and
/// the game's own index is two *columns*: `wCurrentMenuItem` is the row inside the column the
/// cursor is in and selection adds two for the right one. So the order is FIGHT, `ITEM`,
/// `PKMN`, RUN, and this pair was the other way round for as long as the four constants have
/// existed.
///
/// **Measured on the cartridge** (2026-09-22, `infra/docs/macros-traps.md`): a `THROW BALL`
/// aiming at 2 walked the cursor to `wTopMenuItemX` 15, `wCurrentMenuItem` 0, pressed A, and
/// the **party list** opened -- `wTopMenuItemY` 1, `wTopMenuItemX` 0, `wListMenuID` `$02`.
/// The frame after the press the game wrote `wCurrentMenuItem` 2, which is the right column's
/// first row plus two, and the right column's first row is PKMN. So `THROW BALL` and `ITEM`
/// opened the party list and `SWITCH` opened the bag, every single time: `THROW BALL` was 63
/// starts and 63 `blocked` on v0.4.3, and `SWITCH` 15 of them.
pub const ITEM: u8 = 1;
/// `$02`. The right column's first row: see [`ITEM`].
pub const PKMN: u8 = 2;
pub const RUN: u8 = 3;
}

View file

@ -448,12 +448,16 @@ impl World {
}
let here = i16::from(self.cursor);
let target = if self.grid {
// Two rows of two: up and down change row, left and right change column.
// Red's battle menu: two **columns** of two, indexed by column. `wCurrentMenuItem` is
// the row inside the column the cursor is in and selection adds two for the right
// column, so the order is FIGHT, ITEM, PKMN, RUN -- up and down move one inside a
// column, left and right move two across (surveyed 2026-09-22; the fake had it
// row-major, which is the same mistake `battle_entry` had).
match facing {
Facing::Down => here + 2,
Facing::Up => here - 2,
Facing::Right if here % 2 == 0 => here + 1,
Facing::Left if here % 2 == 1 => here - 1,
Facing::Down if here % 2 == 0 => here + 1,
Facing::Up if here % 2 == 1 => here - 1,
Facing::Right => here + 2,
Facing::Left => here - 2,
_ => here,
}
} else {
@ -4378,6 +4382,40 @@ fn the_move_list_deals_back_only_where_the_moves_can_be_read() {
assert_eq!(world.pulses.last(), Some(&buttons::A));
}
/// Section 12.11: Red's battle menu is two columns, so its order is FIGHT, ITEM, PKMN, RUN.
///
/// The screen reads `FIGHT PKMN` over `ITEM RUN` and the game's own index does not: it is the row
/// inside the column the cursor is in, plus two for the right column. `battle_entry` had `PKMN` 1
/// and `ITEM` 2, which is the row-major reading of the picture, so **every macro that opened the
/// bag opened the party list and every macro that opened the party list opened the bag**.
///
/// **Surveyed on the cartridge** (`infra/docs/macros-traps.md`): a `THROW BALL` aiming at 2 walked
/// the cursor to `wTopMenuItemX` 15 / `wCurrentMenuItem` 0, pressed A, and the party list opened --
/// `wTopMenuItemY` 1, `wTopMenuItemX` 0, `wListMenuID` `$02` -- with the game writing
/// `wCurrentMenuItem` 2 on the frame after the press. On v0.4.3 `THROW BALL` was 63 starts and 63
/// `blocked`, and `SWITCH` 15 of them: never the macro's own list, always the other one.
#[test]
fn the_battle_menus_two_columns_put_item_under_fight_and_pkmn_beside_it() {
use super::cartridge::battle_entry;
assert_eq!(
[battle_entry::FIGHT, battle_entry::ITEM, battle_entry::PKMN, battle_entry::RUN],
[0, 1, 2, 3],
"the left column is FIGHT then ITEM, the right is PKMN then RUN"
);
// And the seam reads the same order back off the fake's own geometry: DOWN moves one inside a
// column, RIGHT moves two across.
let mut world = World::battle();
let menu = |world: &mut World| super::palette::battle_menu(world);
assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::FIGHT });
world.on_pulse(buttons::DOWN);
assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::ITEM });
world.on_pulse(buttons::UP);
world.on_pulse(buttons::RIGHT);
assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::PKMN });
world.on_pulse(buttons::DOWN);
assert_eq!(menu(&mut world), BattleMenu::Main { cursor: battle_entry::RUN });
}
/// Section 12.11: a cursor step waits for the list it was built for.
///
/// **Measured on the cartridge, v0.4.3** (`infra/docs/macros-traps.md`): `THROW BALL` was **63

View file

@ -406,8 +406,14 @@ pub fn battle(memory: &mut dyn MemoryReader) -> Option<Battle> {
&& (left || right);
let menu = if main {
// FIGHT and PKMN are the left column, ITEM and RUN the right; the cursor index the game
// keeps is within the column, and `.rightColumn` adds two to it on selection.
// **FIGHT and ITEM are the left column, PKMN and RUN the right.** The screen reads
// `FIGHT PKMN` over `ITEM RUN` and the game's index is by column: `wCurrentMenuItem` is
// the row inside the column the cursor is in, and `.rightColumn` adds two to it on
// selection -- so the order is FIGHT, ITEM, PKMN, RUN. Surveyed on the cartridge
// 2026-09-22 (`infra/docs/macros-traps.md`): A at `wTopMenuItemX` 15 with
// `wCurrentMenuItem` 0 opens the **party** list and the game then writes
// `wCurrentMenuItem` 2. `macros::cartridge::battle_entry` had this pair the other way
// round, so `ITEM` and `THROW BALL` opened the party list and `SWITCH` opened the bag.
let column = if right { 2 } else { 0 };
BattleMenu::Main { cursor: column + cursor.current.min(1) }
} else if cursor.top_y == 12 && cursor.top_x == 5 {