Harden save loading and derived state resets
This commit is contained in:
parent
695e07ba99
commit
2b0e1f8633
6
BUILD.md
6
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 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
|
||||
planet.save binary snapshot (versioned, currently v13: +Live World clock rate; 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
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@ -606,19 +606,21 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||
save header is versioned (currently **9**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
save header is versioned (currently **13**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
||||
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, v11 also persists the **weather systems** + RNG so a load resumes
|
||||
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
|
||||
— so a load can rewind storms past the saved moment, v13 appends the Live World clock rate);
|
||||
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); 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.
|
||||
load with no step-back history (you can still step forward then back); pre-v13 saves resume with
|
||||
the default live clock rate. 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 —
|
||||
|
||||
@ -15,9 +15,7 @@ set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(raylib)
|
||||
|
||||
add_executable(planetsim
|
||||
src/main.cpp
|
||||
# Engine (raylib-free, testable headless) -- src/sim
|
||||
set(SIM_SOURCES
|
||||
src/sim/IcoSphere.cpp
|
||||
src/sim/Planet.cpp
|
||||
src/sim/PlanetTectonics.cpp
|
||||
@ -34,7 +32,9 @@ add_executable(planetsim
|
||||
src/sim/PlanetFaunaGen.cpp
|
||||
src/sim/PlanetFungiGen.cpp
|
||||
src/sim/PlanetIO.cpp
|
||||
# Viewer (raylib) -- src/render
|
||||
)
|
||||
|
||||
set(RENDER_SOURCES
|
||||
src/render/Colors.cpp
|
||||
src/render/Map2D.cpp
|
||||
src/render/Overlays.cpp
|
||||
@ -44,18 +44,33 @@ add_executable(planetsim
|
||||
src/render/ViewerInput.cpp
|
||||
src/render/ViewerRender.cpp
|
||||
)
|
||||
|
||||
add_library(planetsim_sim STATIC ${SIM_SOURCES})
|
||||
target_include_directories(planetsim_sim PUBLIC src/sim)
|
||||
|
||||
add_executable(planetsim
|
||||
src/main.cpp
|
||||
${RENDER_SOURCES}
|
||||
)
|
||||
# Flat includes ("Planet.hpp", "Viewer.hpp", ...) resolve across both folders.
|
||||
target_include_directories(planetsim PRIVATE src/sim src/render)
|
||||
target_link_libraries(planetsim PRIVATE raylib)
|
||||
target_link_libraries(planetsim PRIVATE planetsim_sim raylib)
|
||||
|
||||
# OpenMP parallelizes the per-cell passes in Planet::step(). Optional: without
|
||||
# it the #pragma omp lines are ignored and the sim runs (correctly) serial.
|
||||
find_package(OpenMP)
|
||||
if(OpenMP_CXX_FOUND)
|
||||
target_link_libraries(planetsim PRIVATE OpenMP::OpenMP_CXX)
|
||||
target_link_libraries(planetsim_sim PUBLIC OpenMP::OpenMP_CXX)
|
||||
endif()
|
||||
|
||||
# Linux system libs raylib needs at link time.
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(planetsim PRIVATE m pthread dl)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
foreach(test_name logic biota ocean live weather)
|
||||
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||
endforeach()
|
||||
|
||||
@ -204,8 +204,9 @@ animated cyclonic spiral markers (eye for cyclones) spinning by hemisphere, in 3
|
||||
The systems (+ their RNG/next-id) are **saved (v11)** alongside the humidity/cloud/rain fields, so a
|
||||
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.
|
||||
can rewind storms past the saved moment. v13 also saves the Live World clock rate. 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)
|
||||
|
||||
@ -228,7 +229,7 @@ Three viewer-only controls over the Live World sim:
|
||||
overlay). `.`/`,` step ±`liveRate` hours and **auto-pause** (frame-step). Weather is integrated
|
||||
and not analytically reversible, so a forward step snapshots the full weather state
|
||||
(`Planet::captureWeather`/`restoreWeather` — humidity/cloud/rain/storms/RNG) into a bounded
|
||||
`wxUndo` ring; **`,` restores the most recent snapshot before now**, reversing clouds/rain/storms
|
||||
`wxUndo` ring; **`,` restores the newest snapshot at or before now**, reversing clouds/rain/storms
|
||||
exactly as well as 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 run also rewind
|
||||
(the ring is bounded, ~one snapshot per real second since the interval scales with `liveRate`). The
|
||||
|
||||
@ -261,6 +261,7 @@ void Viewer::saveGame(const char* path) {
|
||||
os.write(reinterpret_cast<const char*>(&p3), sizeof p3); // v3: Phase-3 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*>(&liveRate), sizeof liveRate); // v13: live clock rate
|
||||
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))); };
|
||||
@ -281,7 +282,7 @@ void Viewer::loadGame(const char* path) {
|
||||
std::ifstream is(path, std::ios::binary);
|
||||
if (!is) { setStatus(std::string("No ") + path); return; }
|
||||
char magic[4] = {0}; uint32_t ver = 0; double em = 0; uint8_t st = 0; double dr = 4.0; uint8_t p3 = 0;
|
||||
uint8_t lw = 0; double lh = 0.0;
|
||||
uint8_t lw = 0; double lh = 0.0; double lr = 1.0;
|
||||
is.read(magic, 4);
|
||||
is.read(reinterpret_cast<char*>(&ver), sizeof ver);
|
||||
is.read(reinterpret_cast<char*>(&em), sizeof em);
|
||||
@ -290,6 +291,7 @@ void Viewer::loadGame(const char* path) {
|
||||
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
||||
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
|
||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
||||
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
||||
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, 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
|
||||
@ -298,32 +300,54 @@ void Viewer::loadGame(const char* path) {
|
||||
phase3 = (p3 != 0); phase3Prompt = false;
|
||||
phase3PromptAt = phase3 ? elapsedMy : (elapsedMy + planet.cfg.phase3AfterMy);
|
||||
driftRate = dr;
|
||||
liveWorld = (lw != 0); liveTime = lh; // v8: resume the Live World clock
|
||||
liveWorld = (lw != 0); liveTime = lh; liveRate = std::clamp(lr, 0.25, 720.0); // v8/v13: resume the Live World clock
|
||||
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
|
||||
bool skippedHistory = false;
|
||||
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))); };
|
||||
bool historyOk = true;
|
||||
const uint64_t cellCount = planet.cells.size();
|
||||
auto rD = [&](std::vector<double>& v){
|
||||
uint64_t m = 0; is.read((char*)&m, 8);
|
||||
if (!is || (m != 0 && m != cellCount)) { historyOk = false; v.clear(); return; }
|
||||
v.resize((size_t)m);
|
||||
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(double)));
|
||||
if (!is) historyOk = false;
|
||||
};
|
||||
uint32_t hn = 0; is.read((char*)&hn, 4);
|
||||
if (!is || hn > (uint32_t)wxUndoMax) historyOk = false;
|
||||
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;
|
||||
uint64_t sc = 0; is.read((char*)&sc, 8);
|
||||
if (!is || sc > (uint64_t)planet.cfg.weatherSystemMax) { historyOk = false; break; }
|
||||
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));
|
||||
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
||||
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
||||
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
||||
historyOk = false;
|
||||
for (const WeatherSystem& ws : f.w.storms)
|
||||
if (!std::isfinite(ws.pos.x) || !std::isfinite(ws.pos.y) || !std::isfinite(ws.pos.z)
|
||||
|| std::fabs(ws.pos.length() - 1.0) > 1e-6
|
||||
|| !std::isfinite(ws.strength) || ws.strength < 0.0 || ws.strength > 1.0
|
||||
|| !std::isfinite(ws.radius) || ws.radius <= 0.0
|
||||
|| !std::isfinite(ws.age) || !std::isfinite(ws.life)
|
||||
|| !std::isfinite(ws.spin)) historyOk = false;
|
||||
if (historyOk) wxUndo.push_back(std::move(f));
|
||||
}
|
||||
if (!historyOk) { wxUndo.clear(); skippedHistory = true; }
|
||||
}
|
||||
paused = true; selectedCell = -1; subgrids.clear();
|
||||
buildBorders(planet, borderR, borders, ridgeBorders);
|
||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||
buildMap2D(planet, mapRect, map2D);
|
||||
refreshView();
|
||||
setStatus(std::string("Loaded ") + path);
|
||||
setStatus(skippedHistory ? std::string("Loaded ") + path + " (history skipped)"
|
||||
: std::string("Loaded ") + path);
|
||||
}
|
||||
|
||||
// Advance the simulation this frame: Phase-1 forming (paced ticks toward
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||
struct Viewer {
|
||||
// ---- Files / save format ------------------------------------------------
|
||||
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 uint32_t SAVE_VERSION = 13; // v13: +liveRate; 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";
|
||||
|
||||
@ -221,12 +221,12 @@ void Viewer::handleInput() {
|
||||
if (IsKeyPressed(KEY_EQUAL) && cfg.subdivisions < 7) { cfg.subdivisions++; regen(); }
|
||||
if (IsKeyPressed(KEY_MINUS) && cfg.subdivisions > 1) { cfg.subdivisions--; regen(); }
|
||||
if (IsKeyPressed(KEY_F2)) {
|
||||
if (loadConfig(CONFIG_PATH, cfg)) {
|
||||
if (loadConfig(configPath, cfg)) {
|
||||
std::string cerr = validateConfig(cfg);
|
||||
if (!cerr.empty()) { cfg = PlanetConfig{}; setStatus("Bad planet.cfg — using defaults"); }
|
||||
if (!cerr.empty()) { cfg = PlanetConfig{}; setStatus("Bad config — using defaults"); }
|
||||
regen();
|
||||
setStatus(cerr.empty() ? "Reloaded planet.cfg" : "Bad planet.cfg — using defaults");
|
||||
} else setStatus("No planet.cfg"); }
|
||||
setStatus(cerr.empty() ? std::string("Reloaded ") + configPath : "Bad config — using defaults");
|
||||
} else setStatus(std::string("No ") + configPath); }
|
||||
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
||||
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
||||
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
||||
|
||||
@ -49,12 +49,31 @@ void Planet::buildGeometry() {
|
||||
cells[i].unit = sphere.positions[i];
|
||||
cells[i].neighbors = sphere.neighbors[i];
|
||||
}
|
||||
clearDerivedState();
|
||||
sBiota.assign(cells.size(), {}); // empty biota population until generateBiota()
|
||||
sHasBiota = false;
|
||||
sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World
|
||||
sHasWeather = false; sStorms.clear();
|
||||
}
|
||||
|
||||
void Planet::clearDerivedState() {
|
||||
sStress.clear(); sBelt.clear(); sBeltNext.clear(); sDelta.clear();
|
||||
sSmoothed.clear(); sOldElev.clear(); sErode.clear();
|
||||
sSub.clear(); sOver.clear(); sColl.clear();
|
||||
|
||||
sFill.clear(); sLakeDepth.clear(); sDischarge.clear();
|
||||
sFlowTo.clear(); sHydroOrder.clear();
|
||||
|
||||
sTemp.clear(); sPrecip.clear(); sMoist.clear();
|
||||
sTempSummer.clear(); sTempWinter.clear();
|
||||
sWind.clear(); sUpwind.clear();
|
||||
|
||||
sInsolation.clear(); sLiveTemp.clear(); sTide.clear();
|
||||
sCurrent.clear();
|
||||
|
||||
sFloraDensity.clear(); sFaunaDensity.clear(); sFungaDensity.clear();
|
||||
}
|
||||
|
||||
void Planet::assignPlates() {
|
||||
plates.clear();
|
||||
plates.resize(cfg.plateCount);
|
||||
|
||||
@ -159,6 +159,7 @@ private:
|
||||
uint32_t rnd();
|
||||
double rndf(); // [0,1)
|
||||
void buildGeometry(); // build icosphere + per-cell unit/neighbors
|
||||
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
|
||||
void assignPlates();
|
||||
void seedInitialRelief();
|
||||
Vec3 driftVelocity(int plateId, const Vec3& pos) const;
|
||||
|
||||
@ -21,7 +21,7 @@ void Planet::classifyBiomes() {
|
||||
const double WETLAND_MOIST = cfg.biomeWetlandMoist, DESERT_MOIST = cfg.biomeDesertMoist;
|
||||
const double GRASS_MOIST = cfg.biomeGrassMoist, TAIGA_MOIST = cfg.biomeTaigaMoist;
|
||||
const double LAKE_MIN_DEPTH = cfg.biomeLakeMinDepth;
|
||||
const bool haveLake = !sLakeDepth.empty();
|
||||
const bool haveLake = ((int)sLakeDepth.size() == n);
|
||||
// Seasons: blend winter temperature into the cold (Tundra/Taiga) cutoffs so cold-winter
|
||||
// continental interiors turn boreal/tundra. The seasonal amplitude is itself geographically
|
||||
// shaped (large only at high-latitude interiors), so this expands cold biomes where seasons
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <ostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <limits>
|
||||
|
||||
// --- Config file (text) + save/load (binary) --------------------------------
|
||||
|
||||
@ -257,10 +258,18 @@ namespace {
|
||||
uint64_t n = v.size(); writePod(os, n);
|
||||
if (n) os.write(reinterpret_cast<const char*>(v.data()), (std::streamsize)(n * sizeof(T)));
|
||||
}
|
||||
template <class T> void readVec(std::istream& is, std::vector<T>& v) {
|
||||
template <class T> bool readVec(std::istream& is, std::vector<T>& v, uint64_t maxCount) {
|
||||
static_assert(std::is_trivially_copyable<T>::value, "readVec needs POD elements");
|
||||
uint64_t n = 0; readPod(is, n); v.resize((size_t)n);
|
||||
uint64_t n = 0; readPod(is, n);
|
||||
if (!is || n > maxCount) { is.setstate(std::ios::failbit); v.clear(); return false; }
|
||||
uint64_t maxBytes = (uint64_t)std::numeric_limits<std::streamsize>::max();
|
||||
if (sizeof(T) != 0 && n > maxBytes / sizeof(T)) { is.setstate(std::ios::failbit); v.clear(); return false; }
|
||||
v.resize((size_t)n);
|
||||
if (n) is.read(reinterpret_cast<char*>(v.data()), (std::streamsize)(n * sizeof(T)));
|
||||
return (bool)is;
|
||||
}
|
||||
bool validBiomeByte(uint8_t b) {
|
||||
return b <= (uint8_t)Biome::Mountains;
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,16 +322,22 @@ void Planet::writeState(std::ostream& os) const {
|
||||
|
||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||
bool hasWeather, bool hasStorms) {
|
||||
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
||||
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
||||
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; }
|
||||
if (!hasWeather) hasStorms = false;
|
||||
|
||||
// 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.
|
||||
uint64_t clen = 0; readPod(is, clen);
|
||||
if (!is || clen > 1000000) return false;
|
||||
std::string cfgText(clen, '\0');
|
||||
is.read(&cfgText[0], (std::streamsize)clen);
|
||||
if (clen) is.read(cfgText.data(), (std::streamsize)clen);
|
||||
if (!is) return false;
|
||||
PlanetConfig c;
|
||||
{ std::istringstream cis(cfgText); parseConfigStream(cis, c); }
|
||||
if (!validateConfig(c).empty()) return false;
|
||||
cfg = c;
|
||||
buildGeometry(); // rebuild unit/neighbors from cfg.subdivisions
|
||||
readPod(is, rngState);
|
||||
@ -335,13 +350,31 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
||||
readPod(is, cell.elevation); readPod(is, cell.plateId);
|
||||
uint8_t oc = 0; readPod(is, oc); cell.oceanic = (oc != 0);
|
||||
readPod(is, cell.geoAge); readPod(is, cell.drift); readPod(is, cell.invader);
|
||||
if (hasBiome) { uint8_t bm = 0; readPod(is, bm); cell.biome = (Biome)bm; } // save v4
|
||||
if (hasBiome) {
|
||||
uint8_t bm = 0; readPod(is, bm);
|
||||
if (!validBiomeByte(bm)) return false;
|
||||
cell.biome = (Biome)bm;
|
||||
} // save v4
|
||||
}
|
||||
if (!readVec(is, plates, cells.size())) return false;
|
||||
if (plates.empty()) return false;
|
||||
for (const Cell& cell : cells)
|
||||
if (cell.plateId < 0 || cell.plateId >= (int)plates.size()) return false;
|
||||
for (const Plate& p : plates)
|
||||
if (!std::isfinite(p.driftAxis.x) || !std::isfinite(p.driftAxis.y) || !std::isfinite(p.driftAxis.z)
|
||||
|| !std::isfinite(p.driftSpeed) || !std::isfinite(p.angSpeed)
|
||||
|| !std::isfinite(p.speedCmYr)) return false;
|
||||
if (!readVec(is, sPrevCount, plates.size())) return false;
|
||||
if (!readVec(is, sStaleStreak, plates.size())) return false;
|
||||
if (!readVec(is, sFreePlateIds, plates.size())) return false;
|
||||
for (int id : sFreePlateIds) if (id < 0 || id >= (int)plates.size()) return false;
|
||||
if (hasMoons) {
|
||||
if (!readVec(is, moons, 16)) return false; // v9: natural satellites
|
||||
for (const Moon& m : moons)
|
||||
if (!std::isfinite(m.orbitRadius) || !std::isfinite(m.periodDays)
|
||||
|| !std::isfinite(m.phase) || !std::isfinite(m.inclination)
|
||||
|| !std::isfinite(m.tideWeight) || !std::isfinite(m.dispRadius)) return false;
|
||||
}
|
||||
readVec(is, plates);
|
||||
readVec(is, sPrevCount);
|
||||
readVec(is, sStaleStreak);
|
||||
readVec(is, sFreePlateIds);
|
||||
if (hasMoons) readVec(is, moons); // v9: natural satellites
|
||||
else generateMoons(); // pre-v9 save: synthesize moons from the seed
|
||||
if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state
|
||||
// v7: discrete biota population. buildGeometry() already sized sBiota empty;
|
||||
@ -353,24 +386,46 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
||||
uint64_t nb = 0; readPod(is, nb);
|
||||
if (!is || nb != sBiota.size()) return false;
|
||||
for (CellBiota& cb : sBiota) {
|
||||
readVec(is, cb.flora); readVec(is, cb.fauna); readVec(is, cb.funga);
|
||||
if (!readVec(is, cb.flora, (uint64_t)cfg.bioFloraSlots)) return false;
|
||||
if (!readVec(is, cb.fauna, (uint64_t)cfg.bioFaunaSlots)) return false;
|
||||
if (!readVec(is, cb.funga, (uint64_t)cfg.bioFungaSlots)) return false;
|
||||
auto validOrg = [](const std::vector<Organism>& v) {
|
||||
for (const Organism& o : v)
|
||||
if (o.archetype >= biotaArchetypes().size() || !validBiomeByte(o.biome)) return false;
|
||||
return true;
|
||||
};
|
||||
if (!validOrg(cb.flora) || !validOrg(cb.fauna) || !validOrg(cb.funga)) return false;
|
||||
if (!cb.flora.empty() || !cb.fauna.empty() || !cb.funga.empty()) sHasBiota = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// v10: Live World weather. Older saves leave it to spin up on entering Live World. The moving
|
||||
// weather systems are transient (not saved): clear them + reseed the weather RNG from the seed.
|
||||
// v10: Live World weather. Older saves leave it to spin up on entering Live World. Pre-v11
|
||||
// saves load without active systems; they respawn from the seed-backed weather RNG.
|
||||
sHasWeather = false; sHumidity.clear(); sCloud.clear(); sRain.clear();
|
||||
sStorms.clear(); sWeatherRng = cfg.seed ? (cfg.seed ^ 0x5701A123u) : 0x5701A123u;
|
||||
sStorms.clear(); sWeatherRng = cfg.seed ? (cfg.seed ^ 0x5701A123u) : 0x5701A123u; sStormNextId = 1;
|
||||
if (hasWeather) {
|
||||
uint8_t hasWx = 0; readPod(is, hasWx);
|
||||
if (hasWx) {
|
||||
readVec(is, sHumidity); readVec(is, sCloud); readVec(is, sRain);
|
||||
if (!is || (int)sHumidity.size() != (int)cells.size()) return false;
|
||||
if (!readVec(is, sHumidity, cells.size())) return false;
|
||||
if (!readVec(is, sCloud, cells.size())) return false;
|
||||
if (!readVec(is, sRain, cells.size())) return false;
|
||||
if ((int)sHumidity.size() != (int)cells.size()
|
||||
|| sCloud.size() != sHumidity.size() || sRain.size() != sHumidity.size()) return false;
|
||||
for (size_t i = 0; i < sHumidity.size(); ++i)
|
||||
if (!std::isfinite(sHumidity[i]) || !std::isfinite(sCloud[i]) || !std::isfinite(sRain[i]))
|
||||
return false;
|
||||
sHasWeather = true;
|
||||
if (hasStorms) { // v11: active weather systems + their RNG
|
||||
readVec(is, sStorms); readPod(is, sWeatherRng); readPod(is, sStormNextId);
|
||||
if (!readVec(is, sStorms, (uint64_t)cfg.weatherSystemMax)) return false;
|
||||
readPod(is, sWeatherRng); readPod(is, sStormNextId);
|
||||
if (!is) return false;
|
||||
for (const WeatherSystem& ws : sStorms)
|
||||
if (!std::isfinite(ws.pos.x) || !std::isfinite(ws.pos.y) || !std::isfinite(ws.pos.z)
|
||||
|| std::fabs(ws.pos.length() - 1.0) > 1e-6
|
||||
|| !std::isfinite(ws.strength) || ws.strength < 0.0 || ws.strength > 1.0
|
||||
|| !std::isfinite(ws.radius) || ws.radius <= 0.0
|
||||
|| !std::isfinite(ws.age) || !std::isfinite(ws.life)
|
||||
|| !std::isfinite(ws.spin)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <cstdint>
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
@ -26,6 +28,41 @@ static void check(bool cond, const char* what) {
|
||||
if (!cond) ++failures;
|
||||
}
|
||||
|
||||
template <class T> static void writePod(std::ostream& os, const T& v) {
|
||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||
}
|
||||
template <class T> static void writeEmptyVec(std::ostream& os) {
|
||||
uint64_t n = 0; writePod(os, n);
|
||||
}
|
||||
|
||||
static void writeManualStatePrefix(std::ostream& os, const std::string& cfgText, int plateId) {
|
||||
uint64_t clen = cfgText.size(); writePod(os, clen);
|
||||
os.write(cfgText.data(), (std::streamsize)cfgText.size());
|
||||
uint32_t rng = 1; int driftIter = 0, erodeIter = 0, targetLand = -1;
|
||||
writePod(os, rng); writePod(os, driftIter); writePod(os, erodeIter); writePod(os, targetLand);
|
||||
uint64_t nc = 12; writePod(os, nc); // subdivisions=0
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
double elevation = 0.0, geoAge = 0.0, drift = 0.0;
|
||||
uint8_t oceanic = 1, biome = (uint8_t)Biome::Ocean;
|
||||
int invader = -1;
|
||||
writePod(os, elevation); writePod(os, plateId); writePod(os, oceanic);
|
||||
writePod(os, geoAge); writePod(os, drift); writePod(os, invader); writePod(os, biome);
|
||||
}
|
||||
}
|
||||
|
||||
static void finishManualState(std::ostream& os) {
|
||||
uint64_t pn = 1; writePod(os, pn);
|
||||
Plate pl{}; pl.id = 0; pl.driftAxis = Vec3{0, 1, 0};
|
||||
os.write(reinterpret_cast<const char*>(&pl), sizeof(Plate));
|
||||
writeEmptyVec<int>(os); // sPrevCount
|
||||
writeEmptyVec<int>(os); // sStaleStreak
|
||||
writeEmptyVec<int>(os); // sFreePlateIds
|
||||
writeEmptyVec<Moon>(os); // moons
|
||||
uint8_t hasBio = 0, hasWx = 0;
|
||||
writePod(os, hasBio);
|
||||
writePod(os, hasWx);
|
||||
}
|
||||
|
||||
// Euler characteristic for the icosphere: V - E + F == 2 (sphere topology).
|
||||
static bool eulerOk(const Planet& p, size_t vertCount) {
|
||||
const std::vector<int>& tri = p.triIndices();
|
||||
@ -149,6 +186,56 @@ int main() {
|
||||
"subgrid is finite, continuous at center, and reaches neighbors");
|
||||
}
|
||||
|
||||
// ---- Derived state reset + save hardening ----------------------------
|
||||
{
|
||||
Planet stale; stale.generate(cfg);
|
||||
stale.computeHydrology();
|
||||
stale.computeInsolation(0.25, 0.5);
|
||||
stale.computeLiveSeason(0.25);
|
||||
stale.computeTides(0.25, 0.5, 10.0);
|
||||
check(!stale.lakeDepth().empty() && !stale.liveTemp().empty() && !stale.tide().empty(),
|
||||
"derived hydrology/live fields can be populated");
|
||||
stale.generate(cfg);
|
||||
check(stale.lakeDepth().empty() && stale.discharge().empty() && stale.flowTo().empty()
|
||||
&& stale.insolation().empty() && stale.liveTemp().empty() && stale.tide().empty(),
|
||||
"generate() clears stale hydrology/live derived fields");
|
||||
|
||||
PlanetConfig smaller = cfg; smaller.subdivisions = 4;
|
||||
stale.computeHydrology();
|
||||
stale.generate(smaller);
|
||||
check(stale.lakeDepth().empty() && stale.cells.size() != planet.cells.size(),
|
||||
"generate() clears derived fields across subdivision changes");
|
||||
|
||||
Planet saved; saved.generate(cfg); saved.computeHydrology();
|
||||
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||
saved.writeState(ss);
|
||||
Planet loaded;
|
||||
bool ok = loaded.readState(ss);
|
||||
check(ok && loaded.lakeDepth().empty() && loaded.discharge().empty() && loaded.flowTo().empty(),
|
||||
"readState() does not keep unsaved hydrology scratch");
|
||||
}
|
||||
|
||||
{
|
||||
std::stringstream badCfg(std::ios::in | std::ios::out | std::ios::binary);
|
||||
std::string cfgText = "subdivisions = 99\nplateCount = 1\n";
|
||||
uint64_t clen = cfgText.size(); writePod(badCfg, clen);
|
||||
badCfg.write(cfgText.data(), (std::streamsize)cfgText.size());
|
||||
Planet r;
|
||||
check(!r.readState(badCfg), "readState() rejects invalid embedded config before geometry rebuild");
|
||||
|
||||
std::stringstream hugeVec(std::ios::in | std::ios::out | std::ios::binary);
|
||||
writeManualStatePrefix(hugeVec, "subdivisions = 0\nplateCount = 1\n", 0);
|
||||
uint64_t tooManyPlates = 13; writePod(hugeVec, tooManyPlates); // cells.size() is 12
|
||||
Planet rv;
|
||||
check(!rv.readState(hugeVec), "readState() rejects oversized vectors");
|
||||
|
||||
std::stringstream badPlate(std::ios::in | std::ios::out | std::ios::binary);
|
||||
writeManualStatePrefix(badPlate, "subdivisions = 0\nplateCount = 1\n", 77);
|
||||
finishManualState(badPlate);
|
||||
Planet rp;
|
||||
check(!rp.readState(badPlate), "readState() rejects invalid cell plate ids");
|
||||
}
|
||||
|
||||
std::printf("\n%s (%d failure%s)\n",
|
||||
failures == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user