#include "StarUniverseServer.hpp" #include "StarFile.hpp" #include "StarLogging.hpp" #include "StarJsonExtra.hpp" #include "StarEncode.hpp" #include "StarRoot.hpp" #include "StarCommandProcessor.hpp" #include "StarTeamManager.hpp" #include "StarChatProcessor.hpp" #include "StarConfiguration.hpp" #include "StarVersioningDatabase.hpp" #include "StarAssets.hpp" #include "StarTcp.hpp" #include "StarCelestialDatabase.hpp" #include "StarWorldTemplate.hpp" #include "StarSecureRandom.hpp" #include "StarSha256.hpp" #include "StarSky.hpp" #include "StarAiDatabase.hpp" #include "StarBiomeDatabase.hpp" #include "StarUniverseServerLuaBindings.hpp" namespace Star { UniverseServer::UniverseServer(String const& storageDir) : Thread("UniverseServer"), m_workerPool("UniverseServerWorkerPool"), m_clients(MinClientConnectionId, MaxClientConnectionId) { String const LockFile = "universe.lock"; m_storageDirectory = storageDir; if (!File::isDirectory(m_storageDirectory)) { Logger::info("UniverseServer: Creating universe storage directory"); File::makeDirectory(m_storageDirectory); } auto& root = Root::singleton(); auto assets = root.assets(); auto configuration = root.configuration(); if (auto assetsDigestOverride = configuration->get("serverOverrideAssetsDigest").optString()) { m_assetsDigest = hexDecode(*assetsDigestOverride); Logger::info("UniverseServer: Overriding assets digest as '{}'", *assetsDigestOverride); } else { m_assetsDigest = assets->digest(); } m_commandProcessor = make_shared(this); m_chatProcessor = make_shared(); m_chatProcessor->setCommandHandler(bind(&CommandProcessor::userCommand, m_commandProcessor.get(), _1, _2, _3)); Logger::info("UniverseServer: Acquiring universe lock file"); m_storageDirectoryLock = LockFile::acquireLock(File::relativeTo(m_storageDirectory, LockFile)); if (!m_storageDirectoryLock) throw UniverseServerException("Could not acquire lock for the universe directory"); if (configuration->get("clearUniverseFiles").toBool()) { Logger::info("UniverseServer: Clearing all universe files"); for (auto file : File::dirList(storageDir)) { if (!file.second && file.first != LockFile) File::remove(File::relativeTo(storageDir, file.first)); } } m_celestialDatabase = make_shared(File::relativeTo(m_storageDirectory, "universe.chunks")); Logger::info("UniverseServer: Loading settings"); loadSettings(); loadTempWorldIndex(); m_lastClockUpdateSent = 0.0; m_stop = false; m_tcpState = TcpState::No; m_storageTriggerDeadline = 0; m_clearBrokenWorldsDeadline = 0; m_maxPlayers = configuration->get("maxPlayers").toUInt(); for (auto const& pair : assets->json("/universe_server.config:speciesShips").iterateObject()) m_speciesShips[pair.first] = jsonToStringList(pair.second); m_teamManager = make_shared(); m_workerPool.start(assets->json("/universe_server.config:workerPoolThreads").toUInt()); m_connectionServer = make_shared(bind(&UniverseServer::packetsReceived, this, _1, _2, _3)); m_pause = make_shared>(false); } UniverseServer::~UniverseServer() { stop(); join(); m_workerPool.stop(); RecursiveMutexLocker locker(m_mainLock); WriteLocker clientsLocker(m_clientsLock); m_connectionServer->removeAllConnections(); m_deadConnections.clear(); // Make sure that all world threads and net sockets (and associated threads) // are shutdown before other member destruction. m_clients.clear(); m_worlds.clear(); } void UniverseServer::setListeningTcp(bool listenTcp) { if (!listenTcp || m_tcpState != TcpState::Fuck) m_tcpState = listenTcp ? TcpState::Yes : TcpState::No; } void UniverseServer::addClient(UniverseConnection remoteConnection) { RecursiveMutexLocker locker(m_mainLock); // Binding requires us to make the given lambda copy constructible, so the // make_shared is requried here. m_connectionAcceptThreads.append(Thread::invoke("UniverseServer::acceptConnection", [this, conn = make_shared(std::move(remoteConnection))]() { acceptConnection(std::move(*conn), {}); })); } UniverseConnection UniverseServer::addLocalClient() { auto pair = LocalPacketSocket::openPair(); addClient(UniverseConnection(std::move(pair.first))); return UniverseConnection(std::move(pair.second)); } void UniverseServer::stop() { m_stop = true; } void UniverseServer::setPause(bool pause) { ReadLocker clientsLocker(m_clientsLock); // Pausing is disabled for multiplayer if (m_clients.size() > 1) pause = false; if (pause == *m_pause) return; *m_pause = pause; if (pause) m_universeClock->stop(); else m_universeClock->start(); for (auto& p : m_clients) m_connectionServer->sendPackets(p.first, {make_shared(*m_pause, GlobalTimescale)}); } void UniverseServer::setTimescale(float timescale) { ReadLocker clientsLocker(m_clientsLock); GlobalTimescale = timescale; for (auto& p : m_clients) m_connectionServer->sendPackets(p.first, {make_shared(*m_pause, GlobalTimescale)}); } void UniverseServer::setTickRate(float tickRate) { ServerGlobalTimestep = 1.0f / tickRate; } List UniverseServer::activeWorlds() const { RecursiveMutexLocker locker(m_mainLock); return m_worlds.keys(); } bool UniverseServer::isWorldActive(WorldId const& worldId) const { RecursiveMutexLocker locker(m_mainLock); return m_worlds.contains(worldId); } List UniverseServer::clientIds() const { ReadLocker clientsLocker(m_clientsLock); return m_clients.keys(); } size_t UniverseServer::numberOfClients() const { ReadLocker clientsLocker(m_clientsLock); return m_clients.size(); } uint32_t UniverseServer::maxClients() const { return m_maxPlayers; } bool UniverseServer::isConnectedClient(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); return m_clients.contains(clientId); } String UniverseServer::clientDescriptor(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->descriptiveName(); else return "disconnected_client"; } String UniverseServer::clientNick(ConnectionId clientId) const { return m_chatProcessor->connectionNick(clientId); } Maybe UniverseServer::findNick(String const& nick) const { return m_chatProcessor->findNick(nick); } Maybe UniverseServer::uuidForClient(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->playerUuid(); return {}; } Maybe UniverseServer::clientForUuid(Uuid const& uuid) const { ReadLocker clientsLocker(m_clientsLock); return getClientForUuid(uuid); } void UniverseServer::adminBroadcast(String const& text) { m_chatProcessor->adminBroadcast(text); } void UniverseServer::adminWhisper(ConnectionId clientId, String const& text) { m_chatProcessor->adminWhisper(clientId, text); } String UniverseServer::adminCommand(String text) { String command = text.extract(); return m_commandProcessor->adminCommand(command, text); } bool UniverseServer::isAdmin(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->isAdmin(); return false; } bool UniverseServer::canBecomeAdmin(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->canBecomeAdmin(); return false; } void UniverseServer::setAdmin(ConnectionId clientId, bool admin) { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) clientContext->setAdmin(admin); } bool UniverseServer::isLocal(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return !clientContext->remoteAddress(); return false; } bool UniverseServer::isPvp(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->team().type == TeamType::PVP; return false; } void UniverseServer::setPvp(ConnectionId clientId, bool pvp) { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) { if (pvp) { TeamNumber pvpTeam = m_teamManager->getPvpTeam(clientContext->playerUuid()); if (pvpTeam == 0) pvpTeam = soloPvpTeam(clientId); clientContext->setTeam(EntityDamageTeam(TeamType::PVP, pvpTeam)); } else clientContext->setTeam(EntityDamageTeam(TeamType::Friendly)); } } RpcThreadPromise UniverseServer::sendWorldMessage(WorldId const& worldId, String const& message, JsonArray const& args) { auto pair = RpcThreadPromise::createPair(); RecursiveMutexLocker locker(m_mainLock); m_pendingWorldMessages[worldId].push_back({ message, args, pair.second }); return pair.first; } void UniverseServer::clientWarpPlayer(ConnectionId clientId, WarpAction action, bool deploy) { RecursiveMutexLocker locker(m_mainLock); m_pendingPlayerWarps[clientId] = pair(std::move(action), std::move(deploy)); } void UniverseServer::clientFlyShip(ConnectionId clientId, Vec3I const& system, SystemLocation const& location) { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (m_pendingFlights.contains(clientId) || m_queuedFlights.contains(clientId)) return; auto clientContext = m_clients.get(clientId); if (!clientContext) return; if (system == Vec3I()) { m_pendingFlights.set(clientId, {Vec3I(), {}}); // find starter world return; } auto clientSystem = clientContext->systemWorld(); bool sameSystem = clientSystem && clientSystem->location() == system; bool sameLocation = clientSystem && clientSystem->clientShipLocation(clientId) == location; if (m_pendingArrivals.contains(clientId) && sameSystem && location && !sameLocation) { // for continuing flight within a system, set the new destination immediately clientSystem->setClientDestination(clientId, location); return; } // don't switch systems while already flying if (!m_pendingArrivals.contains(clientId) || sameSystem) m_pendingFlights.set(clientId, {system, location}); } WorldId UniverseServer::clientWorld(ConnectionId clientId) const { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->playerWorldId(); return WorldId(); } CelestialCoordinate UniverseServer::clientShipCoordinate(ConnectionId clientId) const { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) return clientContext->shipCoordinate(); return CelestialCoordinate(); } ClockPtr UniverseServer::universeClock() const { return m_universeClock; } UniverseSettingsPtr UniverseServer::universeSettings() const { return m_universeSettings; } CelestialDatabase & UniverseServer::celestialDatabase() { return *m_celestialDatabase; } bool UniverseServer::executeForClient(ConnectionId clientId, function action) { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); bool success = false; if (auto clientContext = m_clients.value(clientId)) { if (auto currentWorld = clientContext->playerWorld()) { currentWorld->executeAction([clientId, action, &success](WorldServerThread*, WorldServer* worldServer) { if (auto player = worldServer->clientPlayer(clientId)) { action(worldServer, player); success = true; } }); } } return success; } void UniverseServer::disconnectClient(ConnectionId clientId, String const& reason) { RecursiveMutexLocker locker(m_mainLock); m_pendingDisconnections.add(clientId, reason); } void UniverseServer::banUser(ConnectionId clientId, String const& reason, pair banType, Maybe timeout) { RecursiveMutexLocker locker(m_mainLock); if (timeout) doTempBan(clientId, reason, banType, *timeout); else doPermBan(clientId, reason, banType); m_pendingDisconnections.add(clientId, reason); } bool UniverseServer::unbanUuid(String const& uuidString) { RecursiveMutexLocker locker(m_mainLock); bool entryFound = false; auto config = Root::singleton().configuration(); auto bannedUuids = config->get("bannedUuids").toArray(); eraseWhere(bannedUuids, [&](Json const& entry) { if (entry.getString("uuid") == uuidString) { entryFound = true; return true; } return false; }); config->set("bannedUuids", bannedUuids); eraseWhere(m_tempBans, [&](TimeoutBan const& b) { if (b.uuid && b.uuid->hex() == uuidString) { entryFound = true; return true; } return false; }); return entryFound; } bool UniverseServer::unbanIp(String const& addressString) { RecursiveMutexLocker locker(m_mainLock); auto addressLookup = HostAddress::lookup(addressString); if (addressLookup.isLeft()) { return false; } else { HostAddress address = std::move(addressLookup.right()); String cleanAddressString = toString(address); bool entryFound = false; auto config = Root::singleton().configuration(); auto bannedIPs = config->get("bannedIPs").toArray(); eraseWhere(bannedIPs, [&](Json const& entry) { if (entry.getString("ip") == cleanAddressString) { entryFound = true; return true; } return false; }); config->set("bannedIPs", bannedIPs); eraseWhere(m_tempBans, [&](TimeoutBan const& b) { if (b.ip && b.ip.value() == address) { entryFound = true; return true; } return false; }); return entryFound; } } bool UniverseServer::updatePlanetType(CelestialCoordinate const& coordinate, String const& newType, String const& weatherBiome) { RecursiveMutexLocker locker(m_mainLock); if (!coordinate.isNull() && m_celestialDatabase->coordinateValid(coordinate)) { if (auto celestialParameters = m_celestialDatabase->parameters(coordinate)) { if (auto terrestrialParameters = as(celestialParameters->visitableParameters())) { auto newTerrestrialParameters = make_shared(*terrestrialParameters); newTerrestrialParameters->typeName = newType; auto biomeDatabase = Root::singleton().biomeDatabase(); auto newWeatherPool = biomeDatabase->biomeWeathers(weatherBiome, celestialParameters->seed(), terrestrialParameters->threatLevel); newTerrestrialParameters->weatherPool = newWeatherPool; newTerrestrialParameters->terraformed = true; celestialParameters->setVisitableParameters(newTerrestrialParameters); m_celestialDatabase->updateParameters(coordinate, *celestialParameters); ReadLocker clientsLocker(m_clientsLock); for (auto clientId : m_clients.keys()) m_connectionServer->sendPackets(clientId, {make_shared(coordinate)}); return true; } } } return false; } bool UniverseServer::sendPacket(ConnectionId clientId, PacketPtr packet) { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (m_clients.contains(clientId)) { clientsLocker.unlock(); m_connectionServer->sendPackets(clientId, {packet}); return true; } return false; } void UniverseServer::run() { Logger::info("UniverseServer: Starting UniverseServer with UUID: {}", m_universeSettings->uuid().hex()); int mainWakeupInterval = Root::singleton().assets()->json("/universe_server.config:mainWakeupInterval").toInt(); TcpServerPtr tcpServer; while (!m_stop) { if (m_tcpState == TcpState::Yes && !tcpServer) { auto& root = Root::singleton(); auto configuration = root.configuration(); auto assets = root.assets(); HostAddressWithPort bindAddress(configuration->get("gameServerBind").toString(), configuration->get("gameServerPort").toUInt()); unsigned maxPendingConnections = assets->json("/universe_server.config:maxPendingConnections").toInt(); Logger::info("UniverseServer: listening for incoming TCP connections on {}", bindAddress); try { tcpServer = make_shared(bindAddress); tcpServer->setAcceptCallback([this, maxPendingConnections](TcpSocketPtr socket) { RecursiveMutexLocker locker(m_mainLock); if (m_connectionAcceptThreads.size() < maxPendingConnections) { Logger::info("UniverseServer: Connection received from: {}", socket->remoteAddress()); m_connectionAcceptThreads.append(Thread::invoke("UniverseServer::acceptConnection", [this, socket]() { acceptConnection(UniverseConnection(TcpPacketSocket::open(socket)), socket->remoteAddress().address()); })); } else { Logger::warn("UniverseServer: maximum pending connections, dropping connection from: {}", socket->remoteAddress().address()); } }); } catch (StarException const& e) { Logger::error("UniverseServer: Error setting up TCP, cannot accept connections: {}", e.what()); m_tcpState = TcpState::Fuck; tcpServer.reset(); } } else if (m_tcpState == TcpState::No && tcpServer) { Logger::info("UniverseServer: Not listening for incoming TCP connections"); tcpServer.reset(); } LogMap::set("universe_time", m_universeClock->time()); try { processUniverseFlags(); removeTimedBan(); sendPendingChat(); updateTeams(); updateShips(); sendClockUpdates(); kickErroredPlayers(); reapConnections(); processPlanetTypeChanges(); warpPlayers(); flyShips(); arriveShips(); processChat(); sendClientContextUpdates(); respondToCelestialRequests(); clearBrokenWorlds(); handleWorldMessages(); shutdownInactiveWorlds(); doTriggeredStorage(); } catch (std::exception const& e) { Logger::error("UniverseServer: exception caught: {}", outputException(e, true)); } Thread::sleep(mainWakeupInterval); } Logger::info("UniverseServer: Stopping UniverseServer"); try { m_workerPool.stop(); if (tcpServer) { Logger::info("UniverseServer: Stopping TCP Server"); tcpServer.reset(); } RecursiveMutexLocker locker(m_mainLock); WriteLocker clientsLocker(m_clientsLock); for (auto clientId : m_clients.keys()) doDisconnection(clientId, "ServerShutdown"); saveSettings(); saveTempWorldIndex(); m_worlds.clear(); } catch (std::exception const& e) { Logger::error("UniverseServer: exception caught cleaning up: {}", outputException(e, true)); } } void UniverseServer::processUniverseFlags() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (auto actions = m_universeSettings->pullPendingFlagActions()) { for (auto action : *actions) { if (action.is()) { auto placeDungeonAction = action.get(); if (instanceWorldStoredOrActive(placeDungeonAction.targetInstance)) { auto worldId = InstanceWorldId(placeDungeonAction.targetInstance); m_pendingFlagActions.append(pair{worldId, placeDungeonAction}); } } } } eraseWhere(m_pendingFlagActions, [this](pair const& p) { if (p.first.is() && instanceWorldStoredOrActive(p.first.get())) { // world is stored or active; perform flag actions once it loads if (auto maybeTargetWorld = triggerWorldCreation(p.first)) { if (auto targetWorld = maybeTargetWorld.value()) { if (p.second.is()) { auto placeDungeonAction = p.second.get(); targetWorld->executeAction([placeDungeonAction](WorldServerThread*, WorldServer* worldServer) { worldServer->placeDungeon(placeDungeonAction.dungeonId, placeDungeonAction.targetPosition, 0); }); } return true; } } return false; } else { // world hasn't yet been created; flag actions will be handled by normal creation return true; } }); } void UniverseServer::sendPendingChat() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); for (auto const& p : m_clients) { for (auto const& message : m_chatProcessor->pullPendingMessages(p.first)) m_connectionServer->sendPackets(p.first, {make_shared(message)}); } } void UniverseServer::updateTeams() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); StringMap> connectedPlayers; auto teams = m_teamManager->getPvpTeams(); for (auto const& p : m_clients) { connectedPlayers[p.second->playerName().toLower()].append(p.second->playerUuid()); if (p.second->team().type == TeamType::PVP) p.second->setTeam(EntityDamageTeam(TeamType::PVP, teams.value(p.second->playerUuid(), soloPvpTeam(p.second->clientId())))); else p.second->setTeam(EntityDamageTeam(TeamType::Friendly)); auto channels = m_chatProcessor->clientChannels(p.first); auto team = m_teamManager->getTeam(p.second->playerUuid()); for (auto const& channel : channels) { if (channel != printWorldId(p.second->playerWorldId()) && (!team || channel != team.value().hex())) m_chatProcessor->leaveChannel(p.first, channel); } if (team && !channels.contains(team.value().hex())) m_chatProcessor->joinChannel(p.first, team.value().hex()); } m_teamManager->setConnectedPlayers(connectedPlayers); } void UniverseServer::updateShips() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); for (auto const& p : m_clients) { auto newShipUpgrades = p.second->shipUpgrades(); if (auto shipWorld = getWorld(ClientShipWorldId(p.second->playerUuid()))) { shipWorld->executeAction([&](WorldServerThread*, WorldServer* shipWorld) { String species; Json jSpecies = shipWorld->getProperty("ship.species"); if (jSpecies.isType(Json::Type::String)) species = jSpecies.toString(); else shipWorld->setProperty("ship.species", species = p.second->playerSpecies()); auto const& speciesShips = m_speciesShips.get(species); Json jOldShipLevel = shipWorld->getProperty("ship.level"); unsigned newShipLevel = min(speciesShips.size() - 1, newShipUpgrades.shipLevel); if (jOldShipLevel.isType(Json::Type::Int)) { auto oldShipLevel = jOldShipLevel.toUInt(); if (oldShipLevel < newShipLevel) { for (unsigned i = oldShipLevel + 1; i <= newShipLevel; ++i) { auto shipStructure = WorldStructure(speciesShips[i]); shipWorld->setCentralStructure(shipStructure); newShipUpgrades.apply(shipStructure.configValue("shipUpgrades")); } p.second->setShipUpgrades(newShipUpgrades); p.second->updateShipChunks(shipWorld->readChunks()); } } shipWorld->setProperty("ship.level", newShipUpgrades.shipLevel); shipWorld->setProperty("ship.maxFuel", newShipUpgrades.maxFuel); shipWorld->setProperty("ship.crewSize", newShipUpgrades.crewSize); shipWorld->setProperty("ship.fuelEfficiency", newShipUpgrades.fuelEfficiency); }); } if (auto systemWorld = p.second->systemWorld()) { float speed = newShipUpgrades.shipSpeed; systemWorld->executeClientShipAction(p.first, [speed](SystemClientShip* ship) { if (ship) ship->setSpeed(speed); }); } } } void UniverseServer::sendClockUpdates() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); int64_t currentTime = Time::monotonicMilliseconds(); if (currentTime > m_lastClockUpdateSent + Root::singleton().assets()->json("/universe_server.config:clockUpdatePacketInterval").toInt()) { for (auto clientId : m_clients.keys()) m_connectionServer->sendPackets(clientId, {make_shared(m_universeClock->time())}); m_lastClockUpdateSent = currentTime; } } void UniverseServer::sendClientContextUpdate(ServerClientContextPtr clientContext) { auto clientContextData = clientContext->writeUpdate(); if (!clientContextData.empty()) m_connectionServer->sendPackets(clientContext->clientId(), {make_shared(clientContextData)}); } void UniverseServer::sendClientContextUpdates() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); for (auto const& p : m_clients) sendClientContextUpdate(p.second); } void UniverseServer::kickErroredPlayers() { RecursiveMutexLocker locker(m_mainLock); for (auto const& worldId : m_worlds.keys()) { if (auto world = getWorld(worldId)) { for (auto clientId : world->erroredClients()) m_pendingDisconnections[clientId] = "Incoming client packet has caused exception"; } } } void UniverseServer::reapConnections() { RecursiveMutexLocker locker(m_mainLock); int64_t startTime = Time::monotonicMilliseconds(); int64_t timeout = Root::singleton().assets()->json("/universe_server.config:connectionTimeout").toInt(); eraseWhere(m_connectionAcceptThreads, [](ThreadFunction& function) { if (!function.isRunning()) { try { function.finish(); } catch (std::exception const& e) { Logger::error("UniverseServer: Exception caught accepting new connection: {}", outputException(e, true)); } } return function.isFinished(); }); WriteLocker clientsLocker(m_clientsLock); for (auto p : take(m_pendingDisconnections)) doDisconnection(p.first, p.second); for (auto clientId : m_clients.keys()) { auto clientContext = m_clients.value(clientId); if (!m_connectionServer->connectionIsOpen(clientId)) { Logger::info("UniverseServer: Client {} connection lost", clientContext->descriptiveName()); doDisconnection(clientId, String("Disconnected due to connection lost")); } else { if (clientContext->remoteAddress() && startTime - m_connectionServer->lastActivityTime(clientId) > timeout) { Logger::info("UniverseServer: Kicking client {} due to inactivity", clientContext->descriptiveName()); doDisconnection(clientId, String("Disconnected due to inactivity")); } } } // Once connections are waiting to close, send any pending data and wait up // to the connection timeout for the client to do the closing to ensure the // client has all the data. size_t previousDeadConnections = m_deadConnections.size(); m_deadConnections.filter([startTime, timeout](auto& pair) { if (pair.first.send()) pair.second = startTime; return pair.first.isOpen() && startTime - pair.second < timeout; }); if (previousDeadConnections > m_deadConnections.size()) Logger::info("UniverseServer: Reaped {} dead connections", previousDeadConnections); } void UniverseServer::processPlanetTypeChanges() { RecursiveMutexLocker locker(m_mainLock); for (auto const& worldId : m_worlds.keys()) { if (auto celestialWorldId = worldId.ptr()) { if (auto world = getWorld(worldId)) { if (auto newPlanetType = world->pullNewPlanetType()) updatePlanetType(*celestialWorldId, newPlanetType->first, newPlanetType->second); } } } } void UniverseServer::warpPlayers() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); for (auto const& clientId : m_pendingPlayerWarps.keys()) { auto& warp = m_pendingPlayerWarps.get(clientId); WarpAction warpAction = warp.first; bool deploy = warp.second; auto clientContext = m_clients.value(clientId); if (!clientContext) continue; WarpToWorld warpToWorld = resolveWarpAction(warpAction, clientId, deploy); if (auto maybeToWorld = triggerWorldCreation(warpToWorld.world)) { Logger::info("UniverseServer: Warping player {} to {}", clientId, printWarpAction(warpToWorld)); if (auto toWorld = maybeToWorld.value()) { if (toWorld->spawnTargetValid(warpToWorld.target)) { if (auto currentWorld = clientContext->playerWorld()) { if (auto playerRevivePosition = currentWorld->playerRevivePosition(clientId)) clientContext->setPlayerReturnWarp(WarpToWorld{currentWorld->worldId(), SpawnTargetPosition(*playerRevivePosition)}); clientContext->clearPlayerWorld(); m_connectionServer->sendPackets(clientId, currentWorld->removeClient(clientId)); m_chatProcessor->leaveChannel(clientId, printWorldId(currentWorld->worldId())); } clientContext->setOrbitWarpAction({}); // having stale world ids in the client context is bad, // make sure it's at least null until the next client context update sendClientContextUpdate(clientContext); // Checking the spawn target validity then adding the client is not // perfect, it can still become invalid in between, if we fail at // adding the client we need to warp them back. if (toWorld && toWorld->addClient(clientId, warpToWorld.target, !clientContext->remoteAddress(), clientContext->canBecomeAdmin())) { clientContext->setPlayerWorld(toWorld); m_chatProcessor->joinChannel(clientId, printWorldId(warpToWorld.world)); if (warpToWorld.world.is()) { if (auto clientId = getClientForUuid(warpToWorld.world.get())) { if (auto systemWorld = m_clients.get(*clientId)->systemWorld()) clientContext->setOrbitWarpAction(systemWorld->clientWarpAction(*clientId)); } } } else if (auto returnWarp = clientContext->playerReturnWarp()) { Logger::info("UniverseServer: Warping player {} failed, returning to '{}'", clientId, printWarpAction(returnWarp)); m_pendingPlayerWarps[clientId] = {returnWarp, false}; } else { Logger::info("UniverseServer: Warping player {} failed, returning to ship", clientId); m_pendingPlayerWarps[clientId] = {WarpAlias::OwnShip, false}; } m_connectionServer->sendPackets(clientId, {make_shared(true, warpAction, false)}); m_pendingPlayerWarps.remove(clientId); } else { Logger::info("UniverseServer: Warping player {} failed, invalid spawn target '{}'", clientId, printSpawnTarget(warpToWorld.target)); m_connectionServer->sendPackets(clientId, {make_shared(false, warpAction, true)}); m_pendingPlayerWarps.remove(clientId); } } else { Logger::info("UniverseServer: Warping player {} failed, invalid world '{}' or world failed to load", clientId, printWorldId(warpToWorld.world)); m_connectionServer->sendPackets(clientId, {make_shared(false, warpAction, false)}); m_pendingPlayerWarps.remove(clientId); } } else { // If the world is not created yet, just set a new warp again to wait for // it to create. m_pendingPlayerWarps[clientId] = {warpAction, deploy}; } } } void UniverseServer::flyShips() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); double queuedFlightWaitTime = Root::singleton().assets()->json("/universe_server.config:queuedFlightWaitTime").toDouble(); for (auto clientId : m_queuedFlights.keys()) { if (!m_pendingFlights.contains(clientId) && !m_pendingArrivals.contains(clientId)) { auto& flight = m_queuedFlights.get(clientId); if (flight.second.isNothing()) flight.second = m_universeClock->time() + queuedFlightWaitTime; else if (m_universeClock->time() > *flight.second) m_pendingFlights.set(clientId, flight.first); if (m_pendingFlights.contains(clientId)) m_queuedFlights.remove(clientId); } } eraseWhere(m_pendingFlights, [this](pair> const& p) { ConnectionId clientId = p.first; Vec3I system = p.second.first; SystemLocation location = p.second.second; auto clientContext = m_clients.value(clientId); if (!clientContext) return true; auto clientSystem = clientContext->systemWorld(); if (!clientSystem) system = Vec3I(); if (system != Vec3I() && clientContext->shipCoordinate().location() == system && clientContext->shipLocation() == location) return true; // if the ship is flying to another system do nothing // if the ship is flying within the target system, just update the ship destination if (m_pendingArrivals.contains(clientId)) { return true; } auto maybeClientShip = triggerWorldCreation(ClientShipWorldId(clientContext->playerUuid())); if (!maybeClientShip) return false; // ship is not loaded yet auto clientShip = *maybeClientShip; if (!clientShip) return true; // ship is broken CelestialCoordinate destination = location.maybe().value(CelestialCoordinate(system)); bool interstellar = clientSystem ? clientContext->shipCoordinate().location() != system : true; if (!interstellar) { // don't fly to null locations in the same system if (!location) return true; clientSystem->setClientDestination(clientId, location); } else if (system != Vec3I()) { // changing systems clientSystem->removeClient(clientId); clientContext->setSystemWorld({}); if (location) m_queuedFlights.set(clientId, {{system, location}, {}}); destination = CelestialCoordinate(system); } if (destination.isNull()) Logger::info("Flying ship for player {} to new starter world", clientId); else Logger::info("Flying ship for player {} to {}", clientId, destination); bool startInWarp = system == Vec3I(); clientShip->executeAction([interstellar, startInWarp](WorldServerThread*, WorldServer* worldServer) { worldServer->startFlyingSky(interstellar, startInWarp); }); clientContext->setShipCoordinate(CelestialCoordinate(system)); clientContext->setOrbitWarpAction({}); for (auto clientId : clientShip->clients()) { if (auto clientContext = m_clients.get(clientId)) clientContext->setOrbitWarpAction({}); } m_pendingArrivals.set(clientId, destination); return true; }); } void UniverseServer::arriveShips() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); eraseWhere(m_pendingArrivals, [this](pair& p) { auto& clientId = p.first; auto& coordinate = p.second; if (!coordinate) coordinate = nextStarterWorld().value(); if (!coordinate) return false; auto clientContext = m_clients.value(clientId); if (!clientContext) return true; auto clientSystem = clientContext->systemWorld(); if (!clientSystem) { clientSystem = createSystemWorld(coordinate.location()); if (coordinate.isSystem()) clientSystem->addClient(clientId, clientContext->playerUuid(), clientContext->shipUpgrades().shipSpeed, {}); else clientSystem->addClient(clientId, clientContext->playerUuid(), clientContext->shipUpgrades().shipSpeed, coordinate); clientContext->setSystemWorld(clientSystem); } auto location = clientSystem->clientShipLocation(clientId); if (!location) return false; if (!coordinate.isSystem() && !triggerWorldCreation(CelestialWorldId(coordinate))) return false; Logger::info("UniverseServer: Arriving ship for player {} at {}", clientId, coordinate); // world is loaded, ship has arrived clientContext->setShipCoordinate(coordinate); clientContext->setShipLocation(location); if (auto clientShip = createWorld(ClientShipWorldId(clientContext->playerUuid()))) { auto skyParameters = clientSystem->clientSkyParameters(clientId); clientShip->executeAction([skyParameters](WorldServerThread*, WorldServer* worldServer) { worldServer->stopFlyingSkyAt(skyParameters); }); for (auto shipClientId : clientShip->clients()) { if (auto clientContext = m_clients.get(shipClientId)) clientContext->setOrbitWarpAction(clientSystem->clientWarpAction(clientId)); } } return true; }); } void UniverseServer::respondToCelestialRequests() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); for (auto& p : m_pendingCelestialRequests) { List responses; eraseWhere(p.second, [&responses](WorkerPoolPromise const& request) { if (request.poll()) { responses.append(request.get()); return true; } return false; }); if (m_clients.contains(p.first)) m_connectionServer->sendPackets(p.first, {make_shared(std::move(responses))}); } eraseWhere(m_pendingCelestialRequests, [](auto const& p) { return p.second.empty(); }); } void UniverseServer::processChat() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); for (auto const& p : take(m_pendingChat)) { if (auto clientContext = m_clients.get(p.first)) { for (auto const& chat : p.second) { if (clientContext->remoteAddress()) Logger::info("Chat: <{}> {}", clientContext->playerName(), chat.first); auto team = m_teamManager->getTeam(clientContext->playerUuid()); if (chat.second == ChatSendMode::Broadcast) m_chatProcessor->broadcast(p.first, chat.first); else if (chat.second == ChatSendMode::Party && team.isValid()) m_chatProcessor->message(p.first, MessageContext::Mode::Party, team.value().hex(), chat.first); else m_chatProcessor->message(p.first, MessageContext::Mode::Local, printWorldId(clientContext->playerWorldId()), chat.first); } } } } void UniverseServer::clearBrokenWorlds() { RecursiveMutexLocker locker(m_mainLock); if (Time::monotonicMilliseconds() >= m_clearBrokenWorldsDeadline) { // Clear out all broken worlds eraseWhere(m_worlds, [](auto const& p) { if (!p.second.isValid()) { Logger::info("UniverseServer: Clearing broken world {}", p.first); return true; } else { return false; } }); int clearBrokenWorldsInterval = Root::singleton().assets()->json("/universe_server.config:clearBrokenWorldsInterval").toInt(); m_clearBrokenWorldsDeadline = Time::monotonicMilliseconds() + clearBrokenWorldsInterval; } } void UniverseServer::handleWorldMessages() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); auto it = m_pendingWorldMessages.begin(); while (it != m_pendingWorldMessages.end()) { auto& worldId = it->first; if (auto worldResult = triggerWorldCreation(worldId)) { auto& world = *worldResult; if (world) world->passMessages(std::move(it->second)); else for (auto& message : it->second) message.promise.fail("Error creating world"); it = m_pendingWorldMessages.erase(it); } else ++it; } } void UniverseServer::shutdownInactiveWorlds() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); // Shutdown idle and errored worlds. for (auto const& worldId : m_worlds.keys()) { if (auto world = getWorld(worldId)) { if (world->serverErrorOccurred()) { world->stop(); Logger::error("UniverseServer: World {} has stopped due to an error", worldId); worldDiedWithError(world->worldId()); } else if (world->noClients()) { bool anyPendingWarps = false; for (auto const& p : m_pendingPlayerWarps) { if (resolveWarpAction(p.second.first, p.first, p.second.second).world == world->worldId()) { anyPendingWarps = true; break; } } if (!anyPendingWarps && world->shouldExpire()) { Logger::info("UniverseServer: Stopping idle world {}", worldId); world->stop(); } } if (world->isJoined()) { auto kickClients = world->clients(); if (!kickClients.empty()) { Logger::info("UniverseServer: World {} shutdown, kicking {} players to their own ships", worldId, world->clients().size()); for (auto clientId : world->clients()) clientWarpPlayer(clientId, WarpAlias::OwnShip); } if (worldId.is()) { if (auto clientId = getClientForUuid(worldId.get())) m_clients.get(*clientId)->updateShipChunks(world->readChunks()); } m_worlds.remove(worldId); // Once a world is shutdown, mark its shutdown time in m_tempWorldIndex if (auto instanceWorldId = worldId.maybe()) { if (m_tempWorldIndex.contains(*instanceWorldId)) m_tempWorldIndex[*instanceWorldId].first = m_universeClock->milliseconds(); } } } } // Clear out all temporary worlds shut down more than tempWorldDeleteTime time ago. // Keep around worlds that are currently running or are active in system worlds Set systemLocationWorlds; for (auto p : m_systemWorlds) { for (auto instanceWorldId : p.second->activeInstanceWorlds()) { if (m_tempWorldIndex.contains(instanceWorldId)) systemLocationWorlds.add(instanceWorldId); } } eraseWhere(m_tempWorldIndex, [this, systemLocationWorlds](pair> const& p) { String storageFile = tempWorldFile(p.first); if (!m_worlds.contains(WorldId(p.first)) && !systemLocationWorlds.contains(p.first) && m_universeClock->milliseconds() > int64_t(p.second.first + p.second.second)) { Logger::info("UniverseServer: Expiring temporary world {}", printWorldId(p.first)); if (File::isFile(storageFile)) File::remove(storageFile); return true; } return false; }); // Clear out empty system worlds eraseWhere(m_systemWorlds, [](pair w) { return w.second->clients().empty(); }); } void UniverseServer::doTriggeredStorage() { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (Time::monotonicMilliseconds() >= m_storageTriggerDeadline) { Logger::debug("UniverseServer: periodic sync to disk"); saveSettings(); saveTempWorldIndex(); for (auto const& p : m_clients) { if (auto shipWorld = getWorld(ClientShipWorldId(p.second->playerUuid()))) p.second->updateShipChunks(shipWorld->readChunks()); auto versioningDatabase = Root::singleton().versioningDatabase(); String clientContextFile = File::relativeTo(m_storageDirectory, strf("{}.clientcontext", p.second->playerUuid().hex())); VersionedJson::writeFile(versioningDatabase->makeCurrentVersionedJson("ClientContext", p.second->storeServerData()), clientContextFile); } int storageTriggerInterval = Root::singleton().assets()->json("/universe_server.config:universeStorageInterval").toInt(); m_storageTriggerDeadline = Time::monotonicMilliseconds() + storageTriggerInterval; m_celestialDatabase->cleanupAndCommit(); } } void UniverseServer::saveSettings() { RecursiveMutexLocker locker(m_mainLock); auto versioningDatabase = Root::singleton().versioningDatabase(); auto versionedSettings = versioningDatabase->makeCurrentVersionedJson("UniverseSettings", m_universeSettings->toJson().set("time", m_universeClock->time())); VersionedJson::writeFile(versionedSettings, File::relativeTo(m_storageDirectory, "universe.dat")); } void UniverseServer::loadSettings() { RecursiveMutexLocker locker(m_mainLock); auto loadDefaultSettings = [this]() { m_universeClock = make_shared(); m_universeSettings = make_shared(); }; auto versioningDatabase = Root::singleton().versioningDatabase(); auto storageFile = File::relativeTo(m_storageDirectory, "universe.dat"); if (File::isFile(storageFile)) { try { auto settings = versioningDatabase->loadVersionedJson(VersionedJson::readFile(storageFile), "UniverseSettings"); m_universeSettings = make_shared(settings); m_universeClock = make_shared(); m_universeClock->setTime(settings.getDouble("time")); } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load universe settings file, loading defaults {}", outputException(e, false)); File::rename(storageFile, strf("{}.{}.fail", storageFile, Time::millisecondsSinceEpoch())); loadDefaultSettings(); } } else { loadDefaultSettings(); } m_universeClock->start(); } Maybe UniverseServer::nextStarterWorld() { RecursiveMutexLocker locker(m_mainLock); auto assets = Root::singleton().assets(); String defaultWorldCoordinate = assets->json("/universe_server.config:defaultWorldCoordinate").toString(); if (!defaultWorldCoordinate.empty()) return CelestialCoordinate(defaultWorldCoordinate); if (m_nextRandomizedStarterWorld && m_nextRandomizedStarterWorld->done()) { CelestialCoordinate nextWorld = m_nextRandomizedStarterWorld->get(); m_nextRandomizedStarterWorld.reset(); return nextWorld; } if (!m_nextRandomizedStarterWorld) { m_nextRandomizedStarterWorld = m_workerPool.addProducer([assets, celestialDatabase = m_celestialDatabase]() { Logger::info("Searching for new randomized starter world"); auto filterWorld = [celestialDatabase](CelestialCoordinate const& coordinate, Json const& filter) { auto parameters = celestialDatabase->parameters(coordinate); auto visitableParameters = parameters->visitableParameters(); if (!visitableParameters) return false; if (auto biome = filter.optString("terrestrialBiome")) { auto terrestrialParameters = as(visitableParameters); if (!terrestrialParameters || *biome != terrestrialParameters->primaryBiome) return false; } if (auto size = filter.optString("terrestrialSize")) { auto terrestrialParameters = as(visitableParameters); if (!terrestrialParameters || *size != terrestrialParameters->sizeName) return false; } if (auto dungeon = filter.optString("floatingDungeon")) { auto dungeonParameters = as(visitableParameters); if (!dungeonParameters || *dungeon != dungeonParameters->primaryDungeon) return false; } return true; }; auto findParameters = assets->json("/universe_server.config:findStarterWorldParameters"); auto randomWorld = celestialDatabase->findRandomWorld(findParameters.getUInt("tries"), findParameters.getUInt("range"), [&](CelestialCoordinate const& coordinate) { if (!filterWorld(coordinate, findParameters.get("starterWorld"))) return false; List allChildren; for (auto const& planet : celestialDatabase->children(coordinate.system())) { allChildren.append(planet); for (auto const& satellite : celestialDatabase->children(planet)) allChildren.append(satellite); } for (auto const& requiredSystemWorld : findParameters.getArray("requiredSystemWorlds", {})) { bool worldFound = false; for (auto const& world : allChildren) { if (filterWorld(world, requiredSystemWorld)) { worldFound = true; break; } } if (!worldFound) return false; } return true; }); if (randomWorld) Logger::info("UniverseServer: Found randomized starter world at {}", *randomWorld); else Logger::error("UniverseServer: Could not find randomized starter world!"); return randomWorld.value(); }); } return {}; } void UniverseServer::loadTempWorldIndex() { auto versioningDatabase = Root::singleton().versioningDatabase(); auto storageFile = File::relativeTo(m_storageDirectory, "tempworlds.index"); if (File::isFile(storageFile)) { try { m_tempWorldIndex.clear(); auto settings = versioningDatabase->loadVersionedJson(VersionedJson::readFile(storageFile), "TempWorldIndex"); for (auto p : settings.iterateObject()) { WorldId worldId = parseWorldId(p.first); pair deleteTime = {p.second.get(0).toUInt(), p.second.get(1).toUInt()}; m_tempWorldIndex.insert(worldId.get(), deleteTime); } } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load temp world index file", outputException(e, false)); File::rename(storageFile, strf("{}.{}.fail", storageFile, Time::millisecondsSinceEpoch())); } } // delete temporary instance worlds not found in the index on load auto tempWorldFiles = m_tempWorldIndex.keys().transformed([this](InstanceWorldId const& worldId) { return tempWorldFile(worldId); }); for (auto p : File::dirList(m_storageDirectory)) { if (p.second == false && p.first.endsWith(".tempworld")) { String storageFile = File::relativeTo(m_storageDirectory, p.first); if (!tempWorldFiles.contains(storageFile)) { Logger::info("UniverseServer: Removing unindexed temporary world {}", p.first); File::remove(storageFile); } } } } void UniverseServer::saveTempWorldIndex() { JsonObject worldIndex = JsonObject(); for (auto p : m_tempWorldIndex) { worldIndex.set(printWorldId(p.first), JsonArray{p.second.first, p.second.second}); } auto versioningDatabase = Root::singleton().versioningDatabase(); auto versionedJson = versioningDatabase->makeCurrentVersionedJson("TempWorldIndex", worldIndex); VersionedJson::writeFile(versionedJson, File::relativeTo(m_storageDirectory, "tempworlds.index")); } String UniverseServer::tempWorldFile(InstanceWorldId const& worldId) const { String identifier = worldId.instance; if (worldId.uuid) identifier = strf("{}-{}", identifier, worldId.uuid->hex()); if (worldId.level) identifier = strf("{}-{}", identifier, worldId.level.value()); return File::relativeTo(m_storageDirectory, strf("{}.tempworld", identifier)); } Maybe UniverseServer::isBannedUser(Maybe hostAddress, Uuid playerUuid) const { RecursiveMutexLocker locker(m_mainLock); auto config = Root::singleton().configuration(); if (hostAddress) { for (auto const& ban : m_tempBans) { if (ban.ip) { if (*ban.ip == *hostAddress) { return ban.reason; } } } for (auto banEntry : config->get("bannedIPs").iterateArray()) { if (HostAddress(banEntry.getString("ip")) == *hostAddress) { return banEntry.getString("reason"); } } } for (auto const& ban : m_tempBans) { if (ban.uuid) { if (*ban.uuid == playerUuid) return ban.reason; } } for (auto banEntry : config->get("bannedUuids").iterateArray()) { if (Uuid(banEntry.getString("uuid")) == playerUuid) return banEntry.getString("reason"); } return {}; } void UniverseServer::doTempBan(ConnectionId clientId, String const& reason, pair banType, int timeout) { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) { if (!clientContext->remoteAddress()) return; auto banExpiry = Time::monotonicMilliseconds() + timeout * 1000; // current time is in millis, conversion factor Maybe ip; if (banType.first) ip = clientContext->remoteAddress(); Maybe uuid; if (banType.second) uuid = clientContext->playerUuid(); m_tempBans.append({banExpiry, reason, ip, uuid}); } } void UniverseServer::doPermBan(ConnectionId clientId, String const& reason, pair banType) { RecursiveMutexLocker locker(m_mainLock); ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) { if (!clientContext->remoteAddress()) return; auto config = Root::singleton().configuration(); if (banType.first) { auto bannedIPs = config->get("bannedIPs").toArray(); bannedIPs.append(JsonObject{ {"ip", toString(*clientContext->remoteAddress())}, {"reason", reason}, }); config->set("bannedIPs", bannedIPs); } if (banType.second) { auto bannedUuids = config->get("bannedUuids").toArray(); bannedUuids.append(JsonObject{ {"uuid", clientContext->playerUuid().hex()}, {"reason", reason}, }); config->set("bannedUuids", bannedUuids); } } } void UniverseServer::removeTimedBan() { RecursiveMutexLocker locker(m_mainLock); auto currentTime = Time::monotonicMilliseconds(); eraseWhere(m_tempBans, [currentTime](TimeoutBan const& b) { return b.banExpiry <= currentTime; }); } void UniverseServer::addCelestialRequests(ConnectionId clientId, List requests) { RecursiveMutexLocker locker(m_mainLock); for (auto request : requests) { m_pendingCelestialRequests[clientId].append(m_workerPool.addProducer([this, request]() { return m_celestialDatabase->respondToRequest(request); })); } } void UniverseServer::worldUpdated(WorldServerThread* server) { for (auto clientId : server->clients()) { auto packets = server->pullOutgoingPackets(clientId); m_connectionServer->sendPackets(clientId, std::move(packets)); } } void UniverseServer::systemWorldUpdated(SystemWorldServerThread* systemWorldServer) { for (auto clientId : systemWorldServer->clients()) { auto packets = systemWorldServer->pullOutgoingPackets(clientId); m_connectionServer->sendPackets(clientId, std::move(packets)); } } void UniverseServer::packetsReceived(UniverseConnectionServer*, ConnectionId clientId, List packets) { ReadLocker clientsLocker(m_clientsLock); if (auto clientContext = m_clients.value(clientId)) { clientsLocker.unlock(); for (auto& packet : packets) { if (auto warpAction = as(packet)) { clientWarpPlayer(clientId, warpAction->action, warpAction->deploy); } else if (auto flyShip = as(packet)) { clientFlyShip(clientId, flyShip->system, flyShip->location); } else if (auto chatSend = as(packet)) { RecursiveMutexLocker locker(m_mainLock); m_pendingChat[clientId].append({std::move(chatSend->text), chatSend->sendMode}); } else if (auto clientContextUpdatePacket = as(packet)) { clientContext->readUpdate(std::move(clientContextUpdatePacket->updateData)); } else if (auto clientDisconnectPacket = as(packet)) { disconnectClient(clientId, String()); } else if (auto celestialRequest = as(packet)) { addCelestialRequests(clientId, std::move(celestialRequest->requests)); } else if (is(packet)) { if (auto currentSystem = clientContext->systemWorld()) currentSystem->pushIncomingPacket(clientId, std::move(packet)); } else { if (auto currentWorld = clientContext->playerWorld()) currentWorld->pushIncomingPackets(clientId, {std::move(packet)}); } } } } void UniverseServer::acceptConnection(UniverseConnection connection, Maybe remoteAddress) { auto& root = Root::singleton(); auto assets = root.assets(); auto configuration = root.configuration(); auto versioningDatabase = root.versioningDatabase(); int clientWaitLimit = assets->json("/universe_server.config:clientWaitLimit").toInt(); String serverAssetsMismatchMessage = assets->json("/universe_server.config:serverAssetsMismatchMessage").toString(); String clientAssetsMismatchMessage = assets->json("/universe_server.config:clientAssetsMismatchMessage").toString(); auto connectionSettings = configuration->get("connectionSettings"); RecursiveMutexLocker mainLocker(m_mainLock, false); connection.receiveAny(clientWaitLimit); auto protocolRequest = as(connection.pullSingle()); if (!protocolRequest) { Logger::warn("UniverseServer: client connection aborted, expected ProtocolRequestPacket"); return; } bool legacyClient = protocolRequest->compressionMode() != PacketCompressionMode::Enabled; connection.packetSocket().setLegacy(legacyClient); auto protocolResponse = make_shared(); protocolResponse->setCompressionMode(PacketCompressionMode::Enabled); // Signal that we're OpenStarbound if (protocolRequest->requestProtocolVersion != StarProtocolVersion) { Logger::warn("UniverseServer: client connection aborted, unsupported protocol version {}, supported version {}", protocolRequest->requestProtocolVersion, StarProtocolVersion); protocolResponse->allowed = false; connection.pushSingle(protocolResponse); connection.sendAll(clientWaitLimit); mainLocker.lock(); m_deadConnections.append({std::move(connection), Time::monotonicMilliseconds()}); return; } bool useCompressionStream = false; protocolResponse->allowed = true; if (!legacyClient) { auto compressionName = connectionSettings.getString("compression", "None"); auto compressionMode = NetCompressionModeNames.maybeLeft(compressionName).value(NetCompressionMode::None); useCompressionStream = compressionMode == NetCompressionMode::Zstd; protocolResponse->info = JsonObject{ {"compression", NetCompressionModeNames.getRight(compressionMode)} }; } connection.pushSingle(protocolResponse); connection.sendAll(clientWaitLimit); if (auto compressedSocket = as(&connection.packetSocket())) compressedSocket->setCompressionStreamEnabled(useCompressionStream); String remoteAddressString = remoteAddress ? toString(*remoteAddress) : "local"; Logger::info("UniverseServer: Awaiting connection info from {} ({} client)", remoteAddressString, legacyClient ? "vanilla" : "custom"); connection.receiveAny(clientWaitLimit); auto clientConnect = as(connection.pullSingle()); if (!clientConnect) { Logger::warn("UniverseServer: client connection aborted"); connection.pushSingle(make_shared("connect timeout")); mainLocker.lock(); m_deadConnections.append({std::move(connection), Time::monotonicMilliseconds()}); return; } bool administrator = false; String accountString = !clientConnect->account.empty() ? strf("'{}'", clientConnect->account) : ""; auto connectionFail = [&](String message) { Logger::warn("UniverseServer: Login attempt failed with account '{}' as player '{}' from address {}, error: {}", accountString, clientConnect->playerName, remoteAddressString, message); connection.pushSingle(make_shared(std::move(message))); mainLocker.lock(); m_deadConnections.append({std::move(connection), Time::monotonicMilliseconds()}); }; if (!remoteAddress) { administrator = true; Logger::info("UniverseServer: Logged in player '{}' locally", clientConnect->playerName); } else { if (clientConnect->assetsDigest != m_assetsDigest) { if (!configuration->get("allowAssetsMismatch").toBool()) { connectionFail(serverAssetsMismatchMessage); return; } else if (!clientConnect->allowAssetsMismatch) { connectionFail(clientAssetsMismatchMessage); return; } } if (!m_speciesShips.contains(clientConnect->playerSpecies)) { connectionFail("Unknown player species"); return; } if (!clientConnect->account.empty()) { auto passwordSalt = secureRandomBytes(assets->json("/universe_server.config:passwordSaltLength").toUInt()); Logger::info("UniverseServer: Sending Handshake Challenge"); connection.pushSingle(make_shared(passwordSalt)); connection.sendAll(clientWaitLimit); connection.receiveAny(clientWaitLimit); shared_ptr handshakeResponsePacket = as(connection.pullSingle()); if (!handshakeResponsePacket) { connectionFail("Expected HandshakeResponsePacket."); return; } bool success = false; if (Json account = configuration->get("serverUsers").get(clientConnect->account, {})) { administrator = account.getBool("admin", false); ByteArray passAccountSalt = (account.getString("password") + clientConnect->account).utf8Bytes(); passAccountSalt.append(passwordSalt); ByteArray passHash = sha256(passAccountSalt); if (passHash == handshakeResponsePacket->passHash) success = true; } // Give the same message for missing account vs wrong password to // prevent account detection, overkill given the overall level of // security but hey, why not. if (!success) { connectionFail(strf("No such account '{}' or incorrect password", clientConnect->account)); return; } } else { if (!configuration->get("allowAnonymousConnections").toBool()) { connectionFail("Anonymous connections disallowed"); return; } administrator = configuration->get("anonymousConnectionsAreAdmin").toBool(); } if (auto reason = isBannedUser(remoteAddress, clientConnect->playerUuid)) { connectionFail("You are banned: " + *reason); return; } } String connectionLog = strf("UniverseServer: Logged in account '{}' as player '{}' from address {}", accountString, clientConnect->playerName, remoteAddressString); if (Json& info = clientConnect->info) { if (Json brand = info.get("brand", "custom")) connectionLog += strf(" ({} client)", brand.toString()); if (info.getBool("legacy", false)) connection.packetSocket().setLegacy(legacyClient = true); } Logger::log(LogLevel::Info, connectionLog.utf8Ptr()); mainLocker.lock(); WriteLocker clientsLocker(m_clientsLock); if (auto clashId = getClientForUuid(clientConnect->playerUuid)) { if (administrator) { doDisconnection(*clashId, "Duplicate UUID joined and is Administrator so has priority."); } else { connectionFail("Duplicate player UUID"); return; } } if (m_clients.size() + 1 > m_maxPlayers && !administrator) { connectionFail("Max player connections"); return; } ConnectionId clientId = m_clients.nextId(); auto clientContext = make_shared(clientId, remoteAddress, clientConnect->playerUuid, clientConnect->playerName, clientConnect->playerSpecies, administrator, clientConnect->shipChunks); m_clients.add(clientId, clientContext); m_connectionServer->addConnection(clientId, std::move(connection)); clientsLocker.unlock(); clientContext->registerRpcHandlers(m_teamManager->rpcHandlers()); String clientContextFile = File::relativeTo(m_storageDirectory, strf("{}.clientcontext", clientConnect->playerUuid.hex())); if (File::isFile(clientContextFile)) { try { auto contextStore = versioningDatabase->loadVersionedJson(VersionedJson::readFile(clientContextFile), "ClientContext"); clientContext->loadServerData(contextStore); } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load client context file for , ignoring! {}", clientConnect->playerName, outputException(e, false)); File::rename(clientContextFile, strf("{}.{}.fail", clientContextFile, Time::millisecondsSinceEpoch())); } } // Need to do this after loadServerData because it sets the admin flag if (!administrator) clientContext->setAdmin(false); clientContext->setShipUpgrades(clientConnect->shipUpgrades); m_chatProcessor->connectClient(clientId, clientConnect->playerName); m_connectionServer->sendPackets(clientId, { make_shared(clientId, m_universeSettings->uuid(), m_celestialDatabase->baseInformation()), make_shared(m_universeClock->time()), make_shared(*m_pause, GlobalTimescale) }); setPvp(clientId, false); Vec3I location = clientContext->shipCoordinate().location(); if (location != Vec3I()) { auto clientSystem = createSystemWorld(location); clientSystem->addClient(clientId, clientContext->playerUuid(), clientContext->shipUpgrades().shipSpeed, clientContext->shipLocation()); addCelestialRequests(clientId, {makeLeft(location.vec2()), makeRight(location)}); clientContext->setSystemWorld(clientSystem); } Json introInstance = assets->json("/universe_server.config:introInstance"); String speciesIntroInstance = introInstance.getString(clientConnect->playerSpecies, introInstance.getString("default", "")); if (!speciesIntroInstance.empty() && !clientConnect->introComplete) { Logger::info("UniverseServer: Spawning player in intro instance {}", speciesIntroInstance); WarpAction introWarp = WarpToWorld{InstanceWorldId(speciesIntroInstance, clientContext->playerUuid()), {}}; clientWarpPlayer(clientId, introWarp); } else if (auto reviveWarp = clientContext->playerReviveWarp()) { // Do not revive players at non-persistent instance worlds or on ship worlds that // are not their own ship. bool useReviveWarp = true; if (reviveWarp.world.is()) { String instance = reviveWarp.world.get().instance; auto worldConfig = Root::singleton().assets()->json("/instance_worlds.config").opt(instance); if (!worldConfig || !worldConfig->getBool("persistent", false)) useReviveWarp = false; } if (reviveWarp.world.is() && reviveWarp.world.get() != clientConnect->playerUuid) useReviveWarp = false; if (useReviveWarp) { Logger::info("UniverseServer: Reviving player at {}", reviveWarp.world); clientWarpPlayer(clientId, reviveWarp); } else { Logger::info("UniverseServer: Player revive position is expired, spawning back at own ship"); clientWarpPlayer(clientId, WarpAlias::OwnShip); } } else { Maybe defaultReviveWarp = assets->json("/universe_server.config").optString("defaultReviveWarp"); if (defaultReviveWarp) { Logger::info("UniverseServer: Spawning player at default warp"); clientWarpPlayer(clientId, parseWarpAction(*defaultReviveWarp)); } else { Logger::info("UniverseServer: Spawning player at ship"); clientWarpPlayer(clientId, WarpAlias::OwnShip); } } clientFlyShip(clientId, clientContext->shipCoordinate().location(), clientContext->shipLocation()); Logger::info("UniverseServer: Client {} connected", clientContext->descriptiveName()); ReadLocker m_clientsReadLocker(m_clientsLock); auto players = static_cast(m_clients.size()); auto clients = m_clients.keys(); m_clientsReadLocker.unlock(); for (auto clientId : clients) { m_connectionServer->sendPackets(clientId, { make_shared(players, static_cast(m_maxPlayers)) }); } } WarpToWorld UniverseServer::resolveWarpAction(WarpAction warpAction, ConnectionId clientId, bool deploy) const { auto clientContext = m_clients.value(clientId); if (!clientContext) return {}; WorldId toWorldId; SpawnTarget spawnTarget; if (auto toWorld = warpAction.ptr()) { if (!toWorld->world) toWorldId = clientContext->playerWorldId(); else toWorldId = toWorld->world; spawnTarget = toWorld->target; } else if (auto toPlayerUuid = warpAction.ptr()) { if (auto toClientId = getClientForUuid(*toPlayerUuid)) { if (auto toClientWorld = m_clients.get(*toClientId)->playerWorld()) { if (auto toClientPosition = toClientWorld->playerRevivePosition(*toClientId)) { toWorldId = toClientWorld->worldId(); if (deploy) spawnTarget.reset(); else spawnTarget = SpawnTargetPosition(*toClientPosition); } } } } else if (auto shortcut = warpAction.ptr()) { if (*shortcut == WarpAlias::Return) { if (auto returnWarp = clientContext->playerReturnWarp()) { toWorldId = returnWarp.world; spawnTarget = returnWarp.target; } } else if (*shortcut == WarpAlias::OrbitedWorld) { if (auto warpAction = clientContext->orbitWarpAction()) { if (auto warpToWorld = warpAction->first.maybe()) { toWorldId = warpToWorld->world; spawnTarget = warpToWorld->target; } } } else if (*shortcut == WarpAlias::OwnShip) { toWorldId = ClientShipWorldId(clientContext->playerUuid()); } } return WarpToWorld(toWorldId, spawnTarget); } void UniverseServer::doDisconnection(ConnectionId clientId, String const& reason) { if (auto clientContext = m_clients.value(clientId)) { m_teamManager->playerDisconnected(clientContext->playerUuid()); // The client should revive at their ship if they are in an un-revivable // state WarpToWorld reviveWarp = WarpToWorld(ClientShipWorldId(clientContext->playerUuid())); if (auto currentWorld = clientContext->playerWorld()) { auto currentWorldId = currentWorld->worldId(); if (auto playerRevivePosition = currentWorld->playerRevivePosition(clientId)) reviveWarp = WarpToWorld(currentWorldId, SpawnTargetPosition(*playerRevivePosition)); m_connectionServer->sendPackets(clientId, currentWorld->removeClient(clientId)); m_chatProcessor->leaveChannel(clientId, printWorldId(currentWorld->worldId())); } clientContext->clearPlayerWorld(); clientContext->setPlayerReviveWarp(reviveWarp); if (auto systemWorld = clientContext->systemWorld()) systemWorld->removeClient(clientId); clientContext->clearSystemWorld(); if (m_chatProcessor->hasClient(clientId)) m_chatProcessor->disconnectClient(clientId); if (m_connectionServer->connectionIsOpen(clientId)) { // Send the client the last ship update. if (auto shipWorld = getWorld(ClientShipWorldId(clientContext->playerUuid()))) { clientContext->updateShipChunks(shipWorld->readChunks()); shipWorld->stop(); } sendClientContextUpdate(clientContext); // Then send the disconnect packet. m_connectionServer->sendPackets(clientId, {make_shared(reason)}); } // Write the final client context. auto versioningDatabase = Root::singleton().versioningDatabase(); String clientContextFile = File::relativeTo(m_storageDirectory, strf("{}.clientcontext", clientContext->playerUuid().hex())); VersionedJson::writeFile(versioningDatabase->makeCurrentVersionedJson("ClientContext", clientContext->storeServerData()), clientContextFile); m_clients.remove(clientId); m_deadConnections.append({m_connectionServer->removeConnection(clientId), Time::monotonicMilliseconds()}); Logger::info("UniverseServer: Client {} disconnected for reason: {}", clientContext->descriptiveName(), reason); auto players = static_cast(m_clients.size()); for (auto clientId : m_clients.keys()) { m_connectionServer->sendPackets(clientId, { make_shared(players, static_cast(m_maxPlayers)) }); } } } Maybe UniverseServer::getClientForUuid(Uuid const& uuid) const { for (auto const& p : m_clients) { if (p.second->playerUuid() == uuid) return p.second->clientId(); } return {}; } WorldServerThreadPtr UniverseServer::getWorld(WorldId const& worldId) { if (m_worlds.contains(worldId)) { auto& maybeWorldPromise = m_worlds.get(worldId); try { if (!maybeWorldPromise || !maybeWorldPromise->poll()) return {}; return maybeWorldPromise->get(); } catch (std::exception const& e) { maybeWorldPromise.reset(); Logger::error("UniverseServer: error during world create: {}", outputException(e, true)); worldDiedWithError(worldId); } } return {}; } WorldServerThreadPtr UniverseServer::createWorld(WorldId const& worldId) { if (!m_worlds.contains(worldId)) { if (auto promise = makeWorldPromise(worldId)) m_worlds.add(worldId, promise.take()); else return {}; } auto& maybeWorldPromise = m_worlds.get(worldId); if (!maybeWorldPromise) return {}; try { return maybeWorldPromise->get(); } catch (std::exception const& e) { maybeWorldPromise.reset(); Logger::error("UniverseServer: error during world create: {}", outputException(e, true)); worldDiedWithError(worldId); return {}; } } Maybe UniverseServer::triggerWorldCreation(WorldId const& worldId) { if (!m_worlds.contains(worldId)) { if (auto promise = makeWorldPromise(worldId)) { m_worlds.add(worldId, promise.take()); return {}; } else { return WorldServerThreadPtr(); } } else { auto& maybeWorldPromise = m_worlds.get(worldId); try { // If the promise is reset, this means that the promise threw an // exception, return nullptr to signify error. if (!maybeWorldPromise) return WorldServerThreadPtr(); if (!maybeWorldPromise->poll()) return {}; return maybeWorldPromise->get(); } catch (std::exception const& e) { maybeWorldPromise.reset(); Logger::error("UniverseServer: error during world create: {}", outputException(e, true)); worldDiedWithError(worldId); return WorldServerThreadPtr(); } } } Maybe> UniverseServer::makeWorldPromise(WorldId const& worldId) { if (auto celestialWorld = worldId.ptr()) return celestialWorldPromise(*celestialWorld); else if (auto shipWorld = worldId.ptr()) return shipWorldPromise(*shipWorld); else if (auto instanceWorld = worldId.ptr()) return instanceWorldPromise(*instanceWorld); else return {}; } Maybe> UniverseServer::shipWorldPromise( ClientShipWorldId const& clientShipWorldId) { auto clientId = clientForUuid(clientShipWorldId); if (!clientId) return {}; auto clientContext = m_clients.get(*clientId); auto speciesShips = m_speciesShips; auto celestialDatabase = m_celestialDatabase; auto universeClock = m_universeClock; return m_workerPool.addProducer([this, clientShipWorldId, clientContext, speciesShips, celestialDatabase, universeClock]() { WorldServerPtr shipWorld; auto shipChunks = clientContext->shipChunks(); if (!shipChunks.empty()) { try { Logger::info("UniverseServer: Loading client ship world {}", clientShipWorldId); shipWorld = make_shared(shipChunks); } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load client ship {}, resetting ship to default state! {}", clientShipWorldId, outputException(e, false)); } } if (!shipWorld) { Logger::info("UniverseServer: Creating new client ship world {}", clientShipWorldId); shipWorld = make_shared(Vec2U(2048, 2048), File::ephemeralFile()); auto& species = clientContext->playerSpecies(); auto shipStructure = WorldStructure(speciesShips.get(species).first()); shipStructure = shipWorld->setCentralStructure(shipStructure); ShipUpgrades currentUpgrades = clientContext->shipUpgrades(); currentUpgrades.apply(Root::singleton().assets()->json("/ships/shipupgrades.config")); currentUpgrades.apply(shipStructure.configValue("shipUpgrades")); clientContext->setShipUpgrades(currentUpgrades); shipWorld->setSpawningEnabled(false); shipWorld->setProperty("invinciblePlayers", true); shipWorld->setProperty("ship.level", 0); shipWorld->setProperty("ship.species", species); shipWorld->setProperty("ship.fuel", 0); shipWorld->setProperty("ship.maxFuel", currentUpgrades.maxFuel); shipWorld->setProperty("ship.crewSize", currentUpgrades.crewSize); shipWorld->setProperty("ship.fuelEfficiency", currentUpgrades.fuelEfficiency); } shipWorld->setUniverseSettings(m_universeSettings); shipWorld->setReferenceClock(universeClock); if (auto systemWorld = clientContext->systemWorld()) shipWorld->setOrbitalSky(systemWorld->clientSkyParameters(clientContext->clientId())); else shipWorld->setOrbitalSky(celestialSkyParameters(clientContext->shipCoordinate())); shipWorld->initLua(this); auto shipWorldThread = make_shared(shipWorld, ClientShipWorldId(clientShipWorldId)); shipWorldThread->setPause(m_pause); clientContext->updateShipChunks(shipWorldThread->readChunks()); shipWorldThread->start(); shipWorldThread->setUpdateAction(bind(&UniverseServer::worldUpdated, this, _1)); return shipWorldThread; }); } Maybe> UniverseServer::celestialWorldPromise(CelestialWorldId const& celestialWorldId) { if (!celestialWorldId) return {}; auto storageDirectory = m_storageDirectory; auto celestialDatabase = m_celestialDatabase; auto universeClock = m_universeClock; return m_workerPool.addProducer([this, celestialWorldId, storageDirectory, celestialDatabase, universeClock]() { WorldServerPtr worldServer; String storageFile = File::relativeTo(storageDirectory, strf("{}.world", celestialWorldId.filename())); if (File::isFile(storageFile)) { try { Logger::info("UniverseServer: Loading celestial world {}", celestialWorldId); worldServer = make_shared(File::open(storageFile, IOMode::ReadWrite)); } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load celestial world {}, removing! Cause: {}", celestialWorldId, outputException(e, false)); File::rename(storageFile, strf("{}.{}.fail", storageFile, Time::millisecondsSinceEpoch())); } } if (!worldServer) { Logger::info("UniverseServer: Creating celestial world {}", celestialWorldId); auto worldTemplate = make_shared(celestialWorldId, celestialDatabase); worldServer = make_shared(worldTemplate, File::open(storageFile, IOMode::ReadWrite | IOMode::Truncate)); } worldServer->setUniverseSettings(m_universeSettings); worldServer->setReferenceClock(universeClock); worldServer->initLua(this); auto worldThread = make_shared(worldServer, celestialWorldId); worldThread->setPause(m_pause); worldThread->start(); worldThread->setUpdateAction(bind(&UniverseServer::worldUpdated, this, _1)); return worldThread; }); } Maybe> UniverseServer::instanceWorldPromise(InstanceWorldId const& instanceWorldId) { auto storageDirectory = m_storageDirectory; auto universeClock = m_universeClock; return m_workerPool.addProducer([this, storageDirectory, instanceWorldId, universeClock]() { Json worldConfig = Root::singleton().assets()->json("/instance_worlds.config").get(instanceWorldId.instance); uint64_t worldSeed; if (worldConfig.contains("seed")) worldSeed = worldConfig.getUInt("seed"); else worldSeed = Random::randu64(); String worldType = worldConfig.getString("type"); VisitableWorldParametersPtr worldParameters; if (worldType.equalsIgnoreCase("Terrestrial")) worldParameters = generateTerrestrialWorldParameters(worldConfig.getString("planetType"), worldConfig.getString("planetSize"), worldSeed); else if (worldType.equalsIgnoreCase("Asteroids")) worldParameters = generateAsteroidsWorldParameters(worldSeed); else if (worldType.equalsIgnoreCase("FloatingDungeon")) worldParameters = generateFloatingDungeonWorldParameters(worldConfig.getString("dungeonWorld")); else throw UniverseServerException(strf("Unknown world type: '{}'\n", worldType)); if (instanceWorldId.level) worldParameters->threatLevel = *instanceWorldId.level; if (worldConfig.contains("beamUpRule")) worldParameters->beamUpRule = BeamUpRuleNames.getLeft(worldConfig.getString("beamUpRule")); worldParameters->disableDeathDrops = worldConfig.getBool("disableDeathDrops", false); SkyParameters skyParameters = SkyParameters(worldConfig.get("skyParameters", Json())); auto worldTemplate = make_shared(worldParameters, skyParameters, worldSeed); Json worldProperties = worldConfig.get("worldProperties", JsonObject{}); bool spawningEnabled = worldConfig.getBool("spawningEnabled", true); bool persistent = worldConfig.getBool("persistent", false); bool useUniverseClock = worldConfig.getBool("useUniverseClock", false); WorldServerPtr worldServer; bool worldExisted = false; if (persistent) { String identifier = instanceWorldId.instance; if (instanceWorldId.uuid) identifier = strf("{}-{}", identifier, instanceWorldId.uuid->hex()); if (instanceWorldId.level) identifier = strf("{}-{}", identifier, instanceWorldId.level.value()); String storageFile = File::relativeTo(storageDirectory, strf("unique-{}.world", identifier)); if (File::isFile(storageFile)) { try { Logger::info("UniverseServer: Loading persistent unique instance world {}", instanceWorldId.instance); worldServer = make_shared(File::open(storageFile, IOMode::ReadWrite)); worldExisted = true; } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load persistent unique instance world {}, removing! Cause: {}", instanceWorldId.instance, outputException(e, false)); File::rename(storageFile, strf("{}.{}.fail", storageFile, Time::millisecondsSinceEpoch())); } } if (!worldServer) { Logger::info("UniverseServer: Creating persistent unique instance world {}", instanceWorldId.instance); worldServer = make_shared(worldTemplate, File::open(storageFile, IOMode::ReadWrite | IOMode::Truncate)); } } else { String storageFile = tempWorldFile(instanceWorldId); uint64_t deleteTime = worldConfig.optInt("tempWorldDeleteTime").value(0); if (File::isFile(storageFile)) { if (m_tempWorldIndex.contains(instanceWorldId)) { auto file = File::open(storageFile, IOMode::ReadWrite); if (file->size() > 0) { Logger::info("UniverseServer: Loading temporary instance world {} from storage", instanceWorldId); try { worldServer = make_shared(file); worldExisted = true; } catch (std::exception const& e) { Logger::error("UniverseServer: Could not load temporary instance world '{}', re-creating cause: {}", instanceWorldId, outputException(e, false)); } } } else { File::remove(storageFile); } } if (!worldServer) { Logger::info("UniverseServer: Creating temporary instance world '{}' with expiry time {}", instanceWorldId, deleteTime); worldServer = make_shared(worldTemplate, File::open(storageFile, IOMode::ReadWrite)); m_tempWorldIndex.set(instanceWorldId, pair(m_universeClock->milliseconds(), deleteTime)); } } worldServer->setUniverseSettings(m_universeSettings); for (auto const& p : worldProperties.iterateObject()) worldServer->setProperty(p.first, p.second); worldServer->setProperty("ephemeral", !persistent); worldServer->setSpawningEnabled(spawningEnabled); if (useUniverseClock) worldServer->setReferenceClock(universeClock); if (!worldExisted) { for (auto flagAction : m_universeSettings->currentFlagActionsForInstanceWorld(instanceWorldId.instance)) { if (flagAction.is()) { auto placeDungeonAction = flagAction.get(); worldServer->placeDungeon(placeDungeonAction.dungeonId, placeDungeonAction.targetPosition, 0); } } } worldServer->initLua(this); auto worldThread = make_shared(worldServer, instanceWorldId); worldThread->setPause(m_pause); worldThread->start(); worldThread->setUpdateAction(bind(&UniverseServer::worldUpdated, this, _1)); return worldThread; }); } SystemWorldServerThreadPtr UniverseServer::createSystemWorld(Vec3I const& location) { if (!m_systemWorlds.contains(location)) { SystemWorldServerPtr systemWorld; String storageFile = File::relativeTo(m_storageDirectory, strf("{}_{}_{}.system", location[0], location[1], location[2])); bool loadedFromStorage = false; if (File::isFile(storageFile)) { Logger::info("UniverseServer: Loading system world {} from disk storage", location); try { auto versioningDatabase = Root::singleton().versioningDatabase(); VersionedJson versionedStore = VersionedJson::readFile(storageFile); Json store = versioningDatabase->loadVersionedJson(versionedStore, "System"); systemWorld = make_shared(store, m_universeClock, m_celestialDatabase); loadedFromStorage = true; } catch (std::exception const& e) { Logger::error("UniverseServer: Failed to load system {} from disk storage, re-creating. Cause: {}", location, outputException(e, false)); File::rename(storageFile, strf("{}.{}.fail", storageFile, Time::millisecondsSinceEpoch())); loadedFromStorage = false; } } if (!loadedFromStorage) { Logger::info("UniverseServer: Creating new system world at location {}", location); systemWorld = make_shared(location, m_universeClock, m_celestialDatabase); } auto systemThread = make_shared(location, systemWorld, storageFile); systemThread->setUpdateAction(bind(&UniverseServer::systemWorldUpdated, this, _1)); systemThread->start(); m_systemWorlds.set(location, systemThread); } return m_systemWorlds.get(location); } bool UniverseServer::instanceWorldStoredOrActive(InstanceWorldId const& worldId) const { String storageFile = File::relativeTo(m_storageDirectory, strf("unique-{}.world", worldId.instance)); return m_worlds.value(worldId).isValid() || m_tempWorldIndex.contains(worldId) || File::isFile(storageFile); } void UniverseServer::worldDiedWithError(WorldId world) { if (world.is()) { if (auto clientId = getClientForUuid(world.get())) m_pendingDisconnections.add(*clientId, "Client ship world has errored"); } } SkyParameters UniverseServer::celestialSkyParameters(CelestialCoordinate const& coordinate) const { if (m_celestialDatabase->coordinateValid(coordinate)) return SkyParameters(coordinate, m_celestialDatabase); return SkyParameters(); } }