#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>(store.getArray("subBlockSelectorIndexes"), mem_fn(&Json::toUInt)); foregroundOreSelectorIndexes = transform>(store.getArray("foregroundOreSelectorIndexes"), mem_fn(&Json::toUInt)); backgroundOreSelectorIndexes = transform>(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())}, {"foregroundOreSelectorIndexes", foregroundOreSelectorIndexes.transformed(construct())}, {"backgroundOreSelectorIndexes", backgroundOreSelectorIndexes.transformed(construct())} }; } 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(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 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 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(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(r)); return layer; }); m_regionBlending = store.getFloat("regionBlending"); m_blockNoise = store.opt("blockNoise").apply(construct()); m_blendNoise = store.opt("blendNoise").apply(construct()); m_playerStartSearchRegions = store.getArray("playerStartSearchRegions").transformed(jsonToRectI); } Json WorldLayout::toJson() const { auto terrainDatabase = Root::singleton().terrainDatabase(); return JsonObject{ {"worldSize", jsonFromVec2U(m_worldSize)}, {"biomes", transform(m_biomes, [](auto const& biome) { return biome->toJson(); })}, {"terrainSelectors", transform(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()))}, {"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 const& WorldLayout::blockNoise() const { return m_blockNoise; } Maybe const& WorldLayout::blendNoise() const { return m_blendNoise; } List WorldLayout::playerStartSearchRegions() const { return m_playerStartSearchRegions; } List WorldLayout::getWeighting(int x, int y) const { List 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(0.5f - (x - xMax) / m_regionBlending, 0.0f, 1.0f); else return clamp(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 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 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 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 {} into region with layerIndex {} cellIndex {}", 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(newRegion); // Logger::info("boundaries before region insertion are {}", 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 {}", 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 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::expandRegionInLayer(WorldLayout::WorldLayer targetLayer, size_t cellIndex, int newWidth) const { struct RegionCell { int lBound; int rBound; WorldRegionPtr region; }; // auto printRegionCells = [](List const& cells) { // String output = ""; // for (auto cell : cells) // output += strf("[{} {}] ", cell.lBound, cell.rBound); // return output; // }; // Logger::info("expanding region in layer with cellIndex {} newWidth {}", cellIndex, newWidth); List 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 {}", 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 targetCells; List 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: {}\nother cells are: {}", 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 ({}) must be greater than current cell width {}!", 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: {}\nother cells are: {}", printRegionCells(targetCells), printRegionCells(otherCells)); // split any target cells that now cross the world wrap List 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 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: {}\nother cells are: {}", 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 {}", 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(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 secondaryRegions, List secondarySubRegions, Vec2F secondaryRegionSize, Vec2F subRegionSize) { WorldLayer layer; layer.yStart = yStart; List relativeRegionSizes; float totalRelativeSize = 0.0; int mix = 0; BiomeIndex primaryEnvironmentBiomeIndex = buildRegion(seed, primaryRegionParams).environmentBiomeIndex; Set spawnBiomeIndexes; auto addRegion = [&](RegionParams const& regionParams, RegionParams const& subRegionParams, Vec2F const& regionSizeRange) { WorldRegionPtr region = make_shared(buildRegion(seed, regionParams)); WorldRegionPtr subRegion = make_shared(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 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 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 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}; } }