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

summaryrefslogtreecommitdiff
path: root/source/game/StarWorldGeneration.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/game/StarWorldGeneration.cpp')
-rw-r--r--source/game/StarWorldGeneration.cpp1584
1 files changed, 1584 insertions, 0 deletions
diff --git a/source/game/StarWorldGeneration.cpp b/source/game/StarWorldGeneration.cpp
new file mode 100644
index 0000000..94a8664
--- /dev/null
+++ b/source/game/StarWorldGeneration.cpp
@@ -0,0 +1,1584 @@
+#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<LiquidId> 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<LiquidId>{tile.liquid.liquid, tile.liquid.pressure};
+ else if (tile.liquid.liquid != EmptyLiquidId)
+ return CellularLiquidFlowCell<LiquidId>{tile.liquid.liquid, tile.liquid.level, tile.liquid.pressure};
+ else
+ return CellularLiquidFlowCell<LiquidId>{{}, 0.0f, 0.0f};
+ }
+}
+
+void LiquidWorld::setFlow(Vec2I const& location, CellularLiquidFlowCell<LiquidId> 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<LiquidInteractionResult> 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 %s 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: %s", region);
+ m_worldServer->worldTemplate()->addCustomTerrainRegion(region);
+}
+
+void DungeonGeneratorWorld::markSpace(PolyF const& region) {
+ if (!m_markForActivation)
+ return;
+
+ Logger::debug("Marking poly as dungeon space region: %s", 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: %s direction: %s position: %s", 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<BiomeItemPlacement> 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<Plant>(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<BiomeItemPlacement>& 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<GrassVariant>()) {
+ auto& grass = placement.item.get<GrassVariant>();
+ placePlant(Root::singleton().plantDatabase()->createPlant(grass, seed), placement.position);
+ } else if (placement.item.is<BushVariant>()) {
+ auto& bush = placement.item.get<BushVariant>();
+ placePlant(Root::singleton().plantDatabase()->createPlant(bush, seed), placement.position);
+ } else if (placement.item.is<TreePair>()) {
+ auto& treePair = placement.item.get<TreePair>();
+ 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<ObjectPool>()) {
+ auto& objectPool = placement.item.get<ObjectPool>();
+ 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<TreasureBoxSet>()) {
+ auto& treasureBoxSet = placement.item.get<TreasureBoxSet>();
+ 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 '%s'", 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<Vec2I> const& wireGroup) {
+ List<WireConnection> outbounds;
+ List<WireConnection> 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<WireEntity>(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. %s", 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<WireEntity>(outbound.entityLocation).first();
+ for (auto inbound : inbounds) {
+ auto in = m_worldServer->atTile<WireEntity>(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<Vec2I> const& positions, bool clearAnchoredObjects) {
+ auto entities = m_worldServer->entityQuery(RectF(bounds).padded(1), entityTypeFilter<TileEntity>());
+ auto geometry = m_worldServer->geometry();
+ entities.filter([positions, geometry, clearAnchoredObjects](EntityPtr entity) {
+ auto tileEntity = as<TileEntity>(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<Object>(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<RectF> SpawnerWorld::clientWindows() const {
+ List<RectF> 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.collision == 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<MicroDungeonFactory>();
+}
+
+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 %s...", sector);
+ reapplyBiome(worldStorage, sector);
+}
+
+void WorldGenerator::initEntity(WorldStorage*, EntityId entityId, EntityPtr const& entity) {
+ entity->init(m_worldServer, entityId, EntityMode::Master);
+ if (auto tileEntity = as<TileEntity>(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<TileEntity>(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<Vec2I> WorldGenerator::enqueuePlacement(List<BiomeItemDistribution> distributions, Maybe<DungeonId> id) {
+ auto promise = RpcPromise<Vec2I>::createPair();
+ m_queuedPlacements.append(QueuedPlacement {
+ 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<DungeonGeneratorWorld>(m_worldServer, false);
+
+ RectI sectorTiles = worldStorage->tileArray()->sectorRegion(sector);
+ RectI bounds = sectorTiles.padded(WorldSectorSize - 1);
+
+ List<pair<BiomeItemPlacement, QueuedPlacement*>> 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({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<MicroDungeonNames>()) {
+ auto seed = m_worldServer->worldTemplate()->seedFor(placement.position[0], placement.position[1]);
+ auto const& dungeonName = staticRandomFrom(placement.item.get<MicroDungeonNames>(), seed);
+ Maybe<DungeonId> 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<Vec2I> 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<int>(coord[0], dimensions[0]), coord[1]}; };
+
+ RectI sectorTiles = tileArray->sectorRegion(sector);
+ RectI bounds = sectorTiles.padded(Vec2I(WorldSectorSize - 1, WorldSectorSize - 1));
+
+ bounds.min()[1] = clamp<int>(bounds.min()[1], 0, dimensions[1] - 1);
+ bounds.max()[1] = clamp<int>(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<Vec2I> 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<Vec2I> 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<Vec2I> 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<Vec2I> 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<Vec2I, float> 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<BiomeItemPlacement> 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<GrassVariant>()) {
+ auto& grass = placement.item.get<GrassVariant>();
+ placePlant(worldStorage, Root::singleton().plantDatabase()->createPlant(grass, seed), placement.position);
+ } else if (placement.item.is<BushVariant>()) {
+ auto& bush = placement.item.get<BushVariant>();
+ placePlant(worldStorage, Root::singleton().plantDatabase()->createPlant(bush, seed), placement.position);
+ } else if (placement.item.is<TreePair>()) {
+ auto& treePair = placement.item.get<TreePair>();
+ 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<ObjectPool>()) {
+ auto& objectPool = placement.item.get<ObjectPool>();
+ 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<TreasureBoxSet>()) {
+ auto& treasureBoxSet = placement.item.get<TreasureBoxSet>();
+ 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 %s...", sectorTiles);
+
+ auto entities = m_worldServer->entityQuery(RectF(sectorTiles.padded(1)));
+ List<TileEntityPtr> biomeTileEntities;
+ for (auto entity : entities) {
+ if (auto plant = as<Plant>(entity)) {
+ biomeTileEntities.append(as<TileEntity>(entity));
+ } else if (auto object = as<Object>(entity)) {
+ if (object->biomePlaced())
+ biomeTileEntities.append(as<TileEntity>(entity));
+ }
+ }
+
+ List<Vec2I> 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<TileEntity>(pspace).empty())
+ return false;
+
+ auto tile = worldStorage->tileArray()->tile(pspace);
+ if (tile.foreground != EmptyMaterialId)
+ return false;
+ }
+
+ return true;
+ };
+
+ List<Vec2I> 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<GrassVariant>()) {
+ auto& grass = biomeItemPlacement.item.get<GrassVariant>();
+ simplePlacePlant(Root::singleton().plantDatabase()->createPlant(grass, seed), position);
+ } else if (biomeItemPlacement.item.is<BushVariant>()) {
+ auto& bush = biomeItemPlacement.item.get<BushVariant>();
+ simplePlacePlant(Root::singleton().plantDatabase()->createPlant(bush, seed), position);
+ } else if (biomeItemPlacement.item.is<TreePair>()) {
+ auto& treePair = biomeItemPlacement.item.get<TreePair>();
+ 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<ObjectPool>()) {
+ auto& objectPool = biomeItemPlacement.item.get<ObjectPool>();
+ 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<Vec2I> 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<Vec2I> 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<Vec2I, float> WorldGenerator::determineLiquidLevel(Set<Vec2I> const& spots, Set<Vec2I> const& filled) {
+ Set<Vec2I> openSet(spots);
+ Map<Vec2I, float> results;
+
+ auto geometry = m_worldServer->geometry();
+
+ while (openSet.size() > 0) {
+ Set<Vec2I> cluster;
+ Set<Vec2I> 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<Vec2I>& cluster, Set<Vec2I> const& filled, Map<Vec2I, float>& results) {
+ int maxY = std::numeric_limits<int>::min();
+ int minY = std::numeric_limits<int>::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<Plant>(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;
+}
+
+}