macros: the mart's list byte says the counter is open, not which screen is up
`wListMenuID` keeps PRICEDITEMLISTMENU for a whole mart visit: the clerk's text is printed from inside the mart's own routine and never goes through the display that clears it. So every frame of the Pewter mart read "the priced buy list", including the "Here you are! Thank you!" box the stream was looking at, whose leftover cursor bytes belong to a two-option box. Which screen is up is now read from the figure the game draws, the same construction the dialogue box's `waiting` test and the YES/NO prompt already make: a full-width box waiting for a press is the clerk (`ShopScreen::Talking`), the item window drawn is the buy list, and the item window blank is the counter menu.
This commit is contained in:
parent
da2cde176c
commit
4a1df7c3be
3 changed files with 112 additions and 2 deletions
|
|
@ -309,6 +309,16 @@ pub enum ShopScreen {
|
||||||
Buying,
|
Buying,
|
||||||
/// The bag list, for selling.
|
/// The bag list, for selling.
|
||||||
Selling,
|
Selling,
|
||||||
|
/// The clerk is talking: the counter is open, and the thing on screen is a dialogue box
|
||||||
|
/// waiting for a press rather than a list waiting for a cursor.
|
||||||
|
///
|
||||||
|
/// Row 55 of `infra/docs/macros-traps.md`. `wListMenuID` is not cleared while the mart prints
|
||||||
|
/// its own text -- "Here you are! Thank you!" goes through `PrintText` inside
|
||||||
|
/// `DisplayPokemartDialogue_` and not through `DisplayTextIDInit` -- so the byte that says
|
||||||
|
/// "the priced buy list" outlives the list by the whole of the clerk's conversation, while the
|
||||||
|
/// cursor bytes hold a two-option box's leftovers. A purchase started on one of those frames
|
||||||
|
/// navigates a list that is not there.
|
||||||
|
Talking,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A mart, when one is open.
|
/// A mart, when one is open.
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,21 @@ pub mod poke {
|
||||||
/// One count byte, then the ids, then `$ff`, so at most fourteen items can be both counted
|
/// One count byte, then the ids, then `$ff`, so at most fourteen items can be both counted
|
||||||
/// and terminated inside the buffer. The real marts carry four to nine.
|
/// and terminated inside the buffer. The real marts carry four to nine.
|
||||||
pub const MART_LIST_BYTES: u8 = 16;
|
pub const MART_LIST_BYTES: u8 = 16;
|
||||||
|
/// Where the mart's priced item window writes its first item name, and the letter range a
|
||||||
|
/// name starts with -- the figure [`super::mart_item_window_drawn`] reads.
|
||||||
|
///
|
||||||
|
/// Screen coordinates rather than a symbol, for the reason [`YES_NO_BOX`] gives: what tells
|
||||||
|
/// the buy list from the counter menu is what is drawn, and the bytes that would name it are
|
||||||
|
/// not rewritten between the two. Surveyed in the Pewter mart from the live checkpoint
|
||||||
|
/// (`infra/docs/macros-traps.md` row 55): the counter menu left this cell at `HORIZONTAL`'s
|
||||||
|
/// neighbour `$7f`, and every frame the list was drawn held `P` of `POKE BALL` there, with the
|
||||||
|
/// shared cursor one column to its left.
|
||||||
|
pub const MART_NAME_ROW: u16 = 4;
|
||||||
|
pub const MART_NAME_COLUMN: u16 = 6;
|
||||||
|
/// Red's charmap: `$80`-`$99` are `A`-`Z`.
|
||||||
|
pub const CHAR_UPPER_A: u8 = 0x80;
|
||||||
|
pub const CHAR_UPPER_Z: u8 = 0x99;
|
||||||
|
|
||||||
/// `data/tilesets/tileset_headers.asm`: three counter tile ids per tileset, `-1` for none.
|
/// `data/tilesets/tileset_headers.asm`: three counter tile ids per tileset, `-1` for none.
|
||||||
pub const COUNTER_TILES: u16 = 3;
|
pub const COUNTER_TILES: u16 = 3;
|
||||||
/// The `-1` a tileset with fewer than three counter tiles pads its header with.
|
/// The `-1` a tileset with fewer than three counter tiles pads its header with.
|
||||||
|
|
@ -648,6 +663,23 @@ pub fn submenu(memory: &mut dyn MemoryReader) -> bool {
|
||||||
/// is the only user of that template in the game; the buy list is `PRICEDITEMLISTMENU` and the
|
/// is the only user of that template in the game; the buy list is `PRICEDITEMLISTMENU` and the
|
||||||
/// sell list is the bag's own `ITEMLISTMENU`, which is why selling is only recognised while the
|
/// sell list is the bag's own `ITEMLISTMENU`, which is why selling is only recognised while the
|
||||||
/// mart's choice is still the last template drawn.
|
/// mart's choice is still the last template drawn.
|
||||||
|
///
|
||||||
|
/// **`wListMenuID` says the counter is open, not which of its screens is up** (row 55 of
|
||||||
|
/// `infra/docs/macros-traps.md`, surveyed in the Pewter mart). The doc's claim that the byte is
|
||||||
|
/// "zeroed by `DisplayTextIDInit` at the start of every text display, so a stale value cannot
|
||||||
|
/// outlive one" 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_`, so `$cf94` keeps
|
||||||
|
/// `PRICEDITEMLISTMENU` across the whole visit. Measured on the cartridge from the live
|
||||||
|
/// checkpoint: every frame of a mart visit read `Buying`, the counter menu and the clerk's text
|
||||||
|
/// boxes included, and `wTextBoxID` on the counter menu reads `MONEY_BOX` rather than
|
||||||
|
/// `BUY_SELL_QUIT_MENU` because the money box is the last template drawn.
|
||||||
|
///
|
||||||
|
/// So which screen is up is read from the figure the game draws, the same construction
|
||||||
|
/// [`text_box`]'s `waiting` and [`yes_no_prompt`] already make:
|
||||||
|
///
|
||||||
|
/// - the full-width dialogue box drawn and waiting is the clerk, [`ShopScreen::Talking`];
|
||||||
|
/// - otherwise the item window drawn is the buy list and the item window blank is the counter
|
||||||
|
/// menu ([`mart_item_window_drawn`]).
|
||||||
pub fn shop(memory: &mut dyn MemoryReader) -> Option<Shop> {
|
pub fn shop(memory: &mut dyn MemoryReader) -> Option<Shop> {
|
||||||
if read(memory, ram::wFontLoaded) & poke::BIT_FONT_LOADED == 0 {
|
if read(memory, ram::wFontLoaded) & poke::BIT_FONT_LOADED == 0 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -655,7 +687,15 @@ pub fn shop(memory: &mut dyn MemoryReader) -> Option<Shop> {
|
||||||
let cursor = cursor(memory);
|
let cursor = cursor(memory);
|
||||||
let list = read(memory, ram::wListMenuID);
|
let list = read(memory, ram::wListMenuID);
|
||||||
if list == poke::PRICED_ITEM_LIST_MENU {
|
if list == poke::PRICED_ITEM_LIST_MENU {
|
||||||
return Some(Shop { screen: ShopScreen::Buying, cursor });
|
if text_box(memory).waiting {
|
||||||
|
return Some(Shop { screen: ShopScreen::Talking, cursor });
|
||||||
|
}
|
||||||
|
let screen = if mart_item_window_drawn(memory) {
|
||||||
|
ShopScreen::Buying
|
||||||
|
} else {
|
||||||
|
ShopScreen::BuySellQuit
|
||||||
|
};
|
||||||
|
return Some(Shop { screen, cursor });
|
||||||
}
|
}
|
||||||
if read(memory, ram::wTextBoxID) != poke::BUY_SELL_QUIT_MENU {
|
if read(memory, ram::wTextBoxID) != poke::BUY_SELL_QUIT_MENU {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -665,6 +705,25 @@ pub fn shop(memory: &mut dyn MemoryReader) -> Option<Shop> {
|
||||||
Some(Shop { screen, cursor })
|
Some(Shop { screen, cursor })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the mart's priced item window is the thing drawn over the counter menu.
|
||||||
|
///
|
||||||
|
/// `DisplayListMenuID` writes the item names down a fixed column of the window
|
||||||
|
/// ([`poke::MART_NAME_COLUMN`], from [`poke::MART_NAME_ROW`]) with the shared cursor in the column
|
||||||
|
/// to their left, and leaves that cell blank while only the BUY / SELL / QUIT box is up. Every
|
||||||
|
/// item a mart sells has a name that starts with a letter, so the test is "is there a letter
|
||||||
|
/// there": measured on the cartridge, the cell held `$7f` on the counter menu and the first
|
||||||
|
/// letter of the counter's first item on every frame the list was drawn.
|
||||||
|
///
|
||||||
|
/// A figure test rather than a byte, for the reason [`shop`] gives: the bytes that would name the
|
||||||
|
/// screen are not rewritten between the two, so they cannot tell them apart.
|
||||||
|
fn mart_item_window_drawn(memory: &mut dyn MemoryReader) -> bool {
|
||||||
|
let tile = read(
|
||||||
|
memory,
|
||||||
|
ram::wTileMap + poke::MART_NAME_ROW * poke::SCREEN_WIDTH + poke::MART_NAME_COLUMN,
|
||||||
|
);
|
||||||
|
(poke::CHAR_UPPER_A..=poke::CHAR_UPPER_Z).contains(&tile)
|
||||||
|
}
|
||||||
|
|
||||||
/// The PC, when one is open. `ActivatePC` sets `wMiscFlags`' generic-PC bit and `LogOff` clears
|
/// The PC, when one is open. `ActivatePC` sets `wMiscFlags`' generic-PC bit and `LogOff` clears
|
||||||
/// it, so it covers Bill's PC, the player's PC and Oak's alike.
|
/// it, so it covers Bill's PC, the player's PC and Oak's alike.
|
||||||
pub fn pc(memory: &mut dyn MemoryReader) -> Option<Pc> {
|
pub fn pc(memory: &mut dyn MemoryReader) -> Option<Pc> {
|
||||||
|
|
|
||||||
|
|
@ -371,7 +371,9 @@ fn a_mart_reports_which_of_its_screens_is_up() {
|
||||||
wram.set(ram::wListMenuID, poke::ITEM_LIST_MENU);
|
wram.set(ram::wListMenuID, poke::ITEM_LIST_MENU);
|
||||||
assert_eq!(shop(&mut wram).map(|shop| shop.screen), Some(ShopScreen::Selling));
|
assert_eq!(shop(&mut wram).map(|shop| shop.screen), Some(ShopScreen::Selling));
|
||||||
|
|
||||||
wram.set(ram::wListMenuID, poke::PRICED_ITEM_LIST_MENU);
|
// The priced list byte with the item window drawn is the buy list.
|
||||||
|
wram.set(ram::wListMenuID, poke::PRICED_ITEM_LIST_MENU)
|
||||||
|
.screen_tile(poke::MART_NAME_COLUMN, poke::MART_NAME_ROW, poke::CHAR_UPPER_A);
|
||||||
assert_eq!(shop(&mut wram).map(|shop| shop.screen), Some(ShopScreen::Buying));
|
assert_eq!(shop(&mut wram).map(|shop| shop.screen), Some(ShopScreen::Buying));
|
||||||
|
|
||||||
// The bag list on its own is the start menu's, not a mart's.
|
// The bag list on its own is the start menu's, not a mart's.
|
||||||
|
|
@ -381,6 +383,45 @@ fn a_mart_reports_which_of_its_screens_is_up() {
|
||||||
assert!(shop(&mut wram).is_none());
|
assert!(shop(&mut wram).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Row 55: the byte that says "the priced buy list" outlives the list, so which screen is up is
|
||||||
|
/// read from what is drawn.
|
||||||
|
///
|
||||||
|
/// Surveyed in the Pewter mart from the live checkpoint: `wListMenuID` held
|
||||||
|
/// `PRICEDITEMLISTMENU` on **every frame of the visit** -- the counter menu and the clerk's own
|
||||||
|
/// text boxes included -- because the mart prints its text from inside
|
||||||
|
/// `DisplayPokemartDialogue_` rather than through `DisplayTextIDInit`. The two things that do
|
||||||
|
/// change are the figure on screen: the full-width dialogue box for the clerk, and the item
|
||||||
|
/// window for the list.
|
||||||
|
#[test]
|
||||||
|
fn the_clerks_text_box_is_not_the_marts_buy_list() {
|
||||||
|
// The frame the stream looped on: the counter open, the priced-list byte stale, and the
|
||||||
|
// clerk's "Here you are! Thank you!" waiting for a press.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.set(ram::wListMenuID, poke::PRICED_ITEM_LIST_MENU)
|
||||||
|
.screen_tile(poke::MART_NAME_COLUMN, poke::MART_NAME_ROW, poke::CHAR_UPPER_A)
|
||||||
|
.dialogue_box();
|
||||||
|
assert_eq!(
|
||||||
|
shop(&mut wram).map(|shop| shop.screen),
|
||||||
|
Some(ShopScreen::Talking),
|
||||||
|
"a dialogue box waiting for a press is the clerk, whatever the list byte says"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The same counter with the box gone and the item window drawn: the buy list.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
wram.set(ram::wFontLoaded, poke::BIT_FONT_LOADED)
|
||||||
|
.set(ram::wListMenuID, poke::PRICED_ITEM_LIST_MENU)
|
||||||
|
.screen_tile(poke::MART_NAME_COLUMN, poke::MART_NAME_ROW, poke::CHAR_UPPER_A);
|
||||||
|
assert_eq!(shop(&mut wram).map(|shop| shop.screen), Some(ShopScreen::Buying));
|
||||||
|
|
||||||
|
// And with the item window blank: the BUY / SELL / QUIT menu underneath it.
|
||||||
|
wram.screen_tile(poke::MART_NAME_COLUMN, poke::MART_NAME_ROW, poke::frame::HORIZONTAL);
|
||||||
|
assert_eq!(
|
||||||
|
shop(&mut wram).map(|shop| shop.screen),
|
||||||
|
Some(ShopScreen::BuySellQuit),
|
||||||
|
"no item name in the window means the list is not the thing on screen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_pc_is_the_generic_pc_bit() {
|
fn a_pc_is_the_generic_pc_bit() {
|
||||||
let mut wram = Wram::overworld();
|
let mut wram = Wram::overworld();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue