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 <noreply@anthropic.com>
This commit is contained in:
Jonas Reith 2026-06-28 19:11:39 +02:00
parent f748940004
commit b19d2a1703
8 changed files with 38 additions and 16 deletions

View File

@ -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

View File

@ -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 (525°, SST ≥ `weatherTropicalSST`) or a mid-latitude (3062°) 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 —

View File

@ -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)

View File

@ -279,7 +279,7 @@ void Viewer::loadGame(const char* path) {
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
is.read(reinterpret_cast<char*>(&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);

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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");
{