Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarWorldLayout.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/game/StarWorldLayout.cpp')
-rw-r--r--source/game/StarWorldLayout.cpp987
1 files changed, 987 insertions, 0 deletions
diff --git a/source/game/StarWorldLayout.cpp b/source/game/StarWorldLayout.cpp
new file mode 100644
index 0000000..5c6408c
--- /dev/null
+++ b/source/game/StarWorldLayout.cpp
@@ -0,0 +1,987 @@
+#include "StarWorldLayout.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarWorldGeometry.hpp"
+#include "StarAssets.hpp"
+#include "StarBiomeDatabase.hpp"
+#include "StarTerrainDatabase.hpp"
+#include "StarParallax.hpp"
+#include "StarRoot.hpp"
+
+namespace Star {
+
+WorldRegion::WorldRegion()
+ : terrainSelectorIndex(NullTerrainSelectorIndex),
+ foregroundCaveSelectorIndex(NullTerrainSelectorIndex),
+ backgroundCaveSelectorIndex(NullTerrainSelectorIndex),
+ blockBiomeIndex(NullBiomeIndex),
+ environmentBiomeIndex(NullBiomeIndex) {}
+
+WorldRegion::WorldRegion(Json const& store) {
+ terrainSelectorIndex = store.getUInt("terrainSelectorIndex");
+ foregroundCaveSelectorIndex = store.getUInt("foregroundCaveSelectorIndex");
+ backgroundCaveSelectorIndex = store.getUInt("backgroundCaveSelectorIndex");
+
+ blockBiomeIndex = store.getUInt("blockBiomeIndex");
+ environmentBiomeIndex = store.getUInt("environmentBiomeIndex");
+
+ regionLiquids.caveLiquid = store.getUInt("caveLiquid");
+ regionLiquids.caveLiquidSeedDensity = store.getFloat("caveLiquidSeedDensity");
+
+ regionLiquids.oceanLiquid = store.getUInt("oceanLiquid");
+ regionLiquids.oceanLiquidLevel = store.getInt("oceanLiquidLevel");
+
+ regionLiquids.encloseLiquids = store.getBool("encloseLiquids");
+ regionLiquids.fillMicrodungeons = store.getBool("fillMicrodungeons");
+
+ subBlockSelectorIndexes = transform<List<TerrainSelectorIndex>>(store.getArray("subBlockSelectorIndexes"), mem_fn(&Json::toUInt));
+ foregroundOreSelectorIndexes = transform<List<TerrainSelectorIndex>>(store.getArray("foregroundOreSelectorIndexes"), mem_fn(&Json::toUInt));
+ backgroundOreSelectorIndexes = transform<List<TerrainSelectorIndex>>(store.getArray("backgroundOreSelectorIndexes"), mem_fn(&Json::toUInt));
+}
+
+Json WorldRegion::toJson() const {
+ return JsonObject{
+ {"terrainSelectorIndex", terrainSelectorIndex},
+ {"foregroundCaveSelectorIndex", foregroundCaveSelectorIndex},
+ {"backgroundCaveSelectorIndex", backgroundCaveSelectorIndex},
+
+ {"blockBiomeIndex", blockBiomeIndex},
+ {"environmentBiomeIndex", environmentBiomeIndex},
+
+ {"caveLiquid", regionLiquids.caveLiquid},
+ {"caveLiquidSeedDensity", regionLiquids.caveLiquidSeedDensity},
+
+ {"oceanLiquid", regionLiquids.oceanLiquid},
+ {"oceanLiquidLevel", regionLiquids.oceanLiquidLevel},
+
+ {"encloseLiquids", regionLiquids.encloseLiquids},
+ {"fillMicrodungeons", regionLiquids.fillMicrodungeons},
+
+ {"subBlockSelectorIndexes", subBlockSelectorIndexes.transformed(construct<Json>())},
+ {"foregroundOreSelectorIndexes", foregroundOreSelectorIndexes.transformed(construct<Json>())},
+ {"backgroundOreSelectorIndexes", backgroundOreSelectorIndexes.transformed(construct<Json>())}
+ };
+}
+
+WorldLayout::BlockNoise WorldLayout::BlockNoise::build(Json const& config, uint64_t seed) {
+ BlockNoise blockNoise;
+ blockNoise.horizontalNoise = PerlinF(config.get("horizontalNoise"), staticRandomU64(seed, "HorizontalNoise"));
+ blockNoise.verticalNoise = PerlinF(config.get("verticalNoise"), staticRandomU64(seed, "VerticalNoise"));
+ blockNoise.xNoise = PerlinF(config.get("noise"), staticRandomU64(seed, "XNoise"));
+ blockNoise.yNoise = PerlinF(config.get("noise"), staticRandomU64(seed, "yNoise"));
+ return blockNoise;
+}
+
+WorldLayout::BlockNoise::BlockNoise() {}
+
+WorldLayout::BlockNoise::BlockNoise(Json const& store) {
+ horizontalNoise = PerlinF(store.get("horizontalNoise"));
+ verticalNoise = PerlinF(store.get("verticalNoise"));
+ xNoise = PerlinF(store.get("xNoise"));
+ yNoise = PerlinF(store.get("yNoise"));
+}
+
+Json WorldLayout::BlockNoise::toJson() const {
+ return JsonObject{
+ {"horizontalNoise", horizontalNoise.toJson()},
+ {"verticalNoise", verticalNoise.toJson()},
+ {"xNoise", xNoise.toJson()},
+ {"yNoise", yNoise.toJson()},
+ };
+}
+
+Vec2I WorldLayout::BlockNoise::apply(Vec2I const& input, Vec2U const& worldSize) const {
+ float angle = (input[0] / (float)worldSize[0]) * 2 * Constants::pi;
+ float xc = std::sin(angle) / (2 * Constants::pi) * worldSize[0];
+ float zc = std::cos(angle) / (2 * Constants::pi) * worldSize[0];
+
+ Vec2I noisePos = Vec2I(
+ floor(input[0] + horizontalNoise.get(input[1]) + xNoise.get(xc, input[1], zc)),
+ floor(input[1] + verticalNoise.get(xc, zc) + yNoise.get(xc, input[1], zc))
+ );
+ noisePos[1] = clamp<int>(noisePos[1], 0, worldSize[1]);
+
+ return noisePos;
+}
+
+WorldLayout WorldLayout::buildTerrestrialLayout(TerrestrialWorldParameters const& terrestrialParameters, uint64_t seed) {
+ auto& root = Root::singleton();
+ auto assets = root.assets();
+ auto terrainDatabase = root.terrainDatabase();
+ auto biomeDatabase = root.biomeDatabase();
+
+ RandomSource randSource(seed);
+
+ WorldLayout layout;
+ layout.m_worldSize = terrestrialParameters.worldSize;
+
+ auto addLayer = [&](TerrestrialWorldParameters::TerrestrialLayer const& terrestrialLayer) {
+ RegionParams primaryRegionParams = {
+ terrestrialLayer.layerBaseHeight,
+ terrestrialParameters.threatLevel,
+ terrestrialLayer.primaryRegion.biome,
+ terrestrialLayer.primaryRegion.blockSelector,
+ terrestrialLayer.primaryRegion.fgCaveSelector,
+ terrestrialLayer.primaryRegion.bgCaveSelector,
+ terrestrialLayer.primaryRegion.fgOreSelector,
+ terrestrialLayer.primaryRegion.bgOreSelector,
+ terrestrialLayer.primaryRegion.subBlockSelector,
+ {
+ terrestrialLayer.primaryRegion.caveLiquid,
+ terrestrialLayer.primaryRegion.caveLiquidSeedDensity,
+ terrestrialLayer.primaryRegion.oceanLiquid,
+ terrestrialLayer.primaryRegion.oceanLiquidLevel,
+ terrestrialLayer.primaryRegion.encloseLiquids,
+ terrestrialLayer.primaryRegion.fillMicrodungeons
+ }
+ };
+
+ RegionParams primarySubRegionParams = {
+ terrestrialLayer.layerBaseHeight,
+ terrestrialParameters.threatLevel,
+ terrestrialLayer.primarySubRegion.biome,
+ terrestrialLayer.primarySubRegion.blockSelector,
+ terrestrialLayer.primarySubRegion.fgCaveSelector,
+ terrestrialLayer.primarySubRegion.bgCaveSelector,
+ terrestrialLayer.primarySubRegion.fgOreSelector,
+ terrestrialLayer.primarySubRegion.bgOreSelector,
+ terrestrialLayer.primarySubRegion.subBlockSelector,
+ {
+ terrestrialLayer.primarySubRegion.caveLiquid,
+ terrestrialLayer.primarySubRegion.caveLiquidSeedDensity,
+ terrestrialLayer.primarySubRegion.oceanLiquid,
+ terrestrialLayer.primarySubRegion.oceanLiquidLevel,
+ terrestrialLayer.primarySubRegion.encloseLiquids,
+ terrestrialLayer.primarySubRegion.fillMicrodungeons
+ }
+ };
+
+ List<RegionParams> secondaryRegions;
+ for (auto const& secondaryRegion : terrestrialLayer.secondaryRegions) {
+ RegionParams secondaryRegionParams = {
+ terrestrialLayer.layerBaseHeight,
+ terrestrialParameters.threatLevel,
+ secondaryRegion.biome,
+ secondaryRegion.blockSelector,
+ secondaryRegion.fgCaveSelector,
+ secondaryRegion.bgCaveSelector,
+ secondaryRegion.fgOreSelector,
+ secondaryRegion.bgOreSelector,
+ secondaryRegion.subBlockSelector,
+ {
+ secondaryRegion.caveLiquid,
+ secondaryRegion.caveLiquidSeedDensity,
+ secondaryRegion.oceanLiquid,
+ secondaryRegion.oceanLiquidLevel,
+ secondaryRegion.encloseLiquids,
+ secondaryRegion.fillMicrodungeons
+ }
+ };
+
+ secondaryRegions.append(secondaryRegionParams);
+ }
+
+ List<RegionParams> secondarySubRegions;
+ for (auto const& secondarySubRegion : terrestrialLayer.secondarySubRegions) {
+ RegionParams secondarySubRegionParams = {
+ terrestrialLayer.layerBaseHeight,
+ terrestrialParameters.threatLevel,
+ secondarySubRegion.biome,
+ secondarySubRegion.blockSelector,
+ secondarySubRegion.fgCaveSelector,
+ secondarySubRegion.bgCaveSelector,
+ secondarySubRegion.fgOreSelector,
+ secondarySubRegion.bgOreSelector,
+ secondarySubRegion.subBlockSelector,
+ {
+ secondarySubRegion.caveLiquid,
+ secondarySubRegion.caveLiquidSeedDensity,
+ secondarySubRegion.oceanLiquid,
+ secondarySubRegion.oceanLiquidLevel,
+ secondarySubRegion.encloseLiquids,
+ secondarySubRegion.fillMicrodungeons
+ }
+ };
+
+ secondarySubRegions.append(secondarySubRegionParams);
+ }
+
+ layout.addLayer(seed,
+ terrestrialLayer.layerMinHeight,
+ terrestrialLayer.layerBaseHeight,
+ terrestrialParameters.primaryBiome,
+ primaryRegionParams,
+ primarySubRegionParams,
+ secondaryRegions,
+ secondarySubRegions,
+ terrestrialLayer.secondaryRegionSizeRange,
+ terrestrialLayer.subRegionSizeRange);
+ };
+
+ addLayer(terrestrialParameters.coreLayer);
+ for (auto const& undergroundLayer : reverseIterate(terrestrialParameters.undergroundLayers))
+ addLayer(undergroundLayer);
+
+ addLayer(terrestrialParameters.subsurfaceLayer);
+ addLayer(terrestrialParameters.surfaceLayer);
+ addLayer(terrestrialParameters.atmosphereLayer);
+ addLayer(terrestrialParameters.spaceLayer);
+
+ layout.m_regionBlending = terrestrialParameters.blendSize;
+ if (terrestrialParameters.blockNoiseConfig)
+ layout.m_blockNoise = BlockNoise::build(terrestrialParameters.blockNoiseConfig, seed);
+ if (terrestrialParameters.blendNoiseConfig)
+ layout.m_blendNoise = PerlinF(terrestrialParameters.blendNoiseConfig, staticRandomU64(seed, "BlendNoise"));
+
+ layout.finalize(terrestrialParameters.skyColoring.mainColor);
+
+ return layout;
+}
+
+WorldLayout WorldLayout::buildAsteroidsLayout(AsteroidsWorldParameters const& asteroidParameters, uint64_t seed) {
+ auto assets = Root::singleton().assets();
+
+ RandomSource randSource(seed);
+
+ auto asteroidsConfig = assets->json("/asteroids_worlds.config");
+ auto asteroidTerrainConfig = randSource.randFrom(asteroidsConfig.get("terrains").toArray());
+ auto emptyTerrainConfig = asteroidsConfig.get("emptyTerrain");
+
+ WorldLayout layout;
+ layout.m_worldSize = asteroidParameters.worldSize;
+
+ RegionParams asteroidRegion{
+ (int)asteroidParameters.worldSize[1] / 2,
+ asteroidParameters.threatLevel,
+ asteroidParameters.asteroidBiome,
+ asteroidTerrainConfig.getString("terrainSelector"),
+ asteroidTerrainConfig.getString("caveSelector"),
+ asteroidTerrainConfig.getString("bgCaveSelector"),
+ asteroidTerrainConfig.getString("oreSelector"),
+ asteroidTerrainConfig.getString("oreSelector"),
+ asteroidTerrainConfig.getString("subBlockSelector"),
+ {EmptyLiquidId, 0.0f, EmptyLiquidId, 0, false, false}
+ };
+
+ RegionParams emptyRegion{
+ (int)asteroidParameters.worldSize[1] / 2,
+ asteroidParameters.threatLevel,
+ asteroidParameters.asteroidBiome,
+ emptyTerrainConfig.getString("terrainSelector"),
+ emptyTerrainConfig.getString("caveSelector"),
+ emptyTerrainConfig.getString("bgCaveSelector"),
+ emptyTerrainConfig.getString("oreSelector"),
+ emptyTerrainConfig.getString("oreSelector"),
+ emptyTerrainConfig.getString("subBlockSelector"),
+ {EmptyLiquidId, 0.0f, EmptyLiquidId, 0, false, false}
+ };
+
+ layout.addLayer(seed, 0, emptyRegion);
+ layout.addLayer(seed, asteroidParameters.asteroidBottomLevel, asteroidRegion);
+ layout.addLayer(seed, asteroidParameters.asteroidTopLevel, emptyRegion);
+
+ layout.m_regionBlending = asteroidParameters.blendSize;
+ layout.m_blockNoise = asteroidsConfig.opt("blockNoise").apply(bind(&BlockNoise::build, _1, seed));
+
+ layout.m_playerStartSearchRegions.append(RectI(0, asteroidParameters.asteroidBottomLevel, asteroidParameters.worldSize[0], asteroidParameters.asteroidTopLevel));
+
+ layout.finalize(Color::Black);
+
+ return layout;
+}
+
+WorldLayout WorldLayout::buildFloatingDungeonLayout(FloatingDungeonWorldParameters const& floatingDungeonParameters, uint64_t seed) {
+ auto assets = Root::singleton().assets();
+ auto biomeDatabase = Root::singleton().biomeDatabase();
+
+ RandomSource randSource(seed);
+
+ WorldLayout layout;
+ layout.m_worldSize = floatingDungeonParameters.worldSize;
+
+ RegionParams biomeRegion{
+ (int)floatingDungeonParameters.dungeonSurfaceHeight,
+ floatingDungeonParameters.threatLevel,
+ floatingDungeonParameters.biome,
+ {}, {}, {}, {}, {}, {},
+ {EmptyLiquidId, 0.0f, EmptyLiquidId, 0, false, false}
+ };
+
+ layout.addLayer(seed, 0, biomeRegion);
+ if (floatingDungeonParameters.biome)
+ biomeDatabase->biomeSkyColoring(*floatingDungeonParameters.biome, seed);
+ else
+ layout.finalize(Color::Black);
+
+ return layout;
+}
+
+WorldLayout::WorldLayout() : m_regionBlending(0.0f) {}
+
+WorldLayout::WorldLayout(Json const& store) : WorldLayout() {
+ auto terrainDatabase = Root::singleton().terrainDatabase();
+
+ m_worldSize = jsonToVec2U(store.get("worldSize"));
+
+ m_biomes = store.getArray("biomes").transformed([](Json const& json) {
+ return BiomeConstPtr(make_shared<Biome>(json));
+ });
+
+ m_terrainSelectors = store.getArray("terrainSelectors").transformed([terrainDatabase](Json const& v) {
+ return TerrainSelectorConstPtr(terrainDatabase->loadSelector(v));
+ });
+
+ m_layers = store.getArray("layers").transformed([](Json const& l) {
+ WorldLayer layer;
+ layer.yStart = l.getInt("yStart");
+
+ for (auto const& b : l.getArray("boundaries"))
+ layer.boundaries.append(b.toInt());
+
+ for (auto const& r : l.getArray("cells"))
+ layer.cells.append(make_shared<WorldRegion>(r));
+
+ return layer;
+ });
+
+ m_regionBlending = store.getFloat("regionBlending");
+ m_blockNoise = store.opt("blockNoise").apply(construct<BlockNoise>());
+ m_blendNoise = store.opt("blendNoise").apply(construct<PerlinF>());
+
+ m_playerStartSearchRegions = store.getArray("playerStartSearchRegions").transformed(jsonToRectI);
+}
+
+Json WorldLayout::toJson() const {
+ auto terrainDatabase = Root::singleton().terrainDatabase();
+
+ return JsonObject{
+ {"worldSize", jsonFromVec2U(m_worldSize)},
+ {"biomes", transform<JsonArray>(m_biomes, [](auto const& biome) {
+ return biome->toJson();
+ })},
+ {"terrainSelectors", transform<JsonArray>(m_terrainSelectors, [terrainDatabase](auto const& selector) {
+ return terrainDatabase->storeSelector(selector);
+ })},
+ {"layers", m_layers.transformed([](WorldLayer const& layer) -> Json {
+ return JsonObject{
+ {"yStart", layer.yStart},
+ {"boundaries", JsonArray::from(layer.boundaries.transformed(construct<Json>()))},
+ {"cells", JsonArray::from(layer.cells.transformed(mem_fn(&WorldRegion::toJson)))}
+ };
+ })},
+ {"regionBlending", m_regionBlending},
+ {"blockNoise", m_blockNoise.apply(mem_fn(&BlockNoise::toJson)).value()},
+ {"blendNoise", m_blendNoise.apply(mem_fn(&PerlinF::toJson)).value()},
+ {"playerStartSearchRegions", JsonArray::from(m_playerStartSearchRegions.transformed(jsonFromRectI))}
+ };
+}
+
+Maybe<WorldLayout::BlockNoise> const& WorldLayout::blockNoise() const {
+ return m_blockNoise;
+}
+
+Maybe<PerlinF> const& WorldLayout::blendNoise() const {
+ return m_blendNoise;
+}
+
+List<RectI> WorldLayout::playerStartSearchRegions() const {
+ return m_playerStartSearchRegions;
+}
+
+List<WorldLayout::RegionWeighting> WorldLayout::getWeighting(int x, int y) const {
+ List<RegionWeighting> weighting;
+ WorldGeometry geometry(m_worldSize);
+
+ auto cellWeighting = [&](WorldLayer const& layer, size_t cellIndex, int x) -> float {
+ int xMin = 0;
+ if (cellIndex > 0)
+ xMin = layer.boundaries[cellIndex - 1];
+
+ int xMax = m_worldSize[0];
+ if (cellIndex < layer.boundaries.size())
+ xMax = layer.boundaries[cellIndex];
+
+ if (x > (xMin + xMax) / 2.0f)
+ return clamp<float>(0.5f - (x - xMax) / m_regionBlending, 0.0f, 1.0f);
+ else
+ return clamp<float>(0.5f - (xMin - x) / m_regionBlending, 0.0f, 1.0f);
+ };
+
+ auto addLayerWeighting = [&](WorldLayer const& layer, int x, float weightFactor) {
+ if (layer.cells.empty())
+ return;
+
+ size_t innerCellIndex;
+ int innerCellXValue;
+ tie(innerCellIndex, innerCellXValue) = findContainingCell(layer, x);
+ float innerCellWeight = cellWeighting(layer, innerCellIndex, innerCellXValue);
+
+ size_t leftCellIndex;
+ int leftCellXValue;
+ tie(leftCellIndex, leftCellXValue) = leftCell(layer, innerCellIndex, innerCellXValue);
+ float leftCellWeight = cellWeighting(layer, leftCellIndex, leftCellXValue);
+
+ size_t rightCellIndex;
+ int rightCellXValue;
+ tie(rightCellIndex, rightCellXValue) = rightCell(layer, innerCellIndex, innerCellXValue);
+ float rightCellWeight = cellWeighting(layer, rightCellIndex, rightCellXValue);
+
+ float totalWeight = innerCellWeight + leftCellWeight + rightCellWeight;
+ if (totalWeight <= 0.0f)
+ return;
+
+ innerCellWeight *= weightFactor / totalWeight;
+ leftCellWeight *= weightFactor / totalWeight;
+ rightCellWeight *= weightFactor / totalWeight;
+
+ if (innerCellWeight > 0.0f)
+ weighting.append(RegionWeighting{innerCellWeight, innerCellXValue, layer.cells[innerCellIndex].get()});
+
+ if (leftCellWeight > 0.0f)
+ weighting.append(RegionWeighting{leftCellWeight, leftCellXValue, layer.cells[leftCellIndex].get()});
+
+ if (rightCellWeight > 0.0f)
+ weighting.append(RegionWeighting{rightCellWeight, rightCellXValue, layer.cells[rightCellIndex].get()});
+ };
+
+ auto yi = std::lower_bound(m_layers.begin(), m_layers.end(), y, [](WorldLayer const& layer, int y) {
+ return layer.yStart < y;
+ });
+
+ if (yi == m_layers.end() || yi->yStart != y) {
+ if (yi == m_layers.begin())
+ return {};
+ else
+ --yi;
+ }
+
+ if (y - yi->yStart < (m_regionBlending / 2)) {
+ if (yi == m_layers.begin()) {
+ addLayerWeighting(*yi, x, 1.0f);
+ } else {
+ auto ypi = yi;
+ --ypi;
+
+ float yWeight = 0.5f + (y - yi->yStart) / m_regionBlending;
+ addLayerWeighting(*yi, x, yWeight);
+ addLayerWeighting(*ypi, x, 1.0f - yWeight);
+ }
+ } else {
+ auto yni = yi;
+ ++yni;
+ if (yni == m_layers.end()) {
+ addLayerWeighting(*yi, x, 1.0f);
+ } else if (y <= yni->yStart - (m_regionBlending / 2)) {
+ addLayerWeighting(*yi, x, 1.0f);
+ } else {
+ float yWeight = 0.5f - (yni->yStart - y) / m_regionBlending;
+ addLayerWeighting(*yi, x, 1.0f - yWeight);
+ addLayerWeighting(*yni, x, yWeight);
+ }
+ }
+
+ // Need to return weighting in order of greatest to least
+ sort(weighting, [](RegionWeighting const& lhs, RegionWeighting const& rhs) {
+ return lhs.weight > rhs.weight;
+ });
+
+ return weighting;
+}
+
+List<RectI> WorldLayout::previewAddBiomeRegion(Vec2I const& position, int width) const {
+ auto layerAndCell = findLayerAndCell(position[0], position[1]);
+ auto targetLayer = m_layers[layerAndCell.first];
+ auto targetRegion = targetLayer.cells[layerAndCell.second];
+
+ int insertX = position[0] > 0 ? position[0] : 1;
+
+ // need a dummy region to expand
+ std::shared_ptr<WorldRegion> dummyRegion;
+
+ targetLayer.boundaries.insertAt(layerAndCell.second, insertX);
+ targetLayer.cells.insertAt(layerAndCell.second, dummyRegion);
+
+ targetLayer.boundaries.insertAt(layerAndCell.second, insertX - 1);
+ targetLayer.cells.insertAt(layerAndCell.second, targetRegion);
+
+ auto expandResult = expandRegionInLayer(targetLayer, layerAndCell.second + 1, width);
+
+ return expandResult.second;
+}
+
+List<RectI> WorldLayout::previewExpandBiomeRegion(Vec2I const& position, int width) const {
+ auto layerAndCell = findLayerAndCell(position[0], position[1]);
+ auto targetLayer = m_layers[layerAndCell.first];
+
+ auto expandResult = expandRegionInLayer(targetLayer, layerAndCell.second, width);
+
+ return expandResult.second;
+}
+
+String WorldLayout::setLayerEnvironmentBiome(Vec2I const& position) {
+ auto layerAndCell = findLayerAndCell(position[0], position[1]);
+ auto targetLayer = m_layers[layerAndCell.first];
+ auto targetBiomeIndex = targetLayer.cells[layerAndCell.second]->blockBiomeIndex;
+
+ for (size_t i = 0; i < targetLayer.cells.size(); ++i)
+ targetLayer.cells[i]->environmentBiomeIndex = targetBiomeIndex;
+
+ m_layers[layerAndCell.first] = targetLayer;
+
+ return getBiome(targetBiomeIndex)->baseName;
+}
+
+void WorldLayout::addBiomeRegion(
+ TerrestrialWorldParameters const& terrestrialParameters,
+ uint64_t seed,
+ Vec2I const& position,
+ String biomeName,
+ String const& subBlockSelector,
+ int width) {
+
+ auto layerAndCell = findLayerAndCell(position[0], position[1]);
+
+ // Logger::info("inserting biome %s into region with layerIndex %s cellIndex %s", biomeName, layerAndCell.first, layerAndCell.second);
+
+ auto targetLayer = m_layers[layerAndCell.first];
+
+ // do this annoying dance to figure out which terrestrial layer we're in, so
+ // we can extract the base height
+ TerrestrialWorldParameters::TerrestrialLayer terrestrialLayer = terrestrialParameters.coreLayer;
+ auto checkLayer = [targetLayer, &terrestrialLayer](TerrestrialWorldParameters::TerrestrialLayer const& layer) {
+ if (layer.layerMinHeight == targetLayer.yStart)
+ terrestrialLayer = layer;
+ };
+ for (auto undergroundLayer : terrestrialParameters.undergroundLayers)
+ checkLayer(undergroundLayer);
+ checkLayer(terrestrialParameters.subsurfaceLayer);
+ checkLayer(terrestrialParameters.surfaceLayer);
+ checkLayer(terrestrialParameters.atmosphereLayer);
+ checkLayer(terrestrialParameters.spaceLayer);
+
+ // build a new region using the biomeName and the parameters from the target region
+ auto targetRegion = targetLayer.cells[layerAndCell.second];
+
+ WorldRegion newRegion;
+ newRegion.terrainSelectorIndex = targetRegion->terrainSelectorIndex;
+ newRegion.foregroundCaveSelectorIndex = targetRegion->foregroundCaveSelectorIndex;
+ newRegion.backgroundCaveSelectorIndex = targetRegion->backgroundCaveSelectorIndex;
+ newRegion.foregroundOreSelectorIndexes = targetRegion->foregroundOreSelectorIndexes;
+ newRegion.backgroundOreSelectorIndexes = targetRegion->backgroundOreSelectorIndexes;
+ newRegion.regionLiquids = targetRegion->regionLiquids;
+
+ auto biomeDatabase = Root::singleton().biomeDatabase();
+
+ auto newBiome = biomeDatabase->createBiome(biomeName, staticRandomU64(seed, "BiomeSeed"), terrestrialLayer.layerBaseHeight, terrestrialParameters.threatLevel);
+
+ auto oldBiome = getBiome(targetRegion->blockBiomeIndex);
+
+ newBiome->ores = oldBiome->ores;
+
+ // build new sub block selectors; this is the only region-level property that needs to be
+ // newly constructed for the biome
+
+ TerrainSelectorParameters baseSelectorParameters;
+ baseSelectorParameters.worldWidth = m_worldSize[0];
+ baseSelectorParameters.baseHeight = terrestrialLayer.layerBaseHeight;
+
+ auto terrainDatabase = Root::singleton().terrainDatabase();
+ for (size_t i = 0; i < newBiome->subBlocks.size(); ++i) {
+ auto selector = terrainDatabase->createNamedSelector(subBlockSelector, baseSelectorParameters.withSeed(staticRandomU64(seed, i, "subBlocks")));
+ newRegion.subBlockSelectorIndexes.append(registerTerrainSelector(selector));
+ }
+
+ newRegion.environmentBiomeIndex = targetRegion->environmentBiomeIndex;
+ newRegion.blockBiomeIndex = registerBiome(newBiome);
+
+ WorldRegionPtr newRegionPtr = make_shared<WorldRegion>(newRegion);
+
+ // Logger::info("boundaries before region insertion are %s", targetLayer.boundaries);
+
+ // handle case where insert x position is exactly at world wrap
+ int insertX = position[0] > 0 ? position[0] : 1;
+
+ // insert the new region boundary
+ targetLayer.boundaries.insertAt(layerAndCell.second, insertX);
+ targetLayer.cells.insertAt(layerAndCell.second, newRegionPtr);
+
+ // insert the left side of the (now split) target region
+ targetLayer.boundaries.insertAt(layerAndCell.second, insertX - 1);
+ targetLayer.cells.insertAt(layerAndCell.second, targetRegion);
+
+ // Logger::info("boundaries after region insertion are %s", targetLayer.boundaries);
+
+ // expand the cell to the desired size
+ auto expandResult = expandRegionInLayer(targetLayer, layerAndCell.second + 1, width);
+
+ // update the layer in the template
+ m_layers[layerAndCell.first] = expandResult.first;
+}
+
+void WorldLayout::expandBiomeRegion(Vec2I const& position, int newWidth) {
+ auto layerAndCell = findLayerAndCell(position[0], position[1]);
+
+ auto targetLayer = m_layers[layerAndCell.first];
+
+ auto expandResult = expandRegionInLayer(targetLayer, layerAndCell.second, newWidth);
+
+ m_layers[layerAndCell.first] = expandResult.first;
+}
+
+pair<size_t, size_t> WorldLayout::findLayerAndCell(int x, int y) const {
+ // find the target layer
+ size_t targetLayerIndex;
+ for (size_t i = 0; i < m_layers.size(); ++i) {
+ if (m_layers[i].yStart < y)
+ targetLayerIndex = i;
+ else
+ break;
+ }
+
+ auto targetLayer = m_layers[targetLayerIndex];
+
+ auto targetCell = findContainingCell(targetLayer, x);
+
+ return {targetLayerIndex, targetCell.first};
+}
+
+WorldLayout::WorldLayer::WorldLayer() : yStart(0) {}
+
+pair<WorldLayout::WorldLayer, List<RectI>> WorldLayout::expandRegionInLayer(WorldLayout::WorldLayer targetLayer, size_t cellIndex, int newWidth) const {
+ struct RegionCell {
+ int lBound;
+ int rBound;
+ WorldRegionPtr region;
+ };
+
+ // auto printRegionCells = [](List<RegionCell> const& cells) {
+ // String output = "";
+ // for (auto cell : cells)
+ // output += strf("[%s %s] ", cell.lBound, cell.rBound);
+ // return output;
+ // };
+
+ // Logger::info("expanding region in layer with cellIndex %s newWidth %s", cellIndex, newWidth);
+
+ List<RectI> regionRects;
+
+ if (targetLayer.cells.size() == 1) {
+ Logger::info("Cannot expand region as it already fills the layer");
+ return {targetLayer, regionRects};
+ }
+
+ // Logger::info("boundaries before expansion are %s", targetLayer.boundaries);
+
+ // TODO: this is a messy way to get the top of the layer, but maybe it's ok
+ int layerTop = (int)m_worldSize[1];
+ for (size_t i = 0; i < m_layers.size(); ++i) {
+ if (m_layers[i].yStart == targetLayer.yStart && m_layers.size() > i + 1) {
+ layerTop = m_layers[i + 1].yStart;
+ break;
+ }
+ }
+
+ // if the region is going to cover the full layer width, this is much simpler
+ if (newWidth == (int)m_worldSize[0]) {
+ targetLayer.cells = {targetLayer.cells[cellIndex]};
+ targetLayer.boundaries = {};
+
+ regionRects.append(RectI{0, targetLayer.yStart, (int)m_worldSize[0], layerTop});
+ } else {
+ auto targetRegion = targetLayer.cells[cellIndex];
+
+ // convert cells and boundaries into something more tractable
+ List<RegionCell> targetCells;
+ List<RegionCell> otherCells;
+
+ int lastBoundary = 0;
+ size_t lastCellIndex = targetLayer.cells.size() - 1;
+ for (size_t i = 0; i <= lastCellIndex; ++i) {
+ int nextBoundary = i == lastCellIndex ? (int)m_worldSize[0] : targetLayer.boundaries[i];
+ if (i == cellIndex ||
+ (i == 0 && cellIndex == lastCellIndex && targetLayer.cells[i] == targetRegion) ||
+ (cellIndex == 0 && i == lastCellIndex && targetLayer.cells[i] == targetRegion))
+
+ targetCells.append(RegionCell{lastBoundary, nextBoundary, targetLayer.cells[i]});
+ else
+ otherCells.append(RegionCell{lastBoundary, nextBoundary, targetLayer.cells[i]});
+ lastBoundary = nextBoundary;
+ }
+
+ // Logger::info("before expansion:\ntarget cells are: %s\nother cells are: %s", printRegionCells(targetCells), printRegionCells(otherCells));
+
+ starAssert(targetCells.size() > 0);
+ starAssert(targetCells.size() < 3);
+
+ // check the current width to see how much (if any) to expand
+ int currentWidth = 0;
+ for (auto regionCell : targetCells)
+ currentWidth += (regionCell.rBound - regionCell.lBound);
+
+ if (currentWidth >= newWidth) {
+ Logger::info("New cell width (%s) must be greater than current cell width %s!", newWidth, currentWidth);
+ return {targetLayer, regionRects};
+ }
+
+ // expand the leftmost cell to the right and the rightmost cell to the left (they may be the same cell)
+ int expandRight = ceil(0.5 * (newWidth - currentWidth));
+ int expandLeft = floor(0.5 * (newWidth - currentWidth));
+
+ // build the rects for the areas NEWLY covered by the region; these don't need to be wrapped because
+ // they'll be split when they're consumed
+ regionRects.append(RectI{targetCells[0].rBound, targetLayer.yStart, targetCells[0].rBound + expandRight, layerTop});
+ regionRects.append(RectI{targetCells[targetCells.size() - 1].lBound - expandLeft, targetLayer.yStart, targetCells[targetCells.size() - 1].lBound, layerTop});
+
+ targetCells[0].rBound += expandRight;
+ targetCells[targetCells.size() - 1].lBound -= expandLeft;
+
+ // Logger::info("after expansion:\ntarget cells are: %s\nother cells are: %s", printRegionCells(targetCells), printRegionCells(otherCells));
+
+ // split any target cells that now cross the world wrap
+ List<RegionCell> wrappedTargetCells;
+ for (auto cell : targetCells) {
+ if (cell.lBound < 0) {
+ wrappedTargetCells.append(RegionCell{0, cell.rBound, cell.region});
+ wrappedTargetCells.append(RegionCell{(int)m_worldSize[0] + cell.lBound, (int)m_worldSize[0], cell.region});
+ } else if (cell.rBound > (int)m_worldSize[0]) {
+ wrappedTargetCells.append(RegionCell{cell.lBound, (int)m_worldSize[0], cell.region});
+ wrappedTargetCells.append(RegionCell{0, cell.rBound - (int)m_worldSize[0], cell.region});
+ } else {
+ wrappedTargetCells.append(cell);
+ }
+ }
+
+ targetCells = wrappedTargetCells;
+
+ // modify/delete any overlapped cells
+ for (auto targetCell : targetCells) {
+ List<RegionCell> newOtherCells;
+ for (auto otherCell : otherCells) {
+ bool rInside = otherCell.rBound <= targetCell.rBound && otherCell.rBound >= targetCell.lBound;
+ bool lInside = otherCell.lBound <= targetCell.rBound && otherCell.lBound >= targetCell.lBound;
+ if (rInside && lInside)
+ continue;
+ else if (rInside)
+ newOtherCells.append(RegionCell{otherCell.lBound, targetCell.lBound, otherCell.region});
+ else if (lInside)
+ newOtherCells.append(RegionCell{targetCell.rBound, otherCell.rBound, otherCell.region});
+ else
+ newOtherCells.append(otherCell);
+ }
+ otherCells = newOtherCells;
+ }
+
+ // Logger::info("after de-overlapping:\ntarget cells are: %s\nother cells are: %s", printRegionCells(targetCells), printRegionCells(otherCells));
+
+ // combine lists and sort
+ otherCells.appendAll(targetCells);
+ otherCells.sort([](RegionCell const& a, RegionCell const& b) { return a.rBound < b.rBound; });
+
+ // convert back into cells and boundaries
+ targetLayer.cells.clear();
+ targetLayer.boundaries.clear();
+ for (size_t i = 0; i < otherCells.size(); ++i) {
+ targetLayer.cells.append(otherCells[i].region);
+ if (i < otherCells.size() - 1)
+ targetLayer.boundaries.append(otherCells[i].rBound);
+ }
+ }
+
+ // Logger::info("boundaries after expansion are %s", targetLayer.boundaries);
+
+ return {targetLayer, regionRects};
+}
+
+BiomeIndex WorldLayout::registerBiome(BiomeConstPtr biome) {
+ size_t foundIndex = m_biomes.indexOf(biome);
+ if (foundIndex != NPos)
+ return foundIndex + 1;
+
+ m_biomes.append(biome);
+ return m_biomes.size();
+}
+
+TerrainSelectorIndex WorldLayout::registerTerrainSelector(TerrainSelectorConstPtr terrainSelector) {
+ size_t foundIndex = m_terrainSelectors.indexOf(terrainSelector);
+ if (foundIndex != NPos)
+ return foundIndex + 1;
+
+ m_terrainSelectors.append(terrainSelector);
+ return m_terrainSelectors.size();
+}
+
+WorldRegion WorldLayout::buildRegion(uint64_t seed, RegionParams const& regionParams) {
+ auto terrainDatabase = Root::singleton().terrainDatabase();
+ auto biomeDatabase = Root::singleton().biomeDatabase();
+
+ WorldRegion region;
+
+ TerrainSelectorParameters baseSelectorParameters;
+ baseSelectorParameters.worldWidth = m_worldSize[0];
+ baseSelectorParameters.baseHeight = regionParams.baseHeight;
+
+ TerrainSelectorParameters terrainSelectorParameters = baseSelectorParameters.withSeed(staticRandomU64(seed, "Terrain"));
+ TerrainSelectorParameters foregroundCaveSelectorParameters = baseSelectorParameters.withSeed(staticRandomU64(seed, "ForegroundCaveSeed"));
+ TerrainSelectorParameters backgroundCaveSelectorParameters = baseSelectorParameters.withSeed(staticRandomU64(seed, "BackgroundCave"));
+
+ if (regionParams.terrainSelector)
+ region.terrainSelectorIndex = registerTerrainSelector(terrainDatabase->createNamedSelector(*regionParams.terrainSelector, terrainSelectorParameters));
+ if (regionParams.fgCaveSelector)
+ region.foregroundCaveSelectorIndex = registerTerrainSelector(terrainDatabase->createNamedSelector(*regionParams.fgCaveSelector, foregroundCaveSelectorParameters));
+ if (regionParams.bgCaveSelector)
+ region.backgroundCaveSelectorIndex = registerTerrainSelector(terrainDatabase->createNamedSelector(*regionParams.bgCaveSelector, backgroundCaveSelectorParameters));
+
+ if (regionParams.biomeName) {
+ auto biome = biomeDatabase->createBiome(*regionParams.biomeName, staticRandomU64(seed, "BiomeSeed"), regionParams.baseHeight, regionParams.threatLevel);
+
+ if (regionParams.subBlockSelector) {
+ for (size_t i = 0; i < biome->subBlocks.size(); ++i) {
+ auto selector = terrainDatabase->createNamedSelector(*regionParams.subBlockSelector, terrainSelectorParameters.withSeed(staticRandomU64(seed, i, "subBlocks")));
+ region.subBlockSelectorIndexes.append(registerTerrainSelector(selector));
+ }
+ }
+
+ for (auto const& p : enumerateIterator(biome->ores)) {
+ auto oreSelectorTerrainParameters = terrainSelectorParameters.withCommonality(p.first.second);
+
+ if (regionParams.fgOreSelector) {
+ auto fgSelector = terrainDatabase->createNamedSelector(*regionParams.fgOreSelector, oreSelectorTerrainParameters.withSeed(staticRandomU64(seed, p.second, "FGOreSelector")));
+ region.foregroundOreSelectorIndexes.append(registerTerrainSelector(fgSelector));
+ }
+
+ if (regionParams.bgOreSelector) {
+ auto bgSelector = terrainDatabase->createNamedSelector(*regionParams.bgOreSelector, oreSelectorTerrainParameters.withSeed(staticRandomU64(seed, p.second, "BGOreSelector")));
+ region.backgroundOreSelectorIndexes.append(registerTerrainSelector(bgSelector));
+ }
+ }
+
+ region.blockBiomeIndex = registerBiome(biome);
+ region.environmentBiomeIndex = region.blockBiomeIndex;
+ }
+
+ region.regionLiquids = regionParams.regionLiquids;
+
+ return region;
+}
+
+void WorldLayout::addLayer(uint64_t seed, int yStart, RegionParams regionParams) {
+ WorldLayer layer;
+ layer.yStart = yStart;
+
+ WorldRegionPtr region = make_shared<WorldRegion>(buildRegion(seed, regionParams));
+ layer.cells.append(region);
+
+ m_layers.append(layer);
+}
+
+void WorldLayout::addLayer(uint64_t seed, int yStart, int yBase, String const& primaryBiome,
+ RegionParams primaryRegionParams, RegionParams primarySubRegionParams,
+ List<RegionParams> secondaryRegions, List<RegionParams> secondarySubRegions,
+ Vec2F secondaryRegionSize, Vec2F subRegionSize) {
+ WorldLayer layer;
+ layer.yStart = yStart;
+
+ List<float> relativeRegionSizes;
+ float totalRelativeSize = 0.0;
+ int mix = 0;
+
+ BiomeIndex primaryEnvironmentBiomeIndex = buildRegion(seed, primaryRegionParams).environmentBiomeIndex;
+
+ Set<BiomeIndex> spawnBiomeIndexes;
+
+ auto addRegion = [&](RegionParams const& regionParams, RegionParams const& subRegionParams, Vec2F const& regionSizeRange) {
+ WorldRegionPtr region = make_shared<WorldRegion>(buildRegion(seed, regionParams));
+ WorldRegionPtr subRegion = make_shared<WorldRegion>(buildRegion(seed, subRegionParams));
+ if (!Root::singleton().assets()->json("/terrestrial_worlds.config:useSecondaryEnvironmentBiomeIndex").toBool())
+ region->environmentBiomeIndex = primaryEnvironmentBiomeIndex;
+ subRegion->environmentBiomeIndex = region->environmentBiomeIndex;
+
+ if (regionParams.biomeName == primaryBiome)
+ spawnBiomeIndexes.add(region->blockBiomeIndex);
+ if (subRegionParams.biomeName == primaryBiome)
+ spawnBiomeIndexes.add(subRegion->blockBiomeIndex);
+
+ layer.cells.append(region);
+ layer.cells.append(subRegion);
+ layer.cells.append(region);
+
+ float regionRelativeSize = staticRandomFloatRange(regionSizeRange[0], regionSizeRange[1], seed, ++mix, yStart);
+ float subRegionRelativeSize = staticRandomFloatRange(subRegionSize[0], subRegionSize[1], seed, ++mix, yStart);
+ totalRelativeSize += regionRelativeSize;
+
+ if (subRegionRelativeSize >= 1.0f)
+ throw StarException("Relative size of subRegion must be less than 1.0!");
+
+ subRegionRelativeSize *= regionRelativeSize;
+ regionRelativeSize -= subRegionRelativeSize;
+
+ relativeRegionSizes.append(regionRelativeSize / 2);
+ relativeRegionSizes.append(subRegionRelativeSize);
+ relativeRegionSizes.append(regionRelativeSize / 2);
+ };
+
+ // construct list of region cells and relative sizes
+ addRegion(primaryRegionParams, primarySubRegionParams, Vec2F(1, 1));
+ for (size_t i = 0; i < secondaryRegions.size(); ++i)
+ addRegion(secondaryRegions[i], secondarySubRegions[i], secondaryRegionSize);
+
+ // construct boundaries based on normalized sizes
+ int nextBoundary = staticRandomI32Range(0, m_worldSize[0] - 1, seed, yStart, "LayerOffset");
+ layer.boundaries.append(nextBoundary);
+ for (size_t i = 0; i + 1 < relativeRegionSizes.size(); ++i) {
+ int regionSize = (int)m_worldSize[0] * (relativeRegionSizes[i] / totalRelativeSize);
+ nextBoundary += regionSize;
+ layer.boundaries.append(nextBoundary);
+ }
+
+ // wrap cells + boundaries
+ while (layer.boundaries.last() > (int)m_worldSize[0]) {
+ layer.cells.prepend(layer.cells.takeLast());
+ layer.boundaries.prepend(layer.boundaries.takeLast() - m_worldSize[0]);
+ }
+ layer.cells.prepend(layer.cells.last());
+
+ int yRange = Root::singleton().assets()->json("/world_template.config:playerStartSearchYRange").toInt();
+ int i = 0;
+ int lastBoundary = 0;
+ for (auto region : layer.cells) {
+ int nextBoundary = i < (int)layer.boundaries.size() ? layer.boundaries[i] : m_worldSize[0];
+ if (spawnBiomeIndexes.contains(region->blockBiomeIndex))
+ m_playerStartSearchRegions.append(RectI(lastBoundary, std::max(0, yBase - yRange), nextBoundary, std::min((int)m_worldSize[1], yBase + yRange)));
+ lastBoundary = nextBoundary;
+ i++;
+ }
+
+ m_layers.append(layer);
+}
+
+void WorldLayout::finalize(Color mainSkyColor) {
+ sort(m_layers, [](WorldLayer const& a, WorldLayer const& b) { return a.yStart < b.yStart; });
+
+ // Post-process all parallaxes
+ for (auto const& biome : m_biomes) {
+ if (biome->parallax)
+ biome->parallax->fadeToSkyColor(mainSkyColor);
+ }
+}
+
+pair<size_t, int> WorldLayout::findContainingCell(WorldLayer const& layer, int x) const {
+ x = WorldGeometry(m_worldSize).xwrap(x);
+ auto xi = std::lower_bound(layer.boundaries.begin(), layer.boundaries.end(), x);
+ return {xi - layer.boundaries.begin(), x};
+}
+
+pair<size_t, int> WorldLayout::leftCell(WorldLayer const& layer, size_t cellIndex, int x) const {
+ if (cellIndex == 0)
+ return {layer.cells.size() - 1, x + (int)m_worldSize[0]};
+ else
+ return {cellIndex - 1, x};
+}
+
+pair<size_t, int> WorldLayout::rightCell(WorldLayer const& layer, size_t cellIndex, int x) const {
+ if (cellIndex >= layer.cells.size() - 1)
+ return {0, x - (int)m_worldSize[0]};
+ else
+ return {cellIndex + 1, x};
+}
+
+}