Add geography atlas reshuffle

This commit is contained in:
Jonas Reith 2026-06-29 21:38:23 +02:00
parent 842a7b4229
commit 554878ef5e
10 changed files with 112 additions and 33 deletions

View File

@ -429,7 +429,7 @@ void Viewer::loadGame(const char* path) {
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; } if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt
cfg = planet.cfg; // adopt the loaded config cfg = planet.cfg; // adopt the loaded config
elapsedMy = em; settled = (st != 0); elapsedMy = em; settled = (st != 0);
planet.drifting = settled; // resume drift boosts iff mid-drift planet.drifting = settled; // resume drift boosts iff mid-drift

View File

@ -15,7 +15,7 @@
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing). // ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
struct Viewer { struct Viewer {
// ---- Files / save format ------------------------------------------------ // ---- Files / save format ------------------------------------------------
static constexpr uint32_t SAVE_VERSION = 17; // v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3 static constexpr uint32_t SAVE_VERSION = 18; // v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
static constexpr int EVENT_LOG_MAX = 200; static constexpr int EVENT_LOG_MAX = 200;
const char* CONFIG_PATH = "planet.cfg"; const char* CONFIG_PATH = "planet.cfg";

View File

@ -224,10 +224,17 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover
if (IsKeyPressed(KEY_V)) showVolcanoes = !showVolcanoes; // toggle volcano markers (Live World) if (IsKeyPressed(KEY_V)) showVolcanoes = !showVolcanoes; // toggle volcano markers (Live World)
if (IsKeyPressed(KEY_M) && settled) { // toggle place-name labels (the atlas) if (IsKeyPressed(KEY_M) && settled) { // toggle / reshuffle place-name labels (the atlas)
if (!planet.geographyBuilt()) planet.generateGeography(); // lazily name the world on first use bool reshuffle = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
showNames = !showNames; if (reshuffle) {
setStatus(showNames ? "Place names on" : "Place names off"); planet.reshuffleGeography(); // re-extract current geography + use different names
showNames = true;
setStatus("Place names reshuffled");
} else {
if (!planet.geographyBuilt()) planet.generateGeography(); // lazily name the world on first use
showNames = !showNames;
setStatus(showNames ? "Place names on" : "Place names off");
}
} }
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); } if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); } if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }

View File

@ -620,8 +620,10 @@ void Viewer::renderHUD() {
y += 8; y += 8;
line("hover: cell info | click tile: open detail panel | C close"); line("hover: cell info | click tile: open detail panel | C close");
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga (*6 cycles mean/summer/winter/season)"); line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga (*6 cycles mean/summer/winter/season)");
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s] | K clouds [%s] | V volcanoes [%s] | M names [%s]", line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off", showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off")); showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | Shift+M rename/recheck atlas",
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off"));
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-", line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off")); phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg"); line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");

View File

@ -54,7 +54,7 @@ void Planet::buildGeometry() {
sHasBiota = false; sHasBiota = false;
sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World
sHasWeather = false; sStorms.clear(); sHasWeather = false; sStorms.clear();
geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; sGeoSalt = 0;
sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1); sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1);
sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1); sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1);
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au; volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;

View File

