839 lines
48 KiB
C++
839 lines
48 KiB
C++
#include "Planet.hpp"
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <type_traits>
|
|
#include <istream>
|
|
#include <ostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <limits>
|
|
#include <set>
|
|
#include <utility>
|
|
|
|
// --- 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(volcanoBuildRate) D(volcanoFreeHeight) D(volcanoInitialBuildMax) D(volcanoMaxHeight) \
|
|
D(volcanoDormancyRate) D(volcanoDormantMinYears) D(volcanoDormantMaxYears) \
|
|
D(volcanoExplodeDropFrac) D(volcanoActivityDecay) D(volcanoDeadActivity) \
|
|
D(volcanoBlastRadius) D(volcanoBlastCloud) D(volcanoAshMinYears) D(volcanoAshMaxYears) \
|
|
D(volcanoAshPuffCellsPerWeek) D(volcanoAshCloud) D(volcanoAshCooling) \
|
|
D(geoMountainElev) D(geoRiverMinDischarge) D(geoOceanSepRadians) \
|
|
D(civMinSpacingRadians) D(civClusterExp) D(civMinHabitability) D(civSeedPopulation) D(civGrowthRate) \
|
|
D(civMaxPopulation) D(civTownPop) D(civCityPop) D(civAbandonPop) \
|
|
D(civHabWaterWeight) D(civHabFoodWeight) D(civHabTempOpt) D(civHabElevPenalty) \
|
|
D(civSiteVariety) D(civGrowthMin) D(civHarvestVar) D(civDroughtStrength) D(civDroughtPeriod) D(civDroughtThresh) \
|
|
D(civDroughtArid) D(civColdYearStrength) D(civFloodBonus) D(civFamineRate) \
|
|
D(civStormDeathRate) D(civHurricaneDeathMult) \
|
|
D(civTerritoryBase) D(civTerritoryScale) D(civTerritoryMax) D(civVassalRange) D(civEmpirePop) \
|
|
D(warDeclareRate) D(warAmbition) D(warIdeology) D(warBorder) D(warWarlikeMult) D(warCasualtyRate) \
|
|
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
|
D(diploDriftRate) D(diploAffinity) D(diploWarPenalty) D(diploTruceYears) D(diploWarGrudge) \
|
|
D(diploAllyThreshold) D(diploNonAggThreshold) D(diploRivalThreshold) \
|
|
D(tradeLandRange) D(tradeSeaRange) D(tradeRiverBonus) D(tradeMinVolume) D(tradeProsperityWeight) \
|
|
D(tradeWarBlock) D(tradeAllyBonus) D(tradeDiploBonus) D(tradeTemptWar) \
|
|
D(civColonizeRate) D(civColonyMinPop) D(civColonyMinHab) D(civColonyReach) D(civColonySeaReach) \
|
|
D(civColonySpacing) D(civColonySupply) \
|
|
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(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
|
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
|
|
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<std::string> 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.volcanoBuildRate, 0.0, 1000.0, "volcanoBuildRate"));
|
|
E(rng(cfg.volcanoFreeHeight, -11000.0, 12000.0, "volcanoFreeHeight"));
|
|
E(rng(cfg.volcanoInitialBuildMax, 0.0, 12000.0, "volcanoInitialBuildMax"));
|
|
E(rng(cfg.volcanoMaxHeight, 1.0, 12000.0, "volcanoMaxHeight"));
|
|
E(rng(cfg.volcanoDormancyRate, 0.0, 100.0, "volcanoDormancyRate"));
|
|
E(rng(cfg.volcanoDormantMinYears, 0.0, 1.0e6, "volcanoDormantMinYears"));
|
|
E(rng(cfg.volcanoDormantMaxYears, 0.0, 1.0e6, "volcanoDormantMaxYears"));
|
|
E(rng(cfg.volcanoExplodeDropFrac, 0.0, 1.0, "volcanoExplodeDropFrac"));
|
|
E(rng(cfg.volcanoActivityDecay, 0.0, 1.0, "volcanoActivityDecay"));
|
|
E(rng(cfg.volcanoDeadActivity, 0.0, 1.0, "volcanoDeadActivity"));
|
|
E(rng(cfg.volcanoBlastRadius, 0.0, 3.2, "volcanoBlastRadius"));
|
|
E(rng(cfg.volcanoBlastCloud, 0.0, 10.0, "volcanoBlastCloud"));
|
|
E(rng(cfg.volcanoAshMinYears, 0.0, 1.0e6, "volcanoAshMinYears"));
|
|
E(rng(cfg.volcanoAshMaxYears, 0.0, 1.0e6, "volcanoAshMaxYears"));
|
|
E(rng(cfg.volcanoAshPuffCellsPerWeek, 0.0, 1000.0, "volcanoAshPuffCellsPerWeek"));
|
|
E(rng(cfg.volcanoAshCloud, 0.0, 10.0, "volcanoAshCloud"));
|
|
E(rng(cfg.volcanoAshCooling, 0.0, 40.0, "volcanoAshCooling"));
|
|
E(rng(cfg.geoMountainElev, 0.0, 12000.0, "geoMountainElev"));
|
|
E(rng(cfg.geoRiverMinDischarge, 0.0, 1.0e9, "geoRiverMinDischarge"));
|
|
E(rng(cfg.geoOceanSepRadians, 0.05, 3.14159, "geoOceanSepRadians"));
|
|
E(rng(cfg.civMinSpacingRadians, 0.001, 3.14159, "civMinSpacingRadians"));
|
|
E(rng(cfg.civClusterExp, 0.0, 12.0, "civClusterExp"));
|
|
E(rng(cfg.civMinHabitability, 0.0, 1.0, "civMinHabitability"));
|
|
E(rng(cfg.civSeedPopulation, 1.0, 1.0e9, "civSeedPopulation"));
|
|
E(rng(cfg.civGrowthRate, 0.0, 100.0, "civGrowthRate"));
|
|
E(rng(cfg.civMaxPopulation, 1.0, 1.0e12, "civMaxPopulation"));
|
|
E(rng(cfg.civTownPop, 1.0, 1.0e12, "civTownPop"));
|
|
E(rng(cfg.civCityPop, 1.0, 1.0e12, "civCityPop"));
|
|
E(rng(cfg.civAbandonPop, 0.0, 1.0e9, "civAbandonPop"));
|
|
E(rng(cfg.civHabWaterWeight, 0.0, 1.0, "civHabWaterWeight"));
|
|
E(rng(cfg.civHabFoodWeight, 0.0, 1.0, "civHabFoodWeight"));
|
|
E(rng(cfg.civHabTempOpt, -20.0, 50.0, "civHabTempOpt"));
|
|
E(rng(cfg.civHabElevPenalty, 0.0, 12000.0, "civHabElevPenalty"));
|
|
E(rng(cfg.civSiteVariety, 0.0, 4.0, "civSiteVariety"));
|
|
E(rng(cfg.civGrowthMin, 0.0, 1.0, "civGrowthMin"));
|
|
E(rng(cfg.civHarvestVar, 0.0, 2.0, "civHarvestVar"));
|
|
E(rng(cfg.civDroughtStrength, 0.0, 1.0, "civDroughtStrength"));
|
|
E(rng(cfg.civDroughtPeriod, 0.1, 1000.0, "civDroughtPeriod"));
|
|
E(rng(cfg.civDroughtThresh, -2.0, 2.0, "civDroughtThresh"));
|
|
E(rng(cfg.civDroughtArid, 0.0, 4.0, "civDroughtArid"));
|
|
E(rng(cfg.civColdYearStrength, 0.0, 1.0, "civColdYearStrength"));
|
|
E(rng(cfg.civFloodBonus, 0.0, 2.0, "civFloodBonus"));
|
|
E(rng(cfg.civFamineRate, 0.0, 10.0, "civFamineRate"));
|
|
E(rng(cfg.civStormDeathRate, 0.0, 10.0, "civStormDeathRate"));
|
|
E(rng(cfg.civHurricaneDeathMult, 1.0, 50.0, "civHurricaneDeathMult"));
|
|
E(rng(cfg.civTerritoryBase, 0.0, 3.14159, "civTerritoryBase"));
|
|
E(rng(cfg.civTerritoryScale, 0.0, 3.14159, "civTerritoryScale"));
|
|
E(rng(cfg.civTerritoryMax, 0.01, 3.14159, "civTerritoryMax"));
|
|
E(rng(cfg.civVassalRange, 0.0, 20.0, "civVassalRange"));
|
|
E(rng(cfg.civEmpirePop, 1.0, 1.0e12, "civEmpirePop"));
|
|
E(rng(cfg.warDeclareRate, 0.0, 100.0, "warDeclareRate"));
|
|
E(rng(cfg.warAmbition, 0.0, 100.0, "warAmbition"));
|
|
E(rng(cfg.warIdeology, 0.0, 100.0, "warIdeology"));
|
|
E(rng(cfg.warBorder, 0.0, 100.0, "warBorder"));
|
|
E(rng(cfg.warWarlikeMult, 1.0, 100.0, "warWarlikeMult"));
|
|
E(rng(cfg.warCasualtyRate, 0.0, 1.0, "warCasualtyRate"));
|
|
E(rng(cfg.warConquerScore, 0.0, 1000.0, "warConquerScore"));
|
|
E(rng(cfg.warSackChance, 0.0, 1.0, "warSackChance"));
|
|
E(rng(cfg.warExhaustion, 0.0, 1000.0, "warExhaustion"));
|
|
E(rng(cfg.warRevoltRate, 0.0, 10.0, "warRevoltRate"));
|
|
E(rng(cfg.warMinRealmPop, 0.0, 1.0e12, "warMinRealmPop"));
|
|
E(irng(cfg.warMaxConcurrent, 0, 100000, "warMaxConcurrent"));
|
|
E(rng(cfg.diploDriftRate, 0.0, 10.0, "diploDriftRate"));
|
|
E(rng(cfg.diploAffinity, 0.0, 100.0, "diploAffinity"));
|
|
E(rng(cfg.diploWarPenalty, 0.0, 100.0, "diploWarPenalty"));
|
|
E(rng(cfg.diploTruceYears, 0.0, 1.0e6, "diploTruceYears"));
|
|
E(rng(cfg.diploWarGrudge, 0.0, 2.0, "diploWarGrudge"));
|
|
E(rng(cfg.diploAllyThreshold, -1.0, 1.0, "diploAllyThreshold"));
|
|
E(rng(cfg.diploNonAggThreshold, -1.0, 1.0, "diploNonAggThreshold"));
|
|
E(rng(cfg.diploRivalThreshold, -1.0, 1.0, "diploRivalThreshold"));
|
|
E(rng(cfg.tradeLandRange, 0.0, 3.14159, "tradeLandRange"));
|
|
E(rng(cfg.tradeSeaRange, 0.0, 3.14159, "tradeSeaRange"));
|
|
E(rng(cfg.tradeRiverBonus, 0.0, 3.14159, "tradeRiverBonus"));
|
|
E(rng(cfg.tradeMinVolume, 0.0, 1.0e9, "tradeMinVolume"));
|
|
E(rng(cfg.tradeProsperityWeight, 0.0, 100.0, "tradeProsperityWeight"));
|
|
E(rng(cfg.tradeWarBlock, 0.0, 10.0, "tradeWarBlock"));
|
|
E(rng(cfg.tradeAllyBonus, 0.0, 100.0, "tradeAllyBonus"));
|
|
E(rng(cfg.tradeDiploBonus, 0.0, 10.0, "tradeDiploBonus"));
|
|
E(rng(cfg.tradeTemptWar, 0.0, 100.0, "tradeTemptWar"));
|
|
E(rng(cfg.civColonizeRate, 0.0, 1.0, "civColonizeRate"));
|
|
E(rng(cfg.civColonyMinPop, 0.0, 1.0e12, "civColonyMinPop"));
|
|
E(rng(cfg.civColonyMinHab, 0.0, 1.0, "civColonyMinHab"));
|
|
E(rng(cfg.civColonyReach, 0.0, 3.14159, "civColonyReach"));
|
|
E(rng(cfg.civColonySeaReach, 0.0, 3.14159, "civColonySeaReach"));
|
|
E(rng(cfg.civColonySpacing, 0.0, 3.14159, "civColonySpacing"));
|
|
E(rng(cfg.civColonySupply, 0.0, 100.0, "civColonySupply"));
|
|
E(irng(cfg.civMaxColonies, 0, 1000000, "civMaxColonies"));
|
|
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.geoContinentMinCells, 1, 1000000, "geoContinentMinCells"));
|
|
E(irng(cfg.geoSeaMaxCells, 0, 1000000, "geoSeaMaxCells"));
|
|
E(irng(cfg.geoRangeMinCells, 1, 1000000, "geoRangeMinCells"));
|
|
E(irng(cfg.geoMaxRivers, 0, 100000, "geoMaxRivers"));
|
|
E(irng(cfg.geoMaxPeaks, 0, 100000, "geoMaxPeaks"));
|
|
E(irng(cfg.geoOceanDeep, 1, 1000, "geoOceanDeep"));
|
|
E(irng(cfg.civMaxSettlements, 0, 1000000, "civMaxSettlements"));
|
|
E(irng(cfg.civEmpireMinMembers, 1, 1000000, "civEmpireMinMembers"));
|
|
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 (cfg.volcanoDormantMinYears > cfg.volcanoDormantMaxYears)
|
|
bad.push_back("volcanoDormantMinYears > volcanoDormantMaxYears");
|
|
if (cfg.volcanoAshMinYears > cfg.volcanoAshMaxYears)
|
|
bad.push_back("volcanoAshMinYears > volcanoAshMaxYears");
|
|
if (!(cfg.biomeIceTemp < cfg.biomeTundraTemp && cfg.biomeTundraTemp < cfg.biomeTaigaTemp))
|
|
bad.push_back("biome temperature thresholds must satisfy Ice < Tundra < Taiga");
|
|
if (cfg.biomeHillsElev >= cfg.biomeMountainElev)
|
|
bad.push_back("biomeHillsElev >= biomeMountainElev");
|
|
if (cfg.civAbandonPop >= cfg.civTownPop || cfg.civTownPop >= cfg.civCityPop)
|
|
bad.push_back("civilization population thresholds must satisfy Abandon < Town < City");
|
|
if (cfg.civSeedPopulation <= cfg.civAbandonPop)
|
|
bad.push_back("civSeedPopulation <= civAbandonPop");
|
|
if (!(cfg.diploRivalThreshold < cfg.diploNonAggThreshold && cfg.diploNonAggThreshold < cfg.diploAllyThreshold))
|
|
bad.push_back("diplomacy thresholds must satisfy Rival < NonAggression < Alliance");
|
|
|
|
if (bad.empty()) return {};
|
|
std::string msg = "Bad config:";
|
|
for (auto& s : bad) msg += "\n " + s;
|
|
return msg;
|
|
}
|
|
|
|
namespace {
|
|
struct LegacyVolcano {
|
|
uint32_t id = 0;
|
|
int cell = -1;
|
|
uint8_t kind = 2;
|
|
uint8_t submarine = 0;
|
|
double activity = 0.5;
|
|
double baseElev = 0.0;
|
|
double tStart = 0.0;
|
|
};
|
|
|
|
template <class T> void writePod(std::ostream& os, const T& v) {
|
|
static_assert(std::is_trivially_copyable<T>::value, "writePod needs a POD type");
|
|
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
|
}
|
|
template <class T> void readPod(std::istream& is, T& v) {
|
|
static_assert(std::is_trivially_copyable<T>::value, "readPod needs a POD type");
|
|
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
|
}
|
|
template <class T> void writeVec(std::ostream& os, const std::vector<T>& v) {
|
|
static_assert(std::is_trivially_copyable<T>::value, "writeVec needs POD elements");
|
|
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> 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);
|
|
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;
|
|
}
|
|
bool validWarRecord(const War& w, int settlementCount) {
|
|
return w.attacker >= 0 && w.attacker < settlementCount
|
|
&& w.defender >= 0 && w.defender < settlementCount
|
|
&& w.attacker != w.defender
|
|
&& w.battles >= 0
|
|
&& std::isfinite(w.warscore);
|
|
}
|
|
bool validateWars(std::vector<War>& wars, int settlementCount) {
|
|
std::set<std::pair<int, int>> seen;
|
|
for (const War& w : wars) {
|
|
if (!validWarRecord(w, settlementCount)) return false;
|
|
auto key = std::minmax(w.attacker, w.defender);
|
|
if (!seen.insert(key).second) return false;
|
|
}
|
|
return true;
|
|
}
|
|
bool sanitizeDiplo(std::vector<DiploTie>& ties, int settlementCount) {
|
|
std::set<std::pair<int, int>> seen;
|
|
for (DiploTie& t : ties) {
|
|
if (t.a < 0 || t.a >= settlementCount || t.b < 0 || t.b >= settlementCount || t.a == t.b)
|
|
return false;
|
|
if (t.a > t.b) std::swap(t.a, t.b);
|
|
if (!seen.insert({t.a, t.b}).second) return false;
|
|
if (!std::isfinite(t.attitude)) return false;
|
|
t.attitude = std::clamp(t.attitude, -1.0, 1.0);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
// v15: Live World volcanoes are stateful lifecycle agents. Always written from v15;
|
|
// older readers stop before this block.
|
|
writeVec(os, volcanoes);
|
|
writePod(os, sVolRng);
|
|
// v17: named geographic features (the atlas) + per-cell region indices. Feature records carry a
|
|
// std::string name, so they are written field-by-field (not POD); the index arrays are POD.
|
|
// v18: active geography reshuffle salt, so repeated renames continue after load.
|
|
uint64_t nf = geoFeatures.size(); writePod(os, nf);
|
|
for (const GeoFeature& f : geoFeatures) {
|
|
writePod(os, f.id);
|
|
uint8_t k = (uint8_t)f.kind; writePod(os, k);
|
|
writePod(os, f.anchorCell); writePod(os, f.regionId); writePod(os, f.size);
|
|
uint64_t L = f.name.size(); writePod(os, L);
|
|
if (L) os.write(f.name.data(), (std::streamsize)L);
|
|
}
|
|
writeVec(os, sCellLand); writeVec(os, sCellWater);
|
|
writeVec(os, sCellRange); writeVec(os, sCellRiver);
|
|
writePod(os, sGeoSalt);
|
|
// v19: named ecoregions + per-cell ecoregion index.
|
|
uint64_t ne = ecoRegions.size(); writePod(os, ne);
|
|
for (const Ecoregion& e : ecoRegions) {
|
|
writePod(os, e.id);
|
|
uint8_t b = (uint8_t)e.biome; writePod(os, b);
|
|
writePod(os, e.anchorCell); writePod(os, e.regionId); writePod(os, e.size);
|
|
writePod(os, e.dominantFlora); writePod(os, e.dominantFauna); writePod(os, e.dominantFunga);
|
|
writePod(os, e.floraProductivity); writePod(os, e.faunaProductivity); writePod(os, e.fungaProductivity);
|
|
uint64_t L = e.name.size(); writePod(os, L);
|
|
if (L) os.write(e.name.data(), (std::streamsize)L);
|
|
}
|
|
writeVec(os, sCellEcoregion);
|
|
// v20: civilization settlements (placed once, then grow/decline). sCellSettlement is rebuilt on
|
|
// load, so only the settlement records (id/cell/bank/regionId/population/name) are written.
|
|
uint64_t ns = settlements.size(); writePod(os, ns);
|
|
for (const Settlement& st : settlements) {
|
|
writePod(os, st.id); writePod(os, st.cell); writePod(os, st.bank); writePod(os, st.regionId);
|
|
writePod(os, st.population);
|
|
uint64_t L = st.name.size(); writePod(os, L);
|
|
if (L) os.write(st.name.data(), (std::streamsize)L);
|
|
}
|
|
// v21: conflict & war (civ Step 5). Per-settlement allegiance (conquest), the active wars and the
|
|
// war RNG -- everything territory/culture derives from is otherwise recomputed on load.
|
|
writeVec(os, sSettleAllegiance);
|
|
uint64_t nw = wars.size(); writePod(os, nw);
|
|
for (const War& w : wars) {
|
|
writePod(os, w.id); writePod(os, w.attacker); writePod(os, w.defender);
|
|
writePod(os, w.startYear); writePod(os, w.warscore); writePod(os, w.battles);
|
|
}
|
|
writePod(os, sWarRng); writePod(os, sWarNextId);
|
|
// v22: diplomacy (civ Step 6). Standing realm relations (alliances / rivalries / truces).
|
|
uint64_t nd = diplomacy.size(); writePod(os, nd);
|
|
for (const DiploTie& t : diplomacy) {
|
|
writePod(os, t.a); writePod(os, t.b); writePod(os, t.attitude);
|
|
writePod(os, t.truceUntil); uint8_t k = (uint8_t)t.kind; writePod(os, k);
|
|
}
|
|
}
|
|
|
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
|
bool hasConflict, bool hasDiplo) {
|
|
Planet tmp;
|
|
tmp.drifting = drifting; // readState never serialized this flag; preserve old caller-visible behavior.
|
|
if (!tmp.readStateImpl(is, hasBiome, hasBiota, hasMoons, hasWeather, hasStorms, hasVolcanoes,
|
|
hasStatefulVolcanoes, hasGeography, hasGeoSalt, hasEcoregions,
|
|
hasSettlements, hasConflict, hasDiplo))
|
|
return false;
|
|
*this = std::move(tmp);
|
|
return true;
|
|
}
|
|
|
|
bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
|
bool hasConflict, bool hasDiplo) {
|
|
// 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
|
|
if (!hasVolcanoes || !hasStatefulVolcanoes) hasGeography = false; // geography block follows the volcano block
|
|
if (!hasGeography) hasGeoSalt = false; // salt follows the geography block
|
|
if (!hasGeoSalt) hasEcoregions = false; // ecoregions follow the v18 salt
|
|
if (!hasEcoregions) hasSettlements = false; // settlements follow the ecoregion block
|
|
if (!hasSettlements) hasConflict = false; // conflict follows the settlement block
|
|
if (!hasConflict) hasDiplo = false; // diplomacy follows the conflict 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<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. 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 had pure-function volcanoes with the old struct layout; v15 has stateful lifecycle
|
|
// volcanoes. Consume the old block so the stream stays aligned, but discard it.
|
|
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
|
if (hasVolcanoes) {
|
|
if (!hasStatefulVolcanoes) {
|
|
std::vector<LegacyVolcano> legacy;
|
|
if (!readVec(is, legacy, 100000)) return false;
|
|
readPod(is, sVolRng);
|
|
sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
|
volcanoes.clear();
|
|
if (!is) return false;
|
|
computeBiotaDensity();
|
|
return true;
|
|
}
|
|
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.built)
|
|
|| !std::isfinite(v.timer) || !std::isfinite(v.ashTimer)
|
|
|| !std::isfinite(v.ashCarry) || v.phase > 1) return false;
|
|
}
|
|
// v17: named geographic features (the atlas). Older saves load with none (regenerate on demand).
|
|
geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; sGeoSalt = 0;
|
|
sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1);
|
|
sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1);
|
|
if (hasGeography) {
|
|
uint64_t nf = 0; readPod(is, nf);
|
|
if (!is || nf > 200000) return false;
|
|
geoFeatures.resize((size_t)nf);
|
|
for (GeoFeature& f : geoFeatures) {
|
|
readPod(is, f.id);
|
|
uint8_t k = 0; readPod(is, k);
|
|
if (k > (uint8_t)FeatureKind::River) return false;
|
|
f.kind = (FeatureKind)k;
|
|
readPod(is, f.anchorCell); readPod(is, f.regionId); readPod(is, f.size);
|
|
uint64_t L = 0; readPod(is, L);
|
|
if (!is || L > 256) return false;
|
|
f.name.resize((size_t)L);
|
|
if (L) is.read(&f.name[0], (std::streamsize)L);
|
|
if (!is || f.anchorCell < 0 || f.anchorCell >= (int)cells.size()) return false;
|
|
}
|
|
// Per-cell region index arrays. Accept an empty array (geography saved before generation)
|
|
// and treat it as "all -1"; otherwise it must be exactly cell-sized + reference valid features.
|
|
auto okArr = [&](std::vector<int>& a) {
|
|
if (a.empty()) { a.assign(cells.size(), -1); return true; }
|
|
if (a.size() != cells.size()) return false;
|
|
for (int v : a) if (v < -1 || v >= (int)geoFeatures.size()) return false;
|
|
return true;
|
|
};
|
|
if (!readVec(is, sCellLand, cells.size())) return false;
|
|
if (!readVec(is, sCellWater, cells.size())) return false;
|
|
if (!readVec(is, sCellRange, cells.size())) return false;
|
|
if (!readVec(is, sCellRiver, cells.size())) return false;
|
|
if (!okArr(sCellLand) || !okArr(sCellWater) || !okArr(sCellRange) || !okArr(sCellRiver)) return false;
|
|
if (hasGeoSalt) {
|
|
readPod(is, sGeoSalt);
|
|
if (!is) return false;
|
|
sGeoRng = sGeoSalt ? sGeoSalt : (cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u);
|
|
}
|
|
}
|
|
// v19: named ecoregions. Older saves load with none (regenerate on demand).
|
|
ecoRegions.clear(); sEcoRng = cfg.seed ? (cfg.seed ^ 0xEC011FEu) : 0xEC011FEu;
|
|
sCellEcoregion.assign(cells.size(), -1);
|
|
if (hasEcoregions) {
|
|
uint64_t ne = 0; readPod(is, ne);
|
|
if (!is || ne > 200000) return false;
|
|
ecoRegions.resize((size_t)ne);
|
|
for (Ecoregion& e : ecoRegions) {
|
|
readPod(is, e.id);
|
|
uint8_t b = 0; readPod(is, b);
|
|
if (!validBiomeByte(b)) return false;
|
|
e.biome = (Biome)b;
|
|
readPod(is, e.anchorCell); readPod(is, e.regionId); readPod(is, e.size);
|
|
readPod(is, e.dominantFlora); readPod(is, e.dominantFauna); readPod(is, e.dominantFunga);
|
|
readPod(is, e.floraProductivity); readPod(is, e.faunaProductivity); readPod(is, e.fungaProductivity);
|
|
uint64_t L = 0; readPod(is, L);
|
|
if (!is || L > 256) return false;
|
|
e.name.resize((size_t)L);
|
|
if (L) is.read(&e.name[0], (std::streamsize)L);
|
|
if (!is || e.anchorCell < 0 || e.anchorCell >= (int)cells.size() || e.size < 0) return false;
|
|
auto validArch = [](int a) { return a == -1 || (a >= 0 && a < (int)biotaArchetypes().size()); };
|
|
if (!validArch(e.dominantFlora) || !validArch(e.dominantFauna) || !validArch(e.dominantFunga)) return false;
|
|
if (!std::isfinite(e.floraProductivity) || !std::isfinite(e.faunaProductivity)
|
|
|| !std::isfinite(e.fungaProductivity)) return false;
|
|
}
|
|
if (!readVec(is, sCellEcoregion, cells.size())) return false;
|
|
if (sCellEcoregion.empty()) sCellEcoregion.assign(cells.size(), -1);
|
|
if (sCellEcoregion.size() != cells.size()) return false;
|
|
for (int v : sCellEcoregion) if (v < -1 || v >= (int)ecoRegions.size()) return false;
|
|
}
|
|
// v20: civilization settlements. buildGeometry() already cleared them; older saves stay empty
|
|
// (re-seeded on demand). sCellSettlement is rebuilt from the records (not stored).
|
|
if (hasSettlements) {
|
|
uint64_t ns = 0; readPod(is, ns);
|
|
if (!is || ns > 1000000) return false;
|
|
settlements.resize((size_t)ns);
|
|
for (Settlement& st : settlements) {
|
|
readPod(is, st.id); readPod(is, st.cell); readPod(is, st.bank); readPod(is, st.regionId);
|
|
readPod(is, st.population);
|
|
uint64_t L = 0; readPod(is, L);
|
|
if (!is || L > 256) return false;
|
|
st.name.resize((size_t)L);
|
|
if (L) is.read(&st.name[0], (std::streamsize)L);
|
|
if (!is || st.cell < 0 || st.cell >= (int)cells.size() || !std::isfinite(st.population)) return false;
|
|
}
|
|
sCellSettlement.assign(cells.size(), -1);
|
|
for (int k = 0; k < (int)settlements.size(); ++k) sCellSettlement[settlements[k].cell] = k;
|
|
}
|
|
// v21: conflict & war (civ Step 5). Allegiance + active wars + war RNG (pre-v21 saves keep the
|
|
// buildGeometry defaults: no wars, everyone independent).
|
|
if (hasConflict) {
|
|
if (!readVec(is, sSettleAllegiance, 1000000)) return false;
|
|
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.assign(settlements.size(), -1);
|
|
for (int& a : sSettleAllegiance) if (a < -1 || a >= (int)settlements.size()) a = -1;
|
|
uint64_t nw = 0; readPod(is, nw);
|
|
if (!is || nw > 100000) return false;
|
|
wars.resize((size_t)nw);
|
|
for (War& w : wars) {
|
|
readPod(is, w.id); readPod(is, w.attacker); readPod(is, w.defender);
|
|
readPod(is, w.startYear); readPod(is, w.warscore); readPod(is, w.battles);
|
|
if (!is || !std::isfinite(w.warscore)) return false;
|
|
if (w.attacker < 0 || w.attacker >= (int)settlements.size()
|
|
|| w.defender < 0 || w.defender >= (int)settlements.size()) return false;
|
|
}
|
|
if (!validateWars(wars, (int)settlements.size())) return false;
|
|
readPod(is, sWarRng); readPod(is, sWarNextId);
|
|
if (!sWarRng) sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u;
|
|
if (!sWarNextId) sWarNextId = 1;
|
|
}
|
|
// v22: diplomacy (civ Step 6). Standing realm relations (pre-v22 saves keep none; they re-form live).
|
|
if (hasDiplo) {
|
|
uint64_t nd = 0; readPod(is, nd);
|
|
if (!is || nd > 1000000) return false;
|
|
diplomacy.resize((size_t)nd);
|
|
for (DiploTie& t : diplomacy) {
|
|
readPod(is, t.a); readPod(is, t.b); readPod(is, t.attitude);
|
|
readPod(is, t.truceUntil); uint8_t k = 0; readPod(is, k);
|
|
t.kind = (k <= (uint8_t)DiploKind::Rival) ? (DiploKind)k : DiploKind::Neutral;
|
|
if (!is || !std::isfinite(t.attitude)) return false;
|
|
if (t.a < 0 || t.a >= (int)settlements.size()
|
|
|| t.b < 0 || t.b >= (int)settlements.size()) return false;
|
|
}
|
|
if (!sanitizeDiplo(diplomacy, (int)settlements.size())) return false;
|
|
}
|
|
computeBiotaDensity(); // derived density scalars for the colour views
|
|
return (bool)is;
|
|
}
|