Live World stepper: step weather & storms back too (undo history)

Backward stepping previously only rewound the deterministic sky (day/night, tides, seasons,
moons) and held the weather, because the cloud/rain/storm state is integrated forward and not
analytically reversible. Now a forward step snapshots the full weather state via new
Planet::captureWeather()/restoreWeather() (humidity/cloud/rain/storms + the storm RNG/next-id)
into a bounded wxUndo ring, and the backward step restores the previous snapshot -- so '.' then
',' reverses EVERYTHING, including clouds, rain and moving storms.

Both steps auto-pause (video frame-step feel). A continuous run (unpause) clears the undo
history, after which ',' falls back to the sky-only rewind. Restoring also reseeds the storm RNG
so re-stepping forward replays deterministically. test_weather.cpp adds a capture/restore
round-trip check; 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 18:44:28 +02:00
parent 47fee0b25f
commit 8efb1e343b
10 changed files with 95 additions and 15 deletions

View File

@ -53,7 +53,8 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue) O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue)
K toggle weather clouds/rain cover (Live World) K toggle weather clouds/rain cover (Live World)
Y follow-cam: cycle the 3D camera through active storms (Live World; off after last) Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)
. / , step the live clock forward / back by one rate-unit (back rewinds sky only) . / , step the live clock forward / back by one rate-unit (auto-pauses; back also
rewinds weather + storms via an undo history)
wheel over the 2D map: zoom toward cursor (1-8x); drag pans when zoomed wheel over the 2D map: zoom toward cursor (1-8x); drag pans when zoomed
SPACE pause while forming / re-evolve once settled (or the on-screen button) SPACE pause while forming / re-evolve once settled (or the on-screen button)
[ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month) [ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month)

View File