@ -17,7 +17,7 @@ public:
std::vector<Plate> plates; std::vector<Plate> plates;
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved) 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<Volcano> volcanoes; // Live World: stateful lifecycle volcanoes (saved v15)
std::vector<GeoFeature> geoFeatures; // named geographic features / the atlas (saved v17) std::vector<GeoFeature> geoFeatures; // named geographic features / the atlas (saved v17+)
// Phase flag: false during Phase-1 forming (modest, original tectonics that // Phase flag: false during Phase-1 forming (modest, original tectonics that
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts // settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
@ -146,8 +146,12 @@ public:
// Geography stage (the atlas, PlanetGeography.cpp). generateGeography() extracts named // Geography stage (the atlas, PlanetGeography.cpp). generateGeography() extracts named
// geographic features from the frozen terrain by connectivity (continents/islands, oceans/seas, // 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 // lakes, mountain ranges/peaks, rivers) and names them with a separate RNG (tectonic determinism
// intact). Saved (v17). The per-cell index arrays give O(1) "which features is this cell in". // intact). Saved (v17; active reshuffle salt saved v18). The per-cell index arrays give O(1)
void generateGeography(); // "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 // 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 // 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 // ("" if the atlas isn't built). Appended to geoFeatures (saved). Called by the viewer on a
@ -175,11 +179,13 @@ public:
// older saves leave weather to spin up on entering Live World. // older saves leave weather to spin up on entering Live World.
// hasVolcanoes: whether the stream carries a volcano block (save v14+). hasStatefulVolcanoes // 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. // 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+); older saves load // hasGeography: whether the stream carries the geography/atlas block (save v17+); hasGeoSalt:
// with none (regenerated on demand). // whether the stream carries the active reshuffle salt (save v18+). Older saves load with none
// (regenerated on demand), or with a deterministic first reshuffle if only the salt is absent.
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true, bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true); bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
bool hasGeoSalt = true);
// Helpers for rendering / info. // Helpers for rendering / info.
double cellWidthMeters() const; // approx lateral cell spacing double cellWidthMeters() const; // approx lateral cell spacing
@ -265,10 +271,11 @@ private:
// Volcanoes (Live World; saved v15). Separate RNG (seeded from cfg.seed in placeVolcanoes) // Volcanoes (Live World; saved v15). Separate RNG (seeded from cfg.seed in placeVolcanoes)
// keeps tectonic determinism intact while lifecycle rolls happen during Live World. // keeps tectonic determinism intact while lifecycle rolls happen during Live World.
uint32_t sVolRng = 1; uint32_t sVolRng = 1;
// Geography (the atlas; saved v17). Per-cell feature index arrays (-1 = none) + a separate RNG // Geography (the atlas; saved v17; active reshuffle salt saved v18). Per-cell feature index
// so naming never perturbs the tectonic stream. // arrays (-1 = none) + a separate RNG/salt so naming never perturbs the tectonic stream.
std::vector<int> sCellLand, sCellWater, sCellRange, sCellRiver; std::vector<int> sCellLand, sCellWater, sCellRange, sCellRiver;
uint32_t sGeoRng = 1; uint32_t sGeoRng = 1;
uint32_t sGeoSalt = 0;
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the // Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
// on-demand discrete population (saved). sHasBiota latches once generated/loaded. // on-demand discrete population (saved). sHasBiota latches once generated/loaded.

View File

