#include "StarWorldGeneration.hpp" #include "StarWorldServer.hpp" #include "StarMaterialItem.hpp" #include "StarMaterialDatabase.hpp" #include "StarNpcDatabase.hpp" #include "StarMonsterDatabase.hpp" #include "StarNpc.hpp" #include "StarBiome.hpp" #include "StarSky.hpp" #include "StarWorldTemplate.hpp" #include "StarBiomePlacement.hpp" #include "StarWireEntity.hpp" #include "StarItemDrop.hpp" #include "StarLogging.hpp" #include "StarRoot.hpp" #include "StarItemDatabase.hpp" #include "StarProjectileDatabase.hpp" #include "StarProjectile.hpp" #include "StarObjectDatabase.hpp" #include "StarObject.hpp" #include "StarContainerObject.hpp" #include "StarMonster.hpp" #include "StarEntityMap.hpp" #include "StarPlant.hpp" #include "StarLiquidsDatabase.hpp" #include "StarStagehand.hpp" #include "StarVehicleDatabase.hpp" namespace Star { static int const PlantAdjustmentLimit = 2; LiquidWorld::LiquidWorld(WorldServer* world) { m_worldServer = world; auto& root = Root::singleton(); m_liquidsDatabase = root.liquidsDatabase(); m_materialDatabase = root.materialDatabase(); } Vec2I LiquidWorld::uniqueLocation(Vec2I const& location) const { return m_worldServer->geometry().xwrap(location); } float LiquidWorld::drainLevel(Vec2I const& location) const { if (location[1] > m_worldServer->worldTemplate()->undergroundLevel()) { auto const& tile = m_worldServer->getServerTile(location); if (!m_materialDatabase->blocksLiquidFlow(tile.background)) { auto const& belowTile = m_worldServer->getServerTile(location + Vec2I(0, -1)); if (m_materialDatabase->blocksLiquidFlow(belowTile.background) || m_materialDatabase->blocksLiquidFlow(belowTile.foreground) || belowTile.liquid.source) return m_liquidsDatabase->backgroundDrain(); } } return 0.0f; } CellularLiquidCell LiquidWorld::cell(Vec2I const& location) const { auto const& tile = m_worldServer->getServerTile(location); if (m_materialDatabase->blocksLiquidFlow(tile.foreground)) { return CellularLiquidCollisionCell(); } else { if (tile.liquid.source) return CellularLiquidSourceCell{tile.liquid.liquid, tile.liquid.pressure}; else if (tile.liquid.liquid != EmptyLiquidId) return CellularLiquidFlowCell{tile.liquid.liquid, tile.liquid.level, tile.liquid.pressure}; else return CellularLiquidFlowCell{{}, 0.0f, 0.0f}; } } void LiquidWorld::setFlow(Vec2I const& location, CellularLiquidFlowCell const& flow) { if (flow.liquid) { m_worldServer->setLiquid(location, *flow.liquid, flow.level, flow.pressure); auto const& tile = m_worldServer->getServerTile(location); if (auto materialInteraction = m_materialDatabase->liquidMaterialInteraction(*flow.liquid, tile.background)) { if (!materialInteraction->topOnly && tile.liquid.level >= materialInteraction->consumeLiquid) { if (auto modifyTile = m_worldServer->modifyServerTile(location)) { modifyTile->liquid.take(materialInteraction->consumeLiquid); modifyTile->background = materialInteraction->transformTo; m_worldServer->activateLiquidLocation(location); } } } if (auto modInteraction = m_materialDatabase->liquidModInteraction(*flow.liquid, tile.backgroundMod)) { if (!modInteraction->topOnly && tile.liquid.level >= modInteraction->consumeLiquid) { if (auto modifyTile = m_worldServer->modifyServerTile(location)) { modifyTile->liquid.take(modInteraction->consumeLiquid); modifyTile->backgroundMod = modInteraction->transformTo; m_worldServer->activateLiquidLocation(location); } } } } else { m_worldServer->setLiquid(location, EmptyLiquidId, 0.0f, 0.0f); } } void LiquidWorld::liquidInteraction(Vec2I const& a, LiquidId aLiquid, Vec2I const& b, LiquidId bLiquid) { auto handleInteraction = [this](Vec2I const& target, Maybe interaction) { if (interaction) { if (interaction->isLeft()) { m_worldServer->modifyTile(target, PlaceMaterial{TileLayer::Foreground, interaction->left(), 0}, false); } else { auto liquidLevel = m_worldServer->liquidLevel(target); m_worldServer->setLiquid(target, interaction->right(), liquidLevel.level, liquidLevel.level); } } }; handleInteraction(a, m_liquidsDatabase->interact(aLiquid, bLiquid)); handleInteraction(b, m_liquidsDatabase->interact(bLiquid, aLiquid)); } void LiquidWorld::liquidCollision(Vec2I const& liquidPos, LiquidId liquidId, Vec2I const& blockPos) { auto const& blockTile = m_worldServer->getServerTile(blockPos); if (auto materialInteraction = m_materialDatabase->liquidMaterialInteraction(liquidId, blockTile.foreground)) { if ((!materialInteraction->topOnly || liquidPos[1] > blockPos[1]) && m_worldServer->liquidLevel(liquidPos).level >= materialInteraction->consumeLiquid) { auto modifyLiquidTile = m_worldServer->modifyServerTile(liquidPos); auto modifyBlockTile = m_worldServer->modifyServerTile(blockPos); if (modifyLiquidTile && modifyBlockTile) { modifyLiquidTile->liquid.take(materialInteraction->consumeLiquid); modifyBlockTile->foreground = materialInteraction->transformTo; if (!m_materialDatabase->isMultiColor(materialInteraction->transformTo)) modifyBlockTile->foregroundColorVariant = DefaultMaterialColorVariant; m_worldServer->activateLiquidLocation(liquidPos); } } } if (auto modInteraction = m_materialDatabase->liquidModInteraction(liquidId, blockTile.foregroundMod)) { if ((!modInteraction->topOnly || liquidPos[1] > blockPos[1]) && m_worldServer->liquidLevel(liquidPos).level >= modInteraction->consumeLiquid) { auto modifyLiquidTile = m_worldServer->modifyServerTile(liquidPos); auto modifyBlockTile = m_worldServer->modifyServerTile(blockPos); if (modifyLiquidTile && modifyBlockTile) { modifyLiquidTile->liquid.take(modInteraction->consumeLiquid); modifyBlockTile->foregroundMod = modInteraction->transformTo; m_worldServer->activateLiquidLocation(liquidPos); } } } } FallingBlocksWorld::FallingBlocksWorld(WorldServer* w) : m_worldServer(w), m_materialDatabase(Root::singleton().materialDatabase()) {} FallingBlockType FallingBlocksWorld::blockType(Vec2I const& pos) { auto const& tile = m_worldServer->getServerTile(pos, true); if (tile.rootSource) { return FallingBlockType::Immovable; } if (tile.foreground == EmptyMaterialId) { if (m_worldServer->tileIsOccupied(pos, TileLayer::Foreground)) return FallingBlockType::Immovable; else return FallingBlockType::Open; } else if (m_materialDatabase->isCascadingFallingMaterial(tile.foreground)) { return FallingBlockType::Cascading; } else if (m_materialDatabase->isFallingMaterial(tile.foreground)) { return FallingBlockType::Falling; } else { return FallingBlockType::Immovable; } } void FallingBlocksWorld::moveBlock(Vec2I const& from, Vec2I const& to) { auto fromTile = m_worldServer->modifyServerTile(from, true); auto toTile = m_worldServer->modifyServerTile(to, true); if (!fromTile || !toTile) return; if (m_worldServer->isTileProtected(to)) { for (auto drop : m_worldServer->destroyBlock(TileLayer::Foreground, from, true, true)) m_worldServer->addEntity(ItemDrop::createRandomizedDrop(drop, Vec2F(to))); } else { toTile->foreground = fromTile->foreground; toTile->foregroundMod = NoModId; toTile->foregroundHueShift = fromTile->foregroundHueShift; toTile->foregroundColorVariant = fromTile->foregroundColorVariant; toTile->updateCollision(m_materialDatabase->materialCollisionKind(toTile->foreground)); fromTile->foreground = EmptyMaterialId; fromTile->foregroundMod = NoModId; fromTile->updateCollision(CollisionKind::None); m_worldServer->requestGlobalBreakCheck(); } } DungeonGeneratorWorld::DungeonGeneratorWorld(WorldServer* worldServer, bool markForActivation) : m_worldServer(worldServer), m_markForActivation(markForActivation) {} WorldGeometry DungeonGeneratorWorld::getWorldGeometry() const { return m_worldServer->geometry(); } void DungeonGeneratorWorld::markRegion(RectI const& region) { if (!m_markForActivation) return; Logger::debug("Marking {} as dungeon region", region); m_worldServer->signalRegion(region); m_worldServer->activateLiquidRegion(region); } void DungeonGeneratorWorld::markTerrain(PolyF const& region) { if (!m_markForActivation) return; Logger::debug("Marking poly as dungeon terrain region: {}", region); m_worldServer->worldTemplate()->addCustomTerrainRegion(region); } void DungeonGeneratorWorld::markSpace(PolyF const& region) { if (!m_markForActivation) return; Logger::debug("Marking poly as dungeon space region: {}", region); m_worldServer->worldTemplate()->addCustomSpaceRegion(region); } void DungeonGeneratorWorld::setForegroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) { if (ServerTile* tile = m_worldServer->modifyServerTile(position)) { m_worldServer->modifyLiquid(position, EmptyLiquidId, 0); tile->foreground = material; tile->foregroundHueShift = hueshift; tile->foregroundColorVariant = colorVariant; tile->foregroundMod = NoModId; tile->foregroundModHueShift = MaterialHue(); tile->collision = Root::singleton().materialDatabase()->materialCollisionKind(tile->foreground); tile->collisionCacheDirty = true; } } void DungeonGeneratorWorld::setBackgroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) { if (ServerTile* tile = m_worldServer->modifyServerTile(position)) { m_worldServer->modifyLiquid(position, EmptyLiquidId, 0); tile->background = material; tile->backgroundHueShift = hueshift; tile->backgroundColorVariant = colorVariant; tile->backgroundMod = NoModId; tile->backgroundModHueShift = MaterialHue(); } } void DungeonGeneratorWorld::placeObject(Vec2I const& pos, String const& objectName, Star::Direction direction, Json const& parameters) { m_worldServer->signalRegion(RectI::withSize(pos, {1, 1})); auto objectDatabase = Root::singleton().objectDatabase(); if (auto object = objectDatabase->createForPlacement(m_worldServer, objectName, pos, direction, parameters)) m_worldServer->addEntity(object); else Logger::warn("Failed to place dungeon object: {} direction: {} position: {}", objectName, (int)direction, pos); } void DungeonGeneratorWorld::placeVehicle(Vec2F const& pos, String const& vehicleName, Json const& parameters) { m_worldServer->signalRegion(RectI::withSize(Vec2I(pos), {1, 1})); auto vehicleDatabase = Root::singleton().vehicleDatabase(); auto vehicle = vehicleDatabase->create(vehicleName, parameters.opt().value(JsonObject{}).set("persistent", true)); vehicle->setPosition(pos); m_worldServer->addEntity(vehicle); } void DungeonGeneratorWorld::placeSurfaceBiomeItems(Vec2I const& pos) { List surfaceItems = m_worldServer->worldTemplate()->potentialBiomeItemsAt(pos[0], pos[1]).surfaceBiomeItems; placeBiomeItems(pos, surfaceItems); } void DungeonGeneratorWorld::placeBiomeTree(Vec2I const& pos) { if (auto biome = m_worldServer->worldTemplate()->blockBiome(pos[0], pos[1])) { m_worldServer->signalRegion(RectI::withSize(pos, {1, 1})); auto seed = m_worldServer->worldTemplate()->seedFor(pos[0], pos[1]); if (auto treeVariant = biome->surfacePlaceables.firstTreeType()) placePlant(Root::singleton().plantDatabase()->createPlant(*treeVariant, seed), pos); } } // yay, copy paste coding, kyren WILL kill me void DungeonGeneratorWorld::placePlant(PlantPtr const& plant, Vec2I const& position) { if (!plant) return; auto spaces = plant->spaces(); auto roots = plant->roots(); auto const& primaryRoot = plant->primaryRoot(); auto background = m_worldServer->getServerTile(position).background; bool adjustBackground = background == EmptyMaterialId || background == NullMaterialId; auto withinAdjustment = [=](Vec2I const& pos) { return PlantAdjustmentLimit - std::abs(pos[0]) > 0 && PlantAdjustmentLimit - std::abs(pos[1]) > 0; }; // Bail out if we don't have at least one free space, and root in the primary // root position, or if we're in a dungeon region. auto primaryTile = m_worldServer->getServerTile(position); auto rootTile = m_worldServer->getServerTile(position + primaryRoot); if (isConnectableMaterial(primaryTile.foreground) || !isConnectableMaterial(rootTile.foreground)) return; // First bail out if we can't fit anything we're not adjusting for (auto space : spaces) { Vec2I pspace = space + position; if (withinAdjustment(space) && !m_worldServer->atTile(pspace).empty()) return; // Bail out if we hit a different plant's root tile, or if we're not in the // adjustment space and we hit a non-empty tile. auto tile = m_worldServer->getServerTile(pspace); if (tile.rootSource || (!withinAdjustment(space) && !(tile.foreground == EmptyMaterialId || tile.foreground == NullMaterialId))) return; } // Check all the roots outside of the adjustment limit for (auto root : roots) { root += position; if (!withinAdjustment(root) && !isConnectableMaterial(m_worldServer->getServerTile(root).foreground)) return; } // Clear all the necessary blocks within the adjustment limit for (auto space : spaces) { if (!withinAdjustment(space)) continue; space += position; if (auto tile = m_worldServer->modifyServerTile(space)) { if (isConnectableMaterial(tile->foreground)) *tile = primaryTile; if (adjustBackground) tile->background = EmptyMaterialId; tile->collision = CollisionKind::None; tile->collision = Root::singleton().materialDatabase()->materialCollisionKind(tile->foreground); tile->collisionCacheDirty = true; } else { return; } } // Make all the root blocks a real material based on the primary root. for (auto root : roots) { root += position; if (auto tile = m_worldServer->modifyServerTile(root)) { if (!isRealMaterial(tile->foreground)) { *tile = rootTile; tile->collision = Root::singleton().materialDatabase()->materialCollisionKind(tile->foreground); tile->collisionCacheDirty = true; } } else { return; } } plant->setTilePosition(position); m_worldServer->addEntity(plant); return; } void DungeonGeneratorWorld::placeBiomeItems(Vec2I const& pos, List& potentialItems) { m_worldServer->signalRegion(RectI::withSize(pos, {1, 1})); sort(potentialItems); for (auto const& placement : potentialItems) { auto seed = m_worldServer->worldTemplate()->seedFor(placement.position[0], placement.position[1]); if (placement.item.is()) { auto& grass = placement.item.get(); placePlant(Root::singleton().plantDatabase()->createPlant(grass, seed), placement.position); } else if (placement.item.is()) { auto& bush = placement.item.get(); placePlant(Root::singleton().plantDatabase()->createPlant(bush, seed), placement.position); } else if (placement.item.is()) { auto& treePair = placement.item.get(); TreeVariant treeVariant; if (seed % 2 == 0) treeVariant = treePair.first; else treeVariant = treePair.second; placePlant(Root::singleton().plantDatabase()->createPlant(treeVariant, seed), placement.position); } else if (placement.item.is()) { auto& objectPool = placement.item.get(); auto direction = seed % 2 ? Direction::Left : Direction::Right; auto objectPair = objectPool.select(seed); if (auto object = Root::singleton().objectDatabase()->createForPlacement( m_worldServer, objectPair.first, placement.position, direction, objectPair.second)) m_worldServer->addEntity(object); } else if (placement.item.is()) { auto& treasureBoxSet = placement.item.get(); auto direction = seed % 2 ? Direction::Left : Direction::Right; if (auto treasureContainer = Root::singleton().treasureDatabase()->createTreasureChest(m_worldServer, treasureBoxSet, placement.position, direction, seed)) m_worldServer->addEntity(treasureContainer); } } } void DungeonGeneratorWorld::addDrop(Vec2F const& position, ItemDescriptor const& item) { m_worldServer->addEntity(ItemDrop::createRandomizedDrop(item, position)); } void DungeonGeneratorWorld::spawnNpc(Vec2F const& position, Json const& parameters) { auto kind = parameters.getString("kind"); if (kind.equals("npc", String::CaseInsensitive)) { auto npcDatabase = Root::singleton().npcDatabase(); uint64_t seed = parameters.getUInt("seed", Random::randu64()); String species = parameters.getString("species"); String typeName = parameters.getString("typeName", "default"); JsonObject uniqueParameters = parameters.getObject("parameters", {}); if (!uniqueParameters.contains("persistent")) uniqueParameters["persistent"] = true; auto npc = npcDatabase->createNpc(npcDatabase->generateNpcVariant(species, typeName, m_worldServer->threatLevel(), seed, uniqueParameters)); npc->setPosition(position - npc->feetOffset()); m_worldServer->addEntity(npc); } else if (kind.equals("monster", String::CaseInsensitive)) { auto monsterDatabase = Root::singleton().monsterDatabase(); uint64_t seed = parameters.getUInt("seed", Random::randu64()); String typeName = parameters.getString("typeName"); JsonObject uniqueParameters = parameters.getObject("parameters", {}); if (!uniqueParameters.contains("persistent")) uniqueParameters["persistent"] = true; auto monster = monsterDatabase->createMonster(monsterDatabase->monsterVariant(typeName, seed, uniqueParameters)); monster->setPosition(position); m_worldServer->addEntity(monster); } else throw StarException(strf("Unknown spawnable kind '{}'", kind)); } void DungeonGeneratorWorld::spawnStagehand(Vec2F const& position, Json const& definition) { auto stagehand = Root::singleton().stagehandDatabase()->createStagehand(definition.getString("type"), definition.get("parameters", Json())); stagehand->setPosition(position); m_worldServer->addEntity(stagehand); } void DungeonGeneratorWorld::setLiquid(Vec2I const& pos, LiquidStore const& liquid) { ServerTile* tile = m_worldServer->modifyServerTile(pos); starAssert(tile); if (tile) tile->liquid = liquid; } void DungeonGeneratorWorld::setPlayerStart(Vec2F const& startPosition) { m_worldServer->setPlayerStart(startPosition); } void DungeonGeneratorWorld::connectWireGroup(List const& wireGroup) { List outbounds; List inbounds; for (auto entry : wireGroup) { bool found = false; Vec2F posf = centerOfTile(entry); RectF bounds = {posf - Vec2F(16, 16), posf + Vec2F(16, 16)}; for (auto entity : m_worldServer->query(bounds)) { for (size_t i = 0; i < entity->nodeCount(WireDirection::Input); ++i) { if (entity->tilePosition() + entity->nodePosition({WireDirection::Input, i}) == entry) { inbounds.append(WireConnection{entity->tilePosition(), i}); found = true; } } for (size_t i = 0; i < entity->nodeCount(WireDirection::Output); ++i) { if (entity->tilePosition() + entity->nodePosition({WireDirection::Output, i}) == entry) { outbounds.append(WireConnection{entity->tilePosition(), i}); found = true; } } } if (!found) Logger::warn("Dungeon wire endpoint not found. {}", entry); } if (!outbounds.size() || !inbounds.size()) { Logger::warn("Dungeon wires did not make a circuit."); return; } for (auto outbound : outbounds) { auto out = m_worldServer->atTile(outbound.entityLocation).first(); for (auto inbound : inbounds) { auto in = m_worldServer->atTile(inbound.entityLocation).first(); in->addNodeConnection({WireDirection::Input, inbound.nodeIndex}, outbound); out->addNodeConnection({WireDirection::Output, outbound.nodeIndex}, inbound); } } } void DungeonGeneratorWorld::setForegroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) { ServerTile* tile = m_worldServer->modifyServerTile(position); if (tile) { tile->foregroundMod = mod; tile->foregroundModHueShift = hueshift; } } void DungeonGeneratorWorld::setBackgroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) { ServerTile* tile = m_worldServer->modifyServerTile(position); if (tile) { tile->backgroundMod = mod; tile->foregroundModHueShift = hueshift; } } void DungeonGeneratorWorld::setTileProtection(DungeonId dungeonId, bool isProtected) { m_worldServer->setTileProtection(dungeonId, isProtected); } bool DungeonGeneratorWorld::checkSolid(Vec2I const& position, TileLayer layer) { auto const& tile = m_worldServer->getServerTile(position); return tile.material(layer) != EmptyMaterialId && tile.material(layer) != NullMaterialId; } bool DungeonGeneratorWorld::checkOpen(Vec2I const& position, TileLayer layer) { auto const& tile = m_worldServer->getServerTile(position); return tile.material(layer) == EmptyMaterialId || tile.material(layer) == NullMaterialId; } bool DungeonGeneratorWorld::checkOceanLiquid(Vec2I const& position) { auto const& block = m_worldServer->worldTemplate()->blockInfo(position[0], position[1]); return block.oceanLiquid != EmptyLiquidId && position[1] < block.oceanLiquidLevel; } DungeonId DungeonGeneratorWorld::getDungeonIdAt(Vec2I const& position) { return m_worldServer->getServerTile(position).dungeonId; } void DungeonGeneratorWorld::setDungeonIdAt(Vec2I const& position, DungeonId dungeonId) { if (auto tile = m_worldServer->modifyServerTile(position)) tile->dungeonId = dungeonId; } void DungeonGeneratorWorld::clearTileEntities(RectI const& bounds, Set const& positions, bool clearAnchoredObjects) { auto entities = m_worldServer->entityQuery(RectF(bounds).padded(1), entityTypeFilter()); auto geometry = m_worldServer->geometry(); entities.filter([positions, geometry, clearAnchoredObjects](EntityPtr entity) { auto tileEntity = as(entity); for (auto pos : tileEntity->spaces()) { if (positions.contains(geometry.xwrap(pos + tileEntity->tilePosition()))) return true; } if (clearAnchoredObjects) { for (auto pos : tileEntity->roots()) { if (positions.contains(geometry.xwrap(pos + tileEntity->tilePosition()))) return true; } if (auto object = as(entity)) { for (auto pos : object->anchorPositions()) { if (positions.contains(geometry.xwrap(pos))) return true; } } } return false; }); for (auto entity : entities) m_worldServer->removeEntity(entity->entityId(), false); } SpawnerWorld::SpawnerWorld(WorldServer* server) : m_worldServer(server) {} WorldGeometry SpawnerWorld::geometry() const { return m_worldServer->geometry(); } List SpawnerWorld::clientWindows() const { List windows; for (auto clientId : m_worldServer->clientIds()) windows.append(m_worldServer->clientWindow(clientId)); return windows; } bool SpawnerWorld::signalRegion(RectF const& region) const { return m_worldServer->signalRegion(RectI::integral(region)); } CollisionKind SpawnerWorld::collision(Vec2I const& position) const { return m_worldServer->getServerTile(position + Vec2I(0, 1)).collision; } bool SpawnerWorld::isFreeSpace(RectF const& area) const { return !m_worldServer->polyCollision(PolyF(area)); } bool SpawnerWorld::isBackgroundEmpty(Vec2I const& pos) const { return m_worldServer->getServerTile(pos).background == EmptyMaterialId; } LiquidLevel SpawnerWorld::liquidLevel(Vec2I const& position) const { return m_worldServer->liquidLevel(position); } bool SpawnerWorld::spawningProhibited(RectF const& area) const { RectI region = RectI::integral(area); // Don't spawn the entity if its region overlaps with a dungeon for (int x = region.xMin(); x < region.xMax(); ++x) { for (int y = region.yMin(); y < region.yMax(); ++y) { auto const& tile = m_worldServer->getServerTile({x, y}); if (tile.getCollision() == CollisionKind::Null || tile.dungeonId != NoDungeonId) return true; } } return false; } uint64_t SpawnerWorld::spawnSeed() const { return m_worldServer->worldTemplate()->worldSeed(); } SpawnProfile SpawnerWorld::spawnProfile(Vec2F const& position) const { Vec2I ipos = Vec2I::floor(position); // Block biome, *not* environment biome, includes things like detached // biomes. if (auto biome = m_worldServer->worldTemplate()->blockBiome(ipos[0], ipos[1])) { // Dungeons, including ConstructionDungeonId (player constructed areas) // should be immune from spawning. auto tile = m_worldServer->getServerTile(ipos); if (tile.dungeonId == NoDungeonId) return biome->spawnProfile; } return {}; } float SpawnerWorld::dayLevel() const { return m_worldServer->sky()->dayLevel(); } float SpawnerWorld::threatLevel() const { return m_worldServer->threatLevel(); } EntityId SpawnerWorld::spawnEntity(EntityPtr entity) const { m_worldServer->addEntity(entity); return entity->entityId(); } void SpawnerWorld::despawnEntity(EntityId entityId) { m_worldServer->removeEntity(entityId, false); } EntityPtr SpawnerWorld::getEntity(EntityId entityId) const { return m_worldServer->entity(entityId); } WorldGenerator::WorldGenerator(WorldServer* server) : m_worldServer(server) { m_microDungeonFactory = make_shared(); } void WorldGenerator::generateSectorLevel(WorldStorage* worldStorage, Sector const& sector, SectorGenerationLevel generationLevel) { if (generationLevel == SectorGenerationLevel::BaseTiles) { prepareTiles(worldStorage, sector); } else if (generationLevel == SectorGenerationLevel::MicroDungeons) { if (!worldStorage->floatingDungeonWorld()) generateMicroDungeons(worldStorage, sector); } else if (generationLevel == SectorGenerationLevel::CaveLiquid) { if (!worldStorage->floatingDungeonWorld()) generateCaveLiquid(worldStorage, sector); } else if (generationLevel == SectorGenerationLevel::Finalize) { if (!worldStorage->floatingDungeonWorld()) prepareSector(worldStorage, sector); else prepareSectorBiomeBlocks(worldStorage, sector); m_worldServer->activateLiquidRegion(worldStorage->tileArray()->sectorRegion(sector)); } } void WorldGenerator::sectorLoadLevelChanged(WorldStorage* worldStorage, Sector const& sector, SectorLoadLevel loadLevel) { if (loadLevel == SectorLoadLevel::Loaded) { if (worldStorage->sectorGenerationLevel(sector) == SectorGenerationLevel::Complete) m_worldServer->activateLiquidRegion(worldStorage->tileArray()->sectorRegion(sector)); } } void WorldGenerator::terraformSector(WorldStorage* worldStorage, Sector const& sector) { // Logger::info("terraforming sector {}...", sector); reapplyBiome(worldStorage, sector); } void WorldGenerator::initEntity(WorldStorage*, EntityId entityId, EntityPtr const& entity) { entity->init(m_worldServer, entityId, EntityMode::Master); if (auto tileEntity = as(entity)) m_worldServer->updateTileEntityTiles(tileEntity, false, false); } void WorldGenerator::destructEntity(WorldStorage*, EntityPtr const& entity) { if (entity->isSlave()) throw StarException("Cannot destruct slave entity in WorldStorage, something has gone wrong!"); if (auto tileEntity = as(entity)) m_worldServer->updateTileEntityTiles(tileEntity, true, false); entity->uninit(); } bool WorldGenerator::entityKeepAlive(WorldStorage*, EntityPtr const& entity) const { return entity->isSlave() || (entity->isMaster() && entity->keepAlive()); } bool WorldGenerator::entityPersistent(WorldStorage*, EntityPtr const& entity) const { return entity->isMaster() && entity->persistent(); } RpcPromise WorldGenerator::enqueuePlacement(List distributions, Maybe id) { auto promise = RpcPromise::createPair(); m_queuedPlacements.append(QueuedPlacement { std::move(distributions), id, promise.second, false, }); return promise.first; } void WorldGenerator::replaceBiomeBlocks(ServerTile* tile) { if (auto blockBiome = m_worldServer->worldTemplate()->biome(tile->blockBiomeIndex)) { if (tile->foreground == BiomeMaterialId) { tile->foreground = blockBiome->mainBlock; tile->foregroundHueShift = m_worldServer->worldTemplate()->biomeMaterialHueShift(tile->blockBiomeIndex, tile->foreground); } else if ((tile->foreground >= Biome1MaterialId) && (tile->foreground <= Biome5MaterialId)) { auto& subblocks = blockBiome->subBlocks; if (subblocks.size()) tile->foreground = subblocks[(tile->foreground - Biome1MaterialId) % subblocks.size()]; else tile->foreground = blockBiome->mainBlock; tile->foregroundHueShift = m_worldServer->worldTemplate()->biomeMaterialHueShift(tile->blockBiomeIndex, tile->foreground); } if (tile->background == BiomeMaterialId) { tile->background = blockBiome->mainBlock; tile->backgroundHueShift = m_worldServer->worldTemplate()->biomeMaterialHueShift(tile->blockBiomeIndex, tile->background); } else if ((tile->background >= Biome1MaterialId) && (tile->background <= Biome5MaterialId)) { auto& subblocks = blockBiome->subBlocks; if (subblocks.size()) tile->background = subblocks[(tile->background - Biome1MaterialId) % subblocks.size()]; else tile->background = blockBiome->mainBlock; tile->backgroundHueShift = m_worldServer->worldTemplate()->biomeMaterialHueShift(tile->blockBiomeIndex, tile->background); } } else { if (isBiomeMaterial(tile->foreground)) { tile->foreground = EmptyMaterialId; tile->foregroundHueShift = 0; } if (isBiomeMod(tile->foregroundMod)) { tile->foregroundMod = NoModId; tile->foregroundModHueShift = 0; } if (isBiomeMaterial(tile->background)) { tile->background = EmptyMaterialId; tile->backgroundHueShift = 0; } if (isBiomeMod(tile->backgroundMod)) { tile->backgroundMod = NoModId; tile->backgroundModHueShift = 0; } } } void WorldGenerator::prepareTiles(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { auto materialDatabase = Root::singleton().materialDatabase(); auto planet = m_worldServer->worldTemplate(); // Generate sector. auto tileArray = worldStorage->tileArray(); RectI sectorRegion = tileArray->sectorRegion(sector); for (int x = sectorRegion.xMin(); x < sectorRegion.xMax(); ++x) { for (int y = sectorRegion.yMin(); y < sectorRegion.yMax(); ++y) { Vec2I pos(x, y); ServerTile* tile = tileArray->modifyTile(pos); starAssert(tile); if (!tile) continue; auto blockInfo = planet->blockInfo(pos[0], pos[1]); tile->blockBiomeIndex = blockInfo.blockBiomeIndex; tile->environmentBiomeIndex = blockInfo.environmentBiomeIndex; tile->biomeTransition = blockInfo.biomeTransition; if (tile->foreground == NullMaterialId) { tile->foreground = blockInfo.foreground; tile->foregroundColorVariant = DefaultMaterialColorVariant; tile->foregroundHueShift = planet->biomeMaterialHueShift(tile->blockBiomeIndex, tile->foreground); if (materialDatabase->supportsMod(tile->foreground, blockInfo.foregroundMod)) { tile->foregroundMod = blockInfo.foregroundMod; tile->foregroundModHueShift = planet->biomeModHueShift(tile->blockBiomeIndex, tile->foregroundMod); } } if (tile->background == NullMaterialId) { tile->background = blockInfo.background; tile->backgroundColorVariant = DefaultMaterialColorVariant; tile->backgroundHueShift = planet->biomeMaterialHueShift(tile->blockBiomeIndex, tile->background); if (materialDatabase->supportsMod(tile->background, blockInfo.backgroundMod)) { tile->backgroundMod = blockInfo.backgroundMod; tile->backgroundModHueShift = planet->biomeModHueShift(tile->blockBiomeIndex, tile->backgroundMod); } } if (tile->foreground != EmptyMaterialId) { tile->liquid = LiquidStore(); } else if (blockInfo.oceanLiquid != EmptyLiquidId && pos[1] < blockInfo.oceanLiquidLevel) { float pressure = blockInfo.oceanLiquidLevel - pos[1]; if (tile->background == EmptyMaterialId) tile->liquid = LiquidStore::endless(blockInfo.oceanLiquid, pressure); else if (blockInfo.encloseLiquids) tile->liquid = LiquidStore::filled(blockInfo.oceanLiquid, 1.0f, pressure); } } } } void WorldGenerator::generateMicroDungeons(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { auto facade = make_shared(m_worldServer, false); RectI sectorTiles = worldStorage->tileArray()->sectorRegion(sector); RectI bounds = sectorTiles.padded(WorldSectorSize - 1); List> placementQueue; for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) { auto potential = m_worldServer->worldTemplate()->potentialBiomeItemsAt(x, y); for (auto const& placement : m_worldServer->worldTemplate()->validBiomeItems(x, y, potential)) placementQueue.append({placement, {}}); for (auto& p : m_queuedPlacements) { WorldTemplate::PotentialBiomeItems queuedItems; m_worldServer->worldTemplate()->addPotentialBiomeItems(x, y, queuedItems, p.distributions, BiomePlacementArea::Surface); m_worldServer->worldTemplate()->addPotentialBiomeItems(x, y, queuedItems, p.distributions, BiomePlacementArea::Underground); for (auto placement : m_worldServer->worldTemplate()->validBiomeItems(x, y, queuedItems)) placementQueue.append({std::move(placement), &p}); } } } sort(placementQueue); for (auto const& p : placementQueue) { auto& placement = p.first; auto queued = p.second; if (queued && queued->fulfilled) continue; if (placement.item.is()) { auto seed = m_worldServer->worldTemplate()->seedFor(placement.position[0], placement.position[1]); auto const& dungeonName = staticRandomFrom(placement.item.get(), seed); Maybe dungeonId; starAssert(!dungeonName.empty()); if (auto generateResult = m_microDungeonFactory->generate(bounds, dungeonName, placement.position, seed, m_worldServer->threatLevel(), facade)) { if (queued) { dungeonId = queued->dungeonId; queued->promise.fulfill(placement.position); queued->fulfilled = true; } for (auto position : generateResult->second) { if (ServerTile* tile = m_worldServer->modifyServerTile(position)) { replaceBiomeBlocks(tile); tile->dungeonId = dungeonId.value(tile->dungeonId); } } } } } m_queuedPlacements = m_queuedPlacements.filtered([&](QueuedPlacement& p) { return !p.fulfilled; }); } void WorldGenerator::generateCaveLiquid(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { Set openNodes = caveLiquidSeeds(worldStorage, sector); if (!openNodes.size()) return; auto tileArray = worldStorage->tileArray(); Vec2I dimensions(tileArray->size()); auto wrapCoords = [=](Vec2I const& coord) -> Vec2I { return Vec2I{pmod(coord[0], dimensions[0]), coord[1]}; }; RectI sectorTiles = tileArray->sectorRegion(sector); RectI bounds = sectorTiles.padded(Vec2I(WorldSectorSize - 1, WorldSectorSize - 1)); bounds.min()[1] = clamp(bounds.min()[1], 0, dimensions[1] - 1); bounds.max()[1] = clamp(bounds.max()[1], 0, dimensions[1] - 1); auto materialDatabase = Root::singleton().materialDatabase(); auto samplePoint = sectorTiles.center(); auto blockInfo = m_worldServer->worldTemplate()->blockInfo(samplePoint[0], samplePoint[1]); LiquidId fillLiquid = blockInfo.caveLiquid; bool fillMicrodungeons = blockInfo.fillMicrodungeons; bool encloseLiquids = blockInfo.encloseLiquids; Set badNodes; for (int i = bounds.xMin(); i <= bounds.xMax(); i++) { badNodes.add({i, bounds.yMin()}); badNodes.add({i, bounds.yMax()}); } for (int i = bounds.yMin(); i <= bounds.yMax(); i++) { badNodes.add({bounds.xMin(), i}); badNodes.add({bounds.xMax(), i}); } Set candidateNodes; auto propose = [&](Vec2I const& position) { if (!bounds.contains(position)) return; if (candidateNodes.contains(position)) return; if (badNodes.contains(position)) return; auto tile = tileArray->tile(wrapCoords(position)); starAssert(tile.foreground != NullMaterialId); if (tile.foreground != EmptyMaterialId) { // Not sure why this doesn't poison solid materials, but it does (occasionally) encounter that case if (!BlockCollisionSet.contains(materialDatabase->materialCollisionKind(tile.foreground))) badNodes.add(position); return; } if ((tile.dungeonId != NoDungeonId && (!fillMicrodungeons || tile.dungeonId != BiomeMicroDungeonId)) || (!encloseLiquids && tile.background == EmptyMaterialId) || (tile.liquid.liquid != fillLiquid && tile.liquid.liquid != EmptyLiquidId)) { badNodes.add(position); return; } candidateNodes.add(position); openNodes.add(position); }; while (openNodes.size()) { auto node = *openNodes.begin(); openNodes.remove(node); propose(node + Vec2I(-1, 0)); propose(node + Vec2I(1, 0)); propose(node + Vec2I(0, -1)); } Set visitedNodes; auto poison = [&](Vec2I position) { if (!bounds.contains(position)) return; if (visitedNodes.contains(position)) return; visitedNodes.add(position); auto tile = tileArray->tile(wrapCoords(position)); starAssert(tile.foreground != NullMaterialId); if (tile.foreground != EmptyMaterialId) return; badNodes.add(position); }; while (badNodes.size()) { auto node = *badNodes.begin(); badNodes.remove(node); candidateNodes.remove(node); poison(node + Vec2I(-1, 0)); poison(node + Vec2I(1, 0)); poison(node + Vec2I(0, 1)); // upwards, not downwards } Set solidSurroundings(candidateNodes); auto solids = [&](Vec2I position) { auto tile = tileArray->tile(wrapCoords(position)); starAssert(tile.foreground != NullMaterialId); if (tile.foreground != EmptyMaterialId) solidSurroundings.add(position); }; for (auto position : candidateNodes) { solids(position + Vec2I(1, 0)); solids(position + Vec2I(-1, 0)); solids(position + Vec2I(0, 1)); solids(position + Vec2I(0, -1)); } MaterialId biomeBlock = m_worldServer->worldTemplate()->biome(tileArray->tile(samplePoint).blockBiomeIndex)->mainBlock; Map drops = determineLiquidLevel(candidateNodes, solidSurroundings); for (auto iter = drops.begin(); iter != drops.end(); ++iter) { auto tile = tileArray->modifyTile(wrapCoords(iter->first)); starAssert(tile); if (!tile) continue; if (iter->second) tile->liquid = LiquidStore::filled(fillLiquid, 1.0f, iter->second); if (encloseLiquids && tile->background == EmptyMaterialId) tile->background = biomeBlock; } } void WorldGenerator::prepareSector(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { auto materialDatabase = Root::singleton().materialDatabase(); auto planet = m_worldServer->worldTemplate(); auto tileArray = worldStorage->tileArray(); RectI sectorTiles = tileArray->sectorRegion(sector); for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) { Vec2I position(x, y); ServerTile* tile = tileArray->modifyTile(position); starAssert(tile); if (!tile) continue; starAssert(tile->foreground != NullMaterialId); if (tile->liquid.source) { auto blockInfo = planet->blockInfo(position[0], position[1]); // make sure that ocean liquid never exists on tiles without empty // background (except in real dungeons) if (!isRealDungeon(tile->dungeonId) && tile->background != EmptyMaterialId) tile->liquid.source = false; // pressurize liquid under the ocean if (blockInfo.oceanLiquid != EmptyLiquidId && position[1] < blockInfo.oceanLiquidLevel) { float pressure = blockInfo.oceanLiquidLevel - position[1]; tile->liquid.pressure = pressure; } } if (!isRealMaterial(tile->foreground)) tile->foregroundColorVariant = DefaultMaterialColorVariant; if (!isRealMaterial(tile->background)) tile->backgroundColorVariant = DefaultMaterialColorVariant; replaceBiomeBlocks(tile); placeBiomeGrass(worldStorage, tile, position); tile->collision = maxCollision(tile->collision, materialDatabase->materialCollisionKind(tile->foreground)); } } List placementQueue; for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) { auto tile = tileArray->tile(Vec2I(x, y)); if (tile.dungeonId == NoDungeonId) { auto potential = m_worldServer->worldTemplate()->potentialBiomeItemsAt(x, y); for (auto const& placement : m_worldServer->worldTemplate()->validBiomeItems(x, y, potential)) placementQueue.append(placement); } } } sort(placementQueue); for (auto const& placement : placementQueue) { auto seed = m_worldServer->worldTemplate()->seedFor(placement.position[0], placement.position[1]); if (placement.item.is()) { auto& grass = placement.item.get(); placePlant(worldStorage, Root::singleton().plantDatabase()->createPlant(grass, seed), placement.position); } else if (placement.item.is()) { auto& bush = placement.item.get(); placePlant(worldStorage, Root::singleton().plantDatabase()->createPlant(bush, seed), placement.position); } else if (placement.item.is()) { auto& treePair = placement.item.get(); TreeVariant treeVariant; if (seed % 2 == 0) treeVariant = treePair.first; else treeVariant = treePair.second; placePlant(worldStorage, Root::singleton().plantDatabase()->createPlant(treeVariant, seed), placement.position); } else if (placement.item.is()) { auto& objectPool = placement.item.get(); auto direction = seed % 2 ? Direction::Left : Direction::Right; auto objectPair = objectPool.select(seed); if (auto object = Root::singleton().objectDatabase()->createForPlacement( m_worldServer, objectPair.first, placement.position, direction, objectPair.second)) m_worldServer->addEntity(object); } else if (placement.item.is()) { auto& treasureBoxSet = placement.item.get(); auto direction = seed % 2 ? Direction::Left : Direction::Right; if (auto treasureContainer = Root::singleton().treasureDatabase()->createTreasureChest( m_worldServer, treasureBoxSet, placement.position, direction, seed)) m_worldServer->addEntity(treasureContainer); } } for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) { if (auto* tile = worldStorage->tileArray()->modifyTile({x, y})) tile->collisionCacheDirty = true; } } } void WorldGenerator::prepareSectorBiomeBlocks(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { auto tileArray = worldStorage->tileArray(); auto materialDatabase = Root::singleton().materialDatabase(); RectI sectorTiles = tileArray->sectorRegion(sector); for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) { Vec2I position(x, y); ServerTile* tile = tileArray->modifyTile(position); replaceBiomeBlocks(tile); placeBiomeGrass(worldStorage, tile, position); tile->collision = maxCollision(tile->collision, materialDatabase->materialCollisionKind(tile->foreground)); } } } void WorldGenerator::placeBiomeGrass(WorldStorage* worldStorage, ServerTile* tile, Vec2I const& position) { if (auto blockBiome = m_worldServer->worldTemplate()->biome(tile->blockBiomeIndex)) { // determine layer for grass mod calculation TileLayer modLayer = tile->foreground != EmptyMaterialId ? TileLayer::Foreground : TileLayer::Background; // don't place mods in dungeons unless explicitly specified, also don't // touch non-grass mods if (tile->mod(modLayer) == BiomeModId || tile->mod(modLayer) == UndergroundBiomeModId || (tile->dungeonId == NoDungeonId && tile->mod(modLayer) == NoModId)) { // check whether we're floor or ceiling auto tileAbove = worldStorage->tileArray()->tile(position + Vec2I(0, 1)); auto tileBelow = worldStorage->tileArray()->tile(position + Vec2I(0, -1)); bool isFloor = (tile->foreground != EmptyMaterialId && tileAbove.foreground == EmptyMaterialId) || (tile->background != EmptyMaterialId && tileAbove.background == EmptyMaterialId); bool isCeiling = !isFloor && ((tile->foreground != EmptyMaterialId && tileBelow.foreground == EmptyMaterialId) || (tile->background != EmptyMaterialId && tileBelow.background == EmptyMaterialId)); // get the appropriate placeables for above/below ground BiomePlaceables const* placeables; if ((isFloor && tileAbove.background != EmptyMaterialId) || (isCeiling && tileBelow.background != EmptyMaterialId)) placeables = &blockBiome->undergroundPlaceables; else placeables = &blockBiome->surfacePlaceables; // determine the proper grass mod or lack thereof ModId grassModId = NoModId; if (isFloor) { auto grassChance = staticRandomFloat(m_worldServer->worldTemplate()->worldSeed(), position[0], position[1]); if (isRealMod(placeables->grassMod) && grassChance <= placeables->grassModDensity) grassModId = placeables->grassMod; } else if (isCeiling) { auto grassChance = staticRandomFloat(m_worldServer->worldTemplate()->worldSeed(), position[0], position[1]); if (isRealMod(placeables->ceilingGrassMod) && grassChance <= placeables->ceilingGrassModDensity) grassModId = placeables->ceilingGrassMod; } // set the selected grass mod if (modLayer == TileLayer::Foreground) { tile->foregroundMod = grassModId; tile->backgroundMod = NoModId; } else { tile->foregroundMod = NoModId; tile->backgroundMod = grassModId; } } // update hue shifts appropriately tile->foregroundModHueShift = m_worldServer->worldTemplate()->biomeModHueShift(tile->blockBiomeIndex, tile->foregroundMod); tile->backgroundModHueShift = m_worldServer->worldTemplate()->biomeModHueShift(tile->blockBiomeIndex, tile->backgroundMod); } } void WorldGenerator::reapplyBiome(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { auto materialDatabase = Root::singleton().materialDatabase(); auto planet = m_worldServer->worldTemplate(); auto tileArray = worldStorage->tileArray(); RectI sectorTiles = tileArray->sectorRegion(sector); // Logger::info("Reapplying biome in sector {}...", sectorTiles); auto entities = m_worldServer->entityQuery(RectF(sectorTiles.padded(1))); List biomeTileEntities; for (auto entity : entities) { if (auto plant = as(entity)) { biomeTileEntities.append(as(entity)); } else if (auto object = as(entity)) { if (object->biomePlaced()) biomeTileEntities.append(as(entity)); } } List biomeItemTiles; for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) { Vec2I position(x, y); ServerTile* tile = m_worldServer->modifyServerTile(position); starAssert(tile); if (!tile) continue; auto blockInfo = planet->blockBiomeInfo(position[0], position[1]); if (blockInfo.blockBiomeIndex != tile->blockBiomeIndex) { auto newBiome = planet->biome(blockInfo.blockBiomeIndex); auto oldBiome = planet->biome(tile->blockBiomeIndex); biomeTileEntities.filter([&, position](TileEntityPtr tileEntity) { if (tileEntity->tilePosition() == position) { m_worldServer->removeEntity(tileEntity->entityId(), false); return false; } return true; }); // update biome index tile->blockBiomeIndex = blockInfo.blockBiomeIndex; tile->environmentBiomeIndex = blockInfo.environmentBiomeIndex; tile->biomeTransition = true; // replace biome blocks if (tile->foreground == oldBiome->mainBlock || oldBiome->subBlocks.contains(tile->foreground)) { tile->foreground = blockInfo.foreground; tile->foregroundColorVariant = DefaultMaterialColorVariant; if (tile->foreground == newBiome->mainBlock) tile->foregroundHueShift = newBiome->materialHueShift; } if (tile->background == oldBiome->mainBlock || oldBiome->subBlocks.contains(tile->background)) { tile->background = blockInfo.background; tile->backgroundColorVariant = DefaultMaterialColorVariant; if (tile->background == newBiome->mainBlock) tile->backgroundHueShift = newBiome->materialHueShift; } if (tile->foreground != EmptyMaterialId || tile->background != EmptyMaterialId) { // remove old biome mods if (tile->foregroundMod == oldBiome->surfacePlaceables.grassMod || tile->foregroundMod == oldBiome->surfacePlaceables.ceilingGrassMod || tile->foregroundMod == oldBiome->undergroundPlaceables.grassMod || tile->foregroundMod == oldBiome->undergroundPlaceables.ceilingGrassMod) { tile->foregroundMod = NoModId; tile->foregroundModHueShift = 0; } if (tile->backgroundMod == oldBiome->surfacePlaceables.grassMod || tile->backgroundMod == oldBiome->surfacePlaceables.ceilingGrassMod || tile->backgroundMod == oldBiome->undergroundPlaceables.grassMod || tile->backgroundMod == oldBiome->undergroundPlaceables.ceilingGrassMod) { tile->backgroundMod = NoModId; tile->backgroundModHueShift = 0; } // apply new biome mods TileLayer modLayer = tile->foreground != EmptyMaterialId ? TileLayer::Foreground : TileLayer::Background; if (tile->mod(modLayer) == NoModId) { // check whether we're floor or ceiling auto tileAbove = worldStorage->tileArray()->tile(position + Vec2I(0, 1)); auto tileBelow = worldStorage->tileArray()->tile(position + Vec2I(0, -1)); bool isFloor = tile->foreground != EmptyMaterialId && tileAbove.foreground == EmptyMaterialId; bool isCeiling = !isFloor && tile->foreground != EmptyMaterialId && tileBelow.foreground == EmptyMaterialId; bool isModFloor, isModCeiling; if (modLayer == TileLayer::Foreground) { isModFloor = isFloor; isModCeiling = isCeiling; } else { isModFloor = tile->background != EmptyMaterialId && tileAbove.background == EmptyMaterialId; isModCeiling = !isModFloor && tile->background != EmptyMaterialId && tileBelow.background == EmptyMaterialId; } // get the appropriate placeables for above/below ground BiomePlaceables const* placeables; if ((isFloor && tileAbove.background != EmptyMaterialId) || (isCeiling && tileBelow.background != EmptyMaterialId)) placeables = &newBiome->undergroundPlaceables; else placeables = &newBiome->surfacePlaceables; // determine the proper grass mod or lack thereof ModId grassModId = NoModId; if (isModFloor) { auto grassChance = staticRandomFloat(m_worldServer->worldTemplate()->worldSeed(), position[0], position[1]); if (isRealMod(placeables->grassMod) && grassChance <= placeables->grassModDensity) grassModId = placeables->grassMod; } else if (isModCeiling) { auto grassChance = staticRandomFloat(m_worldServer->worldTemplate()->worldSeed(), position[0], position[1]); if (isRealMod(placeables->ceilingGrassMod) && grassChance <= placeables->ceilingGrassModDensity) grassModId = placeables->ceilingGrassMod; } // set the selected grass mod if (modLayer == TileLayer::Foreground && materialDatabase->supportsMod(tile->foreground, grassModId)) { tile->foregroundMod = grassModId; tile->backgroundMod = NoModId; } else if (modLayer == TileLayer::Background && materialDatabase->supportsMod(tile->background, grassModId)) { tile->foregroundMod = NoModId; tile->backgroundMod = grassModId; } } } else { tile->foregroundMod = NoModId; tile->backgroundMod = NoModId; } tile->collision = maxCollision(tile->collision, materialDatabase->materialCollisionKind(tile->foreground)); } if (tile->biomeTransition && !blockInfo.biomeTransition) { tile->biomeTransition = false; if (!isSolidColliding(tile->collision)) biomeItemTiles.append(position); } } } auto simplePlacePlant = [&](PlantPtr const& plant, Vec2I const& position) { if (!plant) return false; auto spaces = plant->spaces(); auto roots = plant->roots(); auto const& primaryRoot = plant->primaryRoot(); auto blockBiome = planet->worldLayout()->getBiome(worldStorage->tileArray()->tile(position).blockBiomeIndex); auto positionValid = [&](Vec2I const& pos) { auto primaryTile = worldStorage->tileArray()->tile(pos); auto primaryRootTile = worldStorage->tileArray()->tile(pos + primaryRoot); if (isConnectableMaterial(primaryTile.foreground) || !isConnectableMaterial(primaryRootTile.foreground)) return false; for (auto root : roots) { auto rootTile = worldStorage->tileArray()->tile(root + pos); if (!isConnectableMaterial(rootTile.foreground) || rootTile.blockBiomeIndex != primaryTile.blockBiomeIndex || (rootTile.foreground != blockBiome->mainBlock && !blockBiome->subBlocks.contains(rootTile.foreground))) return false; } for (auto space : spaces) { Vec2I pspace = space + pos; if (!m_worldServer->atTile(pspace).empty()) return false; auto tile = worldStorage->tileArray()->tile(pspace); if (tile.foreground != EmptyMaterialId) return false; } return true; }; List tryPositions = { position, position + Vec2I{-1, 0}, position + Vec2I{1, 0}, position + Vec2I{-2, 0}, position + Vec2I{2, 0}, position + Vec2I{-1, 1}, position + Vec2I{-1, -1}, position + Vec2I{1, 1}, position + Vec2I{1, -1} }; for (auto pos : tryPositions) { if (positionValid(pos)) { plant->setTilePosition(pos); m_worldServer->addEntity(plant); return true; } } return false; }; auto placeBiomeItem = [&](BiomeItemPlacement biomeItemPlacement, Vec2I position) { auto seed = m_worldServer->worldTemplate()->seedFor(position[0], position[1]); if (biomeItemPlacement.item.is()) { auto& grass = biomeItemPlacement.item.get(); simplePlacePlant(Root::singleton().plantDatabase()->createPlant(grass, seed), position); } else if (biomeItemPlacement.item.is()) { auto& bush = biomeItemPlacement.item.get(); simplePlacePlant(Root::singleton().plantDatabase()->createPlant(bush, seed), position); } else if (biomeItemPlacement.item.is()) { auto& treePair = biomeItemPlacement.item.get(); TreeVariant treeVariant; if (seed % 2 == 0) treeVariant = treePair.first; else treeVariant = treePair.second; simplePlacePlant(Root::singleton().plantDatabase()->createPlant(treeVariant, seed), position); } else if (biomeItemPlacement.item.is()) { auto& objectPool = biomeItemPlacement.item.get(); auto direction = seed % 2 ? Direction::Left : Direction::Right; auto objectPair = objectPool.select(seed); if (auto object = Root::singleton().objectDatabase()->createForPlacement(m_worldServer, objectPair.first, position, direction, objectPair.second)) { if (object->biomePlaced()) m_worldServer->addEntity(object); } } }; for (auto position : biomeItemTiles) { ServerTile* tile = m_worldServer->modifyServerTile(position); auto blockBiome = planet->worldLayout()->getBiome(tile->blockBiomeIndex); auto tileAbove = m_worldServer->getServerTile(position + Vec2I{0, 1}); auto tileBelow = m_worldServer->getServerTile(position + Vec2I{0, -1}); if (tile->background != EmptyMaterialId) { for (auto const& itemDistribution : blockBiome->undergroundPlaceables.itemDistributions) { if (itemDistribution.mode() == BiomePlacementMode::Background) { if (auto itemToPlace = itemDistribution.itemToPlace(position[0], position[1])) placeBiomeItem(*itemToPlace, position); } } if (isSolidColliding(tileAbove.collision)) { for (auto const& itemDistribution : blockBiome->undergroundPlaceables.itemDistributions) { if (itemDistribution.mode() == BiomePlacementMode::Ceiling) { if (auto itemToPlace = itemDistribution.itemToPlace(position[0], position[1])) placeBiomeItem(*itemToPlace, position); } } } if (isSolidColliding(tileBelow.collision)) { for (auto const& itemDistribution : blockBiome->undergroundPlaceables.itemDistributions) { if (itemDistribution.mode() == BiomePlacementMode::Floor) { if (auto itemToPlace = itemDistribution.itemToPlace(position[0], position[1])) placeBiomeItem(*itemToPlace, position); } } } } else { if (isSolidColliding(tileBelow.collision)) { for (auto const& itemDistribution : blockBiome->surfacePlaceables.itemDistributions) { if (itemDistribution.mode() == BiomePlacementMode::Floor) { if (auto itemToPlace = itemDistribution.itemToPlace(position[0], position[1])) placeBiomeItem(*itemToPlace, position); } } } } } } Set WorldGenerator::caveLiquidSeeds(WorldStorage* worldStorage, ServerTileSectorArray::Sector const& sector) { RectI sectorTiles = worldStorage->tileArray()->sectorRegion(sector); auto samplePoint = sectorTiles.center(); auto blockInfo = m_worldServer->worldTemplate()->blockInfo(samplePoint[0], samplePoint[1]); float seedDensity = blockInfo.caveLiquidSeedDensity; Set nodes; if (seedDensity > 0) { int frequency = int(100 / seedDensity); for (int y = frequency * floor(sectorTiles.min()[1] / frequency); y < sectorTiles.max()[1]; y += frequency) { for (int x = frequency * floor(sectorTiles.min()[0] / frequency); x < sectorTiles.max()[0]; x += frequency) { if (sectorTiles.contains(Vec2I(x, y))) nodes.add(Vec2I(x, y)); } } } return nodes; } Map WorldGenerator::determineLiquidLevel(Set const& spots, Set const& filled) { Set openSet(spots); Map results; auto geometry = m_worldServer->geometry(); while (openSet.size() > 0) { Set cluster; Set openCluster; openCluster.add(*(openSet.begin())); while (openCluster.size() > 0) { Vec2I node = *(openCluster.begin()); openCluster.remove(node); if (openSet.contains(node)) { openSet.remove(node); cluster.add(node); openCluster.add(geometry.xwrap(Vec2I(node.x(), node.y() + 1))); openCluster.add(geometry.xwrap(Vec2I(node.x(), node.y() - 1))); openCluster.add(geometry.xwrap(Vec2I(node.x() + 1, node.y()))); openCluster.add(geometry.xwrap(Vec2I(node.x() - 1, node.y()))); } } levelCluster(cluster, filled, results); } return results; } void WorldGenerator::levelCluster(Set& cluster, Set const& filled, Map& results) { int maxY = std::numeric_limits::min(); int minY = std::numeric_limits::max(); for (auto iter = cluster.begin(); iter != cluster.end(); iter++) { auto droplet = (*iter); if (filled.contains(droplet + Vec2I(1, 0)) && filled.contains(droplet + Vec2I(-1, 0)) && filled.contains(droplet + Vec2I(0, -1))) { if (droplet.y() > maxY) maxY = droplet.y(); if (!filled.contains(droplet + Vec2I(0, 1))) { if (droplet.y() <= minY) minY = droplet.y(); } } else { if (droplet.y() <= minY) minY = droplet.y() - 1; } } int liquidLevel = std::min(maxY, minY); for (auto iter = cluster.begin(); iter != cluster.end(); iter++) { int pressure = (liquidLevel - (*iter).y()); if (pressure >= 0) results[*iter] = 1.0f + pressure; } } bool WorldGenerator::placePlant(WorldStorage* worldStorage, PlantPtr const& plant, Vec2I const& position) { if (!plant) return false; auto spaces = plant->spaces(); auto roots = plant->roots(); auto const& primaryRoot = plant->primaryRoot(); auto background = m_worldServer->getServerTile(position).background; bool adjustBackground = background == EmptyMaterialId || background == NullMaterialId; auto withinAdjustment = [=](Vec2I const& pos) { return PlantAdjustmentLimit - std::abs(pos[0]) > 0 && PlantAdjustmentLimit - std::abs(pos[1]) > 0; }; // Bail out if we don't have at least one free space, and root in the primary // root position, or if we're in a dungeon region. auto primaryTile = worldStorage->tileArray()->tile(position); auto rootTile = worldStorage->tileArray()->tile(position + primaryRoot); if (primaryTile.dungeonId != NoDungeonId || rootTile.dungeonId != NoDungeonId) return false; if (isConnectableMaterial(primaryTile.foreground) || !isConnectableMaterial(rootTile.foreground)) return false; // First bail out if we can't fit anything we're not adjusting for (auto space : spaces) { Vec2I pspace = space + position; if (withinAdjustment(space) && !m_worldServer->atTile(pspace).empty()) return false; // Bail out if we hit a different plant's root tile, or if we're not in the // adjustment space and we hit a non-empty tile. auto tile = worldStorage->tileArray()->tile(pspace); if (tile.rootSource || (!withinAdjustment(space) && tile.foreground != EmptyMaterialId)) return false; } // Check all the roots outside of the adjustment limit for (auto root : roots) { root += position; if (!withinAdjustment(root) && !isConnectableMaterial(worldStorage->tileArray()->tile(root).foreground)) return false; } // Clear all the necessary blocks within the adjustment limit for (auto space : spaces) { if (!withinAdjustment(space)) continue; space += position; if (auto tile = worldStorage->tileArray()->modifyTile(space)) { if (isConnectableMaterial(tile->foreground)) *tile = primaryTile; if (adjustBackground) tile->background = EmptyMaterialId; tile->collision = CollisionKind::None; tile->collisionCacheDirty = true; } else { return false; } } // Make all the root blocks a real material based on the primary root. for (auto root : roots) { root += position; if (auto tile = worldStorage->tileArray()->modifyTile(root)) { if (!isRealMaterial(tile->foreground)) { *tile = rootTile; tile->collision = Root::singleton().materialDatabase()->materialCollisionKind(tile->foreground); tile->collisionCacheDirty = true; } } else { return false; } } plant->setTilePosition(position); m_worldServer->addEntity(plant); return true; } }