probes: a mid-step survey, and the errands on the pad dump

`FLY_PROBE_CATCH=step` holds one direction from a checkpoint and prints, per frame, the
coordinates, the grid's verdict, the tiles the cross-check disagreed on and every candidate for
"a step is in progress". It is what row 54 turns on: whether the coordinates change at the start
of a step or at its end.

The pad dump gains section 13's two errands -- which building each names, whether the ledgers have
paid it, and what the walk would aim at -- because an aim with no press settles where it stands.
This commit is contained in:
acamilo 2026-09-22 14:57:33 +00:00
parent cb9a88c7a3
commit ed1d321b02

View file

@ -313,6 +313,27 @@ fn pad(gb: &mut Emulator, adapter: &PokemonRedReward, label: &str) {
ways.iter().map(|exit| exit.id).collect::<Vec<_>>()
);
}
// Section 13's two errands, which is what row 54's `GO HEAL` cycle turns on: which building
// the errand names, whether the ledgers have paid it, and what the walk would aim at. An aim
// with no press settles where it stands, so an aim on the fly's own tile is a macro that
// completes without moving.
println!("\n### The errands\n");
println!("- `area_here` = {:?}", palette::area_here(state));
for kind in [geography::Amenity::Mart, geography::Amenity::Center] {
let at = geography::amenity_of(palette::area_here(state).unwrap_or(0), kind);
let paid = at.is_some_and(|map| state.map_visited(map));
println!(
"- {kind:?}: building {:?} (map_visited {paid}), `errand` = {:?}, `amenity_goals` = {:?}",
at,
palette::errand(state, kind),
palette::amenity_goals(state, kind),
);
}
println!("- `counter_pending` = {}", palette::counter_pending(state));
println!("- `heal_goals` = {:?}", palette::heal_goals(state));
println!("- `errand_place` = {:?}", palette::errand_place(state));
println!("- `stranded` = {}", palette::stranded(state));
println!();
println!("- `objective_goals` = {:?}", palette::objective_goals(state));
println!("- `objective_targets` = {:?}", palette::objective_targets(state));
println!("- `untalked_people` = {:?}", palette::untalked_people(state));
@ -482,6 +503,113 @@ fn nurse_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64)
println!("```");
}
/// The survey the whole-map grid's mid-step refusal turns on (`infra/docs/macros-traps.md` row 54).
///
/// Section 15 checks the decode against the screen buffer over the fly's own tile and its four
/// neighbours, and the residual of 2026-09-22 measured that check refusing on **every frame the
/// fly is mid-step**: standing still Pewter City decoded on 118 of 120 frames, and the one frame
/// the survey caught disagreed by exactly one tile row in the direction of travel. A walk planned
/// on such a frame is planned over the ten-by-nine window, which is the oscillation of row 23.
///
/// Two things have to be measured before that can be fixed honestly, and neither can be argued
/// from the disassembly alone:
///
/// 1. **when `wXCoord` / `wYCoord` change** — at the start of a step or at the end of it. That
/// decides whether the screen is behind the coordinates or the coordinates ahead of the screen.
/// 2. **which byte says "a step is in progress"**. `docs/design/macros-wram.md` says the reviewed
/// symbol list carries none, so every plausible candidate is dumped across a whole step and the
/// one that tracks it is the reading.
///
/// It holds one direction from the checkpoint and prints a line per frame: the coordinates, the
/// grid's verdict, the tiles the cross-check disagreed on, and the candidates.
fn step_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) {
use flybrain_gb::pokemon_red::macros::state::Walkable;
let frames = env_usize("FLY_PROBE_STEP_FRAMES", 96);
let step = |gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64, mask: u8| {
gb.set_buttons(mask);
gb.run_frame().expect("a frame should complete");
*ms += MS_PER_FRAME;
adapter.sample(gb, *ms);
};
// Somewhere the fly is its own master and the map is on screen, so that a refusal below is
// about the step and not about a text box.
for _ in 0..600 {
if scene::detect(gb) == scene::Scene::Overworld && state::controllable(gb) {
break;
}
step(gb, adapter, ms, flybrain_gb::buttons::B);
}
let Some(here) = state::player(gb) else {
println!("\nNo player at the checkpoint, so there is no step to survey.");
return;
};
println!("\n## The mid-step survey, on map {:#04x} from ({}, {})\n", here.map, here.x, here.y);
// A direction with walkable ground on the other side of it, so the hold is a step rather than
// a turn into a wall.
let facings = [
(flybrain_gb::buttons::DOWN, 0i16, 1i16, "DOWN"),
(flybrain_gb::buttons::UP, 0, -1, "UP"),
(flybrain_gb::buttons::LEFT, -1, 0, "LEFT"),
(flybrain_gb::buttons::RIGHT, 1, 0, "RIGHT"),
];
let mut chosen = None;
for (mask, dx, dy, name) in facings {
let (Ok(x), Ok(y)) =
(u8::try_from(i16::from(here.x) + dx), u8::try_from(i16::from(here.y) + dy))
else {
continue;
};
if state::walkable(gb, x, y) == Walkable::Yes {
chosen = Some((mask, name));
break;
}
}
let Some((mask, name)) = chosen else {
println!("Every neighbour of the fly is a wall, so there is no step to survey.");
return;
};
println!("Holding {name} for {frames} frames.\n");
println!("```");
println!(
"frame coords grid s1+1,+3,+5,+7,+8,+9 scy scx cfc5 d730 d736"
);
for frame in 0..frames {
let sprite: Vec<String> = [1u16, 3, 5, 7, 8, 9]
.into_iter()
.map(|offset| format!("{:02x}", gb.read8(ram::wSpriteStateData1 + offset)))
.collect();
let scy = gb.read8(0xff42);
let scx = gb.read8(0xff43);
let cfc5 = gb.read8(0xcfc5);
let d730 = gb.read8(ram::wStatusFlags5);
let d736 = gb.read8(ram::wMovementFlags);
let verdict = match state::map_grid(gb) {
Ok(_) => "ok".to_string(),
Err(refusal) => {
let shown: Vec<String> = state::grid_disagreement(gb)
.into_iter()
.filter(|(_, _, decoded, screen)| decoded != screen)
.map(|(x, y, decoded, screen)| format!("({x},{y}){decoded:?}/{screen:?}"))
.collect();
format!("{} {}", refusal.label(), shown.join(" "))
}
};
let coords = state::player(gb)
.map(|player| format!("({:>2},{:>2})", player.x, player.y))
.unwrap_or_else(|| " none ".to_string());
println!(
"{frame:>5} {coords} {verdict:<30} {} {scy:>3} {scx:>3} {cfc5:02x} {d730:02x} {d736:02x}",
sprite.join(",")
);
step(gb, adapter, ms, mask);
}
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.");
@ -527,6 +655,13 @@ fn main() {
return;
}
// Row 54's mid-step survey: hold one direction and watch the grid's cross-check, the
// coordinates and every candidate for "a step is in progress" across a whole step.
if std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "step") {
step_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;