planetsim/src/sim/Planet.hpp
Jonas Reith 4cb0ef9328 Civ Step 8: cultural evolution (assimilation, conversion, schism; save v23)
Cultures stop being static one-per-continent blocs. Culture identities are
now stateful: the list is append-only (seeded once at the dawn, schism
children appended later, records frozen after creation) and the
per-settlement culture is mutable state.

- stepCulture(year) in the yearly tick (pure hashes, no RNG):
  assimilation (a conquered settlement adopts its ruler's culture, which
  drops the Step-5 revolt cultBonus -> assimilation pacifies provinces),
  border conversion (population x trade-prestige pressure; realm capitals
  exempt), schism (a far-flung coherent cluster -- typically overseas
  colonies -- breaks away as a new people with a local-bank NameGen name
  and ethos/faith re-derived from its own lands).
- computeCultures() became a pure derived refresh (seeds only when the
  list is empty); seedCultures() reproduces the old per-continent peoples
  byte-identically for the dawn and pre-v23 loads.
- Colonies inherit the founder's culture at founding; the Step-3
  mono-cultural vassalage rule is culture-matched (regionId fallback) so
  schism clusters found their own realms -> colonial independence wars.
- Per-cell culture view colours by the owning settlement's culture, so a
  conquered city keeps its colour until it assimilates.
- Save v23: culture identities + sSettleCulture + next-id counter, also in
  the step-back frames; snapshots rewind via truncate-and-replay. Pre-v23
  saves re-seed on the next refresh.
- Kind-7 world events; Cultures tab hides extinct peoples and shows a
  schism child's founding year. Knobs cult* in planet.cfg.
- New test_cultevo.cpp (30 checks); all 15 existing suites still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:02:11 +02:00

423 lines
28 KiB
C++

