Persist step-back history in saves (v12): rewind storms past a loaded moment

Weather is integrated and path-dependent, so reversing it can only restore recorded snapshots --
and a save held only the current moment, so after a load there was no past to step back to and
loaded storms froze on backward step. Save bumped to v12: it now appends the most recent
wxSaveMax(40) step-back frames (each = humidity/cloud/rain + storms + RNG) after the planet
state. loadGame restores them (and still drops any stale pre-load history), so after a load you
can step backward ~40 steps and the storms reverse along their saved track.

Pre-v12 saves load with no step-back history (you can still step forward then back). Adds ~10 MB
to a save (the user chose the short window). All five suites pass; GUI build clean. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonas Reith 2026-06-28 19:30:54 +02:00
parent 7631eb4964
commit 1358e81426
5 changed files with 48 additions and 13 deletions

View File

@ -76,9 +76,10 @@ CLI flags (applied before the first load/generate):
planet.cfg human-editable key=value config of every PlanetConfig parameter; planet.cfg human-editable key=value config of every PlanetConfig parameter;
auto-created on first run, reload live with F2. Range-checked on auto-created on first run, reload live with F2. Range-checked on
load; an invalid file reverts to safe defaults (not overwritten). load; an invalid file reverts to safe defaults (not overwritten).
planet.save binary snapshot (versioned, currently v11: +weather systems/storms; v10 planet.save binary snapshot (versioned, currently v12: +step-back history (~40 frames, so a
+weather fields; v9 +moons; v8 +Live World clock; v7 +biota): seed + config + load can rewind storms); v11 +weather systems/storms; v10 +weather fields; v9
full planet state; F5 writes it, F9 reloads and resumes deterministically. As of v6 +moons; v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
writes it, F9 reloads and resumes deterministically. As of v6
the config is stored as a self-describing key=value block (like the config is stored as a self-describing key=value block (like
planet.cfg), so adding/removing config fields no longer breaks saves planet.cfg), so adding/removing config fields no longer breaks saves
(unknown keys ignored, missing keys default). v6 cannot load pre-v6 (unknown keys ignored, missing keys default). v6 cannot load pre-v6

View File

@ -427,10 +427,12 @@ Working and verified (logic tested headless):
snapshots the full state — `Planet::captureWeather()`/`restoreWeather()` (humidity/cloud/rain/ snapshots the full state — `Planet::captureWeather()`/`restoreWeather()` (humidity/cloud/rain/
storms/RNG) into a bounded `wxUndo` ring — and **`,` restores the previous snapshot**, so the storms/RNG) into a bounded `wxUndo` ring — and **`,` restores the previous snapshot**, so the
step really reverses *everything* (clouds, rain, **moving storms**) plus the deterministic sky. step really reverses *everything* (clouds, rain, **moving storms**) plus the deterministic sky.
`liveAdvance` records a snapshot at ~one-step cadence on **any** forward advance (continuous run or A manual step **always** records (rate-independent, via `wxPushSnapshot`); a continuous run records
manual step), so storms born during a continuous run also rewind (an earlier version cleared the a throttled snapshot (~1/sec) so a run is rewindable too. `,` searches the ring by time, the ring
history on run, which left run-born storms frozen on step-back); `,` searches the ring by time, the drops oldest past `wxUndoMax`. The most recent `wxSaveMax`(40) frames are **persisted in the save
ring drops oldest past `wxUndoMax`. `S` in Live World aliases the forward step. (v12)** so a load can rewind storms past the saved moment; a load also drops any stale pre-load
history. With no recorded past (e.g. immediately after a pre-v12 load) `,` rewinds the sky only and
says so. `S` in Live World aliases the forward step.
- Mouse hover (in either view) shows per-cell info. Clicking a tile opens a - Mouse hover (in either view) shows per-cell info. Clicking a tile opens a
right-side detail panel: tile info header + the tile's subgrid drawn as a right-side detail panel: tile info header + the tile's subgrid drawn as a
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
@ -610,11 +612,13 @@ key=value text block** instead of a raw POD dump, v7 appends the **biota populat
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World** block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
clock — a flag byte + `liveTime`, v9 appends the **moons** block, v10 appends the **weather** block — clock — a flag byte + `liveTime`, v9 appends the **moons** block, v10 appends the **weather** block —
humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG so a load resumes humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG so a load resumes
active storms); newer-than-supported is active storms, v12 appends the most recent **step-back frames**`wxSaveMax`(40) weather snapshots
— so a load can rewind storms past the saved moment); newer-than-supported is
rejected. Older saves (no biota block) load fine with an empty population (press `L`); rejected. Older saves (no biota block) load fine with an empty population (press `L`);
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10 pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
saves spin weather up live; pre-v11 saves load with no active storms (they respawn). `loadGame` saves spin weather up live; pre-v11 saves load with no active storms (they respawn); pre-v12 saves
clears the step-back `wxUndo` history so a load can't restore stale pre-load weather. load with no step-back history (you can still step forward then back). A load drops any **stale**
pre-load `wxUndo` history and reloads the saved one.
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved **As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults), config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults),
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves — written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —

View File

