ratchet: getting nearer the objective is progress the coverage figure cannot carry
Two Stuck rollbacks fired on rung 10 inside half an hour, both on a fly that was walking, and both were the ratchet working to contract: the stall window is reset by exploration -- one tile the run has never stood on -- and a fly crossing a town it has already covered to reach the rung's own door earns none of it. Entering a map for the first time already counts, because a new map is ground nobody has stood on; re-entering one does not, which is what the museum was. So the window gains a second signal, passed in by the caller and meaningless to the ratchet itself, exactly as coverage is: the Pokemon loop answers with "nearer the objective, in map hops, than this run has ever been", over the same map graph GO OBJECTIVE walks. It can fire at most once per step of the road, it spends no budget, it captures nothing and it skips no trigger. Nothing in the macro layer reads it back and no button is bound on it. The checkpointed ratchet state is untouched: the signal is a level on one sample, not a counter.
This commit is contained in:
parent
26f957e084
commit
e0285016aa
6 changed files with 290 additions and 5 deletions
|
|
@ -264,6 +264,27 @@ pub trait MacroPalette: Send {
|
|||
/// Give up on whatever is running, because the sim loop is rolling the game back. The
|
||||
/// abandoned macro is reported by the next [`MacroPalette::take_finished`].
|
||||
fn cancel(&mut self);
|
||||
|
||||
/// Whether the last [`MacroPalette::observe`] saw the fly **nearer its objective than it has
|
||||
/// been** since that objective was set, measured in map hops.
|
||||
///
|
||||
/// The ratchet's stall window is reset by exploration -- one new tile
|
||||
/// (`docs/design/ladder.md`, the 2026-09-17 progress rule) -- and a fly crossing a town it
|
||||
/// has already covered to reach the rung's own door earns no new ground while it does it.
|
||||
/// That is the rung-10 stall of 2026-09-22 in one line: two "Stuck" rollbacks inside half an
|
||||
/// hour, both of them on a fly that was walking, both of them landing it back where it had
|
||||
/// started. Getting nearer the objective than this run has ever been is the other thing that
|
||||
/// is plainly progress, and it is a *level* rather than a counter so nothing is checkpointed
|
||||
/// and nothing can drift: it is true on the frame the distance falls and false after.
|
||||
///
|
||||
/// Read by the sim loop and by nothing else. No macro is ranked by it, no button is bound on
|
||||
/// it and it presses nothing (`docs/design/macros.md` section 12): it is the loop's own
|
||||
/// answer to "is this run getting somewhere".
|
||||
///
|
||||
/// The default is `false`, which is a palette with no objective to be nearer to.
|
||||
fn nearer_the_objective(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Every macro channel a game's palette can ever bind, in the contract's own order, or empty for
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use super::super::state::PokeState;
|
|||
use super::cartridge::{Areas, Frontiers, MacroState, Pushed, Stood, Talked, Targets, Tile};
|
||||
use super::geography;
|
||||
use super::executor::{MacroAbort, MacroMachine, Refusal};
|
||||
use super::palette::{MacroId, Palette};
|
||||
use super::palette::{self, MacroId, Palette};
|
||||
use super::plan;
|
||||
use super::state::{GameState, Scene};
|
||||
|
||||
|
|
@ -88,6 +88,17 @@ pub struct PokemonPalette {
|
|||
/// frame. It is a *cache* rather than a ledger: nothing about the run is in it, only what the
|
||||
/// cartridge's own tables say about the ground.
|
||||
grids: MapGrids,
|
||||
/// The fewest map hops between the fly and its objective this run has managed, and which
|
||||
/// objective that was (`docs/design/macros.md` section 12.15).
|
||||
///
|
||||
/// Session state beside the ledgers and never checkpointed, and unlike them it is not a fact
|
||||
/// about the map at all: it is the one reading the *sim loop* takes from the macro layer, for
|
||||
/// the ratchet's stall window. The objective is carried with the number because the ladder's
|
||||
/// next rung changes as the run climbs, and "nearer" means nothing across two different
|
||||
/// places.
|
||||
nearest: Option<(u8, u32)>,
|
||||
/// Whether the last `observe` was the frame that number fell on.
|
||||
nearer: bool,
|
||||
/// The brain clock of the frame being decided, from [`MacroPalette::clock`].
|
||||
///
|
||||
/// The blocked ledger is a *window*, so it needs the same clock the loop publishes rather
|
||||
|
|
@ -113,6 +124,8 @@ impl PokemonPalette {
|
|||
frontiers: Frontiers::default(),
|
||||
pushed: Pushed::default(),
|
||||
grids: MapGrids::default(),
|
||||
nearest: None,
|
||||
nearer: false,
|
||||
now_ms: 0.0,
|
||||
}
|
||||
}
|
||||
|
|
@ -187,7 +200,7 @@ impl MacroPalette for PokemonPalette {
|
|||
}
|
||||
|
||||
fn observe(&mut self, memory: &mut dyn MemoryReader, ledger: &dyn RunLedger) -> Observed {
|
||||
let (scene, bindings, standing) = {
|
||||
let (scene, bindings, standing, approach) = {
|
||||
let Self {
|
||||
machine,
|
||||
mode,
|
||||
|
|
@ -224,8 +237,17 @@ impl MacroPalette for PokemonPalette {
|
|||
// -- the coordinates and the loaded map header are from different frames, and a tile
|
||||
// recorded from that pair is a tile of nowhere.
|
||||
let standing = (!state.scripted()).then(|| state.player()).flatten();
|
||||
// How far the objective is, over the same map graph `GO OBJECTIVE` walks (section
|
||||
// 12.15). Read from the same frame and the same state everything else is, and only
|
||||
// where the fly is its own master, for the same reason the ground is.
|
||||
let approach = standing.and_then(|player| {
|
||||
let objective = palette::objective_place(&mut state)?;
|
||||
let hops =
|
||||
geography::hops(geography::region_at(player.map, player.y), objective.map)?;
|
||||
Some((objective.map, hops))
|
||||
});
|
||||
*cached = Some(palette);
|
||||
(scene, bindings, standing)
|
||||
(scene, bindings, standing, approach)
|
||||
};
|
||||
// Section 12.7: the macro layer's own answer to "has the run stood here", because the
|
||||
// adapter's reward ledger cannot record a doormat.
|
||||
|
|
@ -248,6 +270,22 @@ impl MacroPalette for PokemonPalette {
|
|||
self.areas.record(kind, area);
|
||||
}
|
||||
}
|
||||
// Section 12.15: nearer the objective than this run has ever been, which is the other
|
||||
// thing that is plainly progress and which the ratchet's stall window cannot see in the
|
||||
// exploration ledger. A level, true on the frame the number falls and false after, so
|
||||
// there is nothing to checkpoint and nothing to drift. A different objective starts the
|
||||
// measurement again: the ladder's next rung moves as the run climbs and "nearer" means
|
||||
// nothing across two different places.
|
||||
self.nearer = match (self.nearest, approach) {
|
||||
(_, None) => false,
|
||||
(None, Some(_)) => false,
|
||||
(Some((was, best)), Some((map, hops))) => map == was && hops < best,
|
||||
};
|
||||
self.nearest = match (self.nearest, approach) {
|
||||
(_, None) => self.nearest,
|
||||
(Some((was, best)), Some((map, hops))) if map == was => Some((map, best.min(hops))),
|
||||
(_, Some(now)) => Some(now),
|
||||
};
|
||||
// The talked entry `observe_frame` may just have earned, into the ledger the next frame
|
||||
// reads.
|
||||
self.record_talk();
|
||||
|
|
@ -343,6 +381,10 @@ impl MacroPalette for PokemonPalette {
|
|||
Some((name, outcome(abort)))
|
||||
}
|
||||
|
||||
fn nearer_the_objective(&self) -> bool {
|
||||
self.nearer
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
self.machine.cancel();
|
||||
// A cancelled macro talked to nothing, and a stale entry would silence a person for the
|
||||
|
|
@ -413,6 +455,8 @@ mod tests {
|
|||
use crate::adapter::{MapEdge, MapExit};
|
||||
use crate::macros::NoLedger;
|
||||
use crate::pokemon_red::fake_wram::{REDS_HOUSE_1F, Wram};
|
||||
use crate::pokemon_red::macros::geography::Amenity;
|
||||
use crate::pokemon_red::maps;
|
||||
use crate::pokemon_red::macros::cartridge::{Edge, ExitId, MacroState};
|
||||
|
||||
/// A ledger with one exit in it, for the wiring test below.
|
||||
|
|
@ -441,6 +485,67 @@ mod tests {
|
|||
assert_eq!(palette.take_finished(), None);
|
||||
}
|
||||
|
||||
/// A ledger whose objective is one map, for the approach reading below.
|
||||
struct Bound(u8);
|
||||
|
||||
impl RunLedger for Bound {
|
||||
fn exit_visited(&self, _exit: MapExit) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn objective(&self) -> Option<crate::adapter::MapPlace> {
|
||||
Some(crate::adapter::MapPlace {
|
||||
map: self.0,
|
||||
tile: None,
|
||||
warp: None,
|
||||
edge: None,
|
||||
target: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_objective_getting_nearer_is_read_once_per_step_of_the_road() {
|
||||
// Section 12.15, the rung-10 stall. The ratchet's stall window is reset by ground never
|
||||
// stood on, and a fly walking a road it has already covered earns none -- so this is the
|
||||
// other reading, and what it has to be is a *level* that is true on the frame the hop
|
||||
// count falls and false on every frame after it. Pewter's own museum is the road: the
|
||||
// upper floor is three hops from the gym, the ground floor two, the town one.
|
||||
let mut wram = Wram::new();
|
||||
wram.started().map(maps::PEWTER_MUSEUM_2F, 4, 4, 3, 6).facing(0).house_collision();
|
||||
let mut palette = PokemonPalette::new(7);
|
||||
// Both of Pewter's errands discharged, so the objective is the rung's own place for the
|
||||
// whole test: section 13 puts an unvisited mart or centre *ahead* of it, and that is a
|
||||
// different place to be near.
|
||||
palette.areas.record(Amenity::Mart, maps::PEWTER_CITY);
|
||||
palette.areas.record(Amenity::Center, maps::PEWTER_CITY);
|
||||
let ledger = Bound(maps::PEWTER_GYM);
|
||||
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(!palette.nearer_the_objective(), "the first reading is a measurement, not a step");
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(!palette.nearer_the_objective(), "standing still is not nearer");
|
||||
|
||||
wram.map(maps::PEWTER_MUSEUM_1F, 4, 4, 3, 6);
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(palette.nearer_the_objective(), "down the stairs is one hop nearer");
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(!palette.nearer_the_objective(), "and it is read once, not held");
|
||||
|
||||
// Back upstairs is not progress, and it does not undo the number either: the measurement
|
||||
// is the best this run has managed, so walking the road twice pays once.
|
||||
wram.map(maps::PEWTER_MUSEUM_2F, 4, 4, 3, 6);
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(!palette.nearer_the_objective());
|
||||
wram.map(maps::PEWTER_MUSEUM_1F, 4, 4, 3, 6);
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(!palette.nearer_the_objective(), "ground already gained is not gained again");
|
||||
|
||||
wram.map(maps::PEWTER_CITY, 4, 4, 3, 6);
|
||||
palette.observe(&mut wram, &ledger);
|
||||
assert!(palette.nearer_the_objective(), "out of the front door is nearer still");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standing_on_new_ground_is_what_clears_a_maps_frontier_mark() {
|
||||
// Section 12.14's other half, wired: the mark is written by a `GO FRONTIER` that could
|
||||
|
|
|
|||
|
|
@ -341,6 +341,39 @@ pub fn next_hop(from: Region, to: u8) -> Option<u8> {
|
|||
None
|
||||
}
|
||||
|
||||
/// How many hops the shortest known route from `from` to the map `to` takes, or `None` when none
|
||||
/// is known.
|
||||
///
|
||||
/// [`next_hop`]'s own breadth-first walk, counting instead of naming: `Some(0)` when the fly is
|
||||
/// already on `to`, `Some(1)` for a door out of this map into it, and `None` for a map the table
|
||||
/// cannot route to -- which is the same "nothing is guessed" [`next_hop`] answers with.
|
||||
///
|
||||
/// What it is *for* is the ratchet (`docs/design/ladder.md`, the 2026-09-17 progress rule): the
|
||||
/// stall window is reset by exploration, and a fly crossing a town it has already covered to
|
||||
/// reach the rung's own door earns no new ground while it does it. "Nearer the objective than
|
||||
/// this run has ever been" is the other thing that is plainly progress, and it is this number
|
||||
/// falling. Nothing about the *choice* reads it: no macro is ranked by it and no button is bound
|
||||
/// on it.
|
||||
pub fn hops(from: Region, to: u8) -> Option<u32> {
|
||||
if from.map == to {
|
||||
return Some(0);
|
||||
}
|
||||
let mut seen: HashSet<Region> = HashSet::from([from]);
|
||||
let mut queue: VecDeque<(Region, u32)> = VecDeque::new();
|
||||
queue.push_back((from, 0));
|
||||
while let Some((region, depth)) = queue.pop_front() {
|
||||
if region.map == to {
|
||||
return Some(depth);
|
||||
}
|
||||
for next in region_neighbours(region) {
|
||||
if seen.insert(next) {
|
||||
queue.push_back((next, depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// A building an area has at most one of, and which this run may not have been into yet.
|
||||
///
|
||||
/// `docs/design/macros.md` section 13: the two errands. A kind rather than two parallel tables
|
||||
|
|
@ -593,6 +626,34 @@ mod tests {
|
|||
assert_eq!(amenity_at(maps::PEWTER_MUSEUM_2F), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hop_count_is_the_road_measured_rather_than_named() {
|
||||
let at = |map: u8| Region::whole(map);
|
||||
assert_eq!(hops(at(maps::PEWTER_CITY), maps::PEWTER_CITY), Some(0), "already there");
|
||||
assert_eq!(hops(at(maps::PEWTER_CITY), maps::PEWTER_GYM), Some(1), "one door");
|
||||
assert_eq!(hops(at(maps::PEWTER_MUSEUM_1F), maps::PEWTER_GYM), Some(2));
|
||||
assert_eq!(hops(at(maps::PEWTER_MUSEUM_2F), maps::PEWTER_GYM), Some(3));
|
||||
// The count agrees with the hop by hop answer, which is the thing it has to: walking the
|
||||
// road one `next_hop` at a time takes exactly this many steps.
|
||||
let mut here = at(maps::PEWTER_MUSEUM_2F);
|
||||
let mut steps = 0;
|
||||
while let Some(hop) = next_hop(here, maps::PEWTER_GYM) {
|
||||
here = Region::whole(hop);
|
||||
steps += 1;
|
||||
assert!(steps < 10, "the road to the gym does not wander");
|
||||
}
|
||||
assert_eq!(here.map, maps::PEWTER_GYM);
|
||||
assert_eq!(steps, 3);
|
||||
// A split map is measured from the piece the fly is standing in, exactly as `next_hop` is.
|
||||
assert_eq!(hops(region_at(maps::ROUTE_2, 11), maps::PEWTER_CITY), Some(1));
|
||||
// Five from the south half, because the belt of trees between the halves needs CUT and
|
||||
// the road is the forest: the south gate, the forest, the north gate, Route 2's north
|
||||
// half, Pewter.
|
||||
assert_eq!(hops(region_at(maps::ROUTE_2, 43), maps::PEWTER_CITY), Some(5));
|
||||
// And nothing is guessed.
|
||||
assert_eq!(hops(at(maps::PALLET_TOWN), 0xf0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_maps_area_is_its_town_indoors_and_out() {
|
||||
// Outdoors a map is its own area, including a route -- which has no amenity row, so it
|
||||
|
|
|
|||
|
|
@ -186,6 +186,40 @@ impl Ratchet {
|
|||
game_over: bool,
|
||||
capture: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnOnce() -> Snapshot,
|
||||
{
|
||||
self.observe_with_progress(safe, rank, coverage, now, game_over, false, capture)
|
||||
}
|
||||
|
||||
/// [`Ratchet::observe_with_game_over`] plus a progress signal the coverage figure cannot
|
||||
/// carry.
|
||||
///
|
||||
/// `progressed` is the caller saying "something happened on this sample that is plainly
|
||||
/// progress, and it is not a number that only grows". The stall window is restarted by it
|
||||
/// exactly as a rise in `coverage` restarts it, and nothing else changes: no budget is
|
||||
/// spent, no snapshot is taken and no trigger is skipped.
|
||||
///
|
||||
/// It exists because coverage is the *only* progress the window could see, and coverage is
|
||||
/// ground never stood on. A fly crossing a town it has already covered to reach the rung's
|
||||
/// own door is getting somewhere and earns none. Measured on rung 10, 2026-09-22: two
|
||||
/// "Stuck" rollbacks inside half an hour, both on a fly that was walking, each one landing
|
||||
/// it back where it had started. What the Pokemon loop passes here is "nearer the objective,
|
||||
/// in map hops, than this run has ever been" (`docs/design/macros.md` section 12.15), which
|
||||
/// can only fire as many times as the road is long.
|
||||
///
|
||||
/// Nothing in this module knows what the signal means, which is the same bargain `coverage`
|
||||
/// is: the ladder, the map graph and the objective all belong to the adapter.
|
||||
pub fn observe_with_progress<F>(
|
||||
&mut self,
|
||||
safe: bool,
|
||||
rank: u64,
|
||||
coverage: u64,
|
||||
now: u64,
|
||||
game_over: bool,
|
||||
progressed: bool,
|
||||
capture: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnOnce() -> Snapshot,
|
||||
{
|
||||
|
|
@ -200,7 +234,7 @@ impl Ratchet {
|
|||
} else {
|
||||
self.unsafe_since = None;
|
||||
}
|
||||
if coverage > self.state.coverage {
|
||||
if coverage > self.state.coverage || progressed {
|
||||
self.state.last_progress = now;
|
||||
}
|
||||
self.state.coverage = self.state.coverage.max(coverage);
|
||||
|
|
@ -458,4 +492,51 @@ mod tests {
|
|||
assert!(!ratchet.observe(true, 1, 2, 230_000, || snapshot(1)));
|
||||
assert!(ratchet.observe(true, 1, 2, 239_001, || snapshot(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_map_the_run_has_never_stood_on_is_coverage_and_resets_the_window() {
|
||||
// The half of the 2026-09-17 rule that already held, pinned because the other half is
|
||||
// new: entering a map for the first time is ground never stood on, so it arrives here as
|
||||
// a rise in `coverage` and needs nothing of its own. Re-entering a map the run has
|
||||
// covered is not, which is exactly the rung-10 museum and why the signal below exists.
|
||||
let mut ratchet = Ratchet::new();
|
||||
ratchet.observe(true, 1, 100, 0, || snapshot(1));
|
||||
// A new map: eighty tiles nobody had stood on.
|
||||
assert!(!ratchet.observe(true, 1, 180, 100_000, || snapshot(1)));
|
||||
// Walking it again for two minutes adds none, and the window ages from the arrival.
|
||||
assert!(!ratchet.observe(true, 1, 180, 219_000, || snapshot(1)));
|
||||
assert!(ratchet.observe(true, 1, 180, 220_001, || snapshot(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getting_nearer_the_objective_resets_the_stall_window_without_spending_anything() {
|
||||
// The rung-10 rollbacks of 2026-09-22: a fly walking a town it has already covered
|
||||
// toward the rung's own door earns no coverage while it does it, and the window aged out
|
||||
// twice in half an hour. A caller's progress signal restarts the window exactly as
|
||||
// coverage does -- and does nothing else.
|
||||
let mut ratchet = Ratchet::new();
|
||||
ratchet.observe(true, 1, 100, 0, || snapshot(1));
|
||||
// 119 s of walking ground the run has covered, and then one step nearer the gym.
|
||||
assert!(!ratchet.observe_with_progress(true, 1, 100, 119_000, false, true, || snapshot(1)));
|
||||
let state = ratchet.state;
|
||||
assert_eq!(state.last_progress, 119_000);
|
||||
assert_eq!((state.recoveries, state.attempts), (0, 0), "progress spends no budget");
|
||||
// The window now runs from there rather than from the start.
|
||||
assert!(!ratchet.observe(true, 1, 100, 238_000, || snapshot(1)));
|
||||
assert!(ratchet.observe(true, 1, 100, 239_001, || snapshot(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_progress_signal_cannot_conjure_a_recovery_or_skip_a_trigger() {
|
||||
// It restarts the window; it is not a trigger and it is not a snapshot. With nothing
|
||||
// archived there is nothing to recover to, and an unsafe sample still never recovers.
|
||||
let mut ratchet = Ratchet::new();
|
||||
assert!(!ratchet.observe_with_progress(true, 0, 0, 0, false, true, || snapshot(1)));
|
||||
assert!(ratchet.snapshot.is_none(), "progress captures nothing");
|
||||
let mut armed = Ratchet::new();
|
||||
armed.observe(true, 1, 1, 0, || snapshot(1));
|
||||
// Unsafe, stalled, and told that something progressed: still no recovery, because an
|
||||
// unsafe sample would restore on top of whatever made it unsafe.
|
||||
assert!(!armed.observe_with_progress(false, 1, 1, 400_000, false, true, || snapshot(1)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -500,6 +500,15 @@ impl MacroLayer {
|
|||
/// The sim loop passes this to [`flybrain_core::decoder::PopulationDecoder::decode_bound`] on
|
||||
/// the same frame, which is why it is the layer's to answer: the bindings are what the last
|
||||
/// `observe` dealt, and nothing else in the loop knows them.
|
||||
/// Whether this frame saw the fly nearer its objective than the run has managed before.
|
||||
///
|
||||
/// Straight through from the palette ([`MacroPalette::nearer_the_objective`]), for the
|
||||
/// ratchet's stall window and for nothing else: the macro layer neither reads it back nor
|
||||
/// presses anything because of it.
|
||||
pub fn nearer_the_objective(&self) -> bool {
|
||||
self.palette.nearer_the_objective()
|
||||
}
|
||||
|
||||
pub fn bound_channels(&self) -> Vec<String> {
|
||||
self.bindings
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -1054,12 +1054,20 @@ impl Sim {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let recover = self.ratchet.observe_with_game_over(
|
||||
// The stall window's second progress signal (`docs/design/ladder.md`, the 2026-09-17
|
||||
// rule as amended 2026-09-22): coverage is ground never stood on, and a fly crossing a
|
||||
// town it has already covered to reach the rung's own door earns none of it while it is
|
||||
// plainly getting somewhere. The macro layer answers with the map graph it already walks
|
||||
// (`docs/design/macros.md` section 12.15); in raw mode there is no layer and no
|
||||
// objective, and the answer is false.
|
||||
let nearer = self.macros.as_ref().is_some_and(MacroLayer::nearer_the_objective);
|
||||
let recover = self.ratchet.observe_with_progress(
|
||||
safe,
|
||||
u64::from(progress.rank),
|
||||
progress.unique_locations as u64,
|
||||
ms as u64,
|
||||
self.adapter.game_over(),
|
||||
nearer,
|
||||
|| captured.expect("the ratchet only captures when a snapshot was prepared"),
|
||||
);
|
||||
if recover {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue