diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 6e0e73d..660255a 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -429,7 +429,7 @@ void Viewer::loadGame(const char* path) { is.read(reinterpret_cast(&lh), sizeof lh); } // v8: Live World clock if (ver >= 13) is.read(reinterpret_cast(&lr), sizeof lr); // v13: Live World rate if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; } - if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, 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 elapsedMy = em; settled = (st != 0); planet.drifting = settled; // resume drift boosts iff mid-drift diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index e17c3fc..d097ae5 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -15,7 +15,7 @@ // ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing). struct Viewer { // ---- 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 EVENT_LOG_MAX = 200; const char* CONFIG_PATH = "planet.cfg"; diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index 59ffa9d..50dc3ec 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -224,10 +224,17 @@ void Viewer::handleInput() { if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows 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_M) && settled) { // toggle place-name labels (the atlas) - 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_M) && settled) { // toggle / reshuffle place-name labels (the atlas) + bool reshuffle = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + if (reshuffle) { + 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_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); } diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 010834d..08cbb44 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -620,8 +620,10 @@ void Viewer::renderHUD() { y += 8; 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(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]", - 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")); + 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")); + 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 | +/-", phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off")); line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg"); diff --git a/src/sim/Planet.cpp b/src/sim/Planet.cpp index 2aad852..fab022c 100644 --- a/src/sim/Planet.cpp +++ b/src/sim/Planet.cpp @@ -54,7 +54,7 @@ void Planet::buildGeometry() { sHasBiota = false; sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World 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); sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1); volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au; diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index 7b3372a..609c8d2 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -17,7 +17,7 @@ public: std::vector plates; std::vector moons; // Live World: 1-3 natural satellites (generated + saved) std::vector volcanoes; // Live World: stateful lifecycle volcanoes (saved v15) - std::vector geoFeatures; // named geographic features / the atlas (saved v17) + std::vector geoFeatures; // named geographic features / the atlas (saved v17+) // Phase flag: false during Phase-1 forming (modest, original tectonics that // 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 // 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). The per-cell index arrays give O(1) "which features is this cell in". - void generateGeography(); + // 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 @@ -175,11 +179,13 @@ public: // 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+); older saves load - // with none (regenerated on demand). + // hasGeography: whether the stream carries the geography/atlas block (save v17+); hasGeoSalt: + // 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 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. 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) // keeps tectonic determinism intact while lifecycle rolls happen during Live World. uint32_t sVolRng = 1; - // Geography (the atlas; saved v17). Per-cell feature index arrays (-1 = none) + a separate RNG - // so naming never perturbs the tectonic stream. + // 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 sCellLand, sCellWater, sCellRange, sCellRiver; uint32_t sGeoRng = 1; + uint32_t sGeoSalt = 0; // Biota: derived density scalars (0..1; recomputed each tick, not saved) and the // on-demand discrete population (saved). sHasBiota latches once generated/loaded. diff --git a/src/sim/PlanetGeography.cpp b/src/sim/PlanetGeography.cpp index f18f475..6a81257 100644 --- a/src/sim/PlanetGeography.cpp +++ b/src/sim/PlanetGeography.cpp @@ -26,15 +26,18 @@ const char* featureKindName(FeatureKind k) { return "?"; } -void Planet::generateGeography() { +void Planet::generateGeography(uint32_t nameSalt) { const int n = (int)cells.size(); 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, // 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(); 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. 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 ^= h >> 15; h *= 0x2545F491u; h ^= h >> 13; return h ? h : 1u; @@ -69,8 +72,8 @@ void Planet::generateGeography() { return formatName(k, p); }; auto bankOf = [&](int regionId, int fallbackAnchor) { - return regionId >= 0 ? namegen::bankForRegion(cfg.seed, regionId) - : namegen::bankForRegion(cfg.seed, 1000 + fallbackAnchor); + return regionId >= 0 ? namegen::bankForRegion(atlasSeed, regionId) + : namegen::bankForRegion(atlasSeed, 1000 + fallbackAnchor); }; // The label/focus cell: the member nearest the component's (normalized) centroid direction. auto centroidCell = [&](const std::vector& comp) { @@ -115,7 +118,7 @@ void Planet::generateGeography() { int anchor = centroidCell(comp); FeatureKind k = (int)comp.size() >= cfg.geoContinentMinCells ? FeatureKind::Continent : FeatureKind::Island; 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 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 // 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 @@ -261,8 +270,11 @@ std::string Planet::nameNewLand(int cell) { 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). - int bank = namegen::bankForRegion(cfg.seed, 1000 + cell); - uint32_t seed = (cfg.seed ^ 0x6E0C12A7u) + (uint32_t)(cell * 2654435761u) + const uint32_t seedSalt = sGeoSalt * 0x9E3779B9u; + 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; auto inUse = [&](const std::string& nm) { for (const GeoFeature& f : geoFeatures) if (f.name == nm) return true; return false; diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 995693b..362638f 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -381,6 +381,7 @@ void Planet::writeState(std::ostream& os) const { 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); @@ -391,16 +392,18 @@ void Planet::writeState(std::ostream& os) const { } writeVec(os, sCellLand); writeVec(os, sCellWater); writeVec(os, sCellRange); writeVec(os, sCellRiver); + writePod(os, sGeoSalt); } bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, 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, // 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 // 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 @@ -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; } // 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); sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1); 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, 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); + } } computeBiotaDensity(); // derived density scalars for the colour views return (bool)is; diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 8465fe6..c6702ac 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -363,7 +363,7 @@ struct PlanetConfig { // --- Geography (the atlas) -- see PlanetGeography.cpp ---------------------- // 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. int geoContinentMinCells = 40; // land component >= this many cells = Continent (else Island) int geoSeaMaxCells = 60; // ocean/basin <= this many cells = Sea (else Ocean) diff --git a/test_geography.cpp b/test_geography.cpp index c68d446..6be70b0 100644 --- a/test_geography.cpp +++ b/test_geography.cpp @@ -9,7 +9,7 @@ // src/sim/PlanetGeography.cpp src/sim/PlanetIO.cpp -o /tmp/tg && /tmp/tg // // 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 "NameGen.hpp" @@ -18,6 +18,7 @@ #include #include #include +#include static int failures = 0; static void check(bool cond, const char* what) { @@ -68,6 +69,48 @@ int main() { check(continents + islands >= 1, "at least one land mass"); 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 kinds; + std::vector anchors; + std::vector 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 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"); { 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; 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); p.writeState(ss); Planet r; 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()); if (match) for (size_t i = 0; i < F.size(); ++i)