DarkflameServer/dMasterServer/MasterServer.cpp
2024-03-08 15:44:02 -06:00

868 lines
26 KiB
C++

#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <map>
#include <string>
#include <thread>
#include <fstream>
#include <bcrypt/BCrypt.hpp>
#include <csignal>
//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<uint32_t, std::string> 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<uint32_t>(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<eConnectionType>(packet->data[1]) == eConnectionType::MASTER) {
switch (static_cast<eMasterMessageType>(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<bool>(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<bool>(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<uint16_t>(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<uint32_t>(len);
for (uint32_t i = 0; len > i; i++) {
char character;
inStream.Read<char>(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<uint32_t>(len);
for (uint32_t i = 0; i < len; i++) {
char character; inStream.Read<char>(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;
}