#pragma once
#include "Vec3.hpp"
#include "IcoSphere.hpp"
#include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig
#include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota
#include "PlanetGeography.hpp" // FeatureKind, GeoFeature
#include "PlanetCiv.hpp" // Settlement, SettleTier, CivUpdate
#include "PlanetNation.hpp" // Nation, NationTier, GovType
#include "PlanetCulture.hpp" // Culture, CultureEthos, Faith
#include "PlanetConflict.hpp" // WarEvent, ConflictUpdate (War is in PlanetTypes.hpp)
#include "PlanetTrade.hpp" // TradeKind, TradeLink
#include "PlanetEcoregions.hpp" // Ecoregion
#include <vector>
#include <memory>
#include <cstdint>
#include <string>
#include <iosfwd>
class Planet {
public:
PlanetConfig cfg;
std::vector<Cell> cells;
std::vector<Plate> plates;
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved)
std::vector<Volcano> volcanoes; // Live World: stateful lifecycle volcanoes (saved v15)
std::vector<GeoFeature> geoFeatures; // named geographic features / the atlas (saved v17+)
std::vector<Ecoregion> ecoRegions; // named ecological provinces (saved v19+)
std::vector<Settlement> settlements; // civilization: settlements placed once, grow/decline (saved v20+)
std::vector<Nation> nations; // realms grouped from settlements (derived each computeTerritory, not saved)
std::vector<Culture> cultures; // peoples/cultures, one per inhabited continent (derived, not saved)
std::vector<War> wars; // civ Step 5: active wars between realms (stateful, saved v21)
std::vector<DiploTie> diplomacy; // civ Step 6: standing realm relations (stateful, saved v22)
// Phase flag: false during Phase-1 forming (modest, original tectonics that
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
// (collision/arc uplift + isostatic persistence) so Phase 1 stays unchanged
// and tall persistent mountains only grow during drift (where erode() limits
// them). Not serialized -- the orchestrator sets it from the phase.
bool drifting = false;
void generate(const PlanetConfig& c);
double step(); // one tectonic tick; returns max |elevation change| (m) this tick
// Phase 2: stable timestep (My) from the fastest plate (CFL ~ half a cell).
double cflDtMy() const;
// Phase 2: advect plate membership + carried crust (plateId, elevation,
// oceanic, geoAge) over the fixed grid by dt My. Opening gaps become new
// young oceanic crust (spreading); overlaps subduct.
void advect(double dtMy);
// Phase 2: erode the elevation field by dt My (highs wear down, sediment
// deposits downhill in basins / below sea level) and, every seaLevelEvery
// calls, nudge seaLevel toward landFractionTarget. Crust type is untouched.
void erode(double dtMy);
// Phase 3: one hydrology tick over the fixed grid -- recompute the drainage
// network (depression-fill -> lakes, steepest-descent routing -> rivers,
// flow accumulation -> discharge) and apply mass-conserving fluvial erosion
// (stream-power incision + downstream sediment transport/deposition) to the
// elevation field by dt My. computeHydrology() does the routing only (no
// erosion) so the viewer can show rivers/lakes when paused / after load.
void hydrology(double dtMy);
void computeHydrology();
// Phase 3 (climate): compute per-cell temperature + precipitation fields from
// elevation, latitude and prevailing-wind orographic moisture transport (windward
// rain, leeward rain shadow, dry continental interiors). Derived (not saved);
// call before classifyBiomes(), which consumes these fields.
void computeClimate();
const std::vector<double>& temperature() const { return sTemp; } // deg C, annual mean
const std::vector<double>& precipitation() const { return sPrecip; } // relative units
const std::vector<double>& moisture() const { return sMoist; } // 0..1 (median land -> 0.5)
const std::vector<double>& summerTemp() const { return sTempSummer; } // deg C, warmest month
const std::vector<double>& winterTemp() const { return sTempWinter; } // deg C, coldest month
// Live World stage (PlanetLive.cpp): the slow real-time clock's derived fields.
// computeInsolation() = instantaneous solar incidence cos(sun angle), 0..1 -- the
// physical foundation for live weather + the day/night terminator. computeLiveSeason()
// = the live temperature cycling between winterTemp/summerTemp over the year (drives the
// live temperature view + the moving snow line). Both derived/not-saved; call after
// computeClimate(). dayOfYear01/timeOfDay01 are fractions in [0,1).
void computeInsolation(double dayOfYear01, double timeOfDay01);
void computeLiveSeason(double dayOfYear01);
const std::vector<double>& insolation() const { return sInsolation; } // 0..1 cos incidence
const std::vector<double>& liveTemp() const { return sLiveTemp; } // deg C, current season
// Live World sky + oceans (PlanetOcean.cpp). Sub-solar / sub-lunar directions in model
// space at a clock fraction (drive insolation, tides and the 3D sun/moons -- one source of
// truth). generateMoons() seeds 1-3 moons from a separate RNG (tectonic determinism intact).
Vec3 sunDirection(double dayOfYear01, double timeOfDay01) const;
Vec3 moonDirection(int moonIdx, double timeOfDay01, double timeDays) const;
Vec3 moonOrbitNormal(int moonIdx, double timeOfDay01) const; // orbit-plane normal (for the ring)
const std::vector<Moon>& getMoons() const { return moons; }
void generateMoons();
// Tides: equilibrium tidal height (m) per cell from the moons + sun at the given clock.
// Derived/not saved; recomputed each frame like insolation.
void computeTides(double dayOfYear01, double timeOfDay01, double timeDays);
const std::vector<double>& tide() const { return sTide; }
// Ocean surface currents (PlanetOcean.cpp): a per-ocean-cell tangent velocity from wind
// stress + Coriolis deflection + coast-following (gyres). Derived/not saved; needs sWind
// (computeClimate() computes it, then calls this and feeds warm/cold currents back into
// sTemp -- see climateCurrentFactor). Zero on land cells.
void computeOceanCurrents();
const std::vector<Vec3>& current() const { return sCurrent; }
// Weather (Live World): dynamic per-cell humidity / cloud cover / rain advanced on the live
// clock. initWeather() spins the fields up from the climatology; stepWeather(dtHours) runs
// one cycle (evaporate over warm seas -> advect along the wind -> condense into cloud, with
// orographic lift -> rain out -> dissipate). Reads sInsolation/sTemp/sWind/sUpwind/sMoist
// (computeClimate + computeInsolation set those). Saved (v10).
void initWeather();
void stepWeather(double dtHours);
const std::vector<double>& humidity() const { return sHumidity; }
const std::vector<double>& cloud() const { return sCloud; }
const std::vector<double>& rain() const { return sRain; }
const std::vector<WeatherSystem>& storms() const { return sStorms; }
// Snapshot / restore the full weather state (humidity/cloud/rain/storms/RNG) for the viewer's
// step-back undo history -- weather is an integrated path, so backward stepping restores a frame.
WeatherSnapshot captureWeather() const;
void restoreWeather(const WeatherSnapshot& s);
// Volcanoes (Live World, PlanetVolcano.cpp). placeVolcanoes() runs once on entering Live World:
// it seeds a set by tectonic context (high on young ridges, medium on borders, low elsewhere)
// from a separate RNG (tectonic determinism intact), capturing each vent's baseElev and initial
// built height. stepVolcanoes() integrates growth/dormancy/explosions forward, reasserts vent
// elevations, breaches submarine vents into islands, and injects ash cloud + local cooling.
// Saved (v15); step-back restores them via WeatherSnapshot.
void placeVolcanoes(double liveTime = 0.0);
VolcanoUpdate stepVolcanoes(double dtHours);
double volcanoBuilt(const Volcano& v) const; // m built above baseElev
double volcanoErupting(const Volcano& v) const; // 0..1 current visual eruption intensity
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
// fields (temperature + normalized precipitation). Derived + written back into
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
void classifyBiomes();
// Biota stage (flora/fauna/funga). computeBiotaDensity() builds the derived
// per-cell density scalars (0..1) each tick (like climate; not saved); call it
// after classifyBiomes(). generateBiota() does the on-demand slot/point fill of
// the discrete population into sBiota (saved) -- NOT called per tick. See
// PlanetBiota.cpp + PlanetFloraGen/FaunaGen/FungiGen.cpp.
void computeBiotaDensity();
void generateBiota();
bool biotaPopulated() const;
const std::vector<double>& floraDensity() const { return sFloraDensity; }
const std::vector<double>& faunaDensity() const { return sFaunaDensity; }
const std::vector<double>& fungaDensity() const { return sFungaDensity; }
const std::vector<CellBiota>& biota() const { return sBiota; }
// Derived hydrology fields (recomputed each route; not saved). Empty until
// the first computeHydrology()/hydrology() call.
const std::vector<double>& lakeDepth() const { return sLakeDepth; }
const std::vector<double>& discharge() const { return sDischarge; }
const std::vector<int>& flowTo() const { return sFlowTo; }
// Geography stage (the atlas, PlanetGeography.cpp). generateGeography() extracts named
// geographic features from the frozen terrain by connectivity (continents/islands, oceans/seas,
// lakes, mountain ranges/peaks, rivers) and names them with a separate RNG (tectonic determinism
// intact). Saved (v17; active reshuffle salt saved v18). The per-cell index arrays give O(1)
// "which features is this cell in".
void generateGeography(uint32_t nameSalt = 0);
// Rebuild the atlas against the current terrain and use a new naming salt, for manually
// rechecking/relabeling geography after moving between world-building phases.
void reshuffleGeography();
// Name a cell that has just become land (e.g. a volcanic island breaching the sea): join an
// adjacent existing landmass, else create + name a new Island. Returns the land feature's name
// ("" if the atlas isn't built). Appended to geoFeatures (saved). Called by the viewer on a
// volcanic island-formation event so the new island enters the atlas.
std::string nameNewLand(int cell);
bool geographyBuilt() const { return !geoFeatures.empty(); }
const std::vector<GeoFeature>& geography() const { return geoFeatures; }
const std::vector<int>& cellLand() const { return sCellLand; } // continent/island feature index (-1)
const std::vector<int>& cellWater() const { return sCellWater; } // ocean/sea/lake feature index (-1)
const std::vector<int>& cellRange() const { return sCellRange; } // mountain-range feature index (-1)
const std::vector<int>& cellRiver() const { return sCellRiver; } // river feature index (-1)
// Ecoregion atlas (PlanetEcoregions.cpp): connected ecological provinces built from current
// biome, land/water context, productivity and dominant broad biota. Saved (v19).
void generateEcoregions();
bool ecoregionsBuilt() const { return !ecoRegions.empty(); }
const std::vector<Ecoregion>& ecoregions() const { return ecoRegions; }
const std::vector<int>& cellEcoregion() const { return sCellEcoregion; } // ecoregion index (-1)
// Civilization Step 2 (PlanetCiv.cpp). computeHabitability() builds the derived per-cell
// habitability/food score (0..1; like climate, not saved). placeSettlements() seeds the fixed
// settlement set once on the best-spaced fertile cells (separate RNG; tectonic determinism intact;
// auto-builds geography/ecoregions for naming + productivity). stepCivilization() advances each
// settlement's population on the live clock toward a food-driven carrying capacity (grows / shrinks
// / is abandoned). Saved (v20); step-back restores populations via WeatherSnapshot.
void computeHabitability();
void placeSettlements();
CivUpdate stepCivilization(double dtHours, double liveTime);
// Colonization (once per sim year): a kingdom+ realm may found a new settlement on good unclaimed land
// (or an island, from a coastal member) bound to it by allegiance. Appends to `settlements`; returns
// the founding events. Deterministic (pure hashes) so step-back replays it.
std::vector<WarEvent> stepColonization(long year);
bool settlementsPlaced() const { return !settlements.empty(); }
const std::vector<int>& cellSettlement() const { return sCellSettlement; } // settlement index per cell (-1)
const std::vector<double>& habitability() const { return sHabitability; } // 0..1 per cell (derived)
// Territory & nations (PlanetNation.cpp). computeTerritory() groups settlements into realms
// (capital + vassal towns) and claims cells within each settlement's size-scaled influence range
// (wilderness frontiers between realms). Purely derived from the settlement set, so it is recomputed
// (on placement / load / each sim year), not saved -- step-back replays it as populations restore.
void computeTerritory();
bool nationsBuilt() const { return !nations.empty(); }
const std::vector<Nation>& nationList() const { return nations; }
const std::vector<int>& cellNation() const { return sCellNation; } // nation index per cell (-1 = wilderness/sea)
const std::vector<int>& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead)
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() is the DERIVED refresh:
// it seeds the cultures once (one per inhabited continent) when none exist, then only recomputes
// tallies, governments (folded into nation.name) and the per-cell culture view. Runs AFTER
// computeTerritory() (reads nations/sCellNation/sCellSettleOwner). Culture identities + the
// per-settlement culture are STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is
// the once-per-sim-year mutation pass (border conversion / assimilation / schism, pure hashes --
// no RNG touched, so step-back replays it exactly).
void computeCultures();
std::vector<WarEvent> stepCulture(long year);
bool culturesBuilt() const { return !cultures.empty(); }
const std::vector<Culture>& cultureList() const { return cultures; }
const std::vector<int>& cellCulture() const { return sCellCulture; } // culture index per cell (-1 = none)
const std::vector<int>& settleCulture() const { return sSettleCulture; } // culture index per settlement (-1 = none)
// Conflict & war (PlanetConflict.cpp; civ Step 5). stepConflict() runs once per sim year: adjacent
// realms grow hostile, declare wars, fight (casualties in frontier cities), conquer (a city's
// allegiance flips to the victor) or sack (raze) cities, and conquered provinces revolt. Stateful +
// path-dependent -> saved (v21) + snapshotted. computeTerritory() reads sSettleAllegiance so borders
// move as cities change hands. Uses a separate war RNG (tectonic determinism intact).
ConflictUpdate stepConflict(long year);
const std::vector<War>& warList() const { return wars; }
const std::vector<int>& settleAllegiance() const { return sSettleAllegiance; } // overlord capital settlement index (-1 = free)
bool realmsAtWar(int nationA, int nationB) const; // are these two nation indices in an active war?
// Diplomacy (civ Step 6). Relations are updated inside stepConflict(); these query them by nation index.
const std::vector<DiploTie>& diploList() const { return diplomacy; }
DiploKind diploBetween(int nationA, int nationB) const; // relation kind between two nation indices
bool realmsAllied(int nationA, int nationB) const; // are these two realms allied?
// Trade & economy (PlanetTrade.cpp; civ Step 7). computeTrade() links nearby settlements (sea/river/
// overland), accrues prosperity at hubs (which feeds population growth) and fills a per-cell wealth
// field. A pure derived function of settlements + geography + wars/diplomacy -- recomputed, not saved.
void computeTrade();
bool tradeBuilt() const { return !sProsperity.empty(); }
const std::vector<double>& prosperity() const { return sProsperity; } // per settlement (0..~)
const std::vector<TradeLink>& tradeLinks() const { return sTradeLinks; }
const std::vector<double>& cellWealth() const { return sCellWealth; } // per cell (heat map)
// Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined
// environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought =
// current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events.
const std::vector<double>& settlementCondition() const { return sCivCond; }
const std::vector<double>& settlementDrought() const { return sCivDrought; }
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
// Save/load the full simulation state (binary). readState rebuilds geometry
// from the saved cfg.subdivisions, so only dynamic per-cell fields are stored.
// Reloading resumes the simulation exactly (deterministic continuation).
void writeState(std::ostream& os) const;
// hasBiome: whether the stream carries the per-cell biome byte (save v4+). For
// older saves (v3) pass false -- biomes are reclassified after the cells load.
// hasBiota: whether the stream carries the biota population block (save v7+).
// hasMoons: whether the stream carries the moons block (save v9+); older saves
// synthesize moons from the seed instead. hasWeather: the weather block (save v10+);
// older saves leave weather to spin up on entering Live World.
// hasVolcanoes: whether the stream carries a volcano block (save v14+). hasStatefulVolcanoes
// means v15+ lifecycle volcanoes; v14's old pure-function block is consumed and discarded.
// hasGeography: whether the stream carries the geography/atlas block (save v17+); hasGeoSalt:
// whether the stream carries the active reshuffle salt (save v18+); hasEcoregions: whether the
// stream carries the ecoregion atlas (save v19+). Older saves regenerate on demand.
// hasSettlements: whether the stream carries the civilization settlements block (save v20+); older
// saves load with none (re-seeded on demand via the civ key). hasCultures: whether the stream
// carries the stateful culture block (save v23+); older saves re-seed one culture per continent
// on the next computeCultures().
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true,
bool hasConflict = true, bool hasDiplo = true, bool hasCultures = true);
// Helpers for rendering / info.
double cellWidthMeters() const; // approx lateral cell spacing
double minElevation() const;
double maxElevation() const;
const std::vector<int>& triIndices() const { return sphere.triIndices; }
private:
IcoSphere sphere;
uint32_t rngState = 1;
int targetLand = -1; // land-cell count to conserve during drift
uint32_t rnd();
double rndf(); // [0,1)
void buildGeometry(); // build icosphere + per-cell unit/neighbors
bool 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, bool hasCultures);
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
void assignPlates();
void seedInitialRelief();
Vec3 driftVelocity(int plateId, const Vec3& pos) const;
double oceanicBase(double age) const; // age-dependent seafloor depth
// Phase-2 plate-dynamics helpers (see PlanetConfig above).
void setPlateSpeed(Plate& p, double cmYr); // cm/yr -> driftSpeed + angSpeed
void randomizePlateDrift(Plate& p); // random axis + random speed
int acquirePlate(); // reuse a dead slot or append one
void splitPlate(int pid); // fission: cut a plate roughly in two
void maybeSplitPlates(const std::vector<int>& cnt);
void kickStalemates(const std::vector<int>& cnt);
void deleteEnclosedPlates(); // absorb plates ringed by one other
void fuseMiniPlates(); // cluster of mini plates steals + merges
void coalesceBabyPlates(); // merge connected baby cells into one id
void promoteBabyPlates(const std::vector<int>& cnt);
void adjustSeaLevel(); // nudge seaLevel toward land target
// Phase-3 hydrology helpers (see hydrology()).
void routeFlow(); // depression-fill -> lakes, flow, discharge
// Biota helpers (PlanetFloraGen/FaunaGen/FungiGen.cpp). compute*Density write the
// derived scalars; fill* draw the per-cell population (nbr = already-filled,
// same-biome neighbours, for regional consistency).
void computeFloraDensity();
void computeFaunaDensity();
void computeFungaDensity();
double neighbourhoodPrey(int i) const; // mean fauna density over i + neighbours
std::vector<Organism> fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng);
std::vector<Organism> fillFauna(int i, const std::vector<int>& nbr, uint32_t& rng);
std::vector<Organism> fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng);
int driftIter = 0; // counts advect() calls (gates periodic checks)
int erodeIter = 0; // counts erode() calls (gates sea-level control)
std::vector<int> sPrevCount; // per-plate cell count at the previous check
std::vector<int> sStaleStreak; // consecutive stuck windows per plate
std::vector<int> sFreePlateIds; // dead plate slots free for reuse
// Reusable scratch buffers for step()/erode() so they allocate nothing per tick.
std::vector<double> sStress, sBelt, sBeltNext, sDelta, sSmoothed, sOldElev, sErode;
std::vector<uint8_t> sSub, sOver, sColl;
// Phase-3 hydrology scratch (derived from elevation each routeFlow(); not saved).
std::vector<double> sFill, sLakeDepth, sDischarge;
std::vector<int> sFlowTo, sHydroOrder;
// Phase-3 climate scratch (derived each computeClimate(); not saved). sMoist is the
// 0..1-normalized precipitation the biome classifier reads. sTempSummer/sTempWinter are
// the obliquity-driven seasonal extremes around the annual mean sTemp (see Seasons).
std::vector<double> sTemp, sPrecip, sMoist, sTempSummer, sTempWinter;
std::vector<Vec3> sWind;
std::vector<int> sUpwind;
// Live World scratch (derived each frame in Live World; not saved). sInsolation is the
// instantaneous solar incidence; sLiveTemp is the temperature for the current day-of-year;
// sTide is the equilibrium tidal height (m) from the moons + sun.
std::vector<double> sInsolation, sLiveTemp, sTide;
std::vector<Vec3> sCurrent; // ocean surface current velocity (tangent; zero on land)
// Weather (Live World; saved v10). sHasWeather latches once spun up/loaded.
std::vector<double> sHumidity, sCloud, sRain;
bool sHasWeather = false;
// Moving weather systems (saved with weather state). Separate RNG keeps tectonic
// determinism intact (seeded from cfg.seed in initWeather).
std::vector<WeatherSystem> sStorms;
uint32_t sWeatherRng = 1;
uint32_t sStormNextId = 1; // monotonic id for follow-cam tracking
// Volcanoes (Live World; saved v15). Separate RNG (seeded from cfg.seed in placeVolcanoes)
// keeps tectonic determinism intact while lifecycle rolls happen during Live World.
uint32_t sVolRng = 1;
// Geography (the atlas; saved v17; active reshuffle salt saved v18). Per-cell feature index
// arrays (-1 = none) + a separate RNG/salt so naming never perturbs the tectonic stream.
std::vector<int> sCellLand, sCellWater, sCellRange, sCellRiver;
uint32_t sGeoRng = 1;
uint32_t sGeoSalt = 0;
// Ecoregions (saved v19). Per-cell ecoregion index (-1 = none) + separate RNG salt.
std::vector<int> sCellEcoregion;
uint32_t sEcoRng = 1;
// Civilization (saved v20). Per-cell settlement index (-1 = none; rebuilt on load, not saved),
// a derived habitability field, and a separate RNG so placement never perturbs tectonics.
std::vector<int> sCellSettlement;
std::vector<double> sHabitability;
std::vector<double> sCivCond, sCivDrought; // per-settlement live conditions (derived)
uint32_t sCivRng = 1;
// Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell
// (-1 = wilderness/ocean); sSettleNation: nation index per settlement.
std::vector<int> sCellNation, sSettleNation;
// Cultures (civ Step 8: stateful, saved v23). sSettleCulture: culture index per settlement --
// mutable identity state (seeded at the dawn, changed only by stepCulture()/colony founding);
// sCellCulture: the derived per-cell view (refreshed by computeCultures(), not saved).
// sCultureNextId: stable id counter for appended (schism) cultures.
std::vector<int> sCellCulture, sSettleCulture;
uint32_t sCultureNextId = 1;
void seedCultures(); // one-time seeding (dawn / pre-v23 load), PlanetCulture.cpp
// Per-cell owning settlement index (filled by computeTerritory alongside sCellNation; -1 = none).
// Reused by the trade wealth field so the heat map matches territory exactly.
std::vector<int> sCellSettleOwner;
// Trade & economy (civ Step 7; derived, not saved). Per-settlement prosperity, the trade-link list,
// and a per-cell wealth field (owning settlement's prosperity). Recomputed each sim year.
std::vector<double> sProsperity, sCellWealth;
std::vector<TradeLink> sTradeLinks;
// Conflict & war (civ Step 5; stateful, saved v21). sSettleAllegiance[s] = the overlord's capital
// settlement index if conquered, else -1 (independent). Separate war RNG + a stable war-id counter.
std::vector<int> sSettleAllegiance;
uint32_t sWarRng = 1, sWarNextId = 1;
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
std::vector<CellBiota> sBiota;
bool sHasBiota = false;
};
// Human-editable config file (key = value text). All PlanetConfig input
// parameters are written/read via one shared field table. Unknown keys ignored.
// validateConfig returns an empty string if the config is reasonable.
bool loadConfig(const std::string& path, PlanetConfig& cfg);
bool saveConfig(const std::string& path, const PlanetConfig& cfg);
std::string validateConfig(const PlanetConfig& cfg);