@ -422,9 +422,13 @@ Working and verified (logic tested headless):
(`drawMapTris` now derives y from the rect, not the fixed `m.pos`). Mouse-wheel over the map zooms (`drawMapTris` now derives y from the rect, not the fixed `m.pos`). Mouse-wheel over the map zooms
toward the cursor (18×); drag pans when zoomed, else rotates `mapLon`; 2D picking inverts the same toward the cursor (18×); drag pans when zoomed, else rotates `mapLon`; 2D picking inverts the same
rect. (3) **Clock stepper**: the `stepSim` live body is factored into `Viewer::liveAdvance(dtClock, rect. (3) **Clock stepper**: the `stepSim` live body is factored into `Viewer::liveAdvance(dtClock,
dtWeather)`; **`.`** steps forward and **`,`** back by `liveRate` hours — backward rewinds the dtWeather)`; **`.`** steps forward and **`,`** back by `liveRate` hours (both auto-pause, like a
deterministic sky (day/night, tides, seasons, moon phases) but holds weather (`dtWeather=0`, not video frame-step). Weather is an integrated path (not analytically reversible), so a forward step
reversible). `S` in Live World aliases the forward step (no longer runs a stray tectonic tick). snapshots the full state — `Planet::captureWeather()`/`restoreWeather()` (humidity/cloud/rain/
storms/RNG) into a bounded `wxUndo` history — and **`,` restores the previous snapshot**, so the
step really reverses *everything* (clouds, rain, storms) plus the deterministic sky. A continuous
run (unpause) clears the undo history (so `,` then falls back to a sky-only rewind). `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
@ -564,7 +568,8 @@ all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured co
`SPACE` or on-screen button pause · `SPACE` or on-screen button pause ·
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) · `[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
`S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock `S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock
forward/back by one rate-unit (`,` rewinds the sky only — weather can't reverse) · forward/back by one rate-unit (auto-pauses; `,` steps **everything** back incl. weather/storms via
an undo history) ·
`Y` cycle the 3D camera to **follow a storm** (off after the last) · mouse-wheel **over the 2D map** `Y` cycle the 3D camera to **follow a storm** (off after the last) · mouse-wheel **over the 2D map**
zooms toward the cursor (drag pans when zoomed) · `F` fast-forward Phase-1 forming to settled · zooms toward the cursor (drag pans when zoomed) · `F` fast-forward Phase-1 forming to settled ·
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga, `H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,

View File

@ -220,9 +220,12 @@ Three viewer-only controls over the Live World sim:
drag pans when zoomed, else keeps the `mapLon` longitude rotation. drag pans when zoomed, else keeps the `mapLon` longitude rotation.
- **Clock stepper**: the `stepSim` Live-World body is factored into `Viewer::liveAdvance(dtClock, - **Clock stepper**: the `stepSim` Live-World body is factored into `Viewer::liveAdvance(dtClock,
dtWeather)` (clamps `liveTime≥0`, recomputes insolation/season/tides/moons, `stepWeather`, dtWeather)` (clamps `liveTime≥0`, recomputes insolation/season/tides/moons, `stepWeather`,
overlay). `.`/`,` step ±`liveRate` hours; backward passes `dtWeather=0` because weather is an overlay). `.`/`,` step ±`liveRate` hours and **auto-pause** (frame-step). Weather is integrated
integrated, non-reversible path (the deterministic sky — day/night, tides, seasons, moon phases — and not analytically reversible, so a forward step snapshots the full weather state
still rewinds fine). (`Planet::captureWeather`/`restoreWeather` — humidity/cloud/rain/storms/RNG) into a bounded
`wxUndo` ring; **`,` restores the previous snapshot**, reversing clouds/rain/storms exactly as well
as the deterministic sky. A continuous run clears `wxUndo` (then `,` rewinds the sky only). The
snapshot also reseeds the storm RNG so re-stepping forward replays deterministically.
## Headless testing ## Headless testing

View File

@ -227,7 +227,7 @@ void Viewer::regenWorld() { // after generate(): geometry change
buildMap2D(planet, mapRect, map2D); buildMap2D(planet, mapRect, map2D);
selectedCell = -1; subgrids.clear(); selectedCell = -1; subgrids.clear();
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false; settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
liveWorld = false; followId = 0; // reseed/regen drops back to World Creation liveWorld = false; followId = 0; wxUndo.clear(); // reseed/regen drops back to World Creation
planet.drifting = false; // Phase 1: original forming behavior planet.drifting = false; // Phase 1: original forming behavior
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy; phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
rivers.clear(); bigRivers.clear(); rivers.clear(); bigRivers.clear();
@ -304,6 +304,7 @@ void Viewer::stepSim() {
if (liveWorld) { if (liveWorld) {
// --- Live World: advance the slow clock; geology is frozen -------- // --- Live World: advance the slow clock; geology is frozen --------
double dtH = (!paused) ? liveRate * GetFrameTime() : 0.0; // simulated hours this frame double dtH = (!paused) ? liveRate * GetFrameTime() : 0.0; // simulated hours this frame
if (dtH > 0.0) wxUndo.clear(); // a continuous run invalidates the manual step-back history
liveAdvance(dtH, dtH); liveAdvance(dtH, dtH);
return; return;
} }
@ -363,6 +364,29 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
rebuildLiveOverlay(); rebuildLiveOverlay();
} }
// Step the live clock forward one rate-unit. Auto-pauses (like a video frame-step) and snapshots
// the pre-step state so the backward step can restore weather + storms exactly.
void Viewer::liveStepForward() {
paused = true;
if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin());
wxUndo.push_back(WxFrame{ liveTime, planet.captureWeather() });
liveAdvance(liveRate, liveRate);
}
// Step everything back one frame: restore the last snapshot (clock + weather + storms) if we have
// one; otherwise fall back to rewinding the deterministic sky only (day/night, tides, seasons).
void Viewer::liveStepBack() {
paused = true;
if (!wxUndo.empty()) {
WxFrame f = wxUndo.back(); wxUndo.pop_back();
liveTime = f.t;
planet.restoreWeather(f.w);
liveAdvance(0.0, 0.0); // recompute the sky/overlay at the restored time (weather held)
} else {
liveAdvance(-liveRate, 0.0); // no history: deterministic sky rewinds, weather holds
}
}
// The 2D map's projection rect after zoom/pan: mapRect scaled about its centre by mapZoom and // The 2D map's projection rect after zoom/pan: mapRect scaled about its centre by mapZoom and
// shifted by the screen-space pan. The scissor + frame stay the real mapRect, so it clips cleanly. // shifted by the screen-space pan. The scissor + frame stay the real mapRect, so it clips cleanly.
Rectangle Viewer::mapViewRect() const { Rectangle Viewer::mapViewRect() const {

View File

@ -79,6 +79,11 @@ struct Viewer {
bool liveWorld = false, dayNightOn = true; bool liveWorld = false, dayNightOn = true;
double liveTime = 0.0; // hours since the live clock started double liveTime = 0.0; // hours since the live clock started
double liveRate = 1.0; // sim hours advanced per real second (ramps hour->month) double liveRate = 1.0; // sim hours advanced per real second (ramps hour->month)
// Step-back undo history: each forward step snapshots the clock + full weather state so a
// backward step restores everything (weather is an integrated path, not analytically reversible).
struct WxFrame { double t; WeatherSnapshot w; };
std::vector<WxFrame> wxUndo;
static constexpr int wxUndoMax = 180;
std::vector<float> illum; // per-cell day/night brightness (1 = day, floor = night) std::vector<float> illum; // per-cell day/night brightness (1 = day, floor = night)
std::vector<Color> shadedColors;// vcolors + snow/ice tint + day/night dim (live overlay) std::vector<Color> shadedColors;// vcolors + snow/ice tint + day/night dim (live overlay)
Vector3 sunDir{0.0f, 0.0f, 1.0f}; // model-space sub-solar direction (for the 3D sun marker) Vector3 sunDir{0.0f, 0.0f, 1.0f}; // model-space sub-solar direction (for the 3D sun marker)
@ -138,6 +143,8 @@ struct Viewer {
void loadGame(const char* path); void loadGame(const char* path);
void stepSim(); // advance forming / drift+hydrology this frame void stepSim(); // advance forming / drift+hydrology this frame
void liveAdvance(double dtClock, double dtWeather); // advance the Live World clock + fields void liveAdvance(double dtClock, double dtWeather); // advance the Live World clock + fields
void liveStepForward(); // step the clock forward one rate-unit (snapshots for undo)
void liveStepBack(); // step everything back one frame (restores weather/storms)
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect) Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
// ---- Input (ViewerInput.cpp) -------------------------------------------- // ---- Input (ViewerInput.cpp) --------------------------------------------

View File

@ -176,7 +176,7 @@ void Viewer::handleInput() {
refreshView(); // fresh base colours; overlay builds in stepSim refreshView(); // fresh base colours; overlay builds in stepSim
setStatus("Live World started"); setStatus("Live World started");
} else { } else {
paused = true; followId = 0; refreshView(); // back to World Creation (drift), paused paused = true; followId = 0; wxUndo.clear(); refreshView(); // back to World Creation, paused
setStatus("Live World stopped"); setStatus("Live World stopped");
} }
} }
@ -204,13 +204,14 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); } if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); } if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
if (IsKeyPressed(KEY_S)) { // one step if (IsKeyPressed(KEY_S)) { // one step
if (liveWorld) liveAdvance(liveRate, liveRate); // Live World: step the clock forward if (liveWorld) liveStepForward(); // Live World: step the clock forward
else { stepOnce(); refreshView(); } // forming/drift: one tectonic tick else { stepOnce(); refreshView(); } // forming/drift: one tectonic tick
} }
// Live World clock stepper: step by one rate-unit (liveRate hours). Backward rewinds the // Live World clock stepper: step by one rate-unit (liveRate hours). Forward integrates weather;
// deterministic sky (day/night, tides, seasons, moon phases); weather holds (can't reverse). // backward restores the snapshot from the last forward step -> everything (incl. weather +
if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveAdvance(liveRate, liveRate); setStatus("Step forward"); } // storms) steps back, within the current paused stepping session.
if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveAdvance(-liveRate, 0.0); setStatus("Step back (sky only)"); } if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveStepForward(); setStatus("Step forward"); }
if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveStepBack(); setStatus(wxUndo.empty() ? "Step back (sky only)" : "Step back"); }
if (IsKeyPressed(KEY_F)) { // fast-forward to settled if (IsKeyPressed(KEY_F)) { // fast-forward to settled
if (!settled) { if (!settled) {
while (!settled) stepOnce(); while (!settled) stepOnce();

View File

@ -100,6 +100,10 @@ public:
const std::vector<double>& cloud() const { return sCloud; } const std::vector<double>& cloud() const { return sCloud; }
const std::vector<double>& rain() const { return sRain; } const std::vector<double>& rain() const { return sRain; }
const std::vector<WeatherSystem>& storms() const { return sStorms; } const std::vector<WeatherSystem>& storms() const { return sStorms; }
// Snapshot / restore the full weather state (humidity/cloud/rain/storms/RNG) for the viewer's
// step-back undo history -- weather is an integrated path, so backward stepping restores a frame.
WeatherSnapshot captureWeather() const;
void restoreWeather(const WeatherSnapshot& s);
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate // Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
// fields (temperature + normalized precipitation). Derived + written back into // fields (temperature + normalized precipitation). Derived + written back into

View File

@ -64,6 +64,14 @@ struct Moon {
double dispRadius = 0.10; // display sphere radius (visual size) double dispRadius = 0.10; // display sphere radius (visual size)
}; };
// A full snapshot of the (integrated, non-analytic) weather state, for the viewer's step-back
// undo history -- weather can't be reversed in closed form, so we restore a saved frame instead.
struct WeatherSnapshot {
std::vector<double> humidity, cloud, rain;
std::vector<WeatherSystem> storms;
uint32_t rng = 0, nextId = 0;
};
struct Plate { struct Plate {
int id = 0; int id = 0;
PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells

View File

@ -26,6 +26,19 @@ void Planet::initWeather() {
sHasWeather = true; sHasWeather = true;
} }
WeatherSnapshot Planet::captureWeather() const {
WeatherSnapshot s;
s.humidity = sHumidity; s.cloud = sCloud; s.rain = sRain;
s.storms = sStorms; s.rng = sWeatherRng; s.nextId = sStormNextId;
return s;
}
void Planet::restoreWeather(const WeatherSnapshot& s) {
sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain;
sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId;
sHasWeather = !sHumidity.empty();
}
void Planet::stepWeather(double dtHours) { void Planet::stepWeather(double dtHours) {
const int n = (int)cells.size(); const int n = (int)cells.size();
if (!sHasWeather || (int)sHumidity.size() != n || (int)sCloud.size() != n || (int)sRain.size() != n) if (!sHasWeather || (int)sHumidity.size() != n || (int)sCloud.size() != n || (int)sRain.size() != n)

View File

@ -119,6 +119,20 @@ int main() {
rt = false; rt = false;
check(rt, "save v10 round-trips the weather state"); check(rt, "save v10 round-trips the weather state");
std::printf("Weather: snapshot round-trip (step-back undo)\n");
{
Planet wc; wc.generate(cfg); wc.initWeather();
for (int k = 0; k < 60; ++k) { wc.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); wc.stepWeather(1.0); }
WeatherSnapshot snap = wc.captureWeather();
int s0 = (int)wc.storms().size();
for (int k = 0; k < 30; ++k) { wc.computeInsolation(0.25, std::fmod(0.9 + 0.01 * k, 1.0)); wc.stepWeather(1.0); }
wc.restoreWeather(snap); // step back to the saved frame
bool rt = ((int)wc.storms().size() == s0);
for (int i = 0; i < n && rt; ++i)
if (wc.cloud()[i] != snap.cloud[i] || wc.humidity()[i] != snap.humidity[i] || wc.rain()[i] != snap.rain[i]) rt = false;
check(rt, "captureWeather/restoreWeather round-trips the full weather state");
}
std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures); std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures);
return failures ? 1 : 0; return failures ? 1 : 0;
} }