#include #include #include #include #include #include #include #include #include #include //DLU Includes: #include "CDClientDatabase.h" #include "CDClientManager.h" #include "Database.h" #include "MigrationRunner.h" #include "Diagnostics.h" #include "dCommonVars.h" #include "dConfig.h" #include "Logger.h" #include "dServer.h" #include "AssetManager.h" #include "BinaryPathFinder.h" #include "eConnectionType.h" #include "eMasterMessageType.h" //RakNet includes: #include "RakNetDefines.h" //Packet includes: #include "AuthPackets.h" #include "Game.h" #include "InstanceManager.h" #include "MasterPackets.h" #include "PersistentIDManager.h" #include "FdbToSqlite.h" #include "BitStreamUtils.h" #include "Start.h" #include "Server.h" namespace Game { Logger* logger = nullptr; dServer* server = nullptr; InstanceManager* im = nullptr; dConfig* config = nullptr; AssetManager* assetManager = nullptr; Game::signal_t lastSignal = 0; bool universeShutdownRequested = false; std::mt19937 randomEngine; } //namespace Game bool shutdownSequenceStarted = false; int ShutdownSequence(int32_t signal = -1); int32_t FinalizeShutdown(int32_t signal = -1); void HandlePacket(Packet* packet); std::map activeSessions; SystemAddress authServerMasterPeerSysAddr; SystemAddress chatServerMasterPeerSysAddr; int main(int argc, char** argv) { constexpr uint32_t masterFramerate = mediumFramerate; constexpr uint32_t masterFrameDelta = mediumFrameDelta; Diagnostics::SetProcessName("Master"); Diagnostics::SetProcessFileName(argv[0]); Diagnostics::Initialize(); #if defined(_WIN32) && defined(MARIADB_PLUGIN_DIR_OVERRIDE) _putenv_s("MARIADB_PLUGIN_DIR", MARIADB_PLUGIN_DIR_OVERRIDE); #endif //Triggers the shutdown sequence at application exit std::atexit([]() { ShutdownSequence(); }); std::signal(SIGINT, Game::OnSignal); std::signal(SIGTERM, Game::OnSignal); Game::config = new dConfig("masterconfig.ini"); //Create all the objects we need to run our service: Server::SetupLogger("MasterServer"); if (!Game::logger) return EXIT_FAILURE; if (!dConfig::Exists("authconfig.ini")) Log::Info("Could not find authconfig.ini, using default settings"); if (!dConfig::Exists("chatconfig.ini")) Log::Info("Could not find chatconfig.ini, using default settings"); if (!dConfig::Exists("masterconfig.ini")) Log::Info("Could not find masterconfig.ini, using default settings"); if (!dConfig::Exists("sharedconfig.ini")) Log::Info("Could not find sharedconfig.ini, using default settings"); if (!dConfig::Exists("worldconfig.ini")) Log::Info("Could not find worldconfig.ini, using default settings"); const auto clientNetVersionString = Game::config->GetValue("client_net_version"); const uint32_t clientNetVersion = GeneralUtils::TryParse(clientNetVersionString).value_or(171022); Log::Info("Using net version {:d}", clientNetVersion); Log::Info("Starting Master server..."); Log::Info("Version: {:s}", PROJECT_VERSION); Log::Info("Compiled on: {:s}", __TIMESTAMP__); //Connect to the MySQL Database try { Database::Connect(); } catch (sql::SQLException& ex) { Log::Warn("Got an error while connecting to the database: {:s}", ex.what()); Log::Warn("Migrations not run"); return EXIT_FAILURE; } try { std::string clientPathStr = Game::config->GetValue("client_location"); if (clientPathStr.empty()) clientPathStr = "./res"; std::filesystem::path clientPath = std::filesystem::path(clientPathStr); if (clientPath.is_relative()) { clientPath = BinaryPathFinder::GetBinaryDir() / clientPath; } Game::assetManager = new AssetManager(clientPath); } catch (std::runtime_error& ex) { Log::Warn("Got an error while setting up assets: {:s}", ex.what()); Log::Warn("Is the provided client_location in Windows Onedrive? If so, remove it from Onedrive."); return EXIT_FAILURE; } MigrationRunner::RunMigrations(); const auto resServerPath = BinaryPathFinder::GetBinaryDir() / "resServer"; const bool cdServerExists = std::filesystem::exists(resServerPath / "CDServer.sqlite"); const bool oldCDServerExists = std::filesystem::exists(Game::assetManager->GetResPath() / "CDServer.sqlite"); const bool fdbExists = std::filesystem::exists(Game::assetManager->GetResPath() / "cdclient.fdb"); const bool resServerPathExists = std::filesystem::is_directory(resServerPath); if (!resServerPathExists) { Log::Info("{:s} does not exist, creating it.", (resServerPath).string()); if (!std::filesystem::create_directories(resServerPath)) { Log::Warn("Failed to create {:s}", (resServerPath).string()); return EXIT_FAILURE; } } if (!cdServerExists) { if (oldCDServerExists) { // If the file doesn't exist in the new CDServer location, copy it there. We copy because we may not have write permissions from the previous directory. Log::Info("CDServer.sqlite is not located at resServer, but is located at res path. Copying file..."); std::filesystem::copy_file(Game::assetManager->GetResPath() / "CDServer.sqlite", resServerPath / "CDServer.sqlite"); } else { Log::Info("{:s} could not be found in resServer or res. Looking for {:s} to convert to sqlite.", (resServerPath / "CDServer.sqlite").string(), (Game::assetManager->GetResPath() / "cdclient.fdb").string()); auto cdclientStream = Game::assetManager->GetFile("cdclient.fdb"); if (!cdclientStream) { Log::Warn("Failed to load {:s}", (Game::assetManager->GetResPath() / "cdclient.fdb").string()); throw std::runtime_error("Aborting initialization due to missing cdclient.fdb."); } Log::Info("Found {:s}. Converting to SQLite", (Game::assetManager->GetResPath() / "cdclient.fdb").string()); Game::logger->Flush(); if (FdbToSqlite::Convert(resServerPath.string()).ConvertDatabase(cdclientStream) == false) { Log::Warn("Failed to convert fdb to sqlite."); return EXIT_FAILURE; } } } //Connect to CDClient try { CDClientDatabase::Connect((BinaryPathFinder::GetBinaryDir() / "resServer" / "CDServer.sqlite").string()); } catch (CppSQLite3Exception& e) { Log::Warn("Unable to connect to CDServer SQLite Database"); Log::Warn("Error: {:s}", e.errorMessage()); Log::Warn("Error Code: {:d}", e.errorCode()); return EXIT_FAILURE; } // Run migrations should any need to be run. MigrationRunner::RunSQLiteMigrations(); //If the first command line argument is -a or --account then make the user //input a username and password, with the password being hidden. if (argc > 1 && (strcmp(argv[1], "-a") == 0 || strcmp(argv[1], "--account") == 0)) { std::string username; std::string password; std::cout << "Enter a username: "; std::cin >> username; auto accountId = Database::Get()->GetAccountInfo(username); if (accountId) { Log::Info("Account with name \"{:s}\" already exists", username); std::cout << "Do you want to change the password of that account? [y/n]?"; std::string prompt = ""; std::cin >> prompt; if (prompt == "y" || prompt == "yes") { if (accountId->id == 0) return EXIT_FAILURE; //Read the password from the console without echoing it. #ifdef __linux__ //This function is obsolete, but it only meant to be used by the //sysadmin to create their first account. password = getpass("Enter a password: "); #else std::cout << "Enter a password: "; std::cin >> password; #endif // Regenerate hash based on new password char salt[BCRYPT_HASHSIZE]; char hash[BCRYPT_HASHSIZE]; int32_t bcryptState = ::bcrypt_gensalt(12, salt); assert(bcryptState == 0); bcryptState = ::bcrypt_hashpw(password.c_str(), salt, hash); assert(bcryptState == 0); Database::Get()->UpdateAccountPassword(accountId->id, std::string(hash, BCRYPT_HASHSIZE)); Log::Info("Account \"{:s}\" password updated successfully!", username); } else { Log::Info("Account \"{:s}\" was not updated.", username); } return EXIT_SUCCESS; } //Read the password from the console without echoing it. #ifdef __linux__ //This function is obsolete, but it only meant to be used by the //sysadmin to create their first account. password = getpass("Enter a password: "); #else std::cout << "Enter a password: "; std::cin >> password; #endif //Generate new hash for bcrypt char salt[BCRYPT_HASHSIZE]; char hash[BCRYPT_HASHSIZE]; int32_t bcryptState = ::bcrypt_gensalt(12, salt); assert(bcryptState == 0); bcryptState = ::bcrypt_hashpw(password.c_str(), salt, hash); assert(bcryptState == 0); //Create account try { Database::Get()->InsertNewAccount(username, std::string(hash, BCRYPT_HASHSIZE)); } catch (sql::SQLException& e) { Log::Warn("A SQL error occurred!:\n {:s}", e.what()); return EXIT_FAILURE; } Log::Info("Account created successfully!"); return EXIT_SUCCESS; } Game::randomEngine = std::mt19937(time(0)); uint32_t maxClients = 999; uint32_t ourPort = 2000; std::string ourIP = "localhost"; const auto maxClientsString = Game::config->GetValue("max_clients"); if (!maxClientsString.empty()) maxClients = std::stoi(maxClientsString); const auto masterServerPortString = Game::config->GetValue("master_server_port"); if (!masterServerPortString.empty()) ourPort = std::atoi(masterServerPortString.c_str()); const auto externalIPString = Game::config->GetValue("external_ip"); if (!externalIPString.empty()) ourIP = externalIPString; Game::server = new dServer(ourIP, ourPort, 0, maxClients, true, false, Game::logger, "", 0, ServerType::Master, Game::config, &Game::lastSignal); std::string master_server_ip = "localhost"; const auto masterServerIPString = Game::config->GetValue("master_ip"); if (!masterServerIPString.empty()) master_server_ip = masterServerIPString; if (master_server_ip == "") master_server_ip = Game::server->GetIP(); Database::Get()->SetMasterIp(master_server_ip, Game::server->GetPort()); //Create additional objects here: PersistentIDManager::Initialize(); Game::im = new InstanceManager(Game::logger, Game::server->GetIP()); //Depending on the config, start up servers: if (Game::config->GetValue("prestart_servers") != "0") { StartChatServer(); Game::im->GetInstance(0, false, 0); Game::im->GetInstance(1000, false, 0); StartAuthServer(); } auto t = std::chrono::high_resolution_clock::now(); Packet* packet = nullptr; constexpr uint32_t logFlushTime = 15 * masterFramerate; constexpr uint32_t sqlPingTime = 10 * 60 * masterFramerate; constexpr uint32_t shutdownUniverseTime = 10 * 60 * masterFramerate; constexpr uint32_t instanceReadyTimeout = 30 * masterFramerate; uint32_t framesSinceLastFlush = 0; uint32_t framesSinceLastSQLPing = 0; uint32_t framesSinceKillUniverseCommand = 0; Game::logger->Flush(); while (!Game::ShouldShutdown()) { //In world we'd update our other systems here. //Check for packets here: packet = Game::server->Receive(); if (packet) { HandlePacket(packet); Game::server->DeallocatePacket(packet); packet = nullptr; } //Push our log every 15s: if (framesSinceLastFlush >= logFlushTime) { Game::logger->Flush(); framesSinceLastFlush = 0; } else framesSinceLastFlush++; //Every 10 min we ping our sql server to keep it alive hopefully: if (framesSinceLastSQLPing >= sqlPingTime) { //Find out the master's IP for absolutely no reason: std::string masterIP; uint32_t masterPort; auto masterInfo = Database::Get()->GetMasterInfo(); if (masterInfo) { masterIP = masterInfo->ip; masterPort = masterInfo->port; } framesSinceLastSQLPing = 0; } else framesSinceLastSQLPing++; //10m shutdown for universe kill command if (Game::universeShutdownRequested) { if (framesSinceKillUniverseCommand >= shutdownUniverseTime) { //Break main loop and exit Game::lastSignal = -1; } else framesSinceKillUniverseCommand++; } const auto instances = Game::im->GetInstances(); for (auto* instance : instances) { if (instance == nullptr) { break; } auto affirmTimeout = instance->GetAffirmationTimeout(); if (!instance->GetPendingAffirmations().empty()) { affirmTimeout++; } else { affirmTimeout = 0; } instance->SetAffirmationTimeout(affirmTimeout); if (affirmTimeout == instanceReadyTimeout) { instance->Shutdown(); instance->SetIsShuttingDown(true); Game::im->RedirectPendingRequests(instance); } } //Remove dead instances for (auto* instance : instances) { if (instance == nullptr) { break; } if (instance->GetShutdownComplete()) { Game::im->RemoveInstance(instance); } } t += std::chrono::milliseconds(masterFrameDelta); std::this_thread::sleep_until(t); } return ShutdownSequence(EXIT_SUCCESS); } void HandlePacket(Packet* packet) { if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION) { Log::Info("A server has disconnected"); //Since this disconnection is intentional, we'll just delete it as //we'll start a new one anyway if needed: Instance* instance = Game::im->GetInstanceBySysAddr(packet->systemAddress); if (instance) { Log::Info("Actually disconnected from zone {:d} clone {:d} instance {:d} port {:d}", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance->GetPort()); Game::im->RemoveInstance(instance); //Delete the old } if (packet->systemAddress == chatServerMasterPeerSysAddr) { chatServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS; StartChatServer(); } if (packet->systemAddress == authServerMasterPeerSysAddr) { authServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS; StartAuthServer(); } } if (packet->data[0] == ID_CONNECTION_LOST) { Log::Info("A server has lost the connection"); Instance* instance = Game::im->GetInstanceBySysAddr(packet->systemAddress); if (instance) { LWOZONEID zoneID = instance->GetZoneID(); //Get the zoneID so we can recreate a server Game::im->RemoveInstance(instance); //Delete the old } if (packet->systemAddress == chatServerMasterPeerSysAddr) { chatServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS; StartChatServer(); } if (packet->systemAddress == authServerMasterPeerSysAddr) { authServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS; StartAuthServer(); } } if (packet->length < 4) return; if (static_cast(packet->data[1]) == eConnectionType::MASTER) { switch (static_cast(packet->data[3])) { case eMasterMessageType::REQUEST_PERSISTENT_ID: { Log::Info("A persistent ID req"); RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint64_t requestID = 0; inStream.Read(requestID); uint32_t objID = PersistentIDManager::GeneratePersistentID(); MasterPackets::SendPersistentIDResponse(Game::server, packet->systemAddress, requestID, objID); break; } case eMasterMessageType::REQUEST_ZONE_TRANSFER: { Log::Info("Received zone transfer req"); RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint64_t requestID = 0; uint8_t mythranShift = false; uint32_t zoneID = 0; uint32_t zoneClone = 0; inStream.Read(requestID); inStream.Read(mythranShift); inStream.Read(zoneID); inStream.Read(zoneClone); if (shutdownSequenceStarted) { Log::Info("Shutdown sequence has been started. Not creating a new zone."); break; } Instance* in = Game::im->GetInstance(zoneID, false, zoneClone); for (auto* instance : Game::im->GetInstances()) { Log::Info("Instance: {:d}/{:d}/{:d} -> {:d}", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance == in); } if (in && !in->GetIsReady()) //Instance not ready, make a pending request { in->GetPendingRequests().push_back({ requestID, static_cast(mythranShift), packet->systemAddress }); Log::Info("Server not ready, adding pending request {:d} {:d} {:d}", requestID, zoneID, zoneClone); break; } //Instance is ready, transfer Log::Info("Responding to transfer request {:d} for zone {:d} {:d}", requestID, zoneID, zoneClone); Game::im->RequestAffirmation(in, { requestID, static_cast(mythranShift), packet->systemAddress }); break; } case eMasterMessageType::SERVER_INFO: { //MasterPackets::HandleServerInfo(packet); //This is here because otherwise we'd have to include IM in //non-master servers. This packet allows us to add World //servers back if master crashed RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint32_t theirPort = 0; uint32_t theirZoneID = 0; uint32_t theirInstanceID = 0; ServerType theirServerType; LUString theirIP; inStream.Read(theirPort); inStream.Read(theirZoneID); inStream.Read(theirInstanceID); inStream.Read(theirServerType); inStream.Read(theirIP); if (theirServerType == ServerType::World) { if (!Game::im->IsPortInUse(theirPort)) { Instance* in = new Instance(theirIP.string, theirPort, theirZoneID, theirInstanceID, 0, 12, 12); SystemAddress copy; copy.binaryAddress = packet->systemAddress.binaryAddress; copy.port = packet->systemAddress.port; in->SetSysAddr(copy); Game::im->AddInstance(in); } else { auto instance = Game::im->FindInstance( theirZoneID, static_cast(theirInstanceID)); if (instance) { instance->SetSysAddr(packet->systemAddress); } } } if (theirServerType == ServerType::Chat) { SystemAddress copy; copy.binaryAddress = packet->systemAddress.binaryAddress; copy.port = packet->systemAddress.port; chatServerMasterPeerSysAddr = copy; } if (theirServerType == ServerType::Auth) { SystemAddress copy; copy.binaryAddress = packet->systemAddress.binaryAddress; copy.port = packet->systemAddress.port; authServerMasterPeerSysAddr = copy; } Log::Info("Received server info, instance: {:d} port: {:d}", theirInstanceID, theirPort); break; } case eMasterMessageType::SET_SESSION_KEY: { CINSTREAM_SKIP_HEADER; uint32_t sessionKey = 0; inStream.Read(sessionKey); LUString username; inStream.Read(username); for (auto it : activeSessions) { if (it.second == username.string) { activeSessions.erase(it.first); CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::MASTER, eMasterMessageType::NEW_SESSION_ALERT); bitStream.Write(sessionKey); bitStream.Write(username); SEND_PACKET_BROADCAST; break; } } activeSessions.insert(std::make_pair(sessionKey, username.string)); Log::Info("Got sessionKey {:d} for user {:s}", sessionKey, username.string); break; } case eMasterMessageType::REQUEST_SESSION_KEY: { CINSTREAM_SKIP_HEADER; LUWString username; inStream.Read(username); Log::Info("Requesting session key for {:s}", username.GetAsString()); for (auto key : activeSessions) { if (key.second == username.GetAsString()) { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::MASTER, eMasterMessageType::SESSION_KEY_RESPONSE); bitStream.Write(key.first); bitStream.Write(username); Game::server->Send(bitStream, packet->systemAddress, false); break; } } break; } case eMasterMessageType::PLAYER_ADDED: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); LWOMAPID theirZoneID = 0; LWOINSTANCEID theirInstanceID = 0; inStream.Read(theirZoneID); inStream.Read(theirInstanceID); auto instance = Game::im->FindInstance(theirZoneID, theirInstanceID); if (instance) { instance->AddPlayer(Player()); } else { printf("Instance missing? What?"); } break; } case eMasterMessageType::PLAYER_REMOVED: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); LWOMAPID theirZoneID = 0; LWOINSTANCEID theirInstanceID = 0; inStream.Read(theirZoneID); inStream.Read(theirInstanceID); auto instance = Game::im->FindInstance(theirZoneID, theirInstanceID); if (instance) { instance->RemovePlayer(Player()); } break; } case eMasterMessageType::CREATE_PRIVATE_ZONE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint32_t mapId; LWOCLONEID cloneId; std::string password; inStream.Read(mapId); inStream.Read(cloneId); uint32_t len; inStream.Read(len); for (uint32_t i = 0; len > i; i++) { char character; inStream.Read(character); password += character; } Game::im->CreatePrivateInstance(mapId, cloneId, password); break; } case eMasterMessageType::REQUEST_PRIVATE_ZONE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint64_t requestID = 0; uint8_t mythranShift = false; std::string password; inStream.Read(requestID); inStream.Read(mythranShift); uint32_t len; inStream.Read(len); for (uint32_t i = 0; i < len; i++) { char character; inStream.Read(character); password += character; } auto* instance = Game::im->FindPrivateInstance(password); Log::Info("Join private zone: {:d} {:d} {:s} {:p}", requestID, mythranShift, password, fmt::ptr(instance)); if (instance == nullptr) { return; } const auto& zone = instance->GetZoneID(); MasterPackets::SendZoneTransferResponse(Game::server, packet->systemAddress, requestID, (bool)mythranShift, zone.GetMapID(), instance->GetInstanceID(), zone.GetCloneID(), instance->GetIP(), instance->GetPort()); break; } case eMasterMessageType::WORLD_READY: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); LWOMAPID zoneID; LWOINSTANCEID instanceID; inStream.Read(zoneID); inStream.Read(instanceID); Log::Info("Got world ready {:d} {:d}", zoneID, instanceID); auto* instance = Game::im->FindInstance(zoneID, instanceID); if (instance == nullptr) { Log::Warn("Failed to find zone to ready"); return; } Log::Info("Ready zone {:d}", zoneID); Game::im->ReadyInstance(instance); break; } case eMasterMessageType::PREP_ZONE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); int32_t zoneID; inStream.Read(zoneID); if (shutdownSequenceStarted) { Log::Info("Shutdown sequence has been started. Not prepping a new zone."); break; } else { Log::Info("Prepping zone {:d}", zoneID); Game::im->GetInstance(zoneID, false, 0); } break; } case eMasterMessageType::AFFIRM_TRANSFER_RESPONSE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint64_t requestID; inStream.Read(requestID); Log::Info("Got affirmation of transfer {:d}", requestID); auto* instance = Game::im->GetInstanceBySysAddr(packet->systemAddress); if (instance == nullptr) return; Game::im->AffirmTransfer(instance, requestID); Log::Info("Affirmation complete {:d}", requestID); break; } case eMasterMessageType::SHUTDOWN_RESPONSE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); auto* instance = Game::im->GetInstanceBySysAddr(packet->systemAddress); if (instance == nullptr) { return; } Log::Info("Got shutdown response from zone {:d} clone {:d} instance {:d} port {:d}", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance->GetPort()); instance->SetIsShuttingDown(true); break; } case eMasterMessageType::SHUTDOWN_UNIVERSE: { Log::Info("Received shutdown universe command, shutting down in 10 minutes."); Game::universeShutdownRequested = true; break; } default: Log::Info("Unknown master packet ID from server: {:d}", packet->data[3]); } } } int ShutdownSequence(int32_t signal) { if (!Game::logger) return -1; Log::Info("Recieved Signal {:d}", signal); if (shutdownSequenceStarted) { Log::Info("Duplicate Shutdown Sequence"); return -1; } if (!Game::im) { FinalizeShutdown(EXIT_FAILURE); } Game::im->SetIsShuttingDown(true); shutdownSequenceStarted = true; Game::lastSignal = -1; { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::MASTER, eMasterMessageType::SHUTDOWN); Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); Log::Info("Triggered master shutdown"); } PersistentIDManager::SaveToDatabase(); Log::Info("Saved ObjectIDTracker to DB"); // A server might not be finished spinning up yet, remove all of those here. for (auto* instance : Game::im->GetInstances()) { if (!instance->GetIsReady()) { Game::im->RemoveInstance(instance); } } for (auto* instance : Game::im->GetInstances()) { instance->SetIsShuttingDown(true); } Log::Info("Attempting to shutdown instances, max 60 seconds..."); auto t = std::chrono::high_resolution_clock::now(); uint32_t framesSinceShutdownStart = 0; constexpr uint32_t maxShutdownTime = 60 * mediumFramerate; bool allInstancesShutdown = false; Packet* packet = nullptr; while (true) { packet = Game::server->Receive(); if (packet) { HandlePacket(packet); Game::server->DeallocatePacket(packet); packet = nullptr; } allInstancesShutdown = true; for (auto* instance : Game::im->GetInstances()) { if (instance == nullptr) { continue; } if (!instance->GetShutdownComplete()) { allInstancesShutdown = false; } } if (allInstancesShutdown && authServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && chatServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS) { Log::Info("Finished shutting down MasterServer!"); break; } t += std::chrono::milliseconds(mediumFrameDelta); std::this_thread::sleep_until(t); framesSinceShutdownStart++; if (framesSinceShutdownStart == maxShutdownTime) { Log::Info("Finished shutting down by timeout!"); break; } } return FinalizeShutdown(signal); } int32_t FinalizeShutdown(int32_t signal) { //Delete our objects here: Database::Destroy("MasterServer"); if (Game::config) delete Game::config; Game::config = nullptr; if (Game::im) delete Game::im; Game::im = nullptr; if (Game::server) delete Game::server; Game::server = nullptr; if (Game::logger) delete Game::logger; Game::logger = nullptr; if (signal != EXIT_SUCCESS) exit(signal); return signal; }