From 1358e81426cbd875bcd73e1517a453ac210a826c Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Sun, 28 Jun 2026 19:30:54 +0200 Subject: [PATCH] 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 --- BUILD.md | 7 ++++--- CLAUDE.md | 18 +++++++++++------- docs/design-notes.md | 6 ++++-- src/render/Viewer.cpp | 27 +++++++++++++++++++++++++++ src/render/Viewer.hpp | 3 ++- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/BUILD.md b/BUILD.md index 8ff6aa8..8d88769 100644 --- a/BUILD.md +++ b/BUILD.md @@ -76,9 +76,10 @@ CLI flags (applied before the first load/generate): planet.cfg human-editable key=value config of every PlanetConfig parameter; auto-created on first run, reload live with F2. Range-checked on load; an invalid file reverts to safe defaults (not overwritten). - planet.save binary snapshot (versioned, currently v11: +weather systems/storms; v10 - +weather fields; v9 +moons; v8 +Live World clock; v7 +biota): seed + config + - full planet state; F5 writes it, F9 reloads and resumes deterministically. As of v6 + planet.save binary snapshot (versioned, currently v12: +step-back history (~40 frames, so a + load can rewind storms); v11 +weather systems/storms; v10 +weather fields; v9 + +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 planet.cfg), so adding/removing config fields no longer breaks saves (unknown keys ignored, missing keys default). v6 cannot load pre-v6 diff --git a/CLAUDE.md b/CLAUDE.md index d1d9af1..93d2871 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -427,10 +427,12 @@ Working and verified (logic tested headless): snapshots the full state — `Planet::captureWeather()`/`restoreWeather()` (humidity/cloud/rain/ 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. - `liveAdvance` records a snapshot at ~one-step cadence on **any** forward advance (continuous run or - manual step), so storms born during a continuous run also rewind (an earlier version cleared the - history on run, which left run-born storms frozen on step-back); `,` searches the ring by time, the - ring drops oldest past `wxUndoMax`. `S` in Live World aliases the forward step. + A manual step **always** records (rate-independent, via `wxPushSnapshot`); a continuous run records + a throttled snapshot (~1/sec) so a run is rewindable too. `,` searches the ring by time, the ring + drops oldest past `wxUndoMax`. The most recent `wxSaveMax`(40) frames are **persisted in the save + (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 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 @@ -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** 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 -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`); 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` -clears the step-back `wxUndo` history so a load can't restore stale pre-load weather. +saves spin weather up live; pre-v11 saves load with no active storms (they respawn); pre-v12 saves +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 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 — diff --git a/docs/design-notes.md b/docs/design-notes.md index 344f8da..36af118 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -202,8 +202,10 @@ recurve (`weatherSystemSpeed`), **intensify** over warm sea / **decay+cull** ove 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`. 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 -stale pre-load weather state. +load resumes active storms; a load also drops any stale pre-load `wxUndo` step-back ring and (v12) +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) diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index ebd7728..d1a1b81 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -262,6 +262,18 @@ void Viewer::saveGame(const char* path) { os.write(reinterpret_cast(&lw), sizeof lw); // v8: Live World flag os.write(reinterpret_cast(&liveTime), sizeof liveTime); // v8: live clock (hours) 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& 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(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"); } @@ -291,6 +303,21 @@ void Viewer::loadGame(const char* path) { dtMy = settled ? planet.cflDtMy() : 0.0; driftAccum = 0.0; formAccum = 0.0; 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& 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(); buildBorders(planet, borderR, borders, ridgeBorders); buildDriftArrows(planet, driftR, driftArrows, plateLabels); diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index 9d40628..f98c670 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -15,7 +15,8 @@ // ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing). struct Viewer { // ---- 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* SAVE_PATH = "planet.save"; std::string configPath = "planet.cfg"; // initial config (--config overrides)