#include "Planet.hpp" #include #include #include #include #include #include #include #include #include // --- Config file (text) + save/load (binary) -------------------------------- // One shared field table so saveConfig/loadConfig can never drift apart. // D = double field, I = int field, U = uint32 field. #define CONFIG_FIELDS(D, I, U) \ D(radius) D(seaLevel) D(axialTilt) D(continentBase) D(oceanBase) D(upliftGain) D(relax) \ D(collisionFactor) D(arcFactor) D(isostaticPersist) D(rootScale) \ D(peakSoftCapStart) D(peakSoftCapEnd) D(peakFailDrop) \ D(seafloorSubsidence) D(seafloorSeedAge) \ D(maxDriftSpeed) D(ridgeDepth) D(splitFraction) D(splitProbBase) D(splitProbSlope) \ D(stalemateEps) D(stalemateBoost) D(babyPromoteFrac) D(volcanicLandFrac) \ D(volcanicElev) D(landBand) D(erosionLandRate) D(erosionSeaRate) \ D(landFractionTarget) D(seaLevelStep) D(seaLevelTol) \ D(phase3AfterMy) D(phase3DtScale) D(rainfall) D(riverThreshold) D(riverIncision) \ D(riverDischargeExp) D(riverSlopeExp) D(riverTransport) D(depFrac) \ D(biomeEquatorTemp) D(biomePoleDrop) D(biomeLatExp) D(biomeElevLapse) \ D(biomeIceTemp) D(biomeTundraTemp) D(biomeTaigaTemp) D(biomeSavannaTemp) \ D(biomeMountainElev) D(biomeHillsElev) D(biomeBeachBand) D(biomeLowlandElev) \ D(biomeWetlandMoist) D(biomeDesertMoist) D(biomeGrassMoist) D(biomeTaigaMoist) \ D(biomeLakeMinDepth) D(biomeSeasonWeight) \ D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \ D(climateOroRefHeight) D(climateContinentality) D(climateCurrentFactor) \ D(seasonAmpMax) D(seasonLatExp) D(seasonOceanFactor) \ D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \ D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \ D(bioFungaTempMin) D(bioRegionBonus) D(bioMarineBase) D(bioMarineShelfDepth) \ D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \ D(tideAmplitude) D(tideSunFactor) \ D(weatherEvapRate) D(weatherWindKmh) D(weatherSatBase) D(weatherSatTempCoef) \ D(weatherCondense) D(weatherOrographic) D(weatherRainThresh) D(weatherRainRate) \ D(weatherCloudDissip) \ D(weatherSpawnRate) D(weatherSystemSpeed) D(weatherTropicalSST) D(weatherSystemRadius) \ D(weatherSystemCloud) D(weatherSystemRain) D(weatherHurricaneStr) \ D(volcanoProbRidge) D(volcanoProbBorder) D(volcanoProbInterior) \ D(volcanoBuildStep) D(volcanoMaxHeight) D(volcanoEruptFreq) \ D(volcanoAshCloud) D(volcanoAshCooling) \ I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \ I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \ I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \ I(volcanoMaxCount) \ I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \ I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \ U(seed) // Write all config fields as `key = value` lines (no header). Shared by the text // config file (saveConfig) and the self-describing config block embedded in saves. static void writeConfigFields(std::ostream& os, const PlanetConfig& cfg) { #define WRITE(name) os << #name " = " << cfg.name << "\n"; CONFIG_FIELDS(WRITE, WRITE, WRITE) #undef WRITE } // Parse `key = value` lines from any stream into cfg (unknown keys ignored, missing // keys keep cfg's existing value). Shared by loadConfig + readState. This is why // adding/removing config fields no longer breaks saves. static void parseConfigStream(std::istream& is, PlanetConfig& cfg) { auto trim = [](std::string& s) { size_t a = s.find_first_not_of(" \t\r\n"), b = s.find_last_not_of(" \t\r\n"); if (a == std::string::npos) s.clear(); else s = s.substr(a, b - a + 1); }; std::string line; while (std::getline(is, line)) { size_t hash = line.find('#'); if (hash != std::string::npos) line.resize(hash); size_t eq = line.find('='); if (eq == std::string::npos) continue; std::string key = line.substr(0, eq), val = line.substr(eq + 1); trim(key); trim(val); if (key.empty() || val.empty()) continue; #define D(name) if (key == #name) { try { cfg.name = std::stod(val); } catch (...) {} continue; } #define I(name) if (key == #name) { try { cfg.name = std::stoi(val); } catch (...) {} continue; } #define U(name) if (key == #name) { try { cfg.name = (uint32_t)std::stoul(val); } catch (...) {} continue; } CONFIG_FIELDS(D, I, U) #undef D #undef I #undef U } } bool saveConfig(const std::string& path, const PlanetConfig& cfg) { std::ofstream os(path); if (!os) return false; os.precision(15); // enough for the (nice, decimal) defaults; trailing zeros trimmed os << "# Planet config -- edit values, then reload in-app (F2) or restart.\n"; os << "# key = value; '#' starts a comment; unknown keys are ignored.\n\n"; writeConfigFields(os, cfg); return (bool)os; } bool loadConfig(const std::string& path, PlanetConfig& cfg) { std::ifstream is(path); if (!is) return false; parseConfigStream(is, cfg); return true; } std::string validateConfig(const PlanetConfig& cfg) { auto rng = [](double v, double lo, double hi, const char* name) -> std::string { if (v >= lo && v <= hi) return {}; return std::string(name) + " = " + std::to_string(v) + " (expected " + std::to_string(lo) + ".." + std::to_string(hi) + ")"; }; auto irng = [](int v, int lo, int hi, const char* name) -> std::string { if (v >= lo && v <= hi) return {}; return std::string(name) + " = " + std::to_string(v) + " (expected " + std::to_string(lo) + ".." + std::to_string(hi) + ")"; }; std::vector bad; auto E = [&](const std::string& s) { if (!s.empty()) bad.push_back(s); }; E(rng(cfg.radius, 1.0e3, 1.0e8, "radius")); E(rng(cfg.seaLevel, -11000.0, 9000.0, "seaLevel")); E(rng(cfg.axialTilt, 0.0, 180.0, "axialTilt")); E(rng(cfg.continentBase, -2000.0, 6000.0, "continentBase")); E(rng(cfg.oceanBase, -11000.0, 1000.0, "oceanBase")); E(rng(cfg.upliftGain, 100.0, 1.0e7, "upliftGain")); E(rng(cfg.relax, 0.001, 0.5, "relax")); E(rng(cfg.collisionFactor, 0.0, 20.0, "collisionFactor")); E(rng(cfg.arcFactor, 0.0, 20.0, "arcFactor")); E(rng(cfg.isostaticPersist, 0.0, 0.95, "isostaticPersist")); E(rng(cfg.rootScale, 100.0, 20000.0, "rootScale")); E(rng(cfg.peakSoftCapStart, 0.0, 20000.0, "peakSoftCapStart")); E(rng(cfg.peakSoftCapEnd, 0.0, 20000.0, "peakSoftCapEnd")); E(rng(cfg.peakFailDrop, 0.0, 5000.0, "peakFailDrop")); E(rng(cfg.seafloorSubsidence, 0.0, 2000.0, "seafloorSubsidence")); E(rng(cfg.seafloorSeedAge, 0.0, 1000.0, "seafloorSeedAge")); E(rng(cfg.maxDriftSpeed, 0.1, 100.0, "maxDriftSpeed")); E(rng(cfg.ridgeDepth, -8000.0, 0.0, "ridgeDepth")); E(rng(cfg.splitFraction, 0.0, 1.0, "splitFraction")); E(rng(cfg.splitProbBase, 0.0, 1.0, "splitProbBase")); E(rng(cfg.splitProbSlope, 0.0, 1.0, "splitProbSlope")); E(rng(cfg.stalemateEps, 0.0, 1.0, "stalemateEps")); E(rng(cfg.stalemateBoost, 1.0, 5.0, "stalemateBoost")); E(rng(cfg.babyPromoteFrac, 0.001, 0.5, "babyPromoteFrac")); E(rng(cfg.volcanicLandFrac, 0.0, 1.0, "volcanicLandFrac")); E(rng(cfg.volcanicElev, -1000.0, 5000.0, "volcanicElev")); E(rng(cfg.landBand, 0.0, 1.0, "landBand")); E(rng(cfg.erosionLandRate, 0.0, 1.0, "erosionLandRate")); E(rng(cfg.erosionSeaRate, 0.0, 1.0, "erosionSeaRate")); E(rng(cfg.landFractionTarget, 0.01, 0.99, "landFractionTarget")); E(rng(cfg.seaLevelStep, 1.0, 2000.0, "seaLevelStep")); E(rng(cfg.seaLevelTol, 0.001, 0.5, "seaLevelTol")); E(rng(cfg.phase3AfterMy, 0.0, 1.0e6, "phase3AfterMy")); E(rng(cfg.phase3DtScale, 0.001, 1.0, "phase3DtScale")); E(rng(cfg.rainfall, 0.0, 1.0e6, "rainfall")); E(rng(cfg.riverThreshold, 0.0, 1.0e9, "riverThreshold")); E(rng(cfg.riverIncision, 0.0, 1.0e6, "riverIncision")); E(rng(cfg.riverDischargeExp, 0.0, 5.0, "riverDischargeExp")); E(rng(cfg.riverSlopeExp, 0.0, 5.0, "riverSlopeExp")); E(rng(cfg.riverTransport, 0.0, 1.0e6, "riverTransport")); E(rng(cfg.depFrac, 0.0, 1.0, "depFrac")); E(rng(cfg.biomeEquatorTemp, -50.0, 80.0, "biomeEquatorTemp")); E(rng(cfg.biomePoleDrop, 0.0, 150.0, "biomePoleDrop")); E(rng(cfg.biomeLatExp, 0.1, 6.0, "biomeLatExp")); E(rng(cfg.biomeElevLapse, 0.0, 0.05, "biomeElevLapse")); E(rng(cfg.biomeIceTemp, -60.0, 20.0, "biomeIceTemp")); E(rng(cfg.biomeTundraTemp, -60.0, 40.0, "biomeTundraTemp")); E(rng(cfg.biomeTaigaTemp, -60.0, 40.0, "biomeTaigaTemp")); E(rng(cfg.biomeSavannaTemp, -20.0, 60.0, "biomeSavannaTemp")); E(rng(cfg.biomeMountainElev, 0.0, 11000.0, "biomeMountainElev")); E(rng(cfg.biomeHillsElev, 0.0, 11000.0, "biomeHillsElev")); E(rng(cfg.biomeBeachBand, 0.0, 2000.0, "biomeBeachBand")); E(rng(cfg.biomeLowlandElev, 0.0, 11000.0, "biomeLowlandElev")); E(rng(cfg.biomeWetlandMoist, 0.0, 1.0, "biomeWetlandMoist")); E(rng(cfg.biomeDesertMoist, 0.0, 1.0, "biomeDesertMoist")); E(rng(cfg.biomeGrassMoist, 0.0, 1.0, "biomeGrassMoist")); E(rng(cfg.biomeTaigaMoist, 0.0, 1.0, "biomeTaigaMoist")); E(rng(cfg.biomeLakeMinDepth, 0.0, 5000.0, "biomeLakeMinDepth")); E(rng(cfg.biomeSeasonWeight, 0.0, 1.0, "biomeSeasonWeight")); E(rng(cfg.climateOceanMoisture, 0.0, 1.0e3, "climateOceanMoisture")); E(rng(cfg.climateRainEfficiency, 0.0, 1.0, "climateRainEfficiency")); E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic")); E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight")); E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality")); E(rng(cfg.climateCurrentFactor, 0.0, 30.0, "climateCurrentFactor")); E(rng(cfg.seasonAmpMax, 0.0, 60.0, "seasonAmpMax")); E(rng(cfg.seasonLatExp, 0.1, 6.0, "seasonLatExp")); E(rng(cfg.seasonOceanFactor, 0.0, 1.0, "seasonOceanFactor")); E(rng(cfg.bioVegTempMin, -40.0, 30.0, "bioVegTempMin")); E(rng(cfg.bioVegTempOpt, -20.0, 50.0, "bioVegTempOpt")); E(rng(cfg.bioVegMoistRef, 0.01, 1.0, "bioVegMoistRef")); E(rng(cfg.bioFaunaProductivity, 0.0, 2.0, "bioFaunaProductivity")); E(rng(cfg.bioCarnPreyMin, 0.0, 1.0, "bioCarnPreyMin")); E(rng(cfg.bioCarnScale, 0.0, 5.0, "bioCarnScale")); E(rng(cfg.bioFungaMoistRef, 0.01, 1.0, "bioFungaMoistRef")); E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight")); E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin")); E(rng(cfg.bioRegionBonus, 0.0, 10.0, "bioRegionBonus")); E(rng(cfg.bioMarineBase, 0.0, 1.0, "bioMarineBase")); E(rng(cfg.bioMarineShelfDepth, 1.0, 11000.0, "bioMarineShelfDepth")); E(rng(cfg.dayLengthHours, 0.1, 1.0e5, "dayLengthHours")); E(rng(cfg.yearLengthDays, 1.0, 1.0e7, "yearLengthDays")); E(rng(cfg.snowTemp, -60.0, 30.0, "snowTemp")); E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp")); E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude")); E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor")); E(rng(cfg.weatherEvapRate, 0.0, 50.0, "weatherEvapRate")); E(rng(cfg.weatherWindKmh, 0.0, 1000.0, "weatherWindKmh")); E(rng(cfg.weatherSatBase, 0.01, 5.0, "weatherSatBase")); E(rng(cfg.weatherSatTempCoef, 0.0, 1.0, "weatherSatTempCoef")); E(rng(cfg.weatherCondense, 0.0, 50.0, "weatherCondense")); E(rng(cfg.weatherOrographic, 0.0, 1.0, "weatherOrographic")); E(rng(cfg.weatherRainThresh, 0.0, 1.5, "weatherRainThresh")); E(rng(cfg.weatherRainRate, 0.0, 50.0, "weatherRainRate")); E(rng(cfg.weatherCloudDissip, 0.0, 50.0, "weatherCloudDissip")); E(rng(cfg.weatherSpawnRate, 0.0, 10.0, "weatherSpawnRate")); E(rng(cfg.weatherSystemSpeed, 0.0, 500.0, "weatherSystemSpeed")); E(rng(cfg.weatherTropicalSST, -10.0, 40.0, "weatherTropicalSST")); E(rng(cfg.weatherSystemRadius, 0.01, 1.5, "weatherSystemRadius")); E(rng(cfg.weatherSystemCloud, 0.0, 20.0, "weatherSystemCloud")); E(rng(cfg.weatherSystemRain, 0.0, 20.0, "weatherSystemRain")); E(rng(cfg.weatherHurricaneStr, 0.0, 1.0, "weatherHurricaneStr")); E(rng(cfg.volcanoProbRidge, 0.0, 1.0, "volcanoProbRidge")); E(rng(cfg.volcanoProbBorder, 0.0, 1.0, "volcanoProbBorder")); E(rng(cfg.volcanoProbInterior, 0.0, 1.0, "volcanoProbInterior")); E(rng(cfg.volcanoBuildStep, 0.0, 5000.0, "volcanoBuildStep")); E(rng(cfg.volcanoMaxHeight, 0.0, 12000.0, "volcanoMaxHeight")); E(rng(cfg.volcanoEruptFreq, 0.0, 100.0, "volcanoEruptFreq")); E(rng(cfg.volcanoAshCloud, 0.0, 10.0, "volcanoAshCloud")); E(rng(cfg.volcanoAshCooling, 0.0, 40.0, "volcanoAshCooling")); E(irng(cfg.subdivisions, 0, 7, "subdivisions")); E(irng(cfg.plateCount, 1, 100, "plateCount")); E(irng(cfg.beltWidth, 1, 12, "beltWidth")); E(irng(cfg.splitCheckEvery, 1, 10000, "splitCheckEvery")); E(irng(cfg.stalemateWindows, 1, 100, "stalemateWindows")); E(irng(cfg.miniPlateCells, 1, 10000, "miniPlateCells")); E(irng(cfg.fuseMinPlates, 2, 50, "fuseMinPlates")); E(irng(cfg.babyMinCells, 1, 1000, "babyMinCells")); E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery")); E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses")); E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth")); E(irng(cfg.seasonContinentRings, 1, 100, "seasonContinentRings")); E(irng(cfg.weatherSystemMax, 0, 1000, "weatherSystemMax")); E(irng(cfg.volcanoMaxCount, 0, 100000, "volcanoMaxCount")); E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots")); E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots")); E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots")); E(irng(cfg.bioFloraPoints, 1, 100000, "bioFloraPoints")); E(irng(cfg.bioFaunaPoints, 1, 100000, "bioFaunaPoints")); E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints")); E(irng(cfg.bioMarineCoastRings, 1, 100, "bioMarineCoastRings")); if (cfg.oceanBase >= cfg.continentBase) bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)"); if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd) bad.push_back("peakSoftCapStart >= peakSoftCapEnd (grow probability must span a band)"); if (bad.empty()) return {}; std::string msg = "Bad config:"; for (auto& s : bad) msg += "\n " + s; return msg; } namespace { template void writePod(std::ostream& os, const T& v) { static_assert(std::is_trivially_copyable::value, "writePod needs a POD type"); os.write(reinterpret_cast(&v), sizeof(T)); } template void readPod(std::istream& is, T& v) { static_assert(std::is_trivially_copyable::value, "readPod needs a POD type"); is.read(reinterpret_cast(&v), sizeof(T)); } template void writeVec(std::ostream& os, const std::vector& v) { static_assert(std::is_trivially_copyable::value, "writeVec needs POD elements"); uint64_t n = v.size(); writePod(os, n); if (n) os.write(reinterpret_cast(v.data()), (std::streamsize)(n * sizeof(T))); } template bool readVec(std::istream& is, std::vector& v, uint64_t maxCount) { static_assert(std::is_trivially_copyable::value, "readVec needs POD elements"); 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::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(v.data()), (std::streamsize)(n * sizeof(T))); return (bool)is; } bool validBiomeByte(uint8_t b) { return b <= (uint8_t)Biome::Mountains; } } // Full simulation state. Geometry (unit/neighbors) is NOT stored -- it is rebuilt // from cfg.subdivisions on load -- so only the dynamic per-cell fields are saved. void Planet::writeState(std::ostream& os) const { // Config is stored as a self-describing key=value text block (length-prefixed), // not a raw POD dump, so adding/removing config fields never breaks old saves // (unknown keys ignored, missing keys keep their defaults). precision(17) = // max_digits10 for double, so values round-trip exactly (deterministic resume). std::ostringstream cfgss; cfgss.precision(17); writeConfigFields(cfgss, cfg); std::string cfgText = cfgss.str(); uint64_t clen = cfgText.size(); writePod(os, clen); os.write(cfgText.data(), (std::streamsize)clen); writePod(os, rngState); writePod(os, driftIter); writePod(os, erodeIter); writePod(os, targetLand); uint64_t nc = cells.size(); writePod(os, nc); for (const Cell& c : cells) { writePod(os, c.elevation); writePod(os, c.plateId); uint8_t oc = c.oceanic ? 1 : 0; writePod(os, oc); writePod(os, c.geoAge); writePod(os, c.drift); writePod(os, c.invader); uint8_t bm = (uint8_t)c.biome; writePod(os, bm); // save v4: per-cell biome } writeVec(os, plates); writeVec(os, sPrevCount); writeVec(os, sStaleStreak); writeVec(os, sFreePlateIds); writeVec(os, moons); // v9: natural satellites (Live World) // v7: discrete biota population (sBiota). A flag byte gates the block so a // not-yet-populated world stays compact; otherwise three Organism lists per cell. uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio); if (hasBio) { uint64_t nb = sBiota.size(); writePod(os, nb); for (const CellBiota& cb : sBiota) { writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga); } } // 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); writeVec(os, sStorms); writePod(os, sWeatherRng); writePod(os, sStormNextId); // v11 } // v14: Live World volcanoes (placed by tectonic context on entry; eruption state is a pure // function of liveTime, so only the placed set + its RNG need saving). Always written from v14; // older readers stop before this block. writeVec(os, volcanoes); writePod(os, sVolRng); } bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, bool hasWeather, bool hasStorms, bool hasVolcanoes) { // 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; hasVolcanoes = false; } if (!hasWeather) { hasStorms = false; hasVolcanoes = false; } // volcano block follows the weather block // 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'); 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); readPod(is, driftIter); readPod(is, erodeIter); readPod(is, targetLand); uint64_t nc = 0; readPod(is, nc); if (!is || nc != cells.size()) return false; // subdivisions mismatch / corrupt file for (Cell& cell : cells) { 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); 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; } 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; // older saves (hasBiota=false) just keep the empty population (press L to fill). sHasBiota = false; if (hasBiota) { uint8_t hasBio = 0; readPod(is, hasBio); if (hasBio) { uint64_t nb = 0; readPod(is, nb); if (!is || nb != sBiota.size()) return false; for (CellBiota& cb : sBiota) { 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& 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. 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; sStormNextId = 1; if (hasWeather) { uint8_t hasWx = 0; readPod(is, hasWx); if (hasWx) { 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 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; } } } // v14: Live World volcanoes. Older saves load with none (placed on next Live World entry). volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au; if (hasVolcanoes) { if (!readVec(is, volcanoes, 100000)) return false; readPod(is, sVolRng); if (!is) return false; const int nc2 = (int)cells.size(); for (const Volcano& v : volcanoes) if (v.cell < 0 || v.cell >= nc2 || !std::isfinite(v.activity) || !std::isfinite(v.baseElev) || !std::isfinite(v.tStart)) return false; } computeBiotaDensity(); // derived density scalars for the colour views return (bool)is; }