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 f2913c3..03e2e23 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs @@ -937,6 +937,33 @@ impl GridRefusal { /// ([`GridRefusal::NoScreen`]) rather than trusted, because `wOverworldMap` shares its bytes with /// the picture buffer and a battle is exactly when the blocks under it are somebody else's. pub fn map_grid(memory: &mut dyn MemoryReader) -> Result { + // The decode first, so a frame with no header answers `NoHeader` rather than whatever the + // player's coordinates happen to read as: the refusals are in the order they are checked. + let grid = map_grid_decode(memory)?; + let player = player(memory).ok_or(GridRefusal::NoPlayer)?; + // The cross-check. `map_tile_id` reads the screen buffer at the offset + // `_GetTileAndCoordsInFrontOfPlayer` uses, so agreeing with it on the tiles it can answer for + // is agreeing with the cartridge's own reading of the same ground. + let mut checked = 0; + for (x, y) in neighbourhood(player.x, player.y) { + let Some(screen) = map_tile_id(memory, x, y) else { continue }; + if grid.tile_id(x, y) != Some(screen) { + return Err(GridRefusal::ScreenDisagrees); + } + checked += 1; + } + if checked == 0 { + return Err(GridRefusal::NoScreen); + } + Ok(grid) +} + +/// [`map_grid`] without the cross-check: the blocks, the blockset and the collision list, decoded. +/// +/// Split out so [`grid_disagreement`] can say what the decode answered on a frame the check +/// refused. Nothing outside this module and the probes may use it: a grid that has not been +/// checked against the screen is exactly the reading section 15 refuses to trust. +fn map_grid_decode(memory: &mut dyn MemoryReader) -> Result { let size = map_size(memory).ok_or(GridRefusal::NoHeader)?; let player = player(memory).ok_or(GridRefusal::NoPlayer)?; let passable = collision_list(memory).ok_or(GridRefusal::NoCollisionList)?; @@ -979,23 +1006,28 @@ pub fn map_grid(memory: &mut dyn MemoryReader) -> Result { if grid.width() != size.width || grid.height() != size.height { return Err(GridRefusal::NoHeader); } - // The cross-check. `map_tile_id` reads the screen buffer at the offset - // `_GetTileAndCoordsInFrontOfPlayer` uses, so agreeing with it on the tiles it can answer for - // is agreeing with the cartridge's own reading of the same ground. - let mut checked = 0; - for (x, y) in neighbourhood(player.x, player.y) { - let Some(screen) = map_tile_id(memory, x, y) else { continue }; - if grid.tile_id(x, y) != Some(screen) { - return Err(GridRefusal::ScreenDisagrees); - } - checked += 1; - } - if checked == 0 { - return Err(GridRefusal::NoScreen); - } Ok(grid) } +/// The decode and the screen, tile by tile, for the five tiles [`map_grid`] cross-checks. +/// +/// The diagnostic half of [`GridRefusal::ScreenDisagrees`]: the refusal says the two readings +/// disagree and this says *where* and *by how much*, which is the difference between "the grid is +/// off on this map" and "this frame was mid-warp". `(x, y, decoded, screen)`, with `None` for a +/// tile either reading cannot answer for. It decodes the map a second time rather than being +/// folded into [`map_grid`], because the check's job on the hot path is to refuse and this is only +/// ever asked by a probe. +pub fn grid_disagreement(memory: &mut dyn MemoryReader) -> Vec<(u8, u8, Option, Option)> { + let Some(player) = player(memory) else { return Vec::new() }; + let grid = map_grid_decode(memory).ok(); + neighbourhood(player.x, player.y) + .into_iter() + .map(|(x, y)| { + (x, y, grid.as_ref().and_then(|grid| grid.tile_id(x, y)), map_tile_id(memory, x, y)) + }) + .collect() +} + /// Whether a cached grid is still the map that is loaded, checked from the tile the fly is on. /// /// The map id, the map header and the block data are written by different parts of a warp, so diff --git a/services/flysim/crates/flysim/examples/scene_probe.rs b/services/flysim/crates/flysim/examples/scene_probe.rs index 37c447b..d33beeb 100644 --- a/services/flysim/crates/flysim/examples/scene_probe.rs +++ b/services/flysim/crates/flysim/examples/scene_probe.rs @@ -31,6 +31,8 @@ //! | `FLY_PROBE_FRAMES` | 200000 | frames to drive before giving up | //! | `FLY_PROBE_STUCK` | 600 | consecutive frames in one non-overworld scene that count as stuck | +use std::collections::BTreeMap; + use flybrain_core::decoder::PopulationDecoder; use flybrain_core::decoder::gameboy::gameboy_decoder_config_with_macros; use flybrain_core::ordered::NumberMap; @@ -161,6 +163,16 @@ fn pad(gb: &mut Emulator, adapter: &PokemonRedReward, label: &str) { // same call `MacroState::map_grid` makes, and the only reading that can say *which* of section // 15's refusals a frame is. let refusal = flybrain_gb::pokemon_red::state::map_grid(gb).err(); + // Which tile the decode and the screen disagree about, when that is the refusal. The label + // alone cannot tell "the grid is wrong on this map" from "this frame was mid-warp", and the + // two want opposite fixes (`docs/design/macros.md` section 15). Read here, beside the refusal + // itself, because everything below holds a borrow of the emulator. + let disagreement = match refusal { + Some(flybrain_gb::pokemon_red::state::GridRefusal::ScreenDisagrees) => { + flybrain_gb::pokemon_red::state::grid_disagreement(gb) + } + _ => Vec::new(), + }; let ledger = AdapterLedger(adapter); let mut poke = flybrain_gb::pokemon_red::state::PokeState::with_ledger(gb, &ledger); let state: &mut dyn MacroState = &mut poke; @@ -212,6 +224,22 @@ fn pad(gb: &mut Emulator, adapter: &PokemonRedReward, label: &str) { ); } } + // Which tile the decode and the screen disagree about, when that is the refusal. The label + // alone cannot tell "the grid is wrong on this map" from "this frame was mid-warp", and the + // two want opposite fixes (`docs/design/macros.md` section 15). + if !disagreement.is_empty() { + println!("- the decode against the screen, tile by tile:"); + for (x, y, decoded, screen) in disagreement { + println!( + " - ({x:2}, {y:2}) decoded {decoded:?} screen {screen:?}{}", + if decoded.is_some() && screen.is_some() && decoded != screen { + " <- the disagreement" + } else { + "" + } + ); + } + } // The `v` column is the *adapter's* lifetime exploration ledger and nothing else. The // session's own stood ledger (`docs/design/macros.md` section 12.7) is owned by the driver's // palette, which this probe does not reach into, so a doormat the running fly has already @@ -710,6 +738,25 @@ fn main() { && surveyed >= 60 { println!("\nThe fly reached map {map:#04x} at frame {frame}."); + // Stand still for a while and count how many of those frames the whole-map grid can be + // decoded on. A walking fly is mid-step on most frames -- `wYCoord` is the tile it is + // walking *to* while the background is still scrolling -- and the screen buffer the + // cross-check reads is the one that is a tile behind, so "is the grid refused on this + // map" and "is the grid refused while the fly is moving" are different questions with + // different fixes (`docs/design/macros.md` section 15). + let mut refusals: BTreeMap<&'static str, usize> = BTreeMap::new(); + for _ in 0..env_usize("FLY_PROBE_SETTLE", 120) { + gb.set_buttons(flybrain_gb::buttons::NONE); + gb.run_frame().expect("a frame should complete"); + ms += MS_PER_FRAME; + adapter.sample(&mut gb, ms); + let label = match flybrain_gb::pokemon_red::state::map_grid(&mut gb) { + Ok(_) => "decoded", + Err(refusal) => refusal.label(), + }; + *refusals.entry(label).or_insert(0) += 1; + } + println!("\nStanding still on it, frame by frame: {refusals:?}"); cartridge(&mut gb, &adapter, "The save on the surveyed map"); pad(&mut gb, &adapter, "The pad on the surveyed map"); return;