From b19d2a1703b85503b4b160f2a3750ee7fdf19888 Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Sun, 28 Jun 2026 19:11:39 +0200 Subject: [PATCH] Fix load: save weather systems (v11) + clear stale step-back history on load After loading, storms vanished on forward play/step but reappeared on step-back. Two causes: the moving weather systems weren't saved (transient), so a load started with none; and loadGame didn't clear the wxUndo step-back ring, so stepping back restored STALE snapshots from before the load (which still held the old session's storms) -- hence 'back shows them, forward doesn't'. Save bumped to v11: the weather block now also persists sStorms + sWeatherRng + sStormNextId, so a load resumes the active storms and stepping forward continues them deterministically. Pre-v11 saves load with no active storms (they respawn); pre-v10 still spin weather up live. loadGame now clears wxUndo + followId so a load can't restore stale pre-load weather or follow a gone storm. test_weather.cpp checks the storms round-trip; all five suites pass; GUI build clean. Co-Authored-By: Claude Opus 4.8 --- BUILD.md | 6 +++--- CLAUDE.md | 10 ++++++---- docs/design-notes.md | 3 +++ src/render/Viewer.cpp | 3 ++- src/render/Viewer.hpp | 2 +- src/sim/Planet.hpp | 2 +- src/sim/PlanetIO.cpp | 16 +++++++++++++--- test_weather.cpp | 12 +++++++++--- 8 files changed, 38 insertions(+), 16 deletions(-) diff --git a/BUILD.md b/BUILD.md index ed64dcd..8ff6aa8 100644 --- a/BUILD.md +++ b/BUILD.md @@ -76,9 +76,9 @@ 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 v10: +weather; 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 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 a075dc2..d1d9af1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,7 +93,7 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it. oceans, travel with the steering wind (poleward recurve), intensify over warm water, decay over land, and **stamp** travelling cloud/rain onto the grid — so the sky visibly evolves. The intense tropical ones are **hurricanes/typhoons** (spin by hemisphere, eye + animated spiral marker). - Transient (not saved; respawn from the seed). This makes the weather visibly move (the base field + Saved (v11, so a load resumes active storms). This makes the weather visibly move (the base field alone relaxes to a static pattern). ## Current state @@ -400,7 +400,7 @@ Working and verified (logic tested headless): fields in range, clouds form + rain falls, oceans moister than land, determinism, v10 round-trip. - **Live World — moving weather systems (lows / hurricanes / typhoons):** the base cloud/rain field relaxes to a *static* pattern under fixed forcing, so `stepWeather` now also runs a - population of drifting **`WeatherSystem`** agents (PlanetTypes; transient, not saved; separate + population of drifting **`WeatherSystem`** agents (PlanetTypes; **saved v11**; separate `sWeatherRng` seeded from `cfg.seed` → tectonic determinism intact). Each step: **spawn** over warm tropical ocean (5–25°, SST ≥ `weatherTropicalSST`) or a mid-latitude (30–62°) ocean low (capped at `weatherSystemMax`, prob ∝ `weatherSpawnRate`); **move** along the steering wind @@ -609,10 +609,12 @@ save header is versioned (currently **9**; v2 adds the `[`/`]` drift rate, v3 a key=value text block** instead of a raw POD dump, v7 appends the **biota population** 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); newer-than-supported is +humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG so a load resumes +active storms); 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. +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. **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 5458287..344f8da 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -201,6 +201,9 @@ recurve (`weatherSystemSpeed`), **intensify** over warm sea / **decay+cull** ove **stamp** a Gaussian cloud/rain shield onto the grid — so cloud clusters travel and dissipate 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. ## Live World viewer controls (follow-cam, 2D zoom, clock stepper) diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index f7ef365..929c660 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -279,7 +279,7 @@ void Viewer::loadGame(const char* path) { if (ver >= 8) { is.read(reinterpret_cast(&lw), sizeof lw); is.read(reinterpret_cast(&lh), sizeof lh); } // v8: Live World clock if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; } - if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather + if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms cfg = planet.cfg; // adopt the loaded config elapsedMy = em; settled = (st != 0); planet.drifting = settled; // resume drift boosts iff mid-drift @@ -290,6 +290,7 @@ void Viewer::loadGame(const char* path) { settleRun = settleNeed; // keep the settled latch consistent dtMy = settled ? planet.cflDtMy() : 0.0; driftAccum = 0.0; formAccum = 0.0; + wxUndo.clear(); followId = 0; // drop stale step-back history / follow target 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 5f9072f..8e49f4b 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -15,7 +15,7 @@ // ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing). struct Viewer { // ---- Files / save format ------------------------------------------------ - static constexpr uint32_t SAVE_VERSION = 10; // v10: +weather; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3 + 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 const char* CONFIG_PATH = "planet.cfg"; const char* SAVE_PATH = "planet.save"; std::string configPath = "planet.cfg"; // initial config (--config overrides) diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index 41e7b01..f2da3c2 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -143,7 +143,7 @@ public: // synthesize moons from the seed instead. hasWeather: the weather block (save v10+); // older saves leave weather to spin up on entering Live World. bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, - bool hasMoons = true, bool hasWeather = true); + bool hasMoons = true, bool hasWeather = true, bool hasStorms = true); // Helpers for rendering / info. double cellWidthMeters() const; // approx lateral cell spacing diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 4e12bff..1e33f9f 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -301,12 +301,18 @@ void Planet::writeState(std::ostream& os) const { writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga); } } - // v10: Live World weather (humidity/cloud/rain). Flag-gated like biota. + // v10: Live World weather (humidity/cloud/rain). Flag-gated like biota. v11 also persists the + // moving weather systems + their RNG/next-id, so loading restores active storms (and stepping + // forward continues them deterministically) instead of losing them. uint8_t hasWx = (sHasWeather && sHumidity.size() == cells.size()) ? 1 : 0; writePod(os, hasWx); - if (hasWx) { writeVec(os, sHumidity); writeVec(os, sCloud); writeVec(os, sRain); } + if (hasWx) { + writeVec(os, sHumidity); writeVec(os, sCloud); writeVec(os, sRain); + writeVec(os, sStorms); writePod(os, sWeatherRng); writePod(os, sStormNextId); // v11 + } } -bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, bool hasWeather) { +bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, + bool hasWeather, bool hasStorms) { // Read the length-prefixed key=value config block (see writeState). A default // PlanetConfig is parsed over, so fields absent from an older save keep their // current defaults. The length guard rejects pre-v6 (raw-POD-config) saves. @@ -362,6 +368,10 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM readVec(is, sHumidity); readVec(is, sCloud); readVec(is, sRain); if (!is || (int)sHumidity.size() != (int)cells.size()) return false; sHasWeather = true; + if (hasStorms) { // v11: active weather systems + their RNG + readVec(is, sStorms); readPod(is, sWeatherRng); readPod(is, sStormNextId); + if (!is) return false; + } } } computeBiotaDensity(); // derived density scalars for the colour views diff --git a/test_weather.cpp b/test_weather.cpp index 481d906..412ed34 100644 --- a/test_weather.cpp +++ b/test_weather.cpp @@ -108,16 +108,22 @@ int main() { check(ang > 1e-4 && ang < 0.3, "a weather system moves between steps"); } - std::printf("Weather: save v10\n"); + std::printf("Weather: save v11 (fields + storms)\n"); std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); p.writeState(ss); Planet q; - bool ok = q.readState(ss, true, true, true, true); + bool ok = q.readState(ss, true, true, true, true, true); bool rt = ok && (int)q.cloud().size() == n; for (int i = 0; i < n && rt; ++i) if (q.humidity()[i] != p.humidity()[i] || q.cloud()[i] != p.cloud()[i] || q.rain()[i] != p.rain()[i]) rt = false; - check(rt, "save v10 round-trips the weather state"); + check(rt, "save round-trips the weather fields"); + bool srt = ((int)q.storms().size() == (int)p.storms().size()); + for (size_t i = 0; i < q.storms().size() && srt; ++i) + if (q.storms()[i].id != p.storms()[i].id + || q.storms()[i].pos.dot(p.storms()[i].pos) < 1.0 - 1e-9 + || q.storms()[i].strength != p.storms()[i].strength) srt = false; + check(srt, "save v11 round-trips the active weather systems"); std::printf("Weather: snapshot round-trip (step-back undo)\n"); {