@ -26,15 +26,18 @@ const char* featureKindName(FeatureKind k) {
return "?"; return "?";
} }
void Planet::generateGeography() { void Planet::generateGeography(uint32_t nameSalt) {
const int n = (int)cells.size(); const int n = (int)cells.size();
const double sea = cfg.seaLevel; const double sea = cfg.seaLevel;
sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; sGeoSalt = nameSalt;
sGeoRng = nameSalt ? nameSalt : (cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u);
const uint32_t seedSalt = nameSalt * 0x9E3779B9u;
const uint32_t atlasSeed = cfg.seed ^ seedSalt;
const uint32_t geoSeed = (cfg.seed ^ 0x6E0C12A7u) ^ seedSalt;
// Rivers/lakes derive from the hydrology fields -- make sure they exist (routing only, no erosion, // Rivers/lakes derive from the hydrology fields -- make sure they exist (routing only, no erosion,
// so this changes no elevation and is safe to call on a finished world). // so this changes no elevation and is safe to call on a finished world).
if ((int)sDischarge.size() != n || (int)sLakeDepth.size() != n || (int)sFlowTo.size() != n) computeHydrology();
computeHydrology();
geoFeatures.clear(); geoFeatures.clear();
sCellLand.assign(n, -1); sCellWater.assign(n, -1); sCellLand.assign(n, -1); sCellWater.assign(n, -1);
@ -56,7 +59,7 @@ void Planet::generateGeography() {
}; };
// Per-feature seed: a stable hash of (world seed, kind, anchor cell) -- order-independent. // Per-feature seed: a stable hash of (world seed, kind, anchor cell) -- order-independent.
auto featSeed = [&](FeatureKind k, int anchor) -> uint32_t { auto featSeed = [&](FeatureKind k, int anchor) -> uint32_t {
uint32_t h = (cfg.seed ^ 0x6E0C12A7u) + (uint32_t)((int)k) * 0x85EBCA6Bu; uint32_t h = geoSeed + (uint32_t)((int)k) * 0x85EBCA6Bu;
h ^= (uint32_t)(anchor * 2654435761u + 0x165667B1u); h ^= (uint32_t)(anchor * 2654435761u + 0x165667B1u);
h ^= h >> 15; h *= 0x2545F491u; h ^= h >> 13; h ^= h >> 15; h *= 0x2545F491u; h ^= h >> 13;
return h ? h : 1u; return h ? h : 1u;
@ -69,8 +72,8 @@ void Planet::generateGeography() {
return formatName(k, p); return formatName(k, p);
}; };
auto bankOf = [&](int regionId, int fallbackAnchor) { auto bankOf = [&](int regionId, int fallbackAnchor) {
return regionId >= 0 ? namegen::bankForRegion(cfg.seed, regionId) return regionId >= 0 ? namegen::bankForRegion(atlasSeed, regionId)
: namegen::bankForRegion(cfg.seed, 1000 + fallbackAnchor); : namegen::bankForRegion(atlasSeed, 1000 + fallbackAnchor);
}; };
// The label/focus cell: the member nearest the component's (normalized) centroid direction. // The label/focus cell: the member nearest the component's (normalized) centroid direction.
auto centroidCell = [&](const std::vector<int>& comp) { auto centroidCell = [&](const std::vector<int>& comp) {
@ -115,7 +118,7 @@ void Planet::generateGeography() {
int anchor = centroidCell(comp); int anchor = centroidCell(comp);
FeatureKind k = (int)comp.size() >= cfg.geoContinentMinCells ? FeatureKind::Continent : FeatureKind::Island; FeatureKind k = (int)comp.size() >= cfg.geoContinentMinCells ? FeatureKind::Continent : FeatureKind::Island;
int fi = (int)geoFeatures.size(); int fi = (int)geoFeatures.size();
int bank = namegen::bankForRegion(cfg.seed, fi); int bank = namegen::bankForRegion(atlasSeed, fi);
addFeature(k, anchor, fi, (int)comp.size(), bank); // a land feature's regionId is itself addFeature(k, anchor, fi, (int)comp.size(), bank); // a land feature's regionId is itself
for (int i : comp) sCellLand[i] = fi; for (int i : comp) sCellLand[i] = fi;
} }
@ -246,6 +249,12 @@ void Planet::generateGeography() {
} }
} }
void Planet::reshuffleGeography() {
uint32_t x = sGeoSalt ? sGeoSalt : (cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u);
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
generateGeography(x ? x : 0x6E0C12A7u);
}
// Name a cell that has just become land (a volcanic island breaching the sea). Joins an adjacent // Name a cell that has just become land (a volcanic island breaching the sea). Joins an adjacent
// existing landmass if there is one; otherwise mints a new Island feature with a unique name. Returns // existing landmass if there is one; otherwise mints a new Island feature with a unique name. Returns
// the land feature's name. Appended to geoFeatures + sCellLand (saved). Uses the per-cell hash, not a // the land feature's name. Appended to geoFeatures + sCellLand (saved). Uses the per-cell hash, not a
@ -261,8 +270,11 @@ std::string Planet::nameNewLand(int cell) {
if (lf >= 0 && lf < (int)geoFeatures.size()) { sCellLand[cell] = lf; return geoFeatures[lf].name; } if (lf >= 0 && lf < (int)geoFeatures.size()) { sCellLand[cell] = lf; return geoFeatures[lf].name; }
} }
// Isolated new island: mint a unique name (dedup against existing feature names). // Isolated new island: mint a unique name (dedup against existing feature names).
int bank = namegen::bankForRegion(cfg.seed, 1000 + cell); const uint32_t seedSalt = sGeoSalt * 0x9E3779B9u;
uint32_t seed = (cfg.seed ^ 0x6E0C12A7u) + (uint32_t)(cell * 2654435761u) const uint32_t atlasSeed = cfg.seed ^ seedSalt;
const uint32_t geoSeed = (cfg.seed ^ 0x6E0C12A7u) ^ seedSalt;
int bank = namegen::bankForRegion(atlasSeed, 1000 + cell);
uint32_t seed = geoSeed + (uint32_t)(cell * 2654435761u)
+ (uint32_t)geoFeatures.size() * 40503u; + (uint32_t)geoFeatures.size() * 40503u;
auto inUse = [&](const std::string& nm) { auto inUse = [&](const std::string& nm) {
for (const GeoFeature& f : geoFeatures) if (f.name == nm) return true; return false; for (const GeoFeature& f : geoFeatures) if (f.name == nm) return true; return false;

View File

@ -381,6 +381,7 @@ void Planet::writeState(std::ostream& os) const {
writePod(os, sVolRng); writePod(os, sVolRng);
// v17: named geographic features (the atlas) + per-cell region indices. Feature records carry a // 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. // 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); uint64_t nf = geoFeatures.size(); writePod(os, nf);
for (const GeoFeature& f : geoFeatures) { for (const GeoFeature& f : geoFeatures) {
writePod(os, f.id); writePod(os, f.id);
@ -391,16 +392,18 @@ void Planet::writeState(std::ostream& os) const {
} }
writeVec(os, sCellLand); writeVec(os, sCellWater); writeVec(os, sCellLand); writeVec(os, sCellWater);
writeVec(os, sCellRange); writeVec(os, sCellRiver); writeVec(os, sCellRange); writeVec(os, sCellRiver);
writePod(os, sGeoSalt);
} }
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
bool hasGeography) { bool hasGeography, bool hasGeoSalt) {
// Save blocks are append-only by version. If a caller asks for an older prefix, // 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. // 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 (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
if (!hasWeather) { hasStorms = false; hasVolcanoes = false; } // volcano block follows the weather block if (!hasWeather) { hasStorms = false; hasVolcanoes = false; } // volcano block follows the weather block
if (!hasVolcanoes || !hasStatefulVolcanoes) hasGeography = false; // geography block follows the volcano block if (!hasVolcanoes || !hasStatefulVolcanoes) hasGeography = false; // geography block follows the volcano block
if (!hasGeography) hasGeoSalt = false; // salt follows the geography block
// Read the length-prefixed key=value config block (see writeState). A default // 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 // PlanetConfig is parsed over, so fields absent from an older save keep their
@ -529,7 +532,7 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|| !std::isfinite(v.ashCarry) || v.phase > 1) return false; || !std::isfinite(v.ashCarry) || v.phase > 1) return false;
} }
// v17: named geographic features (the atlas). Older saves load with none (regenerate on demand). // v17: named geographic features (the atlas). Older saves load with none (regenerate on demand).
geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; sGeoSalt = 0;
sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1); sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1);
sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1); sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1);
if (hasGeography) { if (hasGeography) {
@ -561,6 +564,11 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
if (!readVec(is, sCellRange, cells.size())) return false; if (!readVec(is, sCellRange, cells.size())) return false;
if (!readVec(is, sCellRiver, cells.size())) return false; if (!readVec(is, sCellRiver, cells.size())) return false;
if (!okArr(sCellLand) || !okArr(sCellWater) || !okArr(sCellRange) || !okArr(sCellRiver)) 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);
}
} }
computeBiotaDensity(); // derived density scalars for the colour views computeBiotaDensity(); // derived density scalars for the colour views
return (bool)is; return (bool)is;

