diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarWorldServer.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarWorldServer.cpp')
-rw-r--r-- | source/game/StarWorldServer.cpp | 2341 |
1 files changed, 2341 insertions, 0 deletions
diff --git a/source/game/StarWorldServer.cpp b/source/game/StarWorldServer.cpp new file mode 100644 index 0000000..061335f --- /dev/null +++ b/source/game/StarWorldServer.cpp @@ -0,0 +1,2341 @@ +#include "StarWorldServer.hpp" +#include "StarLogging.hpp" +#include "StarIterator.hpp" +#include "StarDataStreamExtra.hpp" +#include "StarBiome.hpp" +#include "StarWireProcessor.hpp" +#include "StarWireEntity.hpp" +#include "StarWorldImpl.hpp" +#include "StarWorldGeneration.hpp" +#include "StarItemDescriptor.hpp" +#include "StarItemDrop.hpp" +#include "StarObjectDatabase.hpp" +#include "StarObject.hpp" +#include "StarItemDatabase.hpp" +#include "StarContainerEntity.hpp" +#include "StarItemBag.hpp" +#include "StarPhysicsEntity.hpp" +#include "StarProjectile.hpp" +#include "StarPlayer.hpp" +#include "StarEntityFactory.hpp" +#include "StarBiomeDatabase.hpp" +#include "StarLiquidTypes.hpp" +#include "StarFallingBlocksAgent.hpp" +#include "StarWarpTargetEntity.hpp" +#include "StarUniverseSettings.hpp" + +namespace Star { + +EnumMap<WorldServerFidelity> const WorldServerFidelityNames{ + {WorldServerFidelity::Minimum, "minimum"}, + {WorldServerFidelity::Low, "low"}, + {WorldServerFidelity::Medium, "medium"}, + {WorldServerFidelity::High, "high"} +}; + +WorldServer::WorldServer(WorldTemplatePtr const& worldTemplate, IODevicePtr storage) { + m_worldTemplate = worldTemplate; + m_worldStorage = make_shared<WorldStorage>(m_worldTemplate->size(), storage, make_shared<WorldGenerator>(this)); + m_adjustPlayerStart = true; + m_respawnInWorld = false; + m_tileProtectionEnabled = true; + m_universeSettings = make_shared<UniverseSettings>(); + + init(true); + writeMetadata(); +} + +WorldServer::WorldServer(Vec2U const& size, IODevicePtr storage) + : WorldServer(make_shared<WorldTemplate>(size), storage) {} + +WorldServer::WorldServer(IODevicePtr const& storage) { + m_worldStorage = make_shared<WorldStorage>(storage, make_shared<WorldGenerator>(this)); + m_tileProtectionEnabled = true; + m_universeSettings = make_shared<UniverseSettings>(); + + readMetadata(); + init(false); +} + +WorldServer::WorldServer(WorldChunks const& chunks) { + m_worldStorage = make_shared<WorldStorage>(chunks, make_shared<WorldGenerator>(this)); + m_tileProtectionEnabled = true; + m_universeSettings = make_shared<UniverseSettings>(); + + readMetadata(); + init(false); +} + +WorldServer::~WorldServer() { + m_spawner.uninit(); + writeMetadata(); + m_worldStorage->unloadAll(true); +} + +void WorldServer::setUniverseSettings(UniverseSettingsPtr universeSettings) { + m_universeSettings = move(universeSettings); +} + +UniverseSettingsPtr WorldServer::universeSettings() const { + return m_universeSettings; +} + +void WorldServer::setReferenceClock(ClockPtr clock) { + m_weather.setReferenceClock(clock); + m_sky->setReferenceClock(clock); +} + +WorldStructure WorldServer::setCentralStructure(WorldStructure centralStructure) { + removeCentralStructure(); + + m_centralStructure = move(centralStructure); + m_centralStructure.setAnchorPosition(Vec2I(m_geometry.size()) / 2); + + m_playerStart = Vec2F(m_centralStructure.flaggedBlocks("playerSpawn").first()) + Vec2F(0, 1); + m_adjustPlayerStart = false; + + auto materialDatabase = Root::singleton().materialDatabase(); + for (auto const& foregroundBlock : m_centralStructure.foregroundBlocks()) { + generateRegion(RectI::withSize(foregroundBlock.position, {1, 1})); + if (auto tile = m_tileArray->modifyTile(foregroundBlock.position)) { + if (tile->foreground == EmptyMaterialId) { + tile->foreground = foregroundBlock.materialId; + tile->updateCollision(materialDatabase->materialCollisionKind(foregroundBlock.materialId)); + queueTileUpdates(foregroundBlock.position); + dirtyCollision(RectI::withSize(foregroundBlock.position, {1, 1})); + } + } + } + + for (auto const& backgroundBlock : m_centralStructure.backgroundBlocks()) { + generateRegion(RectI::withSize(backgroundBlock.position, {1, 1})); + if (auto tile = m_tileArray->modifyTile(backgroundBlock.position)) { + if (tile->background == EmptyMaterialId) { + tile->background = backgroundBlock.materialId; + queueTileUpdates(backgroundBlock.position); + } + } + } + + auto objectDatabase = Root::singleton().objectDatabase(); + for (auto structureObject : m_centralStructure.objects()) { + generateRegion(RectI::withSize(structureObject.position, {1, 1})); + if (auto object = objectDatabase->createForPlacement(this, structureObject.name, structureObject.position, structureObject.direction, structureObject.parameters)) + addEntity(object); + } + + for (auto const& pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<CentralStructureUpdatePacket>(m_centralStructure.store())); + + return m_centralStructure; +} + +WorldStructure const& WorldServer::centralStructure() const { + return m_centralStructure; +} + +void WorldServer::removeCentralStructure() { + for (auto const& structureObject : m_centralStructure.objects()) { + if (!structureObject.residual) { + generateRegion(RectI::withSize(structureObject.position, {1, 1})); + for (auto const& objectEntity : atTile<Object>(structureObject.position)) { + if (objectEntity->tilePosition() == structureObject.position && objectEntity->name() == structureObject.name) + removeEntity(objectEntity->entityId(), false); + } + } + } + + for (auto const& backgroundBlock : m_centralStructure.backgroundBlocks()) { + if (!backgroundBlock.residual) { + generateRegion(RectI::withSize(backgroundBlock.position, {1, 1})); + if (auto tile = m_tileArray->modifyTile(backgroundBlock.position)) { + if (tile->background == backgroundBlock.materialId) { + tile->background = EmptyMaterialId; + tile->backgroundMod = NoModId; + queueTileUpdates(backgroundBlock.position); + } + } + } + } + + for (auto const& foregroundBlock : m_centralStructure.foregroundBlocks()) { + if (!foregroundBlock.residual) { + generateRegion(RectI::withSize(foregroundBlock.position, {1, 1})); + if (auto tile = m_tileArray->modifyTile(foregroundBlock.position)) { + if (tile->foreground == foregroundBlock.materialId) { + tile->foreground = EmptyMaterialId; + tile->foregroundMod = NoModId; + tile->updateCollision(CollisionKind::None); + dirtyCollision(RectI::withSize(foregroundBlock.position, {1, 1})); + queueTileUpdates(foregroundBlock.position); + } + } + } + } +} + +bool WorldServer::spawnTargetValid(SpawnTarget const& spawnTarget) const { + if (spawnTarget.is<SpawnTargetUniqueEntity>()) + return (bool)m_entityMap->get<WarpTargetEntity>(m_worldStorage->loadUniqueEntity(spawnTarget.get<SpawnTargetUniqueEntity>())); + return true; +} + +bool WorldServer::addClient(ConnectionId clientId, SpawnTarget const& spawnTarget, bool isLocal) { + if (m_clientInfo.contains(clientId)) + return false; + + Vec2F playerStart; + if (spawnTarget.is<SpawnTargetPosition>()) { + playerStart = spawnTarget.get<SpawnTargetPosition>(); + } else if (spawnTarget.is<SpawnTargetX>()) { + auto targetX = spawnTarget.get<SpawnTargetX>(); + playerStart = findPlayerSpaceStart(targetX); + } else if (spawnTarget.is<SpawnTargetUniqueEntity>()) { + if (auto target = m_entityMap->get<WarpTargetEntity>(m_worldStorage->loadUniqueEntity(spawnTarget.get<SpawnTargetUniqueEntity>()))) + playerStart = target->position() + target->footPosition(); + else + return false; + } else { + playerStart = m_playerStart; + if (m_adjustPlayerStart) { + m_playerStart = findPlayerStart(m_playerStart); + playerStart = m_playerStart; + } + } + RectF spawnRegion = RectF(playerStart, playerStart).padded(m_serverConfig.getInt("playerStartInitialGenRadius")); + generateRegion(RectI::integral(spawnRegion)); + m_spawner.activateEmptyRegion(spawnRegion); + + InterpolationTracker tracker; + if (isLocal) + tracker = InterpolationTracker(m_serverConfig.query("interpolationSettings.local")); + else + tracker = InterpolationTracker(m_serverConfig.query("interpolationSettings.normal")); + + tracker.update(m_currentStep); + + auto clientInfo = m_clientInfo.add(clientId, make_shared<ClientInfo>(clientId, tracker)); + + auto worldStartPacket = make_shared<WorldStartPacket>(); + worldStartPacket->templateData = m_worldTemplate->store(); + tie(worldStartPacket->skyData, clientInfo->skyNetVersion) = m_sky->writeUpdate(); + tie(worldStartPacket->weatherData, clientInfo->weatherNetVersion) = m_weather.writeUpdate(); + worldStartPacket->playerStart = playerStart; + worldStartPacket->playerRespawn = m_playerStart; + worldStartPacket->respawnInWorld = m_respawnInWorld; + worldStartPacket->worldProperties = m_worldProperties; + worldStartPacket->dungeonIdGravity = m_dungeonIdGravity; + worldStartPacket->dungeonIdBreathable = m_dungeonIdBreathable; + worldStartPacket->protectedDungeonIds = m_protectedDungeonIds; + worldStartPacket->clientId = clientId; + worldStartPacket->localInterpolationMode = isLocal; + clientInfo->outgoingPackets.append(worldStartPacket); + + clientInfo->outgoingPackets.append(make_shared<CentralStructureUpdatePacket>(m_centralStructure.store())); + + return true; +} + +List<PacketPtr> WorldServer::removeClient(ConnectionId clientId) { + auto const& info = m_clientInfo.get(clientId); + + for (auto const& entityId : m_entityMap->entityIds()) { + if (connectionForEntity(entityId) == clientId) + removeEntity(entityId, false); + } + + for (auto const& uuid : m_entityMessageResponses.keys()) { + if (m_entityMessageResponses[uuid].first == clientId) { + auto response = m_entityMessageResponses[uuid].second; + if (response.is<ConnectionId>()) { + if (auto clientInfo = m_clientInfo.value(response.get<ConnectionId>())) + clientInfo->outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeLeft("Client disconnected"), uuid)); + } else { + response.get<RpcPromiseKeeper<Json>>().fail("Client disconnected"); + } + m_entityMessageResponses.remove(uuid); + } + } + + auto packets = move(info->outgoingPackets); + m_clientInfo.remove(clientId); + + packets.append(make_shared<WorldStopPacket>("Removed")); + + return packets; +} + +List<ConnectionId> WorldServer::clientIds() const { + return m_clientInfo.keys(); +} + +bool WorldServer::hasClient(ConnectionId clientId) const { + return m_clientInfo.contains(clientId); +} + +RectF WorldServer::clientWindow(ConnectionId clientId) const { + auto i = m_clientInfo.find(clientId); + if (i != m_clientInfo.end()) + return RectF(i->second->clientState.window()); + else + return RectF::null(); +} + +PlayerPtr WorldServer::clientPlayer(ConnectionId clientId) const { + auto i = m_clientInfo.find(clientId); + if (i != m_clientInfo.end()) + return get<Player>(i->second->clientState.playerId()); + else + return {}; +} + +List<EntityId> WorldServer::players() const { + List<EntityId> playerIds; + for (auto pair : m_clientInfo) + playerIds.append(pair.second->clientState.playerId()); + return playerIds; +} + +void WorldServer::handleIncomingPackets(ConnectionId clientId, List<PacketPtr> const& packets) { + auto const& clientInfo = m_clientInfo.get(clientId); + auto& root = Root::singleton(); + auto entityFactory = root.entityFactory(); + auto itemDatabase = root.itemDatabase(); + + for (auto const& packet : packets) { + if (auto worldStartAcknowledge = as<WorldStartAcknowledgePacket>(packet)) { + clientInfo->started = true; + + } else if (!clientInfo->started) { + // clients which have not sent a world start acknowledge are not sending packets intended for this world + continue; + + } else if (auto heartbeat = as<StepUpdatePacket>(packet)) { + clientInfo->interpolationTracker.receiveStepUpdate(heartbeat->remoteStep); + + } else if (auto wcsPacket = as<WorldClientStateUpdatePacket>(packet)) { + clientInfo->clientState.readDelta(wcsPacket->worldClientStateDelta); + + // Need to send all sectors that are now in the client window but were not + // in the old + HashSet<ServerTileSectorArray::Sector> oldSectors = take(clientInfo->activeSectors); + + for (auto const& monitoredRegion : clientInfo->monitoringRegions(m_entityMap)) + clientInfo->activeSectors.addAll(m_tileArray->validSectorsFor(monitoredRegion)); + + clientInfo->pendingSectors.addAll(clientInfo->activeSectors.difference(oldSectors)); + + } else if (auto mtpacket = as<ModifyTileListPacket>(packet)) { + auto unappliedModifications = applyTileModifications(mtpacket->modifications, mtpacket->allowEntityOverlap); + if (!unappliedModifications.empty()) + clientInfo->outgoingPackets.append(make_shared<TileModificationFailurePacket>(unappliedModifications)); + + } else if (auto dtgpacket = as<DamageTileGroupPacket>(packet)) { + damageTiles(dtgpacket->tilePositions, dtgpacket->layer, dtgpacket->sourcePosition, dtgpacket->tileDamage, dtgpacket->sourceEntity); + + } else if (auto clpacket = as<CollectLiquidPacket>(packet)) { + if (auto item = collectLiquid(clpacket->tilePositions, clpacket->liquidId)) + clientInfo->outgoingPackets.append(make_shared<GiveItemPacket>(item)); + + } else if (auto sepacket = as<SpawnEntityPacket>(packet)) { + auto entity = entityFactory->netLoadEntity(sepacket->entityType, move(sepacket->storeData)); + entity->readNetState(move(sepacket->firstNetState)); + addEntity(move(entity)); + + } else if (auto rdpacket = as<RequestDropPacket>(packet)) { + auto drop = m_entityMap->get<ItemDrop>(rdpacket->dropEntityId); + if (drop && drop->canTake()) { + if (auto taken = drop->takeBy(clientInfo->clientState.playerId())) + clientInfo->outgoingPackets.append(make_shared<GiveItemPacket>(taken->descriptor())); + } + + } else if (auto hit = as<HitRequestPacket>(packet)) { + if (hit->remoteHitRequest.destinationConnection() == ServerConnectionId) + m_damageManager->pushRemoteHitRequest(hit->remoteHitRequest); + else + m_clientInfo.get(hit->remoteHitRequest.destinationConnection())->outgoingPackets.append(make_shared<HitRequestPacket>(hit->remoteHitRequest)); + + } else if (auto damage = as<DamageRequestPacket>(packet)) { + if (damage->remoteDamageRequest.destinationConnection() == ServerConnectionId) + m_damageManager->pushRemoteDamageRequest(damage->remoteDamageRequest); + else + m_clientInfo.get(damage->remoteDamageRequest.destinationConnection())->outgoingPackets.append(make_shared<DamageRequestPacket>(damage->remoteDamageRequest)); + + } else if (auto damage = as<DamageNotificationPacket>(packet)) { + m_damageManager->pushRemoteDamageNotification(damage->remoteDamageNotification); + for (auto const& pair : m_clientInfo) { + if (pair.first != clientId && pair.second->needsDamageNotification(damage->remoteDamageNotification)) + pair.second->outgoingPackets.append(make_shared<DamageNotificationPacket>(damage->remoteDamageNotification)); + } + + } else if (auto entityInteract = as<EntityInteractPacket>(packet)) { + auto targetEntityConnection = connectionForEntity(entityInteract->interactRequest.targetId); + if (targetEntityConnection == ServerConnectionId) { + auto interactResult = interact(entityInteract->interactRequest).result(); + clientInfo->outgoingPackets.append(make_shared<EntityInteractResultPacket>(interactResult.take(), entityInteract->requestId, entityInteract->interactRequest.sourceId)); + } else { + auto const& forwardClientInfo = m_clientInfo.get(targetEntityConnection); + forwardClientInfo->outgoingPackets.append(entityInteract); + } + + } else if (auto interactResult = as<EntityInteractResultPacket>(packet)) { + auto const& forwardClientInfo = m_clientInfo.get(connectionForEntity(interactResult->sourceEntityId)); + forwardClientInfo->outgoingPackets.append(interactResult); + + } else if (auto entityCreate = as<EntityCreatePacket>(packet)) { + if (!entityIdInSpace(entityCreate->entityId, clientInfo->clientId)) { + throw WorldServerException::format("WorldServer received entity create packet with illegal entity id %s.", entityCreate->entityId); + } else { + if (m_entityMap->entity(entityCreate->entityId)) { + Logger::error("WorldServer received duplicate entity create packet from client, deleting old entity %s", entityCreate->entityId); + removeEntity(entityCreate->entityId, false); + } + + auto entity = entityFactory->netLoadEntity(entityCreate->entityType, entityCreate->storeData); + entity->readNetState(entityCreate->firstNetState); + entity->init(this, entityCreate->entityId, EntityMode::Slave); + m_entityMap->addEntity(entity); + + if (clientInfo->interpolationTracker.interpolationEnabled()) + entity->enableInterpolation(clientInfo->interpolationTracker.extrapolationHint()); + } + + } else if (auto entityUpdateSet = as<EntityUpdateSetPacket>(packet)) { + float interpolationLeadTime = clientInfo->interpolationTracker.interpolationLeadSteps() * WorldTimestep; + m_entityMap->forAllEntities([&](EntityPtr const& entity) { + EntityId entityId = entity->entityId(); + if (connectionForEntity(entityId) == clientId) { + starAssert(entity->isSlave()); + entity->readNetState(entityUpdateSet->deltas.value(entityId), interpolationLeadTime); + } + }); + clientInfo->pendingForward = true; + + } else if (auto entityDestroy = as<EntityDestroyPacket>(packet)) { + if (auto entity = m_entityMap->entity(entityDestroy->entityId)) { + entity->readNetState(entityDestroy->finalNetState, clientInfo->interpolationTracker.interpolationLeadSteps() * WorldTimestep); + // Before destroying the entity, we should make sure that the entity is + // using the absolute latest data, so we disable interpolation. + entity->disableInterpolation(); + removeEntity(entityDestroy->entityId, entityDestroy->death); + } + + } else if (auto disconnectWires = as<DisconnectAllWiresPacket>(packet)) { + for (auto wireEntity : atTile<WireEntity>(disconnectWires->entityPosition)) { + for (auto connection : wireEntity->connectionsForNode(disconnectWires->wireNode)) { + wireEntity->removeNodeConnection(disconnectWires->wireNode, connection); + for (auto connectedEntity : atTile<WireEntity>(connection.entityLocation)) + connectedEntity->removeNodeConnection({otherWireDirection(disconnectWires->wireNode.direction), connection.nodeIndex}, WireConnection{disconnectWires->entityPosition, disconnectWires->wireNode.nodeIndex}); + } + } + + } else if (auto connectWire = as<ConnectWirePacket>(packet)) { + for (auto source : atTile<WireEntity>(connectWire->inputConnection.entityLocation)) { + for (auto target : atTile<WireEntity>(connectWire->outputConnection.entityLocation)) { + source->addNodeConnection(WireNode{WireDirection::Input, connectWire->inputConnection.nodeIndex}, connectWire->outputConnection); + target->addNodeConnection(WireNode{WireDirection::Output, connectWire->outputConnection.nodeIndex}, connectWire->inputConnection); + } + } + + } else if (auto findUniqueEntity = as<FindUniqueEntityPacket>(packet)) { + clientInfo->outgoingPackets.append(make_shared<FindUniqueEntityResponsePacket>(findUniqueEntity->uniqueEntityId, + m_worldStorage->findUniqueEntity(findUniqueEntity->uniqueEntityId))); + + } else if (auto entityMessagePacket = as<EntityMessagePacket>(packet)) { + EntityPtr entity; + if (entityMessagePacket->entityId.is<EntityId>()) + entity = m_entityMap->entity(entityMessagePacket->entityId.get<EntityId>()); + else + entity = m_entityMap->entity(loadUniqueEntity(entityMessagePacket->entityId.get<String>())); + + if (!entity) { + clientInfo->outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeLeft("Unknown entity"), entityMessagePacket->uuid)); + } else { + if (entity->isMaster()) { + auto response = entity->receiveMessage(clientId, entityMessagePacket->message, entityMessagePacket->args); + if (response) + clientInfo->outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeRight(response.take()), entityMessagePacket->uuid)); + else + clientInfo->outgoingPackets.append(make_shared<EntityMessageResponsePacket>(makeLeft("Message not handled by entity"), entityMessagePacket->uuid)); + } else if (auto const& clientInfo = m_clientInfo.value(connectionForEntity(entity->entityId()))) { + m_entityMessageResponses[entityMessagePacket->uuid] = {clientInfo->clientId, clientId}; + entityMessagePacket->fromConnection = clientId; + clientInfo->outgoingPackets.append(move(entityMessagePacket)); + } + } + + } else if (auto entityMessageResponsePacket = as<EntityMessageResponsePacket>(packet)) { + if (!m_entityMessageResponses.contains(entityMessageResponsePacket->uuid)) + throw WorldServerException("ScriptedEntityResponse received for unknown context!"); + + auto response = m_entityMessageResponses.take(entityMessageResponsePacket->uuid).second; + if (response.is<ConnectionId>()) { + if (auto clientInfo = m_clientInfo.value(response.get<ConnectionId>())) + clientInfo->outgoingPackets.append(move(entityMessageResponsePacket)); + } else { + if (entityMessageResponsePacket->response.isRight()) + response.get<RpcPromiseKeeper<Json>>().fulfill(entityMessageResponsePacket->response.right()); + else + response.get<RpcPromiseKeeper<Json>>().fail(entityMessageResponsePacket->response.left()); + } + } else if (auto pingPacket = as<PingPacket>(packet)) { + clientInfo->outgoingPackets.append(make_shared<PongPacket>()); + + } else if (auto updateWorldProperties = as<UpdateWorldPropertiesPacket>(packet)) { + m_worldProperties.merge(updateWorldProperties->updatedProperties, true); + for (auto const& pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<UpdateWorldPropertiesPacket>(updateWorldProperties->updatedProperties)); + + } else { + throw WorldServerException::format("Improper packet type %s received by client", (int)packet->type()); + } + } +} + +List<PacketPtr> WorldServer::getOutgoingPackets(ConnectionId clientId) { + auto const& clientInfo = m_clientInfo.get(clientId); + return move(clientInfo->outgoingPackets); +} + +WorldServerFidelity WorldServer::fidelity() const { + return m_fidelity; +} + +void WorldServer::setFidelity(WorldServerFidelity fidelity) { + m_fidelity = fidelity; + m_fidelityConfig = m_serverConfig.get("fidelitySettings").get(WorldServerFidelityNames.getRight(m_fidelity)); +} + +void WorldServer::update() { + ++m_currentStep; + for (auto const& pair : m_clientInfo) + pair.second->interpolationTracker.update(m_currentStep); + + List<WorldAction> triggeredActions; + eraseWhere(m_timers, [&triggeredActions](pair<int, WorldAction>& timer) { + if (--timer.first <= 0) { + triggeredActions.append(timer.second); + return true; + } + return false; + }); + for (auto const& action : triggeredActions) + action(this); + + m_spawner.update(); + + bool doBreakChecks = m_tileEntityBreakCheckTimer.wrapTick(m_currentStep) && m_needsGlobalBreakCheck; + if (doBreakChecks) + m_needsGlobalBreakCheck = false; + + List<EntityId> toRemove; + m_entityMap->updateAllEntities([&](EntityPtr const& entity) { + entity->update(m_currentStep); + + if (auto tileEntity = as<TileEntity>(entity)) { + // Only do break checks on objects if all sectors the object touches + // *and surrounding sectors* are active. Objects that this object + // rests on can be up to an entire sector large in any direction. + if (doBreakChecks && regionActive(RectI::integral(tileEntity->metaBoundBox().translated(tileEntity->position())).padded(WorldSectorSize))) + tileEntity->checkBroken(); + updateTileEntityTiles(tileEntity); + } + + if (entity->shouldDestroy() && entity->entityMode() == EntityMode::Master) + toRemove.append(entity->entityId()); + }, [](EntityPtr const& a, EntityPtr const& b) { + return a->entityType() < b->entityType(); + }); + + updateDamage(); + if (shouldRunThisStep("wiringUpdate")) + m_wireProcessor->process(); + + m_sky->update(); + + List<RectI> clientWindows; + List<RectI> clientMonitoringRegions; + for (auto const& pair : m_clientInfo) { + clientWindows.append(pair.second->clientState.window()); + for (auto const& region : pair.second->monitoringRegions(m_entityMap)) + clientMonitoringRegions.appendAll(m_geometry.splitRect(region)); + } + + m_weather.setClientVisibleRegions(clientWindows); + m_weather.update(); + for (auto projectile : m_weather.pullNewProjectiles()) + addEntity(move(projectile)); + + if (shouldRunThisStep("liquidUpdate")) { + m_liquidEngine->setProcessingLimit(m_fidelityConfig.optUInt("liquidEngineBackgroundProcessingLimit")); + m_liquidEngine->setNoProcessingLimitRegions(clientMonitoringRegions); + m_liquidEngine->update(); + } + + if (shouldRunThisStep("fallingBlocksUpdate")) + m_fallingBlocksAgent->update(); + + if (auto delta = shouldRunThisStep("blockDamageUpdate")) + updateDamagedBlocks(*delta * WorldTimestep); + + if (auto delta = shouldRunThisStep("worldStorageTick")) + m_worldStorage->tick(*delta * WorldTimestep); + + if (auto delta = shouldRunThisStep("worldStorageGenerate")) { + m_worldStorage->generateQueue(m_fidelityConfig.optUInt("worldStorageGenerationLevelLimit"), [this](WorldStorage::Sector a, WorldStorage::Sector b) { + auto distanceToClosestPlayer = [this](WorldStorage::Sector sector) { + Vec2F sectorCenter = RectF(*m_worldStorage->regionForSector(sector)).center(); + float distance = highest<float>(); + for (auto const& pair : m_clientInfo) { + if (auto player = get<Player>(pair.second->clientState.playerId())) + distance = min(vmag(sectorCenter - player->position()), distance); + } + return distance; + }; + + return distanceToClosestPlayer(a) < distanceToClosestPlayer(b); + }); + } + + for (EntityId entityId : toRemove) + removeEntity(entityId, true); + + for (auto const& pair : m_clientInfo) { + for (auto const& monitoredRegion : pair.second->monitoringRegions(m_entityMap)) + signalRegion(monitoredRegion.padded(jsonToVec2I(m_serverConfig.get("playerActiveRegionPad")))); + queueUpdatePackets(pair.first); + } + + for (auto& pair : m_clientInfo) + pair.second->pendingForward = false; + + LogMap::set(strf("server_%s_entities", m_worldTemplate->worldSeed()), m_entityMap->size()); + LogMap::set(strf("server_%s_sectors", m_worldTemplate->worldSeed()), strf("%d", m_tileArray->loadedSectorCount())); + LogMap::set(strf("server_%s_world_time", m_worldTemplate->worldSeed()), epochTime()); + LogMap::set(strf("server_%s_active_liquid", m_worldTemplate->worldSeed()), m_liquidEngine->activeCells()); + LogMap::set(strf("server_%s_day_time", m_worldTemplate->worldSeed()), timeOfDay() / dayLength()); + LogMap::set(strf("server_%s_lua_mem", m_worldTemplate->worldSeed()), m_luaRoot->luaMemoryUsage()); +} + +WorldGeometry WorldServer::geometry() const { + return m_geometry; +} + +uint64_t WorldServer::currentStep() const { + return m_currentStep; +} + +MaterialId WorldServer::material(Vec2I const& pos, TileLayer layer) const { + return m_tileArray->tile(pos).material(layer); +} + +MaterialHue WorldServer::materialHueShift(Vec2I const& position, TileLayer layer) const { + auto const& tile = m_tileArray->tile(position); + return layer == TileLayer::Foreground ? tile.foregroundHueShift : tile.backgroundHueShift; +} + +ModId WorldServer::mod(Vec2I const& pos, TileLayer layer) const { + return m_tileArray->tile(pos).mod(layer); +} + +MaterialHue WorldServer::modHueShift(Vec2I const& position, TileLayer layer) const { + auto const& tile = m_tileArray->tile(position); + return layer == TileLayer::Foreground ? tile.foregroundModHueShift : tile.backgroundModHueShift; +} + +MaterialColorVariant WorldServer::colorVariant(Vec2I const& position, TileLayer layer) const { + auto const& tile = m_tileArray->tile(position); + return layer == TileLayer::Foreground ? tile.foregroundColorVariant : tile.backgroundColorVariant; +} + +EntityPtr WorldServer::entity(EntityId entityId) const { + return m_entityMap->entity(entityId); +} + +void WorldServer::addEntity(EntityPtr const& entity) { + if (!entity) + return; + + entity->init(this, m_entityMap->reserveEntityId(), EntityMode::Master); + m_entityMap->addEntity(entity); + + if (auto tileEntity = as<TileEntity>(entity)) + updateTileEntityTiles(tileEntity); +} + +EntityPtr WorldServer::closestEntity(Vec2F const& center, float radius, EntityFilter selector) const { + return m_entityMap->closestEntity(center, radius, selector); +} + +void WorldServer::forAllEntities(EntityCallback callback) const { + m_entityMap->forAllEntities(callback); +} + +void WorldServer::forEachEntity(RectF const& boundBox, EntityCallback callback) const { + m_entityMap->forEachEntity(boundBox, callback); +} + +void WorldServer::forEachEntityLine(Vec2F const& begin, Vec2F const& end, EntityCallback callback) const { + m_entityMap->forEachEntityLine(begin, end, callback); +} + +void WorldServer::forEachEntityAtTile(Vec2I const& pos, EntityCallbackOf<TileEntity> callback) const { + m_entityMap->forEachEntityAtTile(pos, callback); +} + +EntityPtr WorldServer::findEntity(RectF const& boundBox, EntityFilter entityFilter) const { + return m_entityMap->findEntity(boundBox, entityFilter); +} + +EntityPtr WorldServer::findEntityLine(Vec2F const& begin, Vec2F const& end, EntityFilter entityFilter) const { + return m_entityMap->findEntityLine(begin, end, entityFilter); +} + +EntityPtr WorldServer::findEntityAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> entityFilter) const { + return m_entityMap->findEntityAtTile(pos, entityFilter); +} + +bool WorldServer::tileIsOccupied(Vec2I const& pos, TileLayer layer, bool includeEphemeral) const { + return WorldImpl::tileIsOccupied(m_tileArray, m_entityMap, pos, layer, includeEphemeral); +} + +void WorldServer::forEachCollisionBlock(RectI const& region, function<void(CollisionBlock const&)> const& iterator) const { + const_cast<WorldServer*>(this)->freshenCollision(region); + m_tileArray->tileEach(region, [iterator](Vec2I const& pos, ServerTile const& tile) { + if (tile.collision == CollisionKind::Null) { + iterator(CollisionBlock::nullBlock(pos)); + } else { + starAssert(!tile.collisionCacheDirty); + for (auto const& block : tile.collisionCache) + iterator(block); + } + }); +} + +bool WorldServer::isTileConnectable(Vec2I const& pos, TileLayer layer, bool tilesOnly) const { + return m_tileArray->tile(pos).isConnectable(layer, tilesOnly); +} + +bool WorldServer::pointTileCollision(Vec2F const& point, CollisionSet const& collisionSet) const { + return m_tileArray->tile(Vec2I(point.floor())).isColliding(collisionSet); +} + +bool WorldServer::lineTileCollision(Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet) const { + return WorldImpl::lineTileCollision(m_geometry, m_tileArray, begin, end, collisionSet); +} + +Maybe<pair<Vec2F, Vec2I>> WorldServer::lineTileCollisionPoint(Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet) const { + return WorldImpl::lineTileCollisionPoint(m_geometry, m_tileArray, begin, end, collisionSet); +} + +List<Vec2I> WorldServer::collidingTilesAlongLine(Vec2F const& begin, Vec2F const& end, CollisionSet const& collisionSet, int maxSize, bool includeEdges) const { + return WorldImpl::collidingTilesAlongLine(m_geometry, m_tileArray, begin, end, collisionSet, maxSize, includeEdges); +} + +bool WorldServer::rectTileCollision(RectI const& region, CollisionSet const& collisionSet) const { + return WorldImpl::rectTileCollision(m_tileArray, region, collisionSet); +} + +LiquidLevel WorldServer::liquidLevel(Vec2I const& pos) const { + return m_tileArray->tile(pos).liquid; +} + +LiquidLevel WorldServer::liquidLevel(RectF const& region) const { + return WorldImpl::liquidLevel(m_tileArray, region); +} + +void WorldServer::activateLiquidRegion(RectI const& region) { + m_liquidEngine->visitRegion(region); +} + +void WorldServer::activateLiquidLocation(Vec2I const& location) { + m_liquidEngine->visitLocation(location); +} + +void WorldServer::requestGlobalBreakCheck() { + m_needsGlobalBreakCheck = true; +} + +void WorldServer::setSpawningEnabled(bool spawningEnabled) { + m_spawner.setActive(spawningEnabled); +} + +TileModificationList WorldServer::validTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) const { + return WorldImpl::splitTileModifications(m_tileArray, m_entityMap, modificationList, allowEntityOverlap, [this](Vec2I pos, TileModification) { + return !isTileProtected(pos); + }).first; +} + +TileModificationList WorldServer::applyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) { + return doApplyTileModifications(modificationList, allowEntityOverlap); +} + +bool WorldServer::forceModifyTile(Vec2I const& pos, TileModification const& modification, bool allowEntityOverlap) { + return forceApplyTileModifications({{pos, modification}}, allowEntityOverlap).empty(); +} + +TileModificationList WorldServer::forceApplyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap) { + return doApplyTileModifications(modificationList, allowEntityOverlap, true); +} + +TileDamageResult WorldServer::damageTiles(List<Vec2I> const& positions, TileLayer layer, Vec2F const& sourcePosition, TileDamage const& damage, Maybe<EntityId> sourceEntity) { + Set<Vec2I> positionSet; + for (auto const& pos : positions) + positionSet.add(m_geometry.xwrap(pos)); + + Set<EntityPtr> damagedEntities; + auto res = TileDamageResult::None; + + for (auto const& pos : positionSet) { + if (auto tile = m_tileArray->modifyTile(pos)) { + auto tileDamage = damage; + if (isTileProtected(pos)) + tileDamage.type = TileDamageType::Protected; + + auto tileRes = TileDamageResult::None; + if (layer == TileLayer::Foreground) { + Vec2I entityDamagePos = pos; + Set<Vec2I> damagePositionSet = Set<Vec2I>(positionSet); + if (tile->rootSource) { + entityDamagePos = tile->rootSource.value(); + damagePositionSet.add(entityDamagePos); + } + + for (auto entity : m_entityMap->entitiesAtTile(entityDamagePos)) { + if (!damagedEntities.contains(entity)) { + Set<Vec2I> entitySpacesSet; + for (auto const& space : entity->spaces()) + entitySpacesSet.add(m_geometry.xwrap(entity->tilePosition() + space)); + + bool broken = entity->damageTiles(entitySpacesSet.intersection(damagePositionSet).values(), sourcePosition, tileDamage); + if (sourceEntity.isValid() && broken) { + Maybe<String> name; + if (auto object = as<Object>(entity)) + name = object->name(); + sendEntityMessage(*sourceEntity, "tileEntityBroken", { + jsonFromVec2I(pos), + EntityTypeNames.getRight(entity->entityType()), + jsonFromMaybe(name), + }); + } + + if (tileDamage.type == TileDamageType::Protected) + tileRes = TileDamageResult::Protected; + else + tileRes = TileDamageResult::Normal; + + damagedEntities.add(entity); + } + } + } + + // Penetrating damage should carry through to the blocks behind this + // entity. + if (tileRes == TileDamageResult::None || tileDamageIsPenetrating(tileDamage.type)) { + auto materialDatabase = Root::singleton().materialDatabase(); + + if (layer == TileLayer::Foreground && isRealMaterial(tile->foreground)) { + if (!tile->rootSource) { + if (isRealMod(tile->foregroundMod)) { + if (tileDamageIsPenetrating(tileDamage.type)) + tile->foregroundDamage.damage(materialDatabase->materialDamageParameters(tile->foreground), sourcePosition, tileDamage); + else if (materialDatabase->modBreaksWithTile(tile->foregroundMod)) + tile->foregroundDamage.damage(materialDatabase->modDamageParameters(tile->foregroundMod).sum(materialDatabase->materialDamageParameters(tile->foreground)), sourcePosition, tileDamage); + else + tile->foregroundDamage.damage(materialDatabase->modDamageParameters(tile->foregroundMod), sourcePosition, tileDamage); + } else { + tile->foregroundDamage.damage(materialDatabase->materialDamageParameters(tile->foreground), sourcePosition, tileDamage); + } + + // if the tile is broken, send a message back to the source entity with position, layer, dungeonId, and whether the tile was harvested + if (sourceEntity.isValid() && tile->foregroundDamage.dead()) { + sendEntityMessage(*sourceEntity, "tileBroken", { + jsonFromVec2I(pos), + TileLayerNames.getRight(TileLayer::Foreground), + tile->foreground, + tile->dungeonId, + tile->foregroundDamage.harvested(), + }); + } + + queueTileDamageUpdates(pos, TileLayer::Foreground); + m_damagedBlocks.add(pos); + + if (tileDamage.type == TileDamageType::Protected) + tileRes = TileDamageResult::Protected; + else + tileRes = TileDamageResult::Normal; + } + } else if (layer == TileLayer::Background && isRealMaterial(tile->background)) { + if (isRealMod(tile->backgroundMod)) { + if (tileDamageIsPenetrating(tileDamage.type)) + tile->backgroundDamage.damage(materialDatabase->materialDamageParameters(tile->background), sourcePosition, tileDamage); + else if (materialDatabase->modBreaksWithTile(tile->backgroundMod)) + tile->backgroundDamage.damage(materialDatabase->modDamageParameters(tile->backgroundMod).sum(materialDatabase->materialDamageParameters(tile->background)), sourcePosition, tileDamage); + else + tile->backgroundDamage.damage(materialDatabase->modDamageParameters(tile->backgroundMod), sourcePosition, tileDamage); + } else { + tile->backgroundDamage.damage(materialDatabase->materialDamageParameters(tile->background), sourcePosition, tileDamage); + } + + // if the tile is broken, send a message back to the source entity with position and whether the tile was harvested + if (sourceEntity.isValid() && tile->backgroundDamage.dead()) { + sendEntityMessage(*sourceEntity, "tileBroken", { + jsonFromVec2I(pos), + TileLayerNames.getRight(TileLayer::Background), + tile->background, + tile->dungeonId, + tile->backgroundDamage.harvested(), + }); + } + + queueTileDamageUpdates(pos, TileLayer::Background); + m_damagedBlocks.add(pos); + + if (tileDamage.type == TileDamageType::Protected) + tileRes = TileDamageResult::Protected; + else + tileRes = TileDamageResult::Normal; + } + } + + res = max<TileDamageResult>(res, tileRes); + } + } + + return res; +} + +DungeonId WorldServer::dungeonId(Vec2I const& pos) const { + return m_tileArray->tile(pos).dungeonId; +} + +bool WorldServer::isPlayerModified(RectI const& region) const { + return m_tileArray->tileSatisfies(region, [](Vec2I const&, ServerTile const& tile) { + return tile.dungeonId == ConstructionDungeonId || tile.dungeonId == DestroyedBlockDungeonId; + }); +} + +ItemDescriptor WorldServer::collectLiquid(List<Vec2I> const& tilePositions, LiquidId liquidId) { + float bucketSize = Root::singleton().assets()->json("/items/defaultParameters.config:liquidItems.bucketSize").toFloat(); + unsigned drainedUnits = 0; + float nextUnit = bucketSize; + List<ServerTile*> maybeDrainTiles; + + for (auto pos : tilePositions) { + ServerTile* tile = m_tileArray->modifyTile(pos); + if (tile->liquid.liquid == liquidId && !isTileProtected(pos)) { + if (tile->liquid.level >= nextUnit) { + tile->liquid.take(nextUnit); + nextUnit = bucketSize; + drainedUnits++; + + for (auto previousTile : maybeDrainTiles) + previousTile->liquid.take(previousTile->liquid.level); + + maybeDrainTiles.clear(); + } + + if (tile->liquid.level > 0) { + nextUnit -= tile->liquid.level; + maybeDrainTiles.append(tile); + } + + for (auto const& pair : m_clientInfo) { + if (pair.second->activeSectors.contains(m_tileArray->sectorFor(pos))) + pair.second->pendingLiquidUpdates.add(pos); + } + m_liquidEngine->visitLocation(pos); + } + } + + if (drainedUnits > 0) { + auto liquidConfig = Root::singleton().liquidsDatabase()->liquidSettings(liquidId); + if (liquidConfig && liquidConfig->itemDrop) + return liquidConfig->itemDrop.multiply(drainedUnits); + } + + return ItemDescriptor(); +} + +bool WorldServer::placeDungeon(String const& dungeonName, Vec2I const& position, Maybe<DungeonId> dungeonId, bool forcePlacement) { + m_generatingDungeon = true; + m_tileProtectionEnabled = false; + + auto seed = worldTemplate()->seedFor(position[0], position[1]); + auto facade = make_shared<DungeonGeneratorWorld>(this, true); + bool placed = false; + DungeonGenerator dungeonGenerator(dungeonName, seed, m_worldTemplate->threatLevel(), dungeonId); + if (auto generateResult = dungeonGenerator.generate(facade, position, false, forcePlacement)) { + auto worldGenerator = make_shared<WorldGenerator>(this); + for (auto position : generateResult->second) { + if (ServerTile* tile = modifyServerTile(position)) + worldGenerator->replaceBiomeBlocks(tile); + } + placed = true; + } + + m_tileProtectionEnabled = true; + m_generatingDungeon = false; + + return placed; +} + +void WorldServer::addBiomeRegion(Vec2I const& position, String const& biomeName, String const& subBlockSelector, int width) { + width = std::min(width, (int)m_worldTemplate->size()[0]); + + auto regions = m_worldTemplate->previewAddBiomeRegion(position, width); + + if (regions.empty()) { + Logger::info("Aborting addBiomeRegion as it would have no result!"); + return; + } + + for (auto region : regions) { + for (auto sector : m_worldStorage->sectorsForRegion(region)) + m_worldStorage->triggerTerraformSector(sector); + } + + m_worldTemplate->addBiomeRegion(position, biomeName, subBlockSelector, width); +} + +void WorldServer::expandBiomeRegion(Vec2I const& position, int newWidth) { + newWidth = std::min(newWidth, (int)m_worldTemplate->size()[0]); + + auto regions = m_worldTemplate->previewExpandBiomeRegion(position, newWidth); + + if (regions.empty()) { + Logger::info("Aborting expandBiomeRegion as it would have no result!"); + return; + } + + for (auto region : regions) { + for (auto sector : m_worldStorage->sectorsForRegion(region)) + m_worldStorage->triggerTerraformSector(sector); + } + + m_worldTemplate->expandBiomeRegion(position, newWidth); +} + +bool WorldServer::pregenerateAddBiome(Vec2I const& position, int width) { + auto regions = m_worldTemplate->previewAddBiomeRegion(position, width); + + bool generationComplete = true; + for (auto region : regions) + generationComplete = generationComplete && signalRegion(region); + + return generationComplete; +} + +bool WorldServer::pregenerateExpandBiome(Vec2I const& position, int newWidth) { + auto regions = m_worldTemplate->previewExpandBiomeRegion(position, newWidth); + + bool generationComplete = true; + for (auto region : regions) + generationComplete = generationComplete && signalRegion(region); + + return generationComplete; +} + +void WorldServer::setLayerEnvironmentBiome(Vec2I const& position) { + auto biomeName = m_worldTemplate->worldLayout()->setLayerEnvironmentBiome(position); + + auto layoutJson = m_worldTemplate->worldLayout()->toJson(); + for (auto const& pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<WorldLayoutUpdatePacket>(layoutJson)); +} + +void WorldServer::setPlanetType(String const& planetType, String const& primaryBiomeName) { + if (auto terrestrialParameters = as<TerrestrialWorldParameters>(m_worldTemplate->worldParameters())) { + if (terrestrialParameters->typeName != planetType) { + auto newTerrestrialParameters = make_shared<TerrestrialWorldParameters>(*terrestrialParameters); + + newTerrestrialParameters->typeName = planetType; + newTerrestrialParameters->primaryBiome = primaryBiomeName; + + auto biomeDatabase = Root::singleton().biomeDatabase(); + auto newWeatherPool = biomeDatabase->biomeWeathers(primaryBiomeName, m_worldTemplate->worldSeed(), m_worldTemplate->threatLevel()); + newTerrestrialParameters->weatherPool = newWeatherPool; + + auto newSkyColors = biomeDatabase->biomeSkyColoring(primaryBiomeName, m_worldTemplate->worldSeed()); + newTerrestrialParameters->skyColoring = newSkyColors; + + newTerrestrialParameters->airless = biomeDatabase->biomeIsAirless(primaryBiomeName); + newTerrestrialParameters->environmentStatusEffects = {}; + + newTerrestrialParameters->terraformed = true; + + m_worldTemplate->setWorldParameters(newTerrestrialParameters); + + for (auto const& pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<WorldParametersUpdatePacket>(netStoreVisitableWorldParameters(newTerrestrialParameters))); + + auto newSkyParameters = SkyParameters(m_worldTemplate->skyParameters(), newTerrestrialParameters); + m_worldTemplate->setSkyParameters(newSkyParameters); + + auto referenceClock = m_sky->referenceClock(); + m_sky = make_shared<Sky>(m_worldTemplate->skyParameters(), false); + m_sky->setReferenceClock(referenceClock); + + m_weather.setup(m_worldTemplate->weathers(), m_worldTemplate->undergroundLevel(), m_geometry, [this](Vec2I const& pos) { + auto const& tile = m_tileArray->tile(pos); + return !isRealMaterial(tile.background); + }); + + m_newPlanetType = pair<String, String>{planetType, primaryBiomeName}; + } + } +} + +Maybe<pair<String, String>> WorldServer::pullNewPlanetType() { + if (m_newPlanetType) + return m_newPlanetType.take(); + return {}; +} + +bool WorldServer::isTileProtected(Vec2I const& pos) const { + if (!m_tileProtectionEnabled) + return false; + + auto tile = m_tileArray->tile(pos); + return m_protectedDungeonIds.contains(tile.dungeonId); +} + +void WorldServer::setTileProtection(DungeonId dungeonId, bool isProtected) { + bool updated = false; + if (isProtected) { + updated = m_protectedDungeonIds.add(dungeonId); + } else { + updated = m_protectedDungeonIds.remove(dungeonId); + } + + if (updated) { + for (auto const& pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<UpdateTileProtectionPacket>(dungeonId, isProtected)); + } + + Logger::info("Protected dungeonIds for world set to %s", m_protectedDungeonIds); +} + +void WorldServer::setTileProtectionEnabled(bool enabled) { + m_tileProtectionEnabled = enabled; +} + +void WorldServer::setDungeonId(RectI const& tileArea, DungeonId dungeonId) { + for (int x = tileArea.xMin(); x < tileArea.xMax(); ++x) { + for (int y = tileArea.yMin(); y < tileArea.yMax(); ++y) { + auto pos = Vec2I{x, y}; + if (auto tile = m_tileArray->modifyTile(pos)) { + tile->dungeonId = dungeonId; + queueTileUpdates(pos); + } + } + } +} + +void WorldServer::setDungeonGravity(DungeonId dungeonId, Maybe<float> gravity) { + Maybe<float> current = m_dungeonIdGravity.maybe(dungeonId); + if (gravity != current) { + if (gravity) + m_dungeonIdGravity[dungeonId] = *gravity; + else + m_dungeonIdGravity.remove(dungeonId); + + for (auto const& p : m_clientInfo) + p.second->outgoingPackets.append(make_shared<SetDungeonGravityPacket>(dungeonId, gravity)); + } +} + +float WorldServer::gravity(Vec2F const& pos) const { + return gravityFromTile(m_tileArray->tile(Vec2I::round(pos))); +} + +float WorldServer::gravityFromTile(ServerTile const& tile) const { + return m_dungeonIdGravity.maybe(tile.dungeonId).value(m_worldTemplate->gravity()); +} + +bool WorldServer::isFloatingDungeonWorld() const { + return m_worldTemplate && m_worldTemplate->worldParameters() + && m_worldTemplate->worldParameters()->type() == WorldParametersType::FloatingDungeonWorldParameters; +} + +void WorldServer::init(bool firstTime) { + auto& root = Root::singleton(); + auto assets = root.assets(); + auto liquidsDatabase = root.liquidsDatabase(); + + m_serverConfig = assets->json("/worldserver.config"); + setFidelity(WorldServerFidelity::Medium); + + m_worldStorage->setFloatingDungeonWorld(isFloatingDungeonWorld()); + + m_currentStep = 0; + m_generatingDungeon = false; + m_geometry = WorldGeometry(m_worldTemplate->size()); + m_entityMap = m_worldStorage->entityMap(); + m_tileArray = m_worldStorage->tileArray(); + m_damageManager = make_shared<DamageManager>(this, ServerConnectionId); + m_wireProcessor = make_shared<WireProcessor>(m_worldStorage); + m_luaRoot = make_shared<LuaRoot>(); + m_luaRoot->tuneAutoGarbageCollection(m_serverConfig.getFloat("luaGcPause"), m_serverConfig.getFloat("luaGcStepMultiplier")); + + m_sky = make_shared<Sky>(m_worldTemplate->skyParameters(), false); + + m_lightIntensityCalculator.setParameters(assets->json("/lighting.config:intensity")); + + m_entityMessageResponses = {}; + + m_collisionGenerator.init([=](int x, int y) { + return m_tileArray->tile({x, y}).collision; + }); + + m_tileEntityBreakCheckTimer = GameTimer(m_serverConfig.getFloat("tileEntityBreakCheckInterval")); + + m_liquidEngine = make_shared<LiquidCellEngine<LiquidId>>(liquidsDatabase->liquidEngineParameters(), make_shared<LiquidWorld>(this)); + for (auto liquidSettings : liquidsDatabase->allLiquidSettings()) + m_liquidEngine->setLiquidTickDelta(liquidSettings->id, liquidSettings->tickDelta); + + m_fallingBlocksAgent = make_shared<FallingBlocksAgent>(make_shared<FallingBlocksWorld>(this)); + + setupForceRegions(); + + setTileProtection(ProtectedZeroGDungeonId, true); + + try { + m_spawner.init(make_shared<SpawnerWorld>(this)); + + RandomSource rnd = RandomSource(m_worldTemplate->worldSeed()); + + if (firstTime) { + m_generatingDungeon = true; + DungeonId currentDungeonId = 0; + + for (auto const& dungeon : m_worldTemplate->dungeons()) { + Logger::info("Placing dungeon %s", dungeon.dungeon); + int retryCounter = m_serverConfig.getInt("spawnDungeonRetries"); + while (retryCounter > 0) { + --retryCounter; + auto dungeonFacade = make_shared<DungeonGeneratorWorld>(this, true); + Vec2I position = Vec2I((dungeon.baseX + rnd.randInt(0, dungeon.xVariance)) % m_geometry.width(), dungeon.baseHeight); + DungeonGenerator dungeonGenerator(dungeon.dungeon, m_worldTemplate->worldSeed(), m_worldTemplate->threatLevel(), currentDungeonId); + if (auto generateResult = dungeonGenerator.generate(dungeonFacade, position, dungeon.blendWithTerrain, dungeon.force)) { + if (dungeonGenerator.definition()->isProtected()) + setTileProtection(currentDungeonId, true); + + if (auto gravity = dungeonGenerator.definition()->gravity()) + m_dungeonIdGravity[currentDungeonId] = *gravity; + + if (auto breathable = dungeonGenerator.definition()->breathable()) + m_dungeonIdBreathable[currentDungeonId] = *breathable; + + currentDungeonId++; + + // floating dungeon worlds should force immediate generation (since there won't be terrain) to avoid + // bottlenecking "generation" of empty generation levels during loading + if (isFloatingDungeonWorld()) { + for (auto region : generateResult->first) + generateRegion(region); + } + + break; + } + } + } + + m_dungeonIdGravity[ZeroGDungeonId] = 0.0; + m_dungeonIdGravity[ProtectedZeroGDungeonId] = 0.0; + + m_generatingDungeon = false; + } + + if (m_adjustPlayerStart) + m_playerStart = findPlayerStart(firstTime ? Maybe<Vec2F>() : m_playerStart); + + generateRegion(RectI::integral(RectF(m_playerStart, m_playerStart)).padded(m_serverConfig.getInt("playerStartInitialGenRadius"))); + + m_weather.setup(m_worldTemplate->weathers(), m_worldTemplate->undergroundLevel(), m_geometry, [this](Vec2I const& pos) { + auto const& tile = m_tileArray->tile(pos); + return !isRealMaterial(tile.background); + }); + } catch (std::exception const& e) { + m_worldStorage->unloadAll(true); + throw WorldServerException("Exception encountered initializing world", e); + } +} + +Maybe<unsigned> WorldServer::shouldRunThisStep(String const& timingConfiguration) { + Vec2U timing = jsonToVec2U(m_fidelityConfig.get(timingConfiguration)); + if ((m_currentStep + timing[1]) % timing[0] == 0) + return timing[0]; + return {}; +} + +TileModificationList WorldServer::doApplyTileModifications(TileModificationList const& modificationList, bool allowEntityOverlap, bool ignoreTileProtection) { + auto materialDatabase = Root::singleton().materialDatabase(); + + TileModificationList unapplied = modificationList; + size_t unappliedSize = unapplied.size(); + auto it = makeSMutableIterator(unapplied); + while (it.hasNext()) { + Vec2I pos; + TileModification modification; + std::tie(pos, modification) = it.next(); + + if (!ignoreTileProtection && isTileProtected(pos)) + continue; + + if (auto placeMaterial = modification.ptr<PlaceMaterial>()) { + if (!WorldImpl::canPlaceMaterial(m_tileArray, m_entityMap, pos, placeMaterial->layer, placeMaterial->material, allowEntityOverlap)) + continue; + + ServerTile* tile = m_tileArray->modifyTile(pos); + if (!tile) + continue; + + if (placeMaterial->layer == TileLayer::Background) { + tile->background = placeMaterial->material; + if (placeMaterial->materialHueShift) + tile->backgroundHueShift = *placeMaterial->materialHueShift; + else + tile->backgroundHueShift = m_worldTemplate->biomeMaterialHueShift(tile->blockBiomeIndex, placeMaterial->material); + + tile->backgroundColorVariant = DefaultMaterialColorVariant; + if (tile->background == EmptyMaterialId) { + // Remove the background mod if removing the background. + tile->backgroundMod = NoModId; + } else if (tile->liquid.source) { + tile->liquid.pressure = 1.0f; + tile->liquid.source = false; + } + } else { + tile->foreground = placeMaterial->material; + if (placeMaterial->materialHueShift) + tile->foregroundHueShift = *placeMaterial->materialHueShift; + else + tile->foregroundHueShift = m_worldTemplate->biomeMaterialHueShift(tile->blockBiomeIndex, placeMaterial->material); + + tile->foregroundColorVariant = DefaultMaterialColorVariant; + tile->updateCollision(materialDatabase->materialCollisionKind(tile->foreground)); + if (tile->foreground == EmptyMaterialId) { + // Remove the foreground mod if removing the foreground. + tile->foregroundMod = NoModId; + } + if (materialDatabase->blocksLiquidFlow(tile->foreground)) + tile->liquid = LiquidStore(); + } + + tile->dungeonId = ConstructionDungeonId; + + checkEntityBreaks(RectF::withSize(Vec2F(pos), Vec2F(1, 1))); + m_liquidEngine->visitLocation(pos); + m_fallingBlocksAgent->visitLocation(pos); + if (placeMaterial->layer == TileLayer::Foreground) + dirtyCollision(RectI::withSize(pos, {1, 1})); + queueTileUpdates(pos); + + } else if (auto placeMod = modification.ptr<PlaceMod>()) { + if (!WorldImpl::canPlaceMod(m_tileArray, pos, placeMod->layer, placeMod->mod)) + continue; + + ServerTile* tile = m_tileArray->modifyTile(pos); + if (!tile) + continue; + + if (placeMod->layer == TileLayer::Background) { + tile->backgroundMod = placeMod->mod; + if (placeMod->modHueShift) + tile->backgroundModHueShift = *placeMod->modHueShift; + else + tile->backgroundModHueShift = m_worldTemplate->biomeModHueShift(tile->blockBiomeIndex, placeMod->mod); + } else { + tile->foregroundMod = placeMod->mod; + if (placeMod->modHueShift) + tile->foregroundModHueShift = *placeMod->modHueShift; + else + tile->foregroundModHueShift = m_worldTemplate->biomeModHueShift(tile->blockBiomeIndex, placeMod->mod); + } + + m_liquidEngine->visitLocation(pos); + queueTileUpdates(pos); + + } else if (auto placeMaterialColor = modification.ptr<PlaceMaterialColor>()) { + if (!WorldImpl::canPlaceMaterialColorVariant(m_tileArray, pos, placeMaterialColor->layer, placeMaterialColor->color)) + continue; + + WorldTile* tile = m_tileArray->modifyTile(pos); + if (!tile) + continue; + + if (placeMaterialColor->layer == TileLayer::Background) { + tile->backgroundHueShift = 0; + if (!materialDatabase->isMultiColor(tile->background)) + continue; + tile->backgroundColorVariant = placeMaterialColor->color; + } else { + tile->foregroundHueShift = 0; + if (!materialDatabase->isMultiColor(tile->foreground)) + continue; + tile->foregroundColorVariant = placeMaterialColor->color; + } + + queueTileUpdates(pos); + + } else if (auto plpacket = modification.ptr<PlaceLiquid>()) { + modifyLiquid(pos, plpacket->liquid, plpacket->liquidLevel, true); + m_liquidEngine->visitLocation(pos); + m_fallingBlocksAgent->visitLocation(pos); + } + + it.remove(); + + if (!it.hasNext()) { + // If we are at the end, but have made progress by applying at least one + // modification, then start over at the beginning and keep trying more + // modifications until we can't make any more progress. + if (unapplied.size() != unappliedSize) { + unappliedSize = unapplied.size(); + it.toFront(); + } + } + } + + return unapplied; +} + +void WorldServer::updateTileEntityTiles(TileEntityPtr const& entity, bool removing, bool checkBreaks) { + // This method of updating tile entity collision only works if each tile + // entity's collision spaces are a subset of their normal spaces, and thus no + // two tile entities can have collision spaces that overlap. + // NOTE: Some entities may violate this; it's an odd thing to rely on policy + // for and maybe we shouldn't allow tile entity configurations to specify + // material spaces outside of their spaces + + auto& spaces = m_tileEntitySpaces[entity->entityId()]; + + List<MaterialSpace> newMaterialSpaces = removing ? List<MaterialSpace>() : entity->materialSpaces(); + List<Vec2I> newRoots = removing || entity->ephemeral() ? List<Vec2I>() : entity->roots(); + + if (!removing && spaces.materials == newMaterialSpaces && spaces.roots == newRoots) + return; + + auto materialDatabase = Root::singleton().materialDatabase(); + + // remove all old roots + for (auto const& rootPos : spaces.roots) { + if (auto tile = m_tileArray->modifyTile(rootPos + entity->tilePosition())) + tile->rootSource = {}; + } + + // remove all old material spaces + for (auto const& materialSpace : spaces.materials) { + Vec2I pos = materialSpace.space + entity->tilePosition(); + + ServerTile* tile = m_tileArray->modifyTile(pos); + if (tile) { + tile->foreground = EmptyMaterialId; + tile->foregroundMod = NoModId; + tile->rootSource = {}; + if (tile->updateCollision(materialDatabase->materialCollisionKind(tile->foreground))) { + m_liquidEngine->visitLocation(pos); + m_fallingBlocksAgent->visitLocation(pos); + dirtyCollision(RectI::withSize(pos, {1, 1})); + } + queueTileUpdates(pos); + } + } + + if (removing) { + m_tileEntitySpaces.remove(entity->entityId()); + + } else { + // add new material spaces and update the known material spaces entry + for (auto const& materialSpace : newMaterialSpaces) { + Vec2I pos = materialSpace.space + entity->tilePosition(); + + ServerTile* tile = m_tileArray->modifyTile(pos); + if (tile) { + tile->foreground = materialSpace.material; + tile->foregroundMod = NoModId; + if (isRealMaterial(materialSpace.material)) + tile->rootSource = entity->tilePosition(); + if (tile->updateCollision(materialDatabase->materialCollisionKind(tile->foreground))) { + m_liquidEngine->visitLocation(pos); + m_fallingBlocksAgent->visitLocation(pos); + dirtyCollision(RectI::withSize(pos, {1, 1})); + } + queueTileUpdates(pos); + } + } + spaces.materials = move(newMaterialSpaces); + + // add new roots and update known roots entry + for (auto const& rootPos : newRoots) { + if (auto tile = m_tileArray->modifyTile(rootPos + entity->tilePosition())) + tile->rootSource = entity->tilePosition(); + } + spaces.roots = move(newRoots); + } + + // check whether we've broken any other nearby entities + if (checkBreaks) + checkEntityBreaks(entity->metaBoundBox().translated(entity->position())); +} + +ConnectionId WorldServer::connection() const { + return ServerConnectionId; +} + +bool WorldServer::signalRegion(RectI const& region) { + auto sectors = m_worldStorage->sectorsForRegion(region); + if (m_generatingDungeon) { + // When generating a dungeon, all sector activations should immediately + // load whatever is available and make the sector active for writing, but + // should trigger no generation (for world generation speed). + for (auto const& sector : sectors) + m_worldStorage->loadSector(sector); + } else { + for (auto const& sector : sectors) + m_worldStorage->queueSectorActivation(sector); + } + for (auto const& sector : sectors) { + if (!m_worldStorage->sectorActive(sector)) + return false; + } + return true; +} + +void WorldServer::generateRegion(RectI const& region) { + for (auto sector : m_worldStorage->sectorsForRegion(region)) + m_worldStorage->activateSector(sector); +} + +bool WorldServer::regionActive(RectI const& region) { + for (auto const& sector : m_worldStorage->sectorsForRegion(region)) { + if (!m_worldStorage->sectorActive(sector)) + return false; + } + return true; +} + +RpcPromise<Vec2I> WorldServer::enqueuePlacement(List<BiomeItemDistribution> distributions, Maybe<DungeonId> id) { + return m_worldStorage->enqueuePlacement(move(distributions), id); +} + +ServerTile const& WorldServer::getServerTile(Vec2I const& position, bool withSignal) { + if (withSignal) + signalRegion(RectI::withSize(position, {1, 1})); + + return m_tileArray->tile(position); +} + +ServerTile* WorldServer::modifyServerTile(Vec2I const& position, bool withSignal) { + if (withSignal) + signalRegion(RectI::withSize(position, {1, 1})); + + auto tile = m_tileArray->modifyTile(position); + if (tile) { + dirtyCollision(RectI::withSize(position, {1, 1})); + m_liquidEngine->visitLocation(position); + queueTileUpdates(position); + } + return tile; +} + +EntityId WorldServer::loadUniqueEntity(String const& uniqueId) { + return m_worldStorage->loadUniqueEntity(uniqueId); +} + +WorldTemplatePtr WorldServer::worldTemplate() const { + return m_worldTemplate; +} + +SkyPtr WorldServer::sky() const { + return m_sky; +} + +void WorldServer::modifyLiquid(Vec2I const& pos, LiquidId liquid, float quantity, bool additive) { + if (liquid == EmptyLiquidId) + quantity = 0; + + if (ServerTile* tile = m_tileArray->modifyTile(pos)) { + auto materialDatabase = Root::singleton().materialDatabase(); + if (tile->foreground == EmptyMaterialId || !isSolidColliding(materialDatabase->materialCollisionKind(tile->foreground))) { + if (additive && liquid == tile->liquid.liquid) + quantity += tile->liquid.level; + + setLiquid(pos, liquid, quantity, tile->liquid.pressure); + m_liquidEngine->visitLocation(pos); + } + } +} + +void WorldServer::setLiquid(Vec2I const& pos, LiquidId liquid, float level, float pressure) { + if (ServerTile* tile = m_tileArray->modifyTile(pos)) { + if (liquid == EmptyLiquidId) + level = 0; + + if (auto netUpdate = tile->liquid.update(liquid, level, pressure)) { + for (auto const& pair : m_clientInfo) { + if (pair.second->activeSectors.contains(m_tileArray->sectorFor(pos))) + pair.second->pendingLiquidUpdates.add(pos); + } + } + } +} + +List<ItemDescriptor> WorldServer::destroyBlock(TileLayer layer, Vec2I const& pos, bool genItems, bool destroyModFirst) { + auto materialDatabase = Root::singleton().materialDatabase(); + + auto* tile = m_tileArray->modifyTile(pos); + if (!tile) + return {}; + + List<ItemDescriptor> drops; + + if (layer == TileLayer::Background) { + if (isRealMod(tile->backgroundMod) && destroyModFirst + && !materialDatabase->modBreaksWithTile(tile->backgroundMod)) { + if (genItems) { + if (auto drop = materialDatabase->modItemDrop(tile->backgroundMod)) + drops.append(drop); + } + tile->backgroundMod = NoModId; + } else { + if (genItems) { + if (auto drop = materialDatabase->materialItemDrop(tile->background)) + drops.append(drop); + if (auto drop = materialDatabase->modItemDrop(tile->backgroundMod)) + drops.append(drop); + } + tile->background = EmptyMaterialId; + tile->backgroundColorVariant = DefaultMaterialColorVariant; + tile->backgroundHueShift = 0; + tile->backgroundMod = NoModId; + } + + tile->backgroundDamage.reset(); + + } else { + if (isRealMod(tile->foregroundMod) && destroyModFirst + && !materialDatabase->modBreaksWithTile(tile->foregroundMod)) { + if (genItems) { + if (auto drop = materialDatabase->modItemDrop(tile->foregroundMod)) + drops.append(drop); + } + tile->foregroundMod = NoModId; + } else { + if (genItems) { + if (auto drop = materialDatabase->materialItemDrop(tile->foreground)) + drops.append(drop); + if (auto drop = materialDatabase->modItemDrop(tile->foregroundMod)) + drops.append(drop); + } + tile->foreground = EmptyMaterialId; + tile->foregroundColorVariant = DefaultMaterialColorVariant; + tile->foregroundHueShift = 0; + tile->foregroundMod = NoModId; + tile->updateCollision(CollisionKind::None); + dirtyCollision(RectI::withSize(pos, {1, 1})); + } + + tile->foregroundDamage.reset(); + } + + if (tile->background == EmptyMaterialId && tile->foreground == EmptyMaterialId) { + auto blockInfo = m_worldTemplate->blockInfo(pos[0], pos[1]); + if (blockInfo.oceanLiquid != EmptyLiquidId && !blockInfo.encloseLiquids && pos[1] < blockInfo.oceanLiquidLevel) + tile->liquid = LiquidStore::endless(blockInfo.oceanLiquid, blockInfo.oceanLiquidLevel - pos[1]); + } + + tile->dungeonId = DestroyedBlockDungeonId; + + checkEntityBreaks(RectF::withSize(Vec2F(pos), Vec2F(1, 1))); + m_liquidEngine->visitLocation(pos); + m_fallingBlocksAgent->visitLocation(pos); + queueTileUpdates(pos); + queueTileDamageUpdates(pos, layer); + + return drops; +} + +void WorldServer::queueUpdatePackets(ConnectionId clientId) { + auto const& clientInfo = m_clientInfo.get(clientId); + clientInfo->outgoingPackets.append(make_shared<StepUpdatePacket>(m_currentStep)); + + if (shouldRunThisStep("environmentUpdate")) { + ByteArray skyDelta; + tie(skyDelta, clientInfo->skyNetVersion) = m_sky->writeUpdate(clientInfo->skyNetVersion); + + ByteArray weatherDelta; + tie(weatherDelta, clientInfo->weatherNetVersion) = m_weather.writeUpdate(clientInfo->weatherNetVersion); + + if (!skyDelta.empty() || !weatherDelta.empty()) + clientInfo->outgoingPackets.append(make_shared<EnvironmentUpdatePacket>(move(skyDelta), move(weatherDelta))); + } + + for (auto sector : clientInfo->pendingSectors.values()) { + if (!m_worldStorage->sectorActive(sector)) + continue; + + auto tileArrayUpdate = make_shared<TileArrayUpdatePacket>(); + auto sectorTiles = m_tileArray->sectorRegion(sector); + tileArrayUpdate->min = sectorTiles.min(); + tileArrayUpdate->array.resize(Vec2S(sectorTiles.width(), sectorTiles.height())); + for (int x = sectorTiles.xMin(); x < sectorTiles.xMax(); ++x) { + for (int y = sectorTiles.yMin(); y < sectorTiles.yMax(); ++y) + writeNetTile({x, y}, tileArrayUpdate->array(x - sectorTiles.xMin(), y - sectorTiles.yMin())); + } + + clientInfo->outgoingPackets.append(tileArrayUpdate); + clientInfo->pendingSectors.remove(sector); + } + + for (auto pos : clientInfo->pendingTileUpdates) { + auto tileUpdate = make_shared<TileUpdatePacket>(); + tileUpdate->position = pos; + writeNetTile(pos, tileUpdate->tile); + + clientInfo->outgoingPackets.append(tileUpdate); + } + clientInfo->pendingTileUpdates.clear(); + + for (auto pair : clientInfo->pendingTileDamageUpdates) { + auto tile = m_tileArray->tile(pair.first); + if (pair.second == TileLayer::Foreground) + clientInfo->outgoingPackets.append( + make_shared<TileDamageUpdatePacket>(pair.first, TileLayer::Foreground, tile.foregroundDamage)); + else + clientInfo->outgoingPackets.append( + make_shared<TileDamageUpdatePacket>(pair.first, TileLayer::Background, tile.backgroundDamage)); + } + clientInfo->pendingTileDamageUpdates.clear(); + + for (auto pos : clientInfo->pendingLiquidUpdates) { + auto tile = m_tileArray->tile(pos); + clientInfo->outgoingPackets.append(make_shared<TileLiquidUpdatePacket>(pos, tile.liquid.netUpdate())); + } + clientInfo->pendingLiquidUpdates.clear(); + + HashSet<EntityPtr> monitoredEntities; + for (auto const& monitoredRegion : clientInfo->monitoringRegions(m_entityMap)) + monitoredEntities.addAll(m_entityMap->entityQuery(RectF(monitoredRegion))); + + auto entityFactory = Root::singleton().entityFactory(); + auto outOfMonitoredRegionsEntities = HashSet<EntityId>::from(clientInfo->clientSlavesNetVersion.keys()); + for (auto const& monitoredEntity : monitoredEntities) + outOfMonitoredRegionsEntities.remove(monitoredEntity->entityId()); + for (auto entityId : outOfMonitoredRegionsEntities) { + clientInfo->outgoingPackets.append(make_shared<EntityDestroyPacket>(entityId, ByteArray(), false)); + clientInfo->clientSlavesNetVersion.remove(entityId); + } + + HashMap<ConnectionId, shared_ptr<EntityUpdateSetPacket>> updateSetPackets; + if (m_currentStep % clientInfo->interpolationTracker.entityUpdateDelta() == 0) + updateSetPackets.add(ServerConnectionId, make_shared<EntityUpdateSetPacket>(ServerConnectionId)); + for (auto const& p : m_clientInfo) { + if (p.first != clientId && p.second->pendingForward) + updateSetPackets.add(p.first, make_shared<EntityUpdateSetPacket>(p.first)); + } + + for (auto const& monitoredEntity : monitoredEntities) { + EntityId entityId = monitoredEntity->entityId(); + ConnectionId connectionId = connectionForEntity(entityId); + if (connectionId != clientId) { + if (auto version = clientInfo->clientSlavesNetVersion.ptr(entityId)) { + if (auto updateSetPacket = updateSetPackets.value(connectionId)) { + auto updateAndVersion = monitoredEntity->writeNetState(*version); + if (!updateAndVersion.first.empty()) + updateSetPacket->deltas[entityId] = move(updateAndVersion.first); + *version = updateAndVersion.second; + } + } else if (!monitoredEntity->masterOnly()) { + // Client was unaware of this entity until now + auto firstUpdate = monitoredEntity->writeNetState(); + clientInfo->clientSlavesNetVersion.add(entityId, firstUpdate.second); + clientInfo->outgoingPackets.append(make_shared<EntityCreatePacket>(monitoredEntity->entityType(), + entityFactory->netStoreEntity(monitoredEntity), move(firstUpdate.first), entityId)); + } + } + } + + for (auto& p : updateSetPackets) + clientInfo->outgoingPackets.append(move(p.second)); +} + +void WorldServer::updateDamage() { + m_damageManager->update(); + + // Do nothing with damage notifications at the moment. + m_damageManager->pullPendingNotifications(); + + for (auto const& remoteHitRequest : m_damageManager->pullRemoteHitRequests()) + m_clientInfo.get(remoteHitRequest.destinationConnection()) + ->outgoingPackets.append(make_shared<HitRequestPacket>(remoteHitRequest)); + + for (auto const& remoteDamageRequest : m_damageManager->pullRemoteDamageRequests()) + m_clientInfo.get(remoteDamageRequest.destinationConnection()) + ->outgoingPackets.append(make_shared<DamageRequestPacket>(remoteDamageRequest)); + + for (auto const& remoteDamageNotification : m_damageManager->pullRemoteDamageNotifications()) { + for (auto const& pair : m_clientInfo) { + if (pair.second->needsDamageNotification(remoteDamageNotification)) + pair.second->outgoingPackets.append(make_shared<DamageNotificationPacket>(remoteDamageNotification)); + } + } +} + +void WorldServer::sync() { + writeMetadata(); + m_worldStorage->sync(); +} + +WorldChunks WorldServer::readChunks() { + writeMetadata(); + return m_worldStorage->readChunks(); +} + +void WorldServer::updateDamagedBlocks(float dt) { + auto materialDatabase = Root::singleton().materialDatabase(); + + for (auto pos : m_damagedBlocks.values()) { + auto tile = m_tileArray->modifyTile(pos); + if (!tile) { + m_damagedBlocks.remove(pos); + continue; + } + + Vec2F dropPosition = centerOfTile(pos); + if (tile->foregroundDamage.dead()) { + bool harvested = tile->foregroundDamage.harvested(); + for (auto drop : destroyBlock(TileLayer::Foreground, pos, harvested, !tileDamageIsPenetrating(tile->foregroundDamage.damageType()))) + addEntity(ItemDrop::createRandomizedDrop(drop, dropPosition)); + + } else if (tile->foregroundDamage.damaged()) { + if (isRealMaterial(tile->foreground)) { + if (isRealMod(tile->foregroundMod)) { + if (tileDamageIsPenetrating(tile->foregroundDamage.damageType())) + tile->foregroundDamage.recover(materialDatabase->materialDamageParameters(tile->foreground), dt); + else if (materialDatabase->modBreaksWithTile(tile->foregroundMod)) + tile->foregroundDamage.recover(materialDatabase->modDamageParameters(tile->foregroundMod).sum(materialDatabase->materialDamageParameters(tile->foreground)), dt); + else + tile->foregroundDamage.recover(materialDatabase->modDamageParameters(tile->foregroundMod), dt); + } else + tile->foregroundDamage.recover(materialDatabase->materialDamageParameters(tile->foreground), dt); + } else + tile->foregroundDamage.reset(); + + queueTileDamageUpdates(pos, TileLayer::Foreground); + } + + if (tile->backgroundDamage.dead()) { + bool harvested = tile->backgroundDamage.harvested(); + for (auto drop : destroyBlock(TileLayer::Background, pos, harvested, !tileDamageIsPenetrating(tile->backgroundDamage.damageType()))) + addEntity(ItemDrop::createRandomizedDrop(drop, dropPosition)); + + } else if (tile->backgroundDamage.damaged()) { + if (isRealMaterial(tile->background)) { + if (isRealMod(tile->backgroundMod)) { + if (tileDamageIsPenetrating(tile->backgroundDamage.damageType())) + tile->backgroundDamage.recover(materialDatabase->materialDamageParameters(tile->background), dt); + else if (materialDatabase->modBreaksWithTile(tile->backgroundMod)) + tile->backgroundDamage.recover(materialDatabase->modDamageParameters(tile->backgroundMod).sum(materialDatabase->materialDamageParameters(tile->background)), dt); + else + tile->backgroundDamage.recover(materialDatabase->modDamageParameters(tile->backgroundMod), dt); + } else { + tile->backgroundDamage.recover(materialDatabase->materialDamageParameters(tile->background), dt); + } + } else { + tile->backgroundDamage.reset(); + } + + queueTileDamageUpdates(pos, TileLayer::Background); + } + + if (tile->backgroundDamage.healthy() && tile->foregroundDamage.healthy()) + m_damagedBlocks.remove(pos); + } +} + +void WorldServer::checkEntityBreaks(RectF const& rect) { + for (auto tileEntity : m_entityMap->query<TileEntity>(rect)) + tileEntity->checkBroken(); +} + +void WorldServer::queueTileUpdates(Vec2I const& pos) { + for (auto const& pair : m_clientInfo) { + if (pair.second->activeSectors.contains(m_tileArray->sectorFor(pos))) + pair.second->pendingTileUpdates.add(pos); + } +} + +void WorldServer::queueTileDamageUpdates(Vec2I const& pos, TileLayer layer) { + for (auto const& pair : m_clientInfo) { + if (pair.second->activeSectors.contains(m_tileArray->sectorFor(pos))) + pair.second->pendingTileDamageUpdates.add({pos, layer}); + } +} + +void WorldServer::writeNetTile(Vec2I const& pos, NetTile& netTile) const { + auto const& tile = m_tileArray->tile(pos); + netTile.foreground = tile.foreground; + netTile.foregroundHueShift = tile.foregroundHueShift; + netTile.foregroundColorVariant = tile.foregroundColorVariant; + netTile.foregroundMod = tile.foregroundMod; + netTile.foregroundModHueShift = tile.foregroundModHueShift; + netTile.background = tile.background; + netTile.backgroundHueShift = tile.backgroundHueShift; + netTile.backgroundColorVariant = tile.backgroundColorVariant; + netTile.backgroundMod = tile.backgroundMod; + netTile.backgroundModHueShift = tile.backgroundModHueShift; + netTile.liquid = tile.liquid.netUpdate(); + netTile.collision = tile.collision; + netTile.blockBiomeIndex = tile.blockBiomeIndex; + netTile.environmentBiomeIndex = tile.environmentBiomeIndex; + netTile.dungeonId = tile.dungeonId; +} + +void WorldServer::dirtyCollision(RectI const& region) { + auto dirtyRegion = region.padded(CollisionGenerator::BlockInfluenceRadius); + for (int x = dirtyRegion.xMin(); x < dirtyRegion.xMax(); ++x) { + for (int y = dirtyRegion.yMin(); y < dirtyRegion.yMax(); ++y) { + if (auto tile = m_tileArray->modifyTile({x, y})) + tile->collisionCacheDirty = true; + } + } +} + +void WorldServer::freshenCollision(RectI const& region) { + RectI freshenRegion = RectI::null(); + for (int x = region.xMin(); x < region.xMax(); ++x) { + for (int y = region.yMin(); y < region.yMax(); ++y) { + if (auto tile = m_tileArray->modifyTile({x, y})) { + if (tile->collisionCacheDirty) + freshenRegion.combine(RectI(x, y, x + 1, y + 1)); + } + } + } + + if (!freshenRegion.isNull()) { + for (int x = freshenRegion.xMin(); x < freshenRegion.xMax(); ++x) { + for (int y = freshenRegion.yMin(); y < freshenRegion.yMax(); ++y) { + if (auto tile = m_tileArray->modifyTile({x, y})) { + tile->collisionCacheDirty = false; + tile->collisionCache.clear(); + } + } + } + + for (auto collisionBlock : m_collisionGenerator.getBlocks(freshenRegion)) { + if (auto tile = m_tileArray->modifyTile(collisionBlock.space)) + tile->collisionCache.append(move(collisionBlock)); + } + } +} + +void WorldServer::removeEntity(EntityId entityId, bool andDie) { + auto entity = m_entityMap->entity(entityId); + if (!entity) + return; + + if (auto tileEntity = as<TileEntity>(entity)) + updateTileEntityTiles(tileEntity, true); + + if (andDie) + entity->destroy(nullptr); + + for (auto const& pair : m_clientInfo) { + auto& clientInfo = pair.second; + if (auto version = clientInfo->clientSlavesNetVersion.maybeTake(entity->entityId())) { + ByteArray finalDelta = entity->writeNetState(*version).first; + clientInfo->outgoingPackets.append(make_shared<EntityDestroyPacket>(entity->entityId(), move(finalDelta), andDie)); + } + } + + m_entityMap->removeEntity(entityId); + entity->uninit(); +} + +float WorldServer::windLevel(Vec2F const& pos) const { + return WorldImpl::windLevel(m_tileArray, pos, m_weather.wind()); +} + +float WorldServer::lightLevel(Vec2F const& pos) const { + return WorldImpl::lightLevel(m_tileArray, m_entityMap, m_geometry, m_worldTemplate, m_sky, m_lightIntensityCalculator, pos); +} + +void WorldServer::setDungeonBreathable(DungeonId dungeonId, Maybe<bool> breathable) { + Maybe<float> current = m_dungeonIdBreathable.maybe(dungeonId); + if (breathable != current) { + if (breathable) + m_dungeonIdBreathable[dungeonId] = *breathable; + else + m_dungeonIdBreathable.remove(dungeonId); + + for (auto const& p : m_clientInfo) + p.second->outgoingPackets.append(make_shared<SetDungeonBreathablePacket>(dungeonId, breathable)); + } +} + + + +bool WorldServer::breathable(Vec2F const& pos) const { + return WorldImpl::breathable(this, m_tileArray, m_dungeonIdBreathable, m_worldTemplate, pos); +} + +float WorldServer::threatLevel() const { + return m_worldTemplate->threatLevel(); +} + +StringList WorldServer::environmentStatusEffects(Vec2F const& pos) const { + return m_worldTemplate->environmentStatusEffects(floor(pos[0]), floor(pos[1])); +} + +StringList WorldServer::weatherStatusEffects(Vec2F const& pos) const { + if (!m_weather.statusEffects().empty()) { + if (exposedToWeather(pos)) + return m_weather.statusEffects(); + } + + return {}; +} + +bool WorldServer::exposedToWeather(Vec2F const& pos) const { + if (!isUnderground(pos) && liquidLevel(Vec2I::floor(pos)).liquid == EmptyLiquidId) { + auto assets = Root::singleton().assets(); + float weatherRayCheckDistance = assets->json("/weather.config:weatherRayCheckDistance").toFloat(); + float weatherRayCheckWindInfluence = assets->json("/weather.config:weatherRayCheckWindInfluence").toFloat(); + + auto offset = Vec2F(-m_weather.wind() * weatherRayCheckWindInfluence, weatherRayCheckDistance).normalized() * weatherRayCheckDistance; + + return !lineCollision({pos, pos + offset}); + } + + return false; +} + +bool WorldServer::isUnderground(Vec2F const& pos) const { + return m_worldTemplate->undergroundLevel() >= pos[1]; +} + +bool WorldServer::disableDeathDrops() const { + if (m_worldTemplate->worldParameters()) + return m_worldTemplate->worldParameters()->disableDeathDrops; + return false; +} + +List<PhysicsForceRegion> WorldServer::forceRegions() const { + return m_forceRegions; +} + +Json WorldServer::getProperty(String const& propertyName, Json const& def) const { + return m_worldProperties.value(propertyName, def); +} + +void WorldServer::setProperty(String const& propertyName, Json const& property) { + if (m_worldProperties.value(propertyName) == property) + return; + + m_worldProperties[propertyName] = property; + for (auto const& pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<UpdateWorldPropertiesPacket>(JsonObject{{propertyName, property}})); +} + +void WorldServer::timer(int stepsDelay, WorldAction worldAction) { + m_timers.append({stepsDelay, worldAction}); +} + +void WorldServer::startFlyingSky(bool enterHyperspace, bool startInWarp) { + m_sky->startFlying(enterHyperspace, startInWarp); +} + +void WorldServer::stopFlyingSkyAt(SkyParameters const& destination) { + m_sky->stopFlyingAt(destination); + m_sky->setType(SkyType::Orbital); +} + +void WorldServer::setOrbitalSky(SkyParameters const& destination) { + m_sky->jumpTo(destination); + m_sky->setType(SkyType::Orbital); +} + +double WorldServer::epochTime() const { + return m_sky->epochTime(); +} + +uint32_t WorldServer::day() const { + return m_sky->day(); +} + +float WorldServer::dayLength() const { + return m_sky->dayLength(); +} + +float WorldServer::timeOfDay() const { + return m_sky->timeOfDay(); +} + +LuaRootPtr WorldServer::luaRoot() { + return m_luaRoot; +} + +RpcPromise<Vec2F> WorldServer::findUniqueEntity(String const& uniqueId) { + if (auto pos = m_worldStorage->findUniqueEntity(uniqueId)) + return RpcPromise<Vec2F>::createFulfilled(*pos); + else + return RpcPromise<Vec2F>::createFailed("Unknown entity"); +} + +RpcPromise<Json> WorldServer::sendEntityMessage(Variant<EntityId, String> const& entityId, String const& message, JsonArray const& args) { + EntityPtr entity; + if (entityId.is<EntityId>()) + entity = m_entityMap->entity(entityId.get<EntityId>()); + else + entity = m_entityMap->entity(loadUniqueEntity(entityId.get<String>())); + + if (!entity) { + return RpcPromise<Json>::createFailed("Unknown entity"); + } else if (entity->isMaster()) { + if (auto resp = entity->receiveMessage(ServerConnectionId, message, args)) + return RpcPromise<Json>::createFulfilled(resp.take()); + else + return RpcPromise<Json>::createFailed("Message not handled by entity"); + } else { + auto pair = RpcPromise<Json>::createPair(); + auto clientInfo = m_clientInfo.get(connectionForEntity(entity->entityId())); + Uuid uuid; + m_entityMessageResponses[uuid] = {clientInfo->clientId, pair.second}; + clientInfo->outgoingPackets.append(make_shared<EntityMessagePacket>(entity->entityId(), message, args, uuid)); + return pair.first; + } +} + +void WorldServer::setPlayerStart(Vec2F const& startPosition, bool respawnInWorld) { + m_playerStart = startPosition; + m_respawnInWorld = respawnInWorld; + m_adjustPlayerStart = false; + for (auto pair : m_clientInfo) + pair.second->outgoingPackets.append(make_shared<SetPlayerStartPacket>(m_playerStart, m_respawnInWorld)); +} + +Vec2F WorldServer::findPlayerStart(Maybe<Vec2F> firstTry) { + Vec2F spawnRectSize = jsonToVec2F(m_serverConfig.get("playerStartRegionSize")); + auto maximumVerticalSearch = m_serverConfig.getInt("playerStartRegionMaximumVerticalSearch"); + auto maximumTries = m_serverConfig.getInt("playerStartRegionMaximumTries"); + + static const Set<DungeonId> allowedSpawnDungeonIds = {NoDungeonId, SpawnDungeonId, ConstructionDungeonId, DestroyedBlockDungeonId}; + + Vec2F pos; + if (firstTry) + pos = *firstTry; + else + pos = Vec2F(m_worldTemplate->findSensiblePlayerStart().value(Vec2I(0, m_worldTemplate->surfaceLevel()))); + + CollisionSet collideWithAnything{CollisionKind::Null, CollisionKind::Block, CollisionKind::Dynamic, CollisionKind::Platform, CollisionKind::Slippery}; + for (int t = 0; t < maximumTries; ++t) { + bool foundGround = false; + // First go downward until we collide with terrain + for (int i = 0; i < maximumVerticalSearch; ++i) { + RectF spawnRect = RectF(pos[0] - spawnRectSize[0] / 2, pos[1], pos[0] + spawnRectSize[0] / 2, pos[1] + spawnRectSize[1]); + generateRegion(RectI::integral(spawnRect)); + if (rectTileCollision(RectI::integral(spawnRect), collideWithAnything)) { + foundGround = true; + break; + } + --pos[1]; + } + + if (foundGround) { + // Then go up until our spawn region is no longer in the terrain, but bail + // out and try again if we can't signal the region or we are stuck in a + // dungeon. + for (int i = 0; i < maximumVerticalSearch; ++i) { + if (m_tileArray->tile(Vec2I::floor(pos)).liquid.liquid != EmptyLiquidId) + break; + + RectF spawnRect = RectF(pos[0] - spawnRectSize[0] / 2, pos[1], pos[0] + spawnRectSize[0] / 2, pos[1] + spawnRectSize[1]); + + generateRegion(RectI::integral(spawnRect)); + + auto tileDungeonId = getServerTile(Vec2I::floor(pos)).dungeonId; + + if (!allowedSpawnDungeonIds.contains(tileDungeonId)) + break; + + if (!rectTileCollision(RectI::integral(spawnRect), collideWithAnything) && spawnRect.yMax() < m_geometry.height()) + return pos; + + ++pos[1]; + } + } + + pos = Vec2F(m_worldTemplate->findSensiblePlayerStart().value(Vec2I(0, m_worldTemplate->surfaceLevel()))); + } + + return pos; +} + +Vec2F WorldServer::findPlayerSpaceStart(float targetX) { + Vec2F testRectSize = jsonToVec2F(m_serverConfig.get("playerSpaceStartRegionSize")); + auto distanceIncrement = m_serverConfig.getFloat("playerSpaceStartDistanceIncrement"); + auto maximumTries = m_serverConfig.getInt("playerSpaceStartMaximumTries"); + + Vec2F basePos = Vec2F(targetX, m_geometry.height() * 0.5); + + CollisionSet collideWithAnything{CollisionKind::Null, CollisionKind::Block, CollisionKind::Dynamic, CollisionKind::Platform, CollisionKind::Slippery}; + for (int t = 0; t < maximumTries; ++t) { + Vec2F testPos = m_geometry.limit(basePos + Vec2F::withAngle(Random::randf() * 2 * Constants::pi, t * distanceIncrement)); + RectF testRect = RectF::withCenter(testPos, testRectSize); + generateRegion(RectI::integral(testRect)); + if (!rectTileCollision(RectI::integral(testRect), collideWithAnything)) + return testPos; + } + + return basePos; +} + +void WorldServer::readMetadata() { + auto dungeonDefinitions = Root::singleton().dungeonDefinitions(); + auto versioningDatabase = Root::singleton().versioningDatabase(); + + auto metadata = versioningDatabase->loadVersionedJson(m_worldStorage->worldMetadata(), "WorldMetadata"); + + m_playerStart = jsonToVec2F(metadata.get("playerStart")); + m_respawnInWorld = metadata.getBool("respawnInWorld"); + m_adjustPlayerStart = metadata.getBool("adjustPlayerStart"); + m_worldTemplate = make_shared<WorldTemplate>(metadata.get("worldTemplate")); + m_centralStructure = WorldStructure(metadata.get("centralStructure")); + m_protectedDungeonIds = jsonToSet<DungeonId>(metadata.get("protectedDungeonIds"), mem_fn(&Json::toUInt)); + m_worldProperties = metadata.getObject("worldProperties"); + m_spawner.setActive(metadata.getBool("spawningEnabled")); + + m_dungeonIdGravity = transform<HashMap<DungeonId, float>>(metadata.getArray("dungeonIdGravity"), [](Json const& p) { + return make_pair(p.getInt(0), p.getFloat(1)); + }); + + m_dungeonIdBreathable = transform<HashMap<DungeonId, bool>>(metadata.getArray("dungeonIdBreathable"), [](Json const& p) { + return make_pair(p.getInt(0), p.getBool(1)); + }); +} + +void WorldServer::writeMetadata() { + auto versioningDatabase = Root::singleton().versioningDatabase(); + + Json metadata = JsonObject{ + {"playerStart", jsonFromVec2F(m_playerStart)}, + {"respawnInWorld", m_respawnInWorld}, + {"adjustPlayerStart", m_adjustPlayerStart}, + {"worldTemplate", m_worldTemplate->store()}, + {"centralStructure", m_centralStructure.store()}, + {"protectedDungeonIds", jsonFromSet(m_protectedDungeonIds)}, + {"worldProperties", m_worldProperties}, + {"spawningEnabled", m_spawner.active()}, + {"dungeonIdGravity", m_dungeonIdGravity.pairs().transformed([](auto const& p) -> Json { + return JsonArray{p.first, p.second}; + })}, + {"dungeonIdBreathable", m_dungeonIdBreathable.pairs().transformed([](auto const& p) -> Json { + return JsonArray{p.first, p.second}; + })} + }; + + m_worldStorage->setWorldMetadata(versioningDatabase->makeCurrentVersionedJson("WorldMetadata", metadata)); +} + +bool WorldServer::isVisibleToPlayer(RectF const& region) const { + for (auto const& p : m_clientInfo) { + for (auto playerRegion : p.second->monitoringRegions(m_entityMap)) { + if (m_geometry.rectIntersectsRect(RectF(playerRegion), region)) + return true; + } + } + return false; +} + +WorldServer::ClientInfo::ClientInfo(ConnectionId clientId, InterpolationTracker const trackerInit) + : clientId(clientId), skyNetVersion(0), weatherNetVersion(0), pendingForward(false), started(false), interpolationTracker(trackerInit) {} + +List<RectI> WorldServer::ClientInfo::monitoringRegions(EntityMapPtr const& entityMap) const { + return clientState.monitoringRegions([entityMap](EntityId entityId) -> Maybe<RectI> { + if (auto entity = entityMap->entity(entityId)) + return RectI::integral(entity->metaBoundBox().translated(entity->position())); + return {}; + }); +} + +bool WorldServer::ClientInfo::needsDamageNotification(RemoteDamageNotification const& rdn) const { + if (clientId == connectionForEntity(rdn.sourceEntityId) || clientId == connectionForEntity(rdn.damageNotification.targetEntityId)) + return true; + + if (clientSlavesNetVersion.contains(rdn.damageNotification.targetEntityId)) + return true; + + if (clientState.window().contains(Vec2I::floor(rdn.damageNotification.position))) + return true; + + return false; +} + +InteractiveEntityPtr WorldServer::getInteractiveInRange(Vec2F const& targetPosition, Vec2F const& sourcePosition, float maxRange) const { + return WorldImpl::getInteractiveInRange(m_geometry, m_entityMap, targetPosition, sourcePosition, maxRange); +} + +bool WorldServer::canReachEntity(Vec2F const& position, float radius, EntityId targetEntity, bool preferInteractive) const { + return WorldImpl::canReachEntity(m_geometry, m_tileArray, m_entityMap, position, radius, targetEntity, preferInteractive); +} + +RpcPromise<InteractAction> WorldServer::interact(InteractRequest const& request) { + if (auto entity = as<InteractiveEntity>(m_entityMap->entity(request.targetId))) + return RpcPromise<InteractAction>::createFulfilled(entity->interact(request)); + else + return RpcPromise<InteractAction>::createFulfilled(InteractAction()); +} + +void WorldServer::setupForceRegions() { + m_forceRegions.clear(); + + if (!worldTemplate() || !worldTemplate()->worldParameters()) + return; + + auto forceRegionType = worldTemplate()->worldParameters()->worldEdgeForceRegions; + + if (forceRegionType == WorldEdgeForceRegionType::None) + return; + + bool addTopRegion = forceRegionType == WorldEdgeForceRegionType::Top || forceRegionType == WorldEdgeForceRegionType::TopAndBottom; + bool addBottomRegion = forceRegionType == WorldEdgeForceRegionType::Bottom || forceRegionType == WorldEdgeForceRegionType::TopAndBottom; + + auto regionHeight = m_serverConfig.getFloat("worldEdgeForceRegionHeight"); + auto regionForce = m_serverConfig.getFloat("worldEdgeForceRegionForce"); + auto regionVelocity = m_serverConfig.getFloat("worldEdgeForceRegionVelocity"); + auto regionCategoryFilter = PhysicsCategoryFilter::whitelist({"player", "monster", "npc", "vehicle", "itemdrop"}); + auto worldSize = Vec2F(worldTemplate()->size()); + + if (addTopRegion) { + auto topForceRegion = GradientForceRegion(); + topForceRegion.region = PolyF({ + {0, worldSize[1] - regionHeight}, + {worldSize[0], worldSize[1] - regionHeight}, + (worldSize), + {0, worldSize[1]}}); + topForceRegion.gradient = Line2F({0, worldSize[1]}, {0, worldSize[1] - regionHeight}); + topForceRegion.baseTargetVelocity = regionVelocity; + topForceRegion.baseControlForce = regionForce; + topForceRegion.categoryFilter = regionCategoryFilter; + m_forceRegions.append(topForceRegion); + } + + if (addBottomRegion) { + auto bottomForceRegion = GradientForceRegion(); + bottomForceRegion.region = PolyF({ + {0, 0}, + {worldSize[0], 0}, + {worldSize[0], regionHeight}, + {0, regionHeight}}); + bottomForceRegion.gradient = Line2F({0, 0}, {0, regionHeight}); + bottomForceRegion.baseTargetVelocity = regionVelocity; + bottomForceRegion.baseControlForce = regionForce; + bottomForceRegion.categoryFilter = regionCategoryFilter; + m_forceRegions.append(bottomForceRegion); + } +} + +} |