osb/source/game/StarUniverseServer.cpp

2258 lines
87 KiB
C++

#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<CommandProcessor>(this);
m_chatProcessor = make_shared<ChatProcessor>();
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<CelestialMasterDatabase>(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<TeamManager>();
m_workerPool.start(assets->json("/universe_server.config:workerPoolThreads").toUInt());
m_connectionServer = make_shared<UniverseConnectionServer>(bind(&UniverseServer::packetsReceived, this, _1, _2, _3));
m_pause = make_shared<atomic<bool>>(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<UniverseConnection>(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<PausePacket>(*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<PausePacket>(*m_pause, GlobalTimescale)});
}
void UniverseServer::setTickRate(float tickRate) {
ServerGlobalTimestep = 1.0f / tickRate;
}
List<WorldId> 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<ConnectionId> 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<ConnectionId> UniverseServer::findNick(String const& nick) const {
return m_chatProcessor->findNick(nick);
}
Maybe<Uuid> UniverseServer::uuidForClient(ConnectionId clientId) const {
ReadLocker clientsLocker(m_clientsLock);
if (auto clientContext = m_clients.value(clientId))
return clientContext->playerUuid();
return {};
}
Maybe<ConnectionId> 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<Json> UniverseServer::sendWorldMessage(WorldId const& worldId, String const& message, JsonArray const& args) {
auto pair = RpcThreadPromise<Json>::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<WarpAction, bool>(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<void(WorldServer*, PlayerPtr)> 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<bool, bool> banType, Maybe<int> 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<TerrestrialWorldParameters>(celestialParameters->visitableParameters())) {
auto newTerrestrialParameters = make_shared<TerrestrialWorldParameters>(*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<PlanetTypeUpdatePacket>(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<TcpServer>(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<PlaceDungeonFlagAction>()) {
auto placeDungeonAction = action.get<PlaceDungeonFlagAction>();
if (instanceWorldStoredOrActive(placeDungeonAction.targetInstance)) {
auto worldId = InstanceWorldId(placeDungeonAction.targetInstance);
m_pendingFlagActions.append(pair<WorldId, UniverseFlagAction>{worldId, placeDungeonAction});
}
}
}
}
eraseWhere(m_pendingFlagActions, [this](pair<WorldId, UniverseFlagAction> const& p) {
if (p.first.is<InstanceWorldId>() && instanceWorldStoredOrActive(p.first.get<InstanceWorldId>())) {
// 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<PlaceDungeonFlagAction>()) {
auto placeDungeonAction = p.second.get<PlaceDungeonFlagAction>();
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<ChatReceivePacket>(message)});
}
}
void UniverseServer::updateTeams() {
RecursiveMutexLocker locker(m_mainLock);
ReadLocker clientsLocker(m_clientsLock);
StringMap<List<Uuid>> 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<unsigned>(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<UniverseTimeUpdatePacket>(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<ClientContextUpdatePacket>(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<void>& 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<CelestialWorldId>()) {
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<ClientShipWorldId>()) {
if (auto clientId = getClientForUuid(warpToWorld.world.get<ClientShipWorldId>())) {
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<PlayerWarpResultPacket>(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<PlayerWarpResultPacket>(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<PlayerWarpResultPacket>(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<ConnectionId const, pair<Vec3I, SystemLocation>> 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<CelestialCoordinate>().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<ConnectionId const, CelestialCoordinate>& 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<CelestialResponse> responses;
eraseWhere(p.second, [&responses](WorkerPoolPromise<CelestialResponse> 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<CelestialResponsePacket>(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<ClientShipWorldId>()) {
if (auto clientId = getClientForUuid(worldId.get<ClientShipWorldId>()))
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<InstanceWorldId>()) {
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<InstanceWorldId> 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<InstanceWorldId, pair<uint64_t, uint64_t>> 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<Vec3I, SystemWorldServerThreadPtr> 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<Clock>();
m_universeSettings = make_shared<UniverseSettings>();
};
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<UniverseSettings>(settings);
m_universeClock = make_shared<Clock>();
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<CelestialCoordinate> 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<CelestialCoordinate>([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<TerrestrialWorldParameters>(visitableParameters);
if (!terrestrialParameters || *biome != terrestrialParameters->primaryBiome)
return false;
}
if (auto size = filter.optString("terrestrialSize")) {
auto terrestrialParameters = as<TerrestrialWorldParameters>(visitableParameters);
if (!terrestrialParameters || *size != terrestrialParameters->sizeName)
return false;
}
if (auto dungeon = filter.optString("floatingDungeon")) {
auto dungeonParameters = as<FloatingDungeonWorldParameters>(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<CelestialCoordinate> 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<uint64_t, uint64_t> deleteTime = {p.second.get(0).toUInt(), p.second.get(1).toUInt()};
m_tempWorldIndex.insert(worldId.get<InstanceWorldId>(), 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<String> UniverseServer::isBannedUser(Maybe<HostAddress> 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<bool, bool> 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<HostAddress> ip;
if (banType.first)
ip = clientContext->remoteAddress();
Maybe<Uuid> uuid;
if (banType.second)
uuid = clientContext->playerUuid();
m_tempBans.append({banExpiry, reason, ip, uuid});
}
}
void UniverseServer::doPermBan(ConnectionId clientId, String const& reason, pair<bool, bool> 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<CelestialRequest> requests) {
RecursiveMutexLocker locker(m_mainLock);
for (auto request : requests) {
m_pendingCelestialRequests[clientId].append(m_workerPool.addProducer<CelestialResponse>([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<PacketPtr> packets) {
ReadLocker clientsLocker(m_clientsLock);
if (auto clientContext = m_clients.value(clientId)) {
clientsLocker.unlock();
for (auto& packet : packets) {
if (auto warpAction = as<PlayerWarpPacket>(packet)) {
clientWarpPlayer(clientId, warpAction->action, warpAction->deploy);
} else if (auto flyShip = as<FlyShipPacket>(packet)) {
clientFlyShip(clientId, flyShip->system, flyShip->location);
} else if (auto chatSend = as<ChatSendPacket>(packet)) {
RecursiveMutexLocker locker(m_mainLock);
m_pendingChat[clientId].append({std::move(chatSend->text), chatSend->sendMode});
} else if (auto clientContextUpdatePacket = as<ClientContextUpdatePacket>(packet)) {
clientContext->readUpdate(std::move(clientContextUpdatePacket->updateData));
} else if (auto clientDisconnectPacket = as<ClientDisconnectRequestPacket>(packet)) {
disconnectClient(clientId, String());
} else if (auto celestialRequest = as<CelestialRequestPacket>(packet)) {
addCelestialRequests(clientId, std::move(celestialRequest->requests));
} else if (is<SystemObjectSpawnPacket>(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<HostAddress> 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<ProtocolRequestPacket>(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<ProtocolResponsePacket>();
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<CompressedPacketSocket>(&connection.packetSocket()))
compressedSocket->setCompressionStreamEnabled(useCompressionStream);
String remoteAddressString = remoteAddress ? toString(*remoteAddress) : "local";
Logger::info("UniverseServer: Awaiting connection info from {}, {} client", remoteAddressString, legacyClient ? "Starbound" : "OpenStarbound");
connection.receiveAny(clientWaitLimit);
auto clientConnect = as<ClientConnectPacket>(connection.pullSingle());
if (!clientConnect) {
Logger::warn("UniverseServer: client connection aborted");
connection.pushSingle(make_shared<ConnectFailurePacket>("connect timeout"));
mainLocker.lock();
m_deadConnections.append({std::move(connection), Time::monotonicMilliseconds()});
return;
}
bool administrator = false;
String accountString = !clientConnect->account.empty() ? strf("'{}'", clientConnect->account) : "<anonymous>";
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<ConnectFailurePacket>(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<HandshakeChallengePacket>(passwordSalt));
connection.sendAll(clientWaitLimit);
connection.receiveAny(clientWaitLimit);
shared_ptr<HandshakeResponsePacket> handshakeResponsePacket = as<HandshakeResponsePacket>(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;
}
}
Logger::info("UniverseServer: Logged in account '{}' as player '{}' from address {}",
accountString, clientConnect->playerName, remoteAddressString);
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<ServerClientContext>(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 <User: {}>, 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<ConnectSuccessPacket>(clientId, m_universeSettings->uuid(), m_celestialDatabase->baseInformation()),
make_shared<UniverseTimeUpdatePacket>(m_universeClock->time()),
make_shared<PausePacket>(*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<InstanceWorldId>()) {
String instance = reviveWarp.world.get<InstanceWorldId>().instance;
auto worldConfig = Root::singleton().assets()->json("/instance_worlds.config").opt(instance);
if (!worldConfig || !worldConfig->getBool("persistent", false))
useReviveWarp = false;
}
if (reviveWarp.world.is<ClientShipWorldId>() && reviveWarp.world.get<ClientShipWorldId>() != 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<String> 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());
auto players = static_cast<uint16_t>(m_clients.size());
for (auto clientId : m_clients.keys()) {
m_connectionServer->sendPackets(clientId, {
make_shared<ServerInfoPacket>(players, static_cast<uint16_t>(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<WarpToWorld>()) {
if (!toWorld->world)
toWorldId = clientContext->playerWorldId();
else
toWorldId = toWorld->world;
spawnTarget = toWorld->target;
} else if (auto toPlayerUuid = warpAction.ptr<WarpToPlayer>()) {
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<WarpAlias>()) {
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<WarpToWorld>()) {
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<ServerDisconnectPacket>(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<uint16_t>(m_clients.size());
for (auto clientId : m_clients.keys()) {
m_connectionServer->sendPackets(clientId, {
make_shared<ServerInfoPacket>(players, static_cast<uint16_t>(m_maxPlayers))
});
}
}
}
Maybe<ConnectionId> 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<WorldServerThreadPtr> 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<WorkerPoolPromise<WorldServerThreadPtr>> UniverseServer::makeWorldPromise(WorldId const& worldId) {
if (auto celestialWorld = worldId.ptr<CelestialWorldId>())
return celestialWorldPromise(*celestialWorld);
else if (auto shipWorld = worldId.ptr<ClientShipWorldId>())
return shipWorldPromise(*shipWorld);
else if (auto instanceWorld = worldId.ptr<InstanceWorldId>())
return instanceWorldPromise(*instanceWorld);
else
return {};
}
Maybe<WorkerPoolPromise<WorldServerThreadPtr>> 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<WorldServerThreadPtr>([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<WorldServer>(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<WorldServer>(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<WorldServerThread>(shipWorld, ClientShipWorldId(clientShipWorldId));
shipWorldThread->setPause(m_pause);
clientContext->updateShipChunks(shipWorldThread->readChunks());
shipWorldThread->start();
shipWorldThread->setUpdateAction(bind(&UniverseServer::worldUpdated, this, _1));
return shipWorldThread;
});
}
Maybe<WorkerPoolPromise<WorldServerThreadPtr>> UniverseServer::celestialWorldPromise(CelestialWorldId const& celestialWorldId) {
if (!celestialWorldId)
return {};
auto storageDirectory = m_storageDirectory;
auto celestialDatabase = m_celestialDatabase;
auto universeClock = m_universeClock;
return m_workerPool.addProducer<WorldServerThreadPtr>([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<WorldServer>(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<WorldTemplate>(celestialWorldId, celestialDatabase);
worldServer = make_shared<WorldServer>(worldTemplate, File::open(storageFile, IOMode::ReadWrite | IOMode::Truncate));
}
worldServer->setUniverseSettings(m_universeSettings);
worldServer->setReferenceClock(universeClock);
worldServer->initLua(this);
auto worldThread = make_shared<WorldServerThread>(worldServer, celestialWorldId);
worldThread->setPause(m_pause);
worldThread->start();
worldThread->setUpdateAction(bind(&UniverseServer::worldUpdated, this, _1));
return worldThread;
});
}
Maybe<WorkerPoolPromise<WorldServerThreadPtr>> UniverseServer::instanceWorldPromise(InstanceWorldId const& instanceWorldId) {
auto storageDirectory = m_storageDirectory;
auto universeClock = m_universeClock;
return m_workerPool.addProducer<WorldServerThreadPtr>([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<WorldTemplate>(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<WorldServer>(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<WorldServer>(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<WorldServer>(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<WorldServer>(worldTemplate, File::open(storageFile, IOMode::ReadWrite));
m_tempWorldIndex.set(instanceWorldId, pair<uint64_t, uint64_t>(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<PlaceDungeonFlagAction>()) {
auto placeDungeonAction = flagAction.get<PlaceDungeonFlagAction>();
worldServer->placeDungeon(placeDungeonAction.dungeonId, placeDungeonAction.targetPosition, 0);
}
}
}
worldServer->initLua(this);
auto worldThread = make_shared<WorldServerThread>(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<SystemWorldServer>(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<SystemWorldServer>(location, m_universeClock, m_celestialDatabase);
}
auto systemThread = make_shared<SystemWorldServerThread>(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<ClientShipWorldId>()) {
if (auto clientId = getClientForUuid(world.get<ClientShipWorldId>()))
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();
}
}