macros: read the nurse's YES/NO box, and survey her conversation
Row 41 is the Pokemon Center nurse: 2,142 YES starts on one tile of map
0x3a with a party at 70/70. The survey that names it is a new probe mode
(FLY_PROBE_CATCH=nurse) that leaves the box with B and then pulses A,
printing every state the conversation passes through with the two-option
menu's own geometry beside it. It is a ring of forty-six presses and the
box is a choice on exactly one of them.
So the reading: wFontLoaded plus the border DisplayTwoOptionMenu draws,
plus the cursor it parks inside it. Both halves are needed -- the cursor
bytes survive the box closing, so all forty-six frames carry the
geometry. It claims that box and not "a choice is open" in general.
TargetKey gains Answer { at, yes } for the reopened-prompt exclusion.
This commit is contained in:
parent
96677119ad
commit
86f0e7018d
4 changed files with 201 additions and 1 deletions
|
|
@ -286,6 +286,15 @@ pub enum TargetKey {
|
||||||
Thing(TalkTarget),
|
Thing(TalkTarget),
|
||||||
/// A tile of the map to stand on.
|
/// A tile of the map to stand on.
|
||||||
Tile(Tile),
|
Tile(Tile),
|
||||||
|
/// One answer to the YES/NO box at a tile: the key of the reopened-prompt exclusion
|
||||||
|
/// (`docs/design/macros.md` section 12.12).
|
||||||
|
///
|
||||||
|
/// Not a walk's target -- nothing is aimed at it -- but the same ledger and the same window,
|
||||||
|
/// because it is the same fact: an answer that changed nothing is an answer not worth making
|
||||||
|
/// again from this tile for a while. Keyed by the tile rather than by the person, because what
|
||||||
|
/// the box belongs to is whatever the fly is standing in front of, and the box is the only
|
||||||
|
/// thing on screen while it is open.
|
||||||
|
Answer { at: Tile, yes: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which list the shared cursor belongs to right now.
|
/// Which list the shared cursor belongs to right now.
|
||||||
|
|
@ -456,6 +465,20 @@ pub trait MacroState: GameState {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the box on screen is the two-option YES/NO prompt rather than a plain text box.
|
||||||
|
///
|
||||||
|
/// `pokemon_red::state::yes_no_prompt`: the border `DisplayTwoOptionMenu` draws plus the
|
||||||
|
/// cursor it parks inside it, surveyed on the cartridge (`docs/design/macros.md` section
|
||||||
|
/// 12.12). It is what tells the *one* frame of the nurse's conversation that is a choice from
|
||||||
|
/// the forty-five that are text, and the pad is dealt differently for it -- on a choice, an A
|
||||||
|
/// press *is* `YES`, so `NEXT` is the same press under another name (12.10).
|
||||||
|
///
|
||||||
|
/// The default is `false`: a state that cannot answer has no choice open, which leaves the
|
||||||
|
/// dialog pad exactly what it has always been.
|
||||||
|
fn yes_no_prompt(&mut self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether `target` is inside its blocked-target exclusion window on the map that is loaded.
|
/// Whether `target` is inside its blocked-target exclusion window on the map that is loaded.
|
||||||
///
|
///
|
||||||
/// Written when `GO ITEM`, `GO NPC`, `GO OBJECTIVE`, `GO OUT`, `GO WARP`, `GO ROUTE` or
|
/// Written when `GO ITEM`, `GO NPC`, `GO OBJECTIVE`, `GO OUT`, `GO WARP`, `GO ROUTE` or
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ pub fn why_unknown(memory: &mut dyn MemoryReader) -> String {
|
||||||
let box_corners = state::dialog_corners(memory);
|
let box_corners = state::dialog_corners(memory);
|
||||||
format!(
|
format!(
|
||||||
"started={} map={:?} party={} battle={} type={} font={:#04x} textbox={:#04x} \
|
"started={} map={:?} party={} battle={} type={} font={:#04x} textbox={:#04x} \
|
||||||
list={:#04x} cursor=({},{},{},{},{:#04x}) joy={} sim={} flags5={:#04x} \
|
list={:#04x} cursor=({},{},{},{},{:#04x}) prompt={} joy={} sim={} flags5={:#04x} \
|
||||||
flags6={:#04x} move={:#04x} \
|
flags6={:#04x} move={:#04x} \
|
||||||
corners=({:#04x},{:#04x},{:#04x},{:#04x})",
|
corners=({:#04x},{:#04x},{:#04x},{:#04x})",
|
||||||
state::started(memory),
|
state::started(memory),
|
||||||
|
|
@ -120,6 +120,7 @@ pub fn why_unknown(memory: &mut dyn MemoryReader) -> String {
|
||||||
memory.read8(ram::wCurrentMenuItem),
|
memory.read8(ram::wCurrentMenuItem),
|
||||||
memory.read8(ram::wMaxMenuItem),
|
memory.read8(ram::wMaxMenuItem),
|
||||||
memory.read8(ram::wMenuWatchedKeys),
|
memory.read8(ram::wMenuWatchedKeys),
|
||||||
|
state::yes_no_prompt(memory),
|
||||||
memory.read8(ram::wJoyIgnore),
|
memory.read8(ram::wJoyIgnore),
|
||||||
memory.read8(ram::wSimulatedJoypadStatesIndex),
|
memory.read8(ram::wSimulatedJoypadStatesIndex),
|
||||||
memory.read8(ram::wStatusFlags5),
|
memory.read8(ram::wStatusFlags5),
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,17 @@ pub mod poke {
|
||||||
/// `constants/menu_constants.asm` party menu types.
|
/// `constants/menu_constants.asm` party menu types.
|
||||||
pub const BATTLE_PARTY_MENU: u8 = 0x02;
|
pub const BATTLE_PARTY_MENU: u8 = 0x02;
|
||||||
|
|
||||||
|
/// The two-option YES/NO box, as surveyed on the cartridge (`infra/docs/macros-traps.md`,
|
||||||
|
/// row 41): the border `DisplayTwoOptionMenu` draws, and where it parks the cursor.
|
||||||
|
///
|
||||||
|
/// Values rather than a symbol because `wTwoOptionMenuID` is not in the reviewed address list
|
||||||
|
/// and the box's geometry is what is on screen. Read from the rung-10 Pokemon Center
|
||||||
|
/// checkpoint, one raw A pulse at a time: a box at (11, 6)-(19, 11) with the cursor at
|
||||||
|
/// row 8, column 12, one item below the first, watching A and B.
|
||||||
|
pub const YES_NO_BOX: (u16, u16, u16, u16) = (11, 6, 19, 11);
|
||||||
|
pub const YES_NO_CURSOR_Y: u8 = 8;
|
||||||
|
pub const YES_NO_CURSOR_X: u8 = 12;
|
||||||
|
|
||||||
/// `constants/ram_constants.asm`: `wMiscFlags` bit 3.
|
/// `constants/ram_constants.asm`: `wMiscFlags` bit 3.
|
||||||
pub const BIT_USING_GENERIC_PC: u8 = 1 << 3;
|
pub const BIT_USING_GENERIC_PC: u8 = 1 << 3;
|
||||||
/// `wFontLoaded` bit 0.
|
/// `wFontLoaded` bit 0.
|
||||||
|
|
@ -534,6 +545,36 @@ pub fn text_box(memory: &mut dyn MemoryReader) -> TextBox {
|
||||||
TextBox { open, waiting: open && border_drawn(memory, 0, 12, 19, 17) }
|
TextBox { open, waiting: open && border_drawn(memory, 0, 12, 19, 17) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the two-option YES/NO box is the thing on screen: a *choice*, not a plain text box.
|
||||||
|
///
|
||||||
|
/// `docs/design/macros-wram.md` says there is no "a choice is open" flag, and there is not -- so
|
||||||
|
/// this is the same construction [`text_box`] makes for `waiting`: a WRAM flag plus the figure the
|
||||||
|
/// game draws. `DisplayTwoOptionMenu` draws its own little box in the top right and parks the
|
||||||
|
/// shared cursor inside it, and **both halves are needed**: the cursor bytes are not cleared when
|
||||||
|
/// the box closes, so at the rung-10 checkpoint every one of the nurse's forty-six text frames
|
||||||
|
/// reads `wTopMenuItemY` 8, `wTopMenuItemX` 12, `wMaxMenuItem` 1 and `wMenuWatchedKeys` `$03`
|
||||||
|
/// while the box itself is drawn on exactly one of them (`infra/docs/macros-traps.md`, row 41).
|
||||||
|
///
|
||||||
|
/// **What it does not claim.** Red places a two-option menu where the script that asks for it
|
||||||
|
/// says, so a prompt drawn somewhere else reads `false` here and its dialog keeps the pad it has
|
||||||
|
/// always had. This is the box the nurse's "heal your POKeMON?" is drawn in, surveyed; it is not a
|
||||||
|
/// general answer to "is a choice open", and nothing in the palette treats it as one.
|
||||||
|
pub fn yes_no_prompt(memory: &mut dyn MemoryReader) -> bool {
|
||||||
|
if read(memory, ram::wFontLoaded) & poke::BIT_FONT_LOADED == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let cursor = cursor(memory);
|
||||||
|
if cursor.top_y != poke::YES_NO_CURSOR_Y
|
||||||
|
|| cursor.top_x != poke::YES_NO_CURSOR_X
|
||||||
|
|| cursor.max != 1
|
||||||
|
|| cursor.watched_keys != poke::pad::A | poke::pad::B
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let (left, top, right, bottom) = poke::YES_NO_BOX;
|
||||||
|
border_drawn(memory, left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
/// The four screen tiles the dialogue box's `waiting` test reads, in the order `box_drawn` reads
|
/// The four screen tiles the dialogue box's `waiting` test reads, in the order `box_drawn` reads
|
||||||
/// them: top-left, top-right, bottom-left, bottom-right of a box at (0, 12)-(19, 17).
|
/// them: top-left, top-right, bottom-left, bottom-right of a box at (0, 12)-(19, 17).
|
||||||
///
|
///
|
||||||
|
|
@ -1269,6 +1310,10 @@ impl MacroState for PokeState<'_> {
|
||||||
!controllable(self.memory)
|
!controllable(self.memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn yes_no_prompt(&mut self) -> bool {
|
||||||
|
yes_no_prompt(self.memory)
|
||||||
|
}
|
||||||
|
|
||||||
/// The whole loaded map's walkability, from the cache when it is for this map
|
/// The whole loaded map's walkability, from the cache when it is for this map
|
||||||
/// (`docs/design/macros.md` section 15).
|
/// (`docs/design/macros.md` section 15).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,131 @@ fn battle_dump(gb: &mut Emulator, adapter: &PokemonRedReward, pad: &[String], la
|
||||||
println!("- money: {}", state.money());
|
println!("- money: {}", state.money());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One line of the dialogue box, decoded through `constants/charmap.asm`.
|
||||||
|
///
|
||||||
|
/// The survey below is about *which box* is open, and the only thing on screen that says so is the
|
||||||
|
/// text in it: `wTextBoxID` is `$01` for every ordinary `TX_FAR` box the nurse draws, so the id
|
||||||
|
/// cannot tell the welcome from the prompt from the closing line. The tiles can.
|
||||||
|
fn box_line(gb: &mut Emulator, y: u16) -> String {
|
||||||
|
(1..19u16)
|
||||||
|
.map(|x| match gb.read8(ram::wTileMap + y * 20 + x) {
|
||||||
|
0x7f => ' ',
|
||||||
|
byte @ 0x80..=0x99 => (b'A' + (byte - 0x80)) as char,
|
||||||
|
byte @ 0xa0..=0xb9 => (b'a' + (byte - 0xa0)) as char,
|
||||||
|
0xba => 'e',
|
||||||
|
0xe3 => '-',
|
||||||
|
0xe6 => '?',
|
||||||
|
0xe7 => '!',
|
||||||
|
0xe8 => '.',
|
||||||
|
0xef => 'M',
|
||||||
|
0xee => '\u{25bc}',
|
||||||
|
byte @ 0xf6..=0xff => (b'0' + (byte - 0xf6)) as char,
|
||||||
|
_ => '.',
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_end()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The top-right corner of the screen, where a two-option menu's own little box is drawn: the
|
||||||
|
/// tiles at (11..20, 6..11) reduced to which of them hold a text-box frame tile.
|
||||||
|
fn corner_box(gb: &mut Emulator) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for y in 6..12u16 {
|
||||||
|
for x in 11..20u16 {
|
||||||
|
let byte = gb.read8(ram::wTileMap + y * 20 + x);
|
||||||
|
out.push(match byte {
|
||||||
|
0x79 | 0x7b | 0x7d | 0x7e => '+',
|
||||||
|
0x7a | 0x7c => '|',
|
||||||
|
0x7f => '_',
|
||||||
|
_ => '.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.push('/');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything that tells one of the nurse's boxes from another, on one line.
|
||||||
|
fn nurse_frame(gb: &mut Emulator) -> String {
|
||||||
|
let text = state::text_box(gb);
|
||||||
|
format!(
|
||||||
|
"{:?} open={} waiting={} cursor=({},{},{},{},{:#04x}) yesno={} | {} | {}",
|
||||||
|
scene::detect(gb),
|
||||||
|
text.open,
|
||||||
|
text.waiting,
|
||||||
|
gb.read8(ram::wTopMenuItemY),
|
||||||
|
gb.read8(ram::wTopMenuItemX),
|
||||||
|
gb.read8(ram::wCurrentMenuItem),
|
||||||
|
gb.read8(ram::wMaxMenuItem),
|
||||||
|
gb.read8(ram::wMenuWatchedKeys),
|
||||||
|
corner_box(gb),
|
||||||
|
box_line(gb, 14),
|
||||||
|
box_line(gb, 16),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Pokémon Center nurse's whole conversation, box by box, with raw presses (row 41).
|
||||||
|
///
|
||||||
|
/// The rung-10 loop of 2026-09-22 was `YES` 2,142 macro starts on one tile of map `0x3a`, so the
|
||||||
|
/// question the fix turns on is **which box each A press answers**. `wTextBoxID` cannot say --
|
||||||
|
/// every box the nurse draws is `$01` -- and `docs/design/macros-wram.md` says outright that there
|
||||||
|
/// is no "a choice is open" flag, so the reading has to be surveyed: leave the box with B, then
|
||||||
|
/// pulse A and print every state the conversation passes through, with the two-option menu's own
|
||||||
|
/// geometry beside it. The party is printed first because `HEAL`'s precondition is the party and
|
||||||
|
/// the loop's premise is that it is already full.
|
||||||
|
fn nurse_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) {
|
||||||
|
use flybrain_gb::pokemon_red::macros::cartridge::MacroState;
|
||||||
|
|
||||||
|
println!("\n## The party at the checkpoint\n");
|
||||||
|
{
|
||||||
|
let ledger = AdapterLedger(adapter);
|
||||||
|
let mut poke = flybrain_gb::pokemon_red::state::PokeState::with_ledger(gb, &ledger);
|
||||||
|
let state: &mut dyn MacroState = &mut poke;
|
||||||
|
for mon in &state.party().mons {
|
||||||
|
println!(
|
||||||
|
"- slot {} species {:#04x} level {} hp {}/{} status {:?}",
|
||||||
|
mon.slot, mon.species, mon.level, mon.hp, mon.max_hp, mon.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"- `party_needs_rest` = {}, `party_rested` = {}",
|
||||||
|
flybrain_gb::pokemon_red::macros::palette::party_needs_rest(state),
|
||||||
|
flybrain_gb::pokemon_red::macros::palette::party_rested(state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pulse = |gb: &mut Emulator, adapter: &mut PokemonRedReward, mask: u8, ms: &mut f64| {
|
||||||
|
for phase in 0..16 {
|
||||||
|
gb.set_buttons(if phase < 8 { mask } else { 0 });
|
||||||
|
gb.run_frame().expect("a frame should complete");
|
||||||
|
*ms += MS_PER_FRAME;
|
||||||
|
adapter.sample(gb, *ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("\n## The nurse's conversation, one raw A pulse at a time\n");
|
||||||
|
println!("- at the checkpoint: {}", nurse_frame(gb));
|
||||||
|
for _ in 0..20 {
|
||||||
|
if scene::detect(gb) == scene::Scene::Overworld {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pulse(gb, adapter, flybrain_gb::buttons::B, ms);
|
||||||
|
}
|
||||||
|
println!("- after B until the box closes: {}", nurse_frame(gb));
|
||||||
|
println!("\n```");
|
||||||
|
let mut last = String::new();
|
||||||
|
for index in 0..env_usize("FLY_PROBE_PULSES", 120) {
|
||||||
|
pulse(gb, adapter, flybrain_gb::buttons::A, ms);
|
||||||
|
let now = nurse_frame(gb);
|
||||||
|
if now != last {
|
||||||
|
println!("A#{index:<3} {now}");
|
||||||
|
last = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("```");
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let Some(path) = std::env::var_os("FLY_ROM") else {
|
let Some(path) = std::env::var_os("FLY_ROM") else {
|
||||||
println!("FLY_ROM is not set, so there is nothing to probe.");
|
println!("FLY_ROM is not set, so there is nothing to probe.");
|
||||||
|
|
@ -368,6 +493,12 @@ fn main() {
|
||||||
dump(&mut gb, "At the checkpoint");
|
dump(&mut gb, "At the checkpoint");
|
||||||
pad(&mut gb, &adapter, "The pad at the checkpoint");
|
pad(&mut gb, &adapter, "The pad at the checkpoint");
|
||||||
|
|
||||||
|
let catch_nurse = std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "nurse");
|
||||||
|
if catch_nurse {
|
||||||
|
nurse_survey(&mut gb, &mut adapter, &mut ms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let budget = env_usize("FLY_PROBE_FRAMES", 200_000);
|
let budget = env_usize("FLY_PROBE_FRAMES", 200_000);
|
||||||
let stuck_after = env_usize("FLY_PROBE_STUCK", 600);
|
let stuck_after = env_usize("FLY_PROBE_STUCK", 600);
|
||||||
let mut next_burst = ms;
|
let mut next_burst = ms;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue