diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs index 01d556e..c2dd3c4 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/cartridge.rs @@ -465,6 +465,22 @@ pub trait MacroState: GameState { false } + /// Whether a text box is open at all: `wFontLoaded`'s bit, and nothing drawn. + /// + /// The one thing that tells a screen with words on it from a frame of the overworld the + /// cartridge happens to be driving, and [`super::palette::scene_set`] deals + /// [`Scene::Unknown`]'s pad on it (**section 12.13**). `Unknown` is two different states + /// wearing one name: a screen this crate cannot name -- the Pokedex, the trainer card, + /// OPTION -- where `NEXT` and `BACK` are the A and B that leave it; and a *scripted* overworld + /// frame, where `scene::detect` falls through to `Unknown` because the buttons are not + /// reaching the player, and where an A or a B press is a press into somebody else's script. + /// + /// The default is `false`, which narrows: with no reading, `Unknown` deals nothing and the + /// fly waits, which is what the doctrine says a scene with nothing to press does. + fn text_open(&mut self) -> bool { + 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 diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs index b5fd2de..7b96dfd 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs @@ -476,7 +476,26 @@ pub fn scene_set(scene: Scene, state: &mut dyn MacroState) -> Vec { // Section 2 treats `Unknown` like a dialog -- advance only -- and `BACK` is the other half // of advancing, because `Unknown` is also where the Pokédex, the trainer card and OPTION // land (`docs/design/macros-wram.md`) and B is what leaves all three. - Scene::Unknown => vec![Next, Back], + // + // **Only while there is something on screen with words in it** (section 12.13, the + // rung-10 Pewter loop). `Unknown` is the detector's residue and it holds two states, not + // one: a screen this crate cannot name, where A and B are what leave it, and a frame of + // the *overworld* where the cartridge is driving -- a warp in flight, a scripted + // push-back, the museum guide walking the fly through the door -- which + // `scene::detect` calls `Unknown` because the buttons are not reaching the player. On the + // second, `NEXT` and `BACK` are an A and a B pressed into somebody else's script: they + // change nothing, they complete where the fly stands, and they are section 12.2's trap + // with no text box to advance. Measured live on rung 10: `BACK` **678** macro starts in + // 47 minutes, 189 of them on map `0x02` with no box on screen at all. So the pad is + // empty there and the fly waits, which is the doctrine's own answer for a scene with + // nothing sensible to press -- and the cartridge gives the buttons back by itself. + Scene::Unknown => { + if state.text_open() { + vec![Next, Back] + } else { + Vec::new() + } + } // `NEXT`, `YES`, `NO` for a plain box -- A and B both advance one, and what the three // buy is the fly being *able* to answer no. On the one box that **is** a choice, the pad // is the choice's own answers: section 12.12. diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs index 67a7b67..cf804f5 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs @@ -164,6 +164,13 @@ struct World { /// A frame at which the cartridge heals the party, which is what a Pokémon Center does while /// its text box is open (`docs/design/macros.md` section 13). heal_at: Option, + /// Whether a text box is drawn on a scene that is not [`Scene::Dialog`] + /// ([`MacroState::text_open`]). + /// + /// `Dialog` *is* an open box, so the reading is true there by construction; the field is for + /// `Unknown`, which holds both a screen with words on it -- the Pokedex, the trainer card, + /// OPTION -- and a frame of the overworld the cartridge is driving (section 12.13). + box_open: bool, /// Whether the two-option YES/NO box is the thing on screen ([`MacroState::yes_no_prompt`]). /// /// A field rather than a shape of the `list`, because on the cartridge it is a *drawn box* @@ -243,6 +250,7 @@ impl World { pending: None, switch: None, heal_at: None, + box_open: false, prompt: false, scripted: false, scripted_at: None, @@ -658,6 +666,13 @@ impl MacroState for World { self.scripted } + /// `wFontLoaded` is set for every dialogue box, which is what `Dialog` is; on `Unknown` the + /// fixture has to say, because that is the reading that tells a screen from a scripted + /// overworld frame (section 12.13). + fn text_open(&mut self) -> bool { + self.scene == Scene::Dialog || self.box_open + } + /// A drawn box is what the reading rests on, so a prompt cannot be open with no box open: /// `pokemon_red::state::yes_no_prompt` gates on `wFontLoaded` before it looks at the tiles. fn yes_no_prompt(&mut self) -> bool { @@ -990,12 +1005,31 @@ fn the_dialog_row_is_next_yes_and_no() { #[test] fn an_unknown_scene_is_dialog_with_advance_only() { let mut world = World::room(); + world.box_open = true; let palette = Palette::for_scene(Scene::Unknown, &mut world); // Row 9 of `infra/docs/macros-traps.md`, closed by section 13.1: B is what leaves the Pokédex, - // the trainer card and OPTION, and all three read `Unknown`. + // the trainer card and OPTION, and all three read `Unknown` with a box drawn. assert_eq!(names(&palette), ["NEXT", "BACK"]); } +#[test] +fn an_unknown_frame_with_no_box_on_it_deals_nothing() { + // Section 12.13, the rung-10 Pewter loop. The other half of `Unknown` is the overworld with + // the cartridge driving -- a warp in flight, a push-back, the museum guide walking the fly in + // -- where `scene::detect` falls through because the buttons are not reaching the player. + // There is no box to advance and no screen to leave, so `NEXT` and `BACK` are an A and a B + // pressed into somebody else's script: they change nothing and they complete where the fly + // stands, which is section 12.2's trap. The pad is empty and the fly waits. + let mut world = World::room(); + world.scripted = true; + assert!(!world.text_open()); + let palette = Palette::for_scene(Scene::Unknown, &mut world); + assert_eq!(palette.bound(), 0, "an A and a B into a script are not buttons"); + // And the moment the cartridge draws something, both are back. + world.box_open = true; + assert_eq!(names(&Palette::for_scene(Scene::Unknown, &mut world)), ["NEXT", "BACK"]); +} + #[test] fn the_menu_row_is_close_confirm_back() { let mut world = World::room(); @@ -2525,6 +2559,7 @@ fn a_forced_switch_plans_one_entry_and_the_other_scenes_plan_their_one_move() { ] { let mut world = World::room(); world.scene = scene; + world.box_open = scene == Scene::Unknown; assert_eq!(plan(&mut world), entries, "{}", scene.label()); } @@ -3139,6 +3174,10 @@ fn no_playable_scene_deals_an_empty_pad() { ] { let mut world = World::room(); world.scene = scene; + // `Unknown` is dealt on what is drawn (section 12.13): a screen with words on it has + // `NEXT` and `BACK`, and a scripted overworld frame is the one deliberate empty pad, + // which `an_unknown_frame_with_no_box_on_it_deals_nothing` is about. + world.box_open = scene == Scene::Unknown; world.battle = matches!(scene, Scene::Battle { .. }) .then_some((BattleKind::Wild, false, false)); assert!( @@ -4087,6 +4126,9 @@ fn no_playable_scene_and_no_sub_state_deals_an_empty_pad() { ] { let mut world = World::room(); world.scene = scene; + // See the sweep in `no_playable_scene_deals_an_empty_pad`: `Unknown` with nothing drawn + // on it is the overworld being driven by the cartridge, and its pad is empty by design. + world.box_open = scene == Scene::Unknown; world.mons.clear(); worst(&mut world); } diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs index 03e2e23..1faf84d 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs @@ -1342,6 +1342,10 @@ impl MacroState for PokeState<'_> { !controllable(self.memory) } + fn text_open(&mut self) -> bool { + text_box(self.memory).open + } + fn yes_no_prompt(&mut self) -> bool { yes_no_prompt(self.memory) }