@ -202,8 +202,10 @@ recurve (`weatherSystemSpeed`), **intensify** over warm sea / **decay+cull** ove
behind them. Tropical systems past `weatherHurricaneStr` are hurricanes/typhoons; rendered as behind them. Tropical systems past `weatherHurricaneStr` are hurricanes/typhoons; rendered as
animated cyclonic spiral markers (eye for cyclones) spinning by hemisphere, in 3D + 2D, under `K`. animated cyclonic spiral markers (eye for cyclones) spinning by hemisphere, in 3D + 2D, under `K`.
The systems (+ their RNG/next-id) are **saved (v11)** alongside the humidity/cloud/rain fields, so a The systems (+ their RNG/next-id) are **saved (v11)** alongside the humidity/cloud/rain fields, so a
load resumes active storms; `loadGame` clears the step-back `wxUndo` ring so a load can't restore a load resumes active storms; a load also drops any stale pre-load `wxUndo` step-back ring and (v12)
stale pre-load weather state. restores the most recent `wxSaveMax`(40) step-back frames from the file, so stepping back after a load
can rewind storms past the saved moment. Weather is integrated/path-dependent, so reversing it past
a save is only possible via this stored history — it can't be re-derived from the loaded moment.
## Live World viewer controls (follow-cam, 2D zoom, clock stepper) ## Live World viewer controls (follow-cam, 2D zoom, clock stepper)

View File

@ -262,6 +262,18 @@ void Viewer::saveGame(const char* path) {
os.write(reinterpret_cast<const char*>(&lw), sizeof lw); // v8: Live World flag os.write(reinterpret_cast<const char*>(&lw), sizeof lw); // v8: Live World flag
os.write(reinterpret_cast<const char*>(&liveTime), sizeof liveTime); // v8: live clock (hours) os.write(reinterpret_cast<const char*>(&liveTime), sizeof liveTime); // v8: live clock (hours)
planet.writeState(os); planet.writeState(os);
// v12: persist the most recent step-back frames so a load can rewind storms past the moment.
auto wD = [&](const std::vector<double>& v){ uint64_t m = v.size(); os.write((char*)&m, 8); if (m) os.write((const char*)v.data(), (std::streamsize)(m * sizeof(double))); };
uint32_t hn = (uint32_t)std::min<size_t>(wxUndo.size(), (size_t)wxSaveMax);
os.write((char*)&hn, 4);
for (size_t i = wxUndo.size() - hn; i < wxUndo.size(); ++i) {
const WxFrame& f = wxUndo[i];
os.write((char*)&f.t, 8);
wD(f.w.humidity); wD(f.w.cloud); wD(f.w.rain);
uint64_t sc = f.w.storms.size(); os.write((char*)&sc, 8);
if (sc) os.write((const char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
os.write((char*)&f.w.rng, 4); os.write((char*)&f.w.nextId, 4);
}
setStatus(os ? std::string("Saved ") + path : "Save failed"); setStatus(os ? std::string("Saved ") + path : "Save failed");
} }
@ -291,6 +303,21 @@ void Viewer::loadGame(const char* path) {
dtMy = settled ? planet.cflDtMy() : 0.0; dtMy = settled ? planet.cflDtMy() : 0.0;
driftAccum = 0.0; formAccum = 0.0; driftAccum = 0.0; formAccum = 0.0;
wxUndo.clear(); followId = 0; // drop stale step-back history / follow target wxUndo.clear(); followId = 0; // drop stale step-back history / follow target
if (ver >= 12) { // v12: restore the saved step-back frames (rewind past load)
auto rD = [&](std::vector<double>& v){ uint64_t m = 0; is.read((char*)&m, 8);
if (!is || m > 4000000ull) { v.clear(); return; } v.resize((size_t)m);
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(double))); };
uint32_t hn = 0; is.read((char*)&hn, 4);
for (uint32_t k = 0; k < hn && is; ++k) {
WxFrame f; is.read((char*)&f.t, 8);
rD(f.w.humidity); rD(f.w.cloud); rD(f.w.rain);
uint64_t sc = 0; is.read((char*)&sc, 8); if (sc > 1000000ull) sc = 0;
f.w.storms.resize((size_t)sc);
if (sc) is.read((char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
is.read((char*)&f.w.rng, 4); is.read((char*)&f.w.nextId, 4);
if (is) wxUndo.push_back(std::move(f));
}
}
paused = true; selectedCell = -1; subgrids.clear(); paused = true; selectedCell = -1; subgrids.clear();
buildBorders(planet, borderR, borders, ridgeBorders); buildBorders(planet, borderR, borders, ridgeBorders);
buildDriftArrows(planet, driftR, driftArrows, plateLabels); buildDriftArrows(planet, driftR, driftArrows, plateLabels);

View File

@ -15,7 +15,8 @@
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing). // ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
struct Viewer { struct Viewer {
// ---- Files / save format ------------------------------------------------ // ---- Files / save format ------------------------------------------------
static constexpr uint32_t SAVE_VERSION = 11; // v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3 static constexpr uint32_t SAVE_VERSION = 12; // v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
const char* CONFIG_PATH = "planet.cfg"; const char* CONFIG_PATH = "planet.cfg";
const char* SAVE_PATH = "planet.save"; const char* SAVE_PATH = "planet.save";
std::string configPath = "planet.cfg"; // initial config (--config overrides) std::string configPath = "planet.cfg"; // initial config (--config overrides)