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),
|
||||
/// A tile of the map to stand on.
|
||||
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.
|
||||
|
|
@ -456,6 +465,20 @@ pub trait MacroState: GameState {
|
|||
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.
|
||||
///
|
||||
/// 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);
|
||||
format!(
|
||||
"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} \
|
||||
corners=({:#04x},{:#04x},{:#04x},{:#04x})",
|
||||
state::started(memory),
|
||||
|
|
@ -120,6 +120,7 @@ pub fn why_unknown(memory: &mut dyn MemoryReader) -> String {
|
|||
memory.read8(ram::wCurrentMenuItem),
|
||||
memory.read8(ram::wMaxMenuItem),
|
||||
memory.read8(ram::wMenuWatchedKeys),
|
||||
state::yes_no_prompt(memory),
|
||||
memory.read8(ram::wJoyIgnore),
|
||||
memory.read8(ram::wSimulatedJoypadStatesIndex),
|
||||
memory.read8(ram::wStatusFlags5),
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ pub mod poke {
|
|||
/// `constants/menu_constants.asm` party menu types.
|
||||
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.
|
||||
pub const BIT_USING_GENERIC_PC: u8 = 1 << 3;
|
||||
/// `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) }
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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)
|
||||
}
|
||||
|
||||
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
|
||||
/// (`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());
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
let Some(path) = std::env::var_os("FLY_ROM") else {
|
||||
println!("FLY_ROM is not set, so there is nothing to probe.");
|
||||
|
|
@ -368,6 +493,12 @@ fn main() {
|
|||
dump(&mut gb, "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 stuck_after = env_usize("FLY_PROBE_STUCK", 600);
|
||||
let mut next_burst = ms;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue