From 288e958d19b836a52ddfa42463c514f77ed00196 Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 14:02:32 +0000 Subject: [PATCH] survey: FLY_PROBE_SAVE_RANK writes the first safe overworld frame at a rung as a checkpoint --- .../crates/flysim/examples/scene_probe.rs | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/services/flysim/crates/flysim/examples/scene_probe.rs b/services/flysim/crates/flysim/examples/scene_probe.rs index 89115db..79ebdd7 100644 --- a/services/flysim/crates/flysim/examples/scene_probe.rs +++ b/services/flysim/crates/flysim/examples/scene_probe.rs @@ -1346,7 +1346,12 @@ fn dialog_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64 /// (comma-separated names) presses those buttons whenever they are dealt. /// /// [`PokemonPalette`]: flybrain_gb::pokemon_red::macros::PokemonPalette -fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) { +fn route_survey( + gb: &mut Emulator, + adapter: &mut PokemonRedReward, + ms: &mut f64, + checkpoint: &flysim::store::Checkpoint, +) { use flybrain_gb::MacroPalette; use flybrain_gb::pokemon_red::macros::cartridge::{FACINGS, MacroState, TalkTarget, TargetKey}; use flybrain_gb::pokemon_red::macros::path::Way; @@ -1380,6 +1385,9 @@ fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) let mut entries = 0usize; let mut arrived_at = 0usize; let mut last_map: Option = None; + let save_rank: Option = + std::env::var("FLY_PROBE_SAVE_RANK").ok().and_then(|value| value.parse().ok()); + let mut saved = false; // `FLY_PROBE_HOLD=right:96,up:32` holds raw directions first and prints where the fly is // every eight frames: what the cartridge does with a press, before any macro is asked. @@ -1508,6 +1516,28 @@ fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) gb.run_frame().expect("a frame should complete"); *ms += MS_PER_FRAME; adapter.sample(gb, *ms); + // `FLY_PROBE_SAVE_RANK=11` with `FLY_PROBE_SAVE` writes the first frame at or past that + // rung where the fly has the buttons in the overworld and no macro is running: a state the + // stream could be restored into next (row 59, after the badge), carried forward by the + // survey from the source checkpoint. The session ledgers are not in it, as they are in no + // checkpoint. + if let Some(want) = save_rank + && !saved + && adapter.progress().rank >= want + && !running + && matches!(observed.scene, flybrain_gb::SceneId::Overworld) + && state::controllable(gb) + && let Some(path) = std::env::var_os("FLY_PROBE_SAVE") + { + saved = true; + let bytes = save_state(checkpoint, gb, adapter, frame, &path); + println!( + "f{frame:<6} {:?} saved rank {} to `{}` ({bytes} bytes)", + state::player(gb).map(|p| (p.map, p.x, p.y)), + adapter.progress().rank, + std::path::Path::new(&path).display() + ); + } if single_refusals >= catch_after { caught_at = Some(frame); break; @@ -1673,6 +1703,28 @@ fn separator_table(classes: &BTreeMap<(bool, bool, bool), u64>) -> String { out } +/// This state as a `FLYSIM01` checkpoint: the agent half is the source checkpoint's, unchanged -- +/// the release box's own run carried forward by the stub, not a synthesised save -- and `.local/` +/// is not tracked, exactly as every other checkpoint in this workspace. Returns the size written. +fn save_state( + checkpoint: &flysim::store::Checkpoint, + gb: &mut Emulator, + adapter: &PokemonRedReward, + frame: usize, + path: &std::ffi::OsStr, +) -> usize { + let mut runtime = checkpoint.runtime.clone(); + runtime.emulator = gb.export_state().expect("the emulator should export"); + runtime.reward = adapter.export_state(); + runtime.framebuffer = gb.framebuffer().to_vec(); + runtime.emulator_frame = frame as u64; + let bytes = + flysim::store::encode(&checkpoint.agent, &runtime).expect("the envelope should encode"); + flysim::store::write_atomic(std::path::Path::new(path), &bytes) + .expect("the checkpoint should be writable"); + bytes.len() +} + fn main() { let Some(path) = std::env::var_os("FLY_ROM") else { println!("FLY_ROM is not set, so there is nothing to probe."); @@ -1764,7 +1816,7 @@ fn main() { // Row 57's pad survey: earn the session's ledgers from the checkpoint with the real palette, // and read the frame the pad comes down to one refusing button on. if std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "route") { - route_survey(&mut gb, &mut adapter, &mut ms); + route_survey(&mut gb, &mut adapter, &mut ms, &checkpoint); return; } @@ -1877,19 +1929,10 @@ fn main() { // release box's own run carried forward by the stub, not a synthesised save -- and // `.local/` is not tracked, exactly as every other checkpoint in this workspace. if let Some(save) = std::env::var_os("FLY_PROBE_SAVE") { - let mut runtime = checkpoint.runtime.clone(); - runtime.emulator = gb.export_state().expect("the emulator should export"); - runtime.reward = adapter.export_state(); - runtime.framebuffer = gb.framebuffer().to_vec(); - runtime.emulator_frame = frame as u64; - let bytes = flysim::store::encode(&checkpoint.agent, &runtime) - .expect("the envelope should encode"); - flysim::store::write_atomic(std::path::Path::new(&save), &bytes) - .expect("the checkpoint should be writable"); + let bytes = save_state(&checkpoint, &mut gb, &adapter, frame, &save); println!( - "\nWrote this state to `{}` ({} bytes).", - std::path::Path::new(&save).display(), - bytes.len() + "\nWrote this state to `{}` ({bytes} bytes).", + std::path::Path::new(&save).display() ); } // The survey method on the one question the fix turns on: **what does the