#include #include #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif #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 "dLogger.h" #include "dServer.h" #include "AssetManager.h" #include "BinaryPathFinder.h" //RakNet includes: #include "RakNetDefines.h" //Packet includes: #include "AuthPackets.h" #include "Game.h" #include "InstanceManager.h" #include "MasterPackets.h" #include "ObjectIDManager.h" #include "PacketUtils.h" #include "dMessageIdentifiers.h" #include "FdbToSqlite.h" namespace Game { dLogger* logger = nullptr; dServer* server = nullptr; InstanceManager* im = nullptr; dConfig* config = nullptr; AssetManager* assetManager = nullptr; bool shouldShutdown = false; } //namespace Game bool shutdownSequenceStarted = false; void ShutdownSequence(int32_t signal = -1); int32_t FinalizeShutdown(int32_t signal = -1); dLogger* SetupLogger(); void StartAuthServer(); void StartChatServer(); 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(); }); signal(SIGINT, [](int32_t signal) { ShutdownSequence(EXIT_FAILURE); }); signal(SIGTERM, [](int32_t signal) { ShutdownSequence(EXIT_FAILURE); }); //Create all the objects we need to run our service: Game::logger = SetupLogger(); if (!Game::logger) return EXIT_FAILURE; if (!std::filesystem::exists(BinaryPathFinder::GetBinaryDir() / "authconfig.ini")) { Game::logger->Log("MasterServer", "Couldnt find authconfig.ini"); return EXIT_FAILURE; } if (!std::filesystem::exists(BinaryPathFinder::GetBinaryDir() / "chatconfig.ini")) { Game::logger->Log("MasterServer", "Couldnt find chatconfig.ini"); return EXIT_FAILURE; } if (!std::filesystem::exists(BinaryPathFinder::GetBinaryDir() / "masterconfig.ini")) { Game::logger->Log("MasterServer", "Couldnt find masterconfig.ini"); return EXIT_FAILURE; } if (!std::filesystem::exists(BinaryPathFinder::GetBinaryDir() / "sharedconfig.ini")) { Game::logger->Log("MasterServer", "Couldnt find sharedconfig.ini"); return EXIT_FAILURE; } if (!std::filesystem::exists(BinaryPathFinder::GetBinaryDir() / "worldconfig.ini")) { Game::logger->Log("MasterServer", "Couldnt find worldconfig.ini"); return EXIT_FAILURE; } Game::config = new dConfig((BinaryPathFinder::GetBinaryDir() / "masterconfig.ini").string()); Game::logger->SetLogToConsole(Game::config->GetValue("log_to_console") != "0"); Game::logger->SetLogDebugStatements(Game::config->GetValue("log_debug_statements") == "1"); Game::logger->Log("MasterServer", "Starting Master server..."); Game::logger->Log("MasterServer", "Version: %i.%i", PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR); Game::logger->Log("MasterServer", "Compiled on: %s", __TIMESTAMP__); //Connect to the MySQL Database std::string mysql_host = Game::config->GetValue("mysql_host"); std::string mysql_database = Game::config->GetValue("mysql_database"); std::string mysql_username = Game::config->GetValue("mysql_username"); std::string mysql_password = Game::config->GetValue("mysql_password"); try { Database::Connect(mysql_host, mysql_database, mysql_username, mysql_password); } catch (sql::SQLException& ex) { Game::logger->Log("MasterServer", "Got an error while connecting to the database: %s", ex.what()); Game::logger->Log("MigrationRunner", "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) { Game::logger->Log("MasterServer", "Got an error while setting up assets: %s", ex.what()); return EXIT_FAILURE; } MigrationRunner::RunMigrations(); const bool cdServerExists = std::filesystem::exists(BinaryPathFinder::GetBinaryDir() / "resServer" / "CDServer.sqlite"); const bool oldCDServerExists = std::filesystem::exists(Game::assetManager->GetResPath() / "CDServer.sqlite"); const bool fdbExists = std::filesystem::exists(Game::assetManager->GetResPath() / "cdclient.fdb"); auto query = Database::CreatePreppedStmt("select name, score, time, UNIX_TIMESTAMP(last_played) as lastPlayed from leaderboard as l join charinfo as ci on ci.id = l.character_id where game_id = 1864 order by score desc, time desc limit 11;"); auto myResult = query->executeQuery(); while (myResult->next()) { Game::logger->Log("MasterServer", "%s %i %i %i", myResult->getString("name").c_str(), myResult->getInt("score"), myResult->getInt("time"), myResult->getInt("lastPlayed")); } 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. Game::logger->Log("MasterServer", "CDServer.sqlite is not located at resServer, but is located at res path. Copying file..."); std::filesystem::copy_file(Game::assetManager->GetResPath() / "CDServer.sqlite", BinaryPathFinder::GetBinaryDir() / "resServer" / "CDServer.sqlite"); } else { Game::logger->Log("MasterServer", "%s could not be found in resServer or res. Looking for %s to convert to sqlite.", (BinaryPathFinder::GetBinaryDir() / "resServer" / "CDServer.sqlite").c_str(), (Game::assetManager->GetResPath() / "cdclient.fdb").c_str()); AssetMemoryBuffer cdClientBuffer = Game::assetManager->GetFileAsBuffer("cdclient.fdb"); if (!cdClientBuffer.m_Success) { Game::logger->Log("MasterServer", "Failed to load %s", (Game::assetManager->GetResPath() / "cdclient.fdb").c_str()); throw std::runtime_error("Aborting initialization due to missing cdclient.fdb."); } Game::logger->Log("MasterServer", "Found %s. Converting to SQLite", (Game::assetManager->GetResPath() / "cdclient.fdb").c_str()); Game::logger->Flush(); if (FdbToSqlite::Convert((BinaryPathFinder::GetBinaryDir() / "resServer").string()).ConvertDatabase(cdClientBuffer) == false) { Game::logger->Log("MasterServer", "Failed to convert fdb to sqlite."); return EXIT_FAILURE; } cdClientBuffer.close(); } } //Connect to CDClient try { CDClientDatabase::Connect((BinaryPathFinder::GetBinaryDir() / "resServer" / "CDServer.sqlite").string()); } catch (CppSQLite3Exception& e) { Game::logger->Log("WorldServer", "Unable to connect to CDServer SQLite Database"); Game::logger->Log("WorldServer", "Error: %s", e.errorMessage()); Game::logger->Log("WorldServer", "Error Code: %i", e.errorCode()); return EXIT_FAILURE; } // Run migrations should any need to be run. MigrationRunner::RunSQLiteMigrations(); //Get CDClient initial information try { CDClientManager::Instance(); } catch (CppSQLite3Exception& e) { Game::logger->Log("WorldServer", "Failed to initialize CDServer SQLite Database"); Game::logger->Log("WorldServer", "May be caused by corrupted file: %s", (Game::assetManager->GetResPath() / "CDServer.sqlite").string().c_str()); Game::logger->Log("WorldServer", "Error: %s", e.errorMessage()); Game::logger->Log("WorldServer", "Error Code: %i", e.errorCode()); return EXIT_FAILURE; } //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; std::unique_ptr userLookupStatement(Database::CreatePreppedStmt("SELECT id FROM accounts WHERE name=? LIMIT 1;")); userLookupStatement->setString(1, username.c_str()); std::unique_ptr res(userLookupStatement->executeQuery()); if (res->rowsCount() > 0) { Game::logger->Log("MasterServer", "Account with name \"%s\" already exists", username.c_str()); 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"){ uint32_t accountId = 0; res->next(); accountId = res->getUInt(1); if (accountId == 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); std::unique_ptr userUpdateStatement(Database::CreatePreppedStmt("UPDATE accounts SET password = ? WHERE id = ?;")); userUpdateStatement->setString(1, std::string(hash, BCRYPT_HASHSIZE).c_str()); userUpdateStatement->setUInt(2, accountId); userUpdateStatement->execute(); Game::logger->Log("MasterServer", "Account \"%s\" password updated successfully!", username.c_str()); } else { Game::logger->Log("MasterServer", "Account \"%s\" was not updated.", username.c_str()); } 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 { std::unique_ptr statement(Database::CreatePreppedStmt("INSERT INTO accounts (name, password, gm_level) VALUES (?, ?, ?);")); statement->setString(1, username.c_str()); statement->setString(2, std::string(hash, BCRYPT_HASHSIZE).c_str()); statement->setInt(3, 9); statement->execute(); } catch(sql::SQLException& e) { Game::logger->Log("MasterServer", "A SQL error occurred!:\n %s", e.what()); return EXIT_FAILURE; } Game::logger->Log("MasterServer", "Account created successfully!"); return EXIT_SUCCESS; } uint32_t maxClients = 999; uint32_t ourPort = 1000; if (Game::config->GetValue("max_clients") != "") maxClients = std::stoi(Game::config->GetValue("max_clients")); if (Game::config->GetValue("port") != "") ourPort = std::stoi(Game::config->GetValue("port")); Game::server = new dServer(Game::config->GetValue("external_ip"), ourPort, 0, maxClients, true, false, Game::logger, "", 0, ServerType::Master, Game::config, &Game::shouldShutdown); //Query for the database for a server labeled "master" auto* masterLookupStatement = Database::CreatePreppedStmt("SELECT id FROM `servers` WHERE `name` = 'master'"); auto* result = masterLookupStatement->executeQuery(); auto master_server_ip = Game::config->GetValue("master_ip"); if (master_server_ip == "") { master_server_ip = Game::server->GetIP(); } //If we found a server, update it's IP and port to the current one. if (result->next()) { auto* updateStatement = Database::CreatePreppedStmt("UPDATE `servers` SET `ip` = ?, `port` = ? WHERE `id` = ?"); updateStatement->setString(1, master_server_ip.c_str()); updateStatement->setInt(2, Game::server->GetPort()); updateStatement->setInt(3, result->getInt("id")); updateStatement->execute(); delete updateStatement; } else { //If we didn't find a server, create one. auto* insertStatement = Database::CreatePreppedStmt("INSERT INTO `servers` (`name`, `ip`, `port`, `state`, `version`) VALUES ('master', ?, ?, 0, 171023)"); insertStatement->setString(1, master_server_ip.c_str()); insertStatement->setInt(2, Game::server->GetPort()); insertStatement->execute(); delete insertStatement; } //Create additional objects here: ObjectIDManager::Instance()->Initialize(Game::logger); Game::im = new InstanceManager(Game::logger, Game::server->GetIP()); //Depending on the config, start up servers: if (Game::config->GetValue("prestart_servers") != "" && Game::config->GetValue("prestart_servers") == "1") { 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; while (true) { //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; sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT ip, port FROM servers WHERE name='master';"); auto res = stmt->executeQuery(); while (res->next()) { masterIP = res->getString(1).c_str(); masterPort = res->getInt(2); } delete res; delete stmt; framesSinceLastSQLPing = 0; } else framesSinceLastSQLPing++; //10m shutdown for universe kill command if (Game::shouldShutdown) { if (framesSinceKillUniverseCommand >= shutdownUniverseTime) { //Break main loop and exit break; } 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 FinalizeShutdown(EXIT_SUCCESS); } dLogger* SetupLogger() { std::string logPath = (BinaryPathFinder::GetBinaryDir() / ("logs/MasterServer_" + std::to_string(time(nullptr)) + ".log")).string(); bool logToConsole = false; bool logDebugStatements = false; #ifdef _DEBUG logToConsole = true; logDebugStatements = true; #endif return new dLogger(logPath, logToConsole, logDebugStatements); } void HandlePacket(Packet* packet) { if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION) { Game::logger->Log("MasterServer", "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) { Game::logger->Log("MasterServer", "Actually disconnected from zone %i clone %i instance %i port %i", 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) { Game::logger->Log("MasterServer", "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->data[1] == MASTER) { switch (packet->data[3]) { case MSG_MASTER_REQUEST_PERSISTENT_ID: { Game::logger->Log("MasterServer", "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 = ObjectIDManager::Instance()->GeneratePersistentID(); MasterPackets::SendPersistentIDResponse(Game::server, packet->systemAddress, requestID, objID); break; } case MSG_MASTER_REQUEST_ZONE_TRANSFER: { Game::logger->Log("MasterServer", "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) { Game::logger->Log("MasterServer", "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()) { Game::logger->Log("MasterServer", "Instance: %i/%i/%i -> %i", 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 }); Game::logger->Log("MasterServer", "Server not ready, adding pending request %llu %i %i", requestID, zoneID, zoneClone); break; } //Instance is ready, transfer Game::logger->Log("MasterServer", "Responding to transfer request %llu for zone %i %i", requestID, zoneID, zoneClone); Game::im->RequestAffirmation(in, { requestID, static_cast(mythranShift), packet->systemAddress }); break; } case MSG_MASTER_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; std::string theirIP = ""; inStream.Read(theirPort); inStream.Read(theirZoneID); inStream.Read(theirInstanceID); inStream.Read(theirServerType); theirIP = PacketUtils::ReadString(24, packet, false); //24 is the current offset if (theirServerType == ServerType::World && !Game::im->IsPortInUse(theirPort)) { Instance* in = new Instance(theirIP, 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; } Game::logger->Log("MasterServer", "Received server info, instance: %i port: %i", theirInstanceID, theirPort); break; } case MSG_MASTER_SET_SESSION_KEY: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint32_t sessionKey = 0; std::string username; inStream.Read(sessionKey); username = PacketUtils::ReadString(12, packet, false); for (auto it : activeSessions) { if (it.second == username) { activeSessions.erase(it.first); CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_NEW_SESSION_ALERT); bitStream.Write(sessionKey); bitStream.Write(username.size()); for (auto character : username) { bitStream.Write(character); } SEND_PACKET_BROADCAST; break; } } activeSessions.insert(std::make_pair(sessionKey, username)); Game::logger->Log("MasterServer", "Got sessionKey %i for user %s", sessionKey, username.c_str()); break; } case MSG_MASTER_REQUEST_SESSION_KEY: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); std::string username = PacketUtils::ReadString(8, packet, false); for (auto key : activeSessions) { if (key.second == username) { CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_SESSION_KEY_RESPONSE); bitStream.Write(key.first); PacketUtils::WriteString(bitStream, key.second, 64); Game::server->Send(&bitStream, packet->systemAddress, false); break; } } break; } case MSG_MASTER_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 MSG_MASTER_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 MSG_MASTER_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.c_str()); break; } case MSG_MASTER_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.c_str()); Game::logger->Log("MasterServer", "Join private zone: %llu %d %s %p", requestID, mythranShift, password.c_str(), 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 MSG_MASTER_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); Game::logger->Log("MasterServer", "Got world ready %i %i", zoneID, instanceID); auto* instance = Game::im->FindInstance(zoneID, instanceID); if (instance == nullptr) { Game::logger->Log("MasterServer", "Failed to find zone to ready"); return; } Game::logger->Log("MasterServer", "Ready zone %i", zoneID); Game::im->ReadyInstance(instance); break; } case MSG_MASTER_PREP_ZONE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); int32_t zoneID; inStream.Read(zoneID); if (shutdownSequenceStarted) { Game::logger->Log("MasterServer", "Shutdown sequence has been started. Not prepping a new zone."); break; } else { Game::logger->Log("MasterServer", "Prepping zone %i", zoneID); Game::im->GetInstance(zoneID, false, 0); } break; } case MSG_MASTER_AFFIRM_TRANSFER_RESPONSE: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint64_t requestID; inStream.Read(requestID); Game::logger->Log("MasterServer", "Got affirmation of transfer %llu", requestID); auto* instance = Game::im->GetInstanceBySysAddr(packet->systemAddress); if (instance == nullptr) return; Game::im->AffirmTransfer(instance, requestID); Game::logger->Log("MasterServer", "Affirmation complete %llu", requestID); break; } case MSG_MASTER_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; } Game::logger->Log("MasterServer", "Got shutdown response from zone %i clone %i instance %i port %i", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance->GetPort()); instance->SetIsShuttingDown(true); break; } case MSG_MASTER_SHUTDOWN_UNIVERSE: { Game::logger->Log("MasterServer", "Received shutdown universe command, shutting down in 10 minutes."); Game::shouldShutdown = true; break; } default: Game::logger->Log("MasterServer", "Unknown master packet ID from server: %i", packet->data[3]); } } } void StartChatServer() { if (Game::shouldShutdown) { Game::logger->Log("MasterServer", "Currently shutting down. Chat will not be restarted."); return; } #ifdef __APPLE__ //macOS doesn't need sudo to run on ports < 1024 auto result = system(((BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); #elif _WIN32 auto result = system(("start " + (BinaryPathFinder::GetBinaryDir() / "ChatServer.exe").string()).c_str()); #else if (std::atoi(Game::config->GetValue("use_sudo_chat").c_str())) { auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); } else { auto result = system(((BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); } #endif } void StartAuthServer() { if (Game::shouldShutdown) { Game::logger->Log("MasterServer", "Currently shutting down. Auth will not be restarted."); return; } #ifdef __APPLE__ auto result = system(((BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str()); #elif _WIN32 auto result = system(("start " + (BinaryPathFinder::GetBinaryDir() / "AuthServer.exe").string()).c_str()); #else if (std::atoi(Game::config->GetValue("use_sudo_auth").c_str())) { auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str()); } else { auto result = system(((BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str()); } #endif } void ShutdownSequence(int32_t signal) { if (shutdownSequenceStarted) { return; } if (!Game::im) { FinalizeShutdown(EXIT_FAILURE); } Game::im->SetIsShuttingDown(true); shutdownSequenceStarted = true; Game::shouldShutdown = true; { CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_SHUTDOWN); Game::server->Send(&bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); Game::logger->Log("MasterServer", "Triggered master shutdown"); } auto* objIdManager = ObjectIDManager::TryInstance(); if (objIdManager) { objIdManager->SaveToDatabase(); Game::logger->Log("MasterServer", "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); } Game::logger->Log("MasterServer", "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) { Game::logger->Log("MasterServer", "Finished shutting down MasterServer!"); break; } t += std::chrono::milliseconds(mediumFrameDelta); std::this_thread::sleep_until(t); framesSinceShutdownStart++; if (framesSinceShutdownStart == maxShutdownTime) { Game::logger->Log("MasterServer", "Finished shutting down by timeout!"); break; } } FinalizeShutdown(signal); } int32_t FinalizeShutdown(int32_t signal) { //Delete our objects here: Database::Destroy("MasterServer"); if (Game::config) delete Game::config; if (Game::im) delete Game::im; if (Game::server) delete Game::server; if (Game::logger) delete Game::logger; if (signal != EXIT_SUCCESS) exit(signal); return signal; }