View File

@ -363,7 +363,7 @@ struct PlanetConfig {
// --- Geography (the atlas) -- see PlanetGeography.cpp ---------------------- // --- Geography (the atlas) -- see PlanetGeography.cpp ----------------------
// Thresholds for extracting + naming geographic features from the frozen terrain. Generated once // Thresholds for extracting + naming geographic features from the frozen terrain. Generated once
// on a settled world (key M), saved (v17). Tune to control what counts as a continent vs island, // on a settled world (key M), saved (v17+). Tune to control what counts as a continent vs island,
// an ocean vs sea, a named mountain range / major river, and to cap label clutter. // an ocean vs sea, a named mountain range / major river, and to cap label clutter.
int geoContinentMinCells = 40; // land component >= this many cells = Continent (else Island) int geoContinentMinCells = 40; // land component >= this many cells = Continent (else Island)
int geoSeaMaxCells = 60; // ocean/basin <= this many cells = Sea (else Ocean) int geoSeaMaxCells = 60; // ocean/basin <= this many cells = Sea (else Ocean)

View File

@ -9,7 +9,7 @@
// src/sim/PlanetGeography.cpp src/sim/PlanetIO.cpp -o /tmp/tg && /tmp/tg // src/sim/PlanetGeography.cpp src/sim/PlanetIO.cpp -o /tmp/tg && /tmp/tg
// //
// Verifies: extraction (continents/oceans/ranges/rivers/lakes), per-cell membership consistency, // Verifies: extraction (continents/oceans/ranges/rivers/lakes), per-cell membership consistency,
// names non-empty/unique/deterministic, RNG isolation from tectonics, and a v17 save round-trip. // names non-empty/unique/deterministic, RNG isolation from tectonics, and a v18 save round-trip.
#include "Planet.hpp" #include "Planet.hpp"
#include "NameGen.hpp" #include "NameGen.hpp"
@ -18,6 +18,7 @@
#include <algorithm> #include <algorithm>
#include <set> #include <set>
#include <sstream> #include <sstream>
#include <vector>
static int failures = 0; static int failures = 0;
static void check(bool cond, const char* what) { static void check(bool cond, const char* what) {
@ -68,6 +69,48 @@ int main() {
check(continents + islands >= 1, "at least one land mass"); check(continents + islands >= 1, "at least one land mass");
check(oceans >= 1, "at least one ocean"); check(oceans >= 1, "at least one ocean");
std::printf("Geography: reshuffle rechecks atlas with alternate names\n");
{
Planet r; r.generate(cfg); settle(r); drift(r, 40); r.generateGeography();
std::vector<FeatureKind> kinds;
std::vector<int> anchors;
std::vector<std::string> names;
for (const GeoFeature& f : r.geography()) {
kinds.push_back(f.kind);
anchors.push_back(f.anchorCell);
names.push_back(f.name);
}
r.reshuffleGeography();
bool sameFeatures = r.geography().size() == names.size();
bool renamed = false;
if (sameFeatures) {
for (size_t i = 0; i < names.size(); ++i) {
const GeoFeature& f = r.geography()[i];
if (f.kind != kinds[i] || f.anchorCell != anchors[i]) sameFeatures = false;
if (f.name != names[i]) renamed = true;
}
}
check(sameFeatures, "reshuffle rebuilds the same current-terrain features");
check(renamed, "reshuffle uses alternate place names");
check(countKind(r, FeatureKind::Ocean) >= 1, "reshuffle rechecks oceans");
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
r.writeState(ss);
Planet loaded;
check(loaded.readState(ss), "reshuffled atlas save/load preserves v18 salt");
std::vector<std::string> loadedNames;
for (const GeoFeature& f : loaded.geography()) loadedNames.push_back(f.name);
loaded.reshuffleGeography();
bool advancedAfterLoad = loaded.geography().size() == loadedNames.size();
if (advancedAfterLoad) {
bool anyChanged = false;
for (size_t i = 0; i < loadedNames.size(); ++i)
if (loaded.geography()[i].name != loadedNames[i]) anyChanged = true;
advancedAfterLoad = anyChanged;
}
check(advancedAfterLoad, "reshuffle continues with new names after load");
}
std::printf("Geography: rebuild clears stale atlas state\n"); std::printf("Geography: rebuild clears stale atlas state\n");
{ {
Planet r; r.generate(cfg); settle(r); drift(r, 40); r.generateGeography(); Planet r; r.generate(cfg); settle(r); drift(r, 40); r.generateGeography();
@ -183,13 +226,13 @@ int main() {
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false; for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
check(terrainSame, "generateGeography never perturbs tectonic evolution"); check(terrainSame, "generateGeography never perturbs tectonic evolution");
std::printf("Geography: save v17 round-trip\n"); std::printf("Geography: save v18 round-trip\n");
{ {
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
p.writeState(ss); p.writeState(ss);
Planet r; Planet r;
bool ok = r.readState(ss, true, true, true, true, true, true, true, true); bool ok = r.readState(ss, true, true, true, true, true, true, true, true);
check(ok, "readState accepts a v17 stream"); check(ok, "readState accepts a v18 stream");
bool match = (r.geography().size() == F.size()); bool match = (r.geography().size() == F.size());
if (match) if (match)
for (size_t i = 0; i < F.size(); ++i) for (size_t i = 0; i < F.size(); ++i)