#include #include #include #include #include #include "MD5.h" //DLU Includes: #include "dCommonVars.h" #include "dServer.h" #include "dLogger.h" #include "Database.h" #include "dConfig.h" #include "dpWorld.h" #include "dZoneManager.h" #include "Metrics.hpp" #include "PerformanceManager.h" #include "Diagnostics.h" #include "BinaryPathFinder.h" //RakNet includes: #include "RakNetDefines.h" #include "RakNetworkFactory.h" #include "RakString.h" //World includes: #include #include "AuthPackets.h" #include "PacketUtils.h" #include "WorldPackets.h" #include "UserManager.h" #include "dMessageIdentifiers.h" #include "CDClientManager.h" #include "CDClientDatabase.h" #include "GeneralUtils.h" #include "ObjectIDManager.h" #include "ZoneInstanceManager.h" #include "dChatFilter.h" #include "ClientPackets.h" #include "CharacterComponent.h" #include "EntityManager.h" #include "Entity.h" #include "Character.h" #include "ChatPackets.h" #include "GameMessageHandler.h" #include "GameMessages.h" #include "Mail.h" #include "TeamManager.h" #include "SkillComponent.h" #include "DestroyableComponent.h" #include "Game.h" #include "MasterPackets.h" #include "Player.h" #include "PropertyManagementComponent.h" #include "AssetManager.h" #include "eBlueprintSaveResponseType.h" #include "ZCompression.h" namespace Game { dLogger* logger = nullptr; dServer* server = nullptr; dpWorld* physicsWorld = nullptr; dChatFilter* chatFilter = nullptr; dConfig* config = nullptr; AssetManager* assetManager = nullptr; RakPeerInterface* chatServer = nullptr; std::mt19937 randomEngine; SystemAddress chatSysAddr; } bool chatDisabled = false; bool chatConnected = false; bool worldShutdownSequenceStarted = false; bool worldShutdownSequenceComplete = false; void WorldShutdownSequence(); void WorldShutdownProcess(uint32_t zoneId); void FinalizeShutdown(); void SendShutdownMessageToMaster(); dLogger* SetupLogger(int zoneID, int instanceID); void HandlePacketChat(Packet* packet); void HandlePacket(Packet* packet); struct tempSessionInfo { SystemAddress sysAddr; std::string hash; }; std::map m_PendingUsers; int instanceID = 0; int g_CloneID = 0; std::string databaseChecksum = ""; int main(int argc, char** argv) { Diagnostics::SetProcessName("World"); Diagnostics::SetProcessFileName(argv[0]); Diagnostics::Initialize(); // Triggers the shutdown sequence at application exit std::atexit(WorldShutdownSequence); signal(SIGINT, [](int) { WorldShutdownSequence(); }); signal(SIGTERM, [](int) { WorldShutdownSequence(); }); int zoneID = 1000; int cloneID = 0; int maxClients = 8; int ourPort = 2007; //Check our arguments: for (int i = 0; i < argc; ++i) { std::string argument(argv[i]); if (argument == "-zone") zoneID = atoi(argv[i + 1]); if (argument == "-instance") instanceID = atoi(argv[i + 1]); if (argument == "-clone") cloneID = atoi(argv[i + 1]); if (argument == "-maxclients") maxClients = atoi(argv[i + 1]); if (argument == "-port") ourPort = atoi(argv[i + 1]); } //Create all the objects we need to run our service: Game::logger = SetupLogger(zoneID, instanceID); if (!Game::logger) return EXIT_FAILURE; //Read our config: Game::config = new dConfig("worldconfig.ini"); Game::logger->SetLogToConsole(bool(std::stoi(Game::config->GetValue("log_to_console")))); Game::logger->SetLogDebugStatements(Game::config->GetValue("log_debug_statements") == "1"); Game::logger->Log("WorldServer", "Starting World server..."); Game::logger->Log("WorldServer", "Version: %i.%i", PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR); Game::logger->Log("WorldServer", "Compiled on: %s", __TIMESTAMP__); if (Game::config->GetValue("disable_chat") == "1") chatDisabled = true; 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("WorldServer", "Got an error while setting up assets: %s", ex.what()); return EXIT_FAILURE; } // Connect to CDClient try { CDClientDatabase::Connect((Game::assetManager->GetResPath() / "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; } CDClientManager::Instance()->Initialize(); //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"); Diagnostics::SetProduceMemoryDump(Game::config->GetValue("generate_dump") == "1"); if (!Game::config->GetValue("dump_folder").empty()) { Diagnostics::SetOutDirectory(Game::config->GetValue("dump_folder")); } try { Database::Connect(mysql_host, mysql_database, mysql_username, mysql_password); } catch (sql::SQLException& ex) { Game::logger->Log("WorldServer", "Got an error while connecting to the database: %s", ex.what()); return EXIT_FAILURE; } //Find out the master's IP: std::string masterIP = "localhost"; int masterPort = 1000; 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; ObjectIDManager::Instance()->Initialize(); UserManager::Instance()->Initialize(); LootGenerator::Instance(); Game::chatFilter = new dChatFilter(Game::assetManager->GetResPath().string() + "/chatplus_en_us", bool(std::stoi(Game::config->GetValue("dont_generate_dcf")))); Game::server = new dServer(masterIP, ourPort, instanceID, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::World, Game::config, zoneID); //Connect to the chat server: int chatPort = 1501; if (Game::config->GetValue("chat_server_port") != "") chatPort = std::atoi(Game::config->GetValue("chat_server_port").c_str()); auto chatSock = SocketDescriptor(uint16_t(ourPort + 2), 0); Game::chatServer = RakNetworkFactory::GetRakPeerInterface(); Game::chatServer->Startup(1, 30, &chatSock, 1); Game::chatServer->Connect(masterIP.c_str(), chatPort, "3.25 ND1", 8); //Set up other things: Game::randomEngine = std::mt19937(time(0)); //Run it until server gets a kill message from Master: auto lastTime = std::chrono::high_resolution_clock::now(); auto t = std::chrono::high_resolution_clock::now(); Packet* packet = nullptr; int framesSinceLastFlush = 0; int framesSinceMasterDisconnect = 0; int framesSinceChatDisconnect = 0; int framesSinceLastUsersSave = 0; int framesSinceLastSQLPing = 0; int framesSinceLastUser = 0; const float maxPacketProcessingTime = 1.5f; //0.015f; const int maxPacketsToProcess = 1024; bool ready = false; int framesSinceMasterStatus = 0; int framesSinceShutdownSequence = 0; int currentFramerate = highFrameRate; int ghostingStepCount = 0; auto ghostingLastTime = std::chrono::high_resolution_clock::now(); PerformanceManager::SelectProfile(zoneID); //Load our level: if (zoneID != 0) { dpWorld::Instance().Initialize(zoneID); dZoneManager::Instance()->Initialize(LWOZONEID(zoneID, instanceID, cloneID)); g_CloneID = cloneID; // pre calculate the FDB checksum if (Game::config->GetValue("check_fdb") == "1") { std::ifstream fileStream; static const std::vector aliases = { "CDServers.fdb", "cdserver.fdb", "CDClient.fdb", "cdclient.fdb", }; for (const auto& file : aliases) { fileStream.open(Game::assetManager->GetResPath() / file, std::ios::binary | std::ios::in); if (fileStream.is_open()) { break; } } const int bufferSize = 1024; MD5* md5 = new MD5(); char fileStreamBuffer[1024] = {}; while (!fileStream.eof()) { memset(fileStreamBuffer, 0, bufferSize); fileStream.read(fileStreamBuffer, bufferSize); md5->update(fileStreamBuffer, fileStream.gcount()); } fileStream.close(); const char* nullTerminateBuffer = "\0"; md5->update(nullTerminateBuffer, 1); // null terminate the data md5->finalize(); databaseChecksum = md5->hexdigest(); delete md5; Game::logger->Log("WorldServer", "FDB Checksum calculated as: %s", databaseChecksum.c_str()); } } while (true) { Metrics::StartMeasurement(MetricVariable::Frame); Metrics::StartMeasurement(MetricVariable::GameLoop); std::clock_t metricCPUTimeStart = std::clock(); const auto currentTime = std::chrono::high_resolution_clock::now(); float deltaTime = std::chrono::duration(currentTime - lastTime).count(); lastTime = currentTime; const auto occupied = UserManager::Instance()->GetUserCount() != 0; if (!ready) { currentFramerate = highFrameRate; } else { currentFramerate = PerformanceManager::GetServerFramerate(); } //Warning if we ran slow if (deltaTime > currentFramerate) { Game::logger->Log("WorldServer", "We're running behind, dT: %f > %f (framerate)", deltaTime, currentFramerate); } //Check if we're still connected to master: if (!Game::server->GetIsConnectedToMaster()) { framesSinceMasterDisconnect++; int framesToWaitForMaster = ready ? 10 : 200; if (framesSinceMasterDisconnect >= framesToWaitForMaster && !worldShutdownSequenceStarted) { Game::logger->Log("WorldServer", "Game loop running but no connection to master for %d frames, shutting down", framesToWaitForMaster); worldShutdownSequenceStarted = true; } } else framesSinceMasterDisconnect = 0; // Check if we're still connected to chat: if (!chatConnected) { framesSinceChatDisconnect++; // Attempt to reconnect every 30 seconds. if (framesSinceChatDisconnect >= 2000) { framesSinceChatDisconnect = 0; Game::chatServer->Connect(masterIP.c_str(), chatPort, "3.25 ND1", 8); } } else framesSinceChatDisconnect = 0; //In world we'd update our other systems here. if (zoneID != 0 && deltaTime > 0.0f) { Metrics::StartMeasurement(MetricVariable::Physics); dpWorld::Instance().StepWorld(deltaTime); Metrics::EndMeasurement(MetricVariable::Physics); Metrics::StartMeasurement(MetricVariable::UpdateEntities); EntityManager::Instance()->UpdateEntities(deltaTime); Metrics::EndMeasurement(MetricVariable::UpdateEntities); Metrics::StartMeasurement(MetricVariable::Ghosting); if (std::chrono::duration(currentTime - ghostingLastTime).count() >= 1.0f) { EntityManager::Instance()->UpdateGhosting(); ghostingLastTime = currentTime; } Metrics::EndMeasurement(MetricVariable::Ghosting); Metrics::StartMeasurement(MetricVariable::UpdateSpawners); dZoneManager::Instance()->Update(deltaTime); Metrics::EndMeasurement(MetricVariable::UpdateSpawners); } Metrics::StartMeasurement(MetricVariable::PacketHandling); //Check for packets here: packet = Game::server->ReceiveFromMaster(); if (packet) { //We can get messages not handle-able by the dServer class, so handle them if we returned anything. HandlePacket(packet); Game::server->DeallocateMasterPacket(packet); } //Handle our chat packets: packet = Game::chatServer->Receive(); if (packet) { HandlePacketChat(packet); Game::chatServer->DeallocatePacket(packet); } //Handle world-specific packets: float timeSpent = 0.0f; UserManager::Instance()->DeletePendingRemovals(); auto t1 = std::chrono::high_resolution_clock::now(); for (int curPacket = 0; curPacket < maxPacketsToProcess && timeSpent < maxPacketProcessingTime; curPacket++) { packet = Game::server->Receive(); if (packet) { auto t1 = std::chrono::high_resolution_clock::now(); HandlePacket(packet); auto t2 = std::chrono::high_resolution_clock::now(); timeSpent += std::chrono::duration_cast(t2 - t1).count(); Game::server->DeallocatePacket(packet); packet = nullptr; } else { break; } } Metrics::EndMeasurement(MetricVariable::PacketHandling); Metrics::StartMeasurement(MetricVariable::UpdateReplica); //Update our replica objects: Game::server->UpdateReplica(); Metrics::EndMeasurement(MetricVariable::UpdateReplica); //Push our log every 15s: if (framesSinceLastFlush >= 1000) { Game::logger->Flush(); framesSinceLastFlush = 0; } else framesSinceLastFlush++; if (zoneID != 0 && !occupied) { framesSinceLastUser++; //If we haven't had any players for a while, time out and shut down: if (framesSinceLastUser == (cloneID != 0 ? 4000 : 40000)) { worldShutdownSequenceStarted = true; } } else { framesSinceLastUser = 0; } //Save all connected users every 10 minutes: if (framesSinceLastUsersSave >= 40000 && zoneID != 0) { UserManager::Instance()->SaveAllActiveCharacters(); framesSinceLastUsersSave = 0; if (PropertyManagementComponent::Instance() != nullptr) { PropertyManagementComponent::Instance()->Save(); } } else framesSinceLastUsersSave++; //Every 10 min we ping our sql server to keep it alive hopefully: if (framesSinceLastSQLPing >= 40000) { //Find out the master's IP for absolutely no reason: std::string masterIP; int 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++; Metrics::EndMeasurement(MetricVariable::GameLoop); Metrics::StartMeasurement(MetricVariable::Sleep); t += std::chrono::milliseconds(currentFramerate); std::this_thread::sleep_until(t); Metrics::EndMeasurement(MetricVariable::Sleep); if (!ready && Game::server->GetIsConnectedToMaster()) { // Some delay is required here or else we crash the client? framesSinceMasterStatus++; if (framesSinceMasterStatus >= 200) { Game::logger->Log("WorldServer", "Finished loading world with zone (%i), ready up!", Game::server->GetZoneID()); MasterPackets::SendWorldReady(Game::server, Game::server->GetZoneID(), Game::server->GetInstanceID()); ready = true; } } if (worldShutdownSequenceStarted && !worldShutdownSequenceComplete) { WorldShutdownProcess(zoneID); break; } Metrics::AddMeasurement(MetricVariable::CPUTime, (1e6 * (1000.0 * (std::clock() - metricCPUTimeStart))) / CLOCKS_PER_SEC); Metrics::EndMeasurement(MetricVariable::Frame); } FinalizeShutdown(); return EXIT_SUCCESS; } dLogger* SetupLogger(int zoneID, int instanceID) { std::string logPath = (BinaryPathFinder::GetBinaryDir() / ("logs/WorldServer_" + std::to_string(zoneID) + "_" + std::to_string(instanceID) + "_" + 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 HandlePacketChat(Packet* packet) { if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) { Game::logger->Log("WorldServer", "Lost our connection to chat, zone(%i), instance(%i)", Game::server->GetZoneID(), Game::server->GetInstanceID()); chatConnected = false; } if (packet->data[0] == ID_CONNECTION_REQUEST_ACCEPTED) { Game::logger->Log("WorldServer", "Established connection to chat, zone(%i), instance (%i)", Game::server->GetZoneID(), Game::server->GetInstanceID()); Game::chatSysAddr = packet->systemAddress; chatConnected = true; } if (packet->data[0] == ID_USER_PACKET_ENUM) { if (packet->data[1] == CHAT_INTERNAL) { switch (packet->data[3]) { case MSG_CHAT_INTERNAL_ROUTE_TO_PLAYER: { CINSTREAM; LWOOBJID playerID; inStream.Read(playerID); inStream.Read(playerID); auto player = EntityManager::Instance()->GetEntity(playerID); if (!player) return; auto sysAddr = player->GetSystemAddress(); //Write our stream outwards: CBITSTREAM; for (int i = 0; i < inStream.GetNumberOfBytesUsed(); i++) { bitStream.Write(packet->data[i + 16]); //16 bytes == header + playerID to skip } SEND_PACKET; //send routed packet to player break; } case MSG_CHAT_INTERNAL_ANNOUNCEMENT: { CINSTREAM; LWOOBJID header; inStream.Read(header); std::string title; std::string msg; uint32_t len; inStream.Read(len); for (int i = 0; len > i; i++) { char character; inStream.Read(character); title += character; } len = 0; inStream.Read(len); for (int i = 0; len > i; i++) { char character; inStream.Read(character); msg += character; } //Send to our clients: AMFArrayValue args; auto* titleValue = new AMFStringValue(); titleValue->SetStringValue(title.c_str()); auto* messageValue = new AMFStringValue(); messageValue->SetStringValue(msg.c_str()); args.InsertValue("title", titleValue); args.InsertValue("message", messageValue); GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", &args); break; } case MSG_CHAT_INTERNAL_MUTE_UPDATE: { CINSTREAM; LWOOBJID playerId; time_t expire = 0; inStream.Read(playerId); inStream.Read(playerId); inStream.Read(expire); auto* entity = EntityManager::Instance()->GetEntity(playerId); if (entity != nullptr) { entity->GetParentUser()->SetMuteExpire(expire); entity->GetCharacter()->SendMuteNotice(); } break; } case MSG_CHAT_INTERNAL_TEAM_UPDATE: { CINSTREAM; LWOOBJID header; inStream.Read(header); LWOOBJID teamID = 0; char lootOption = 0; char memberCount = 0; std::vector members; inStream.Read(teamID); bool deleteTeam = inStream.ReadBit(); if (deleteTeam) { TeamManager::Instance()->DeleteTeam(teamID); Game::logger->Log("WorldServer", "Deleting team (%llu)", teamID); break; } inStream.Read(lootOption); inStream.Read(memberCount); Game::logger->Log("WorldServer", "Updating team (%llu), (%i), (%i)", teamID, lootOption, memberCount); for (char i = 0; i < memberCount; i++) { LWOOBJID member = LWOOBJID_EMPTY; inStream.Read(member); members.push_back(member); Game::logger->Log("WorldServer", "Updating team member (%llu)", member); } TeamManager::Instance()->UpdateTeam(teamID, lootOption, members); break; } default: Game::logger->Log("WorldServer", "Received an unknown chat internal: %i", int(packet->data[3])); } } } } void HandlePacket(Packet* packet) { if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) { auto user = UserManager::Instance()->GetUser(packet->systemAddress); if (!user) return; auto c = user->GetLastUsedChar(); if (!c) { UserManager::Instance()->DeleteUser(packet->systemAddress); return; } auto* entity = EntityManager::Instance()->GetEntity(c->GetObjectID()); if (!entity) { entity = Player::GetPlayer(packet->systemAddress); } if (entity) { auto* skillComponent = entity->GetComponent(); if (skillComponent != nullptr) { skillComponent->Reset(); } entity->GetCharacter()->SaveXMLToDatabase(); Game::logger->Log("WorldServer", "Deleting player %llu", entity->GetObjectID()); EntityManager::Instance()->DestroyEntity(entity); } { CBITSTREAM; PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION); bitStream.Write(user->GetLoggedInChar()); Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); } UserManager::Instance()->DeleteUser(packet->systemAddress); if (PropertyManagementComponent::Instance() != nullptr) { PropertyManagementComponent::Instance()->Save(); } CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_PLAYER_REMOVED); bitStream.Write((LWOMAPID)Game::server->GetZoneID()); bitStream.Write((LWOINSTANCEID)instanceID); Game::server->SendToMaster(&bitStream); } if (packet->data[0] != ID_USER_PACKET_ENUM) return; if (packet->data[1] == SERVER) { if (packet->data[3] == MSG_SERVER_VERSION_CONFIRM) { AuthPackets::HandleHandshake(Game::server, packet); } } if (packet->data[1] == MASTER) { switch (packet->data[3]) { case MSG_MASTER_REQUEST_PERSISTENT_ID_RESPONSE: { uint64_t requestID = PacketUtils::ReadPacketU64(8, packet); uint32_t objectID = PacketUtils::ReadPacketU32(16, packet); ObjectIDManager::Instance()->HandleRequestPersistentIDResponse(requestID, objectID); break; } case MSG_MASTER_REQUEST_ZONE_TRANSFER_RESPONSE: { uint64_t requestID = PacketUtils::ReadPacketU64(8, packet); ZoneInstanceManager::Instance()->HandleRequestZoneTransferResponse(requestID, packet); break; } case MSG_MASTER_SESSION_KEY_RESPONSE: { //Read our session key and to which user it belongs: 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); //Find them: auto it = m_PendingUsers.find(username); if (it == m_PendingUsers.end()) return; //Convert our key: std::string userHash = std::to_string(sessionKey); userHash = md5(userHash); //Verify it: if (userHash != it->second.hash) { Game::logger->Log("WorldServer", "SOMEONE IS TRYING TO HACK? SESSION KEY MISMATCH: ours: %s != master: %s", userHash.c_str(), it->second.hash.c_str()); Game::server->Disconnect(it->second.sysAddr, SERVER_DISCON_INVALID_SESSION_KEY); return; } else { Game::logger->Log("WorldServer", "User %s authenticated with correct key.", username.c_str()); UserManager::Instance()->DeleteUser(packet->systemAddress); //Create our user and send them in: UserManager::Instance()->CreateUser(it->second.sysAddr, username, userHash); auto zone = dZoneManager::Instance()->GetZone(); if (zone) { float x = 0.0f; float y = 0.0f; float z = 0.0f; if (zone->GetZoneID().GetMapID() == 1100) { auto pos = zone->GetSpawnPos(); x = pos.x; y = pos.y; z = pos.z; } WorldPackets::SendLoadStaticZone(it->second.sysAddr, x, y, z, zone->GetChecksum()); } if (Game::server->GetZoneID() == 0) { //Since doing this reroute breaks the client's request, we have to call this manually. UserManager::Instance()->RequestCharacterList(it->second.sysAddr); } m_PendingUsers.erase(username); //Notify master: { CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_PLAYER_ADDED); bitStream.Write((LWOMAPID)Game::server->GetZoneID()); bitStream.Write((LWOINSTANCEID)instanceID); Game::server->SendToMaster(&bitStream); } } break; } case MSG_MASTER_AFFIRM_TRANSFER_REQUEST: { const uint64_t requestID = PacketUtils::ReadPacketU64(8, packet); Game::logger->Log("MasterServer", "Got affirmation request of transfer %llu", requestID); CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_AFFIRM_TRANSFER_RESPONSE); bitStream.Write(requestID); Game::server->SendToMaster(&bitStream); break; } case MSG_MASTER_SHUTDOWN: { worldShutdownSequenceStarted = true; Game::logger->Log("WorldServer", "Got shutdown request from master, zone (%i), instance (%i)", Game::server->GetZoneID(), Game::server->GetInstanceID()); break; } case MSG_MASTER_NEW_SESSION_ALERT: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); uint32_t sessionKey = inStream.Read(sessionKey); std::string username; uint32_t len; inStream.Read(len); for (int i = 0; i < len; i++) { char character; inStream.Read(character); username += character; } //Find them: User* user = UserManager::Instance()->GetUser(username.c_str()); if (!user) { Game::logger->Log("WorldServer", "Got new session alert for user %s, but they're not logged in.", username.c_str()); return; } //Check the key: if (sessionKey != std::atoi(user->GetSessionKey().c_str())) { Game::logger->Log("WorldServer", "Got new session alert for user %s, but the session key is invalid.", username.c_str()); Game::server->Disconnect(user->GetSystemAddress(), SERVER_DISCON_INVALID_SESSION_KEY); return; } break; } default: Game::logger->Log("WorldServer", "Unknown packet ID from master %i", int(packet->data[3])); } return; } if (packet->data[1] != WORLD) return; switch (packet->data[3]) { case MSG_WORLD_CLIENT_VALIDATION: { std::string username = PacketUtils::ReadString(0x08, packet, true); std::string sessionKey = PacketUtils::ReadString(74, packet, true); std::string clientDatabaseChecksum = PacketUtils::ReadString(packet->length - 33, packet, false); // sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird clientDatabaseChecksum = clientDatabaseChecksum.substr(0, 32); // If the check is turned on, validate the client's database checksum. if (Game::config->GetValue("check_fdb") == "1" && !databaseChecksum.empty()) { uint32_t gmLevel = 0; auto* stmt = Database::CreatePreppedStmt("SELECT gm_level FROM accounts WHERE name=? LIMIT 1;"); stmt->setString(1, username.c_str()); auto* res = stmt->executeQuery(); while (res->next()) { gmLevel = res->getInt(1); } delete stmt; delete res; // Developers may skip this check if (gmLevel < 8 && clientDatabaseChecksum != databaseChecksum) { Game::logger->Log("WorldServer", "Client's database checksum does not match the server's, aborting connection."); Game::server->Disconnect(packet->systemAddress, SERVER_DISCON_KICK); return; } } //Request the session info from Master: CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_REQUEST_SESSION_KEY); PacketUtils::WriteString(bitStream, username, 64); Game::server->SendToMaster(&bitStream); //Insert info into our pending list tempSessionInfo info; info.sysAddr = SystemAddress(packet->systemAddress); info.hash = sessionKey; m_PendingUsers.insert(std::make_pair(username, info)); break; } case MSG_WORLD_CLIENT_CHARACTER_LIST_REQUEST: { //We need to delete the entity first, otherwise the char list could delete it while it exists in the world! if (Game::server->GetZoneID() != 0) { auto user = UserManager::Instance()->GetUser(packet->systemAddress); if (!user) return; EntityManager::Instance()->DestroyEntity(user->GetLastUsedChar()->GetEntity()); } //This loops prevents users who aren't authenticated to double-request the char list, which //would make the login screen freeze sometimes. if (m_PendingUsers.size() > 0) { for (auto it : m_PendingUsers) { if (it.second.sysAddr == packet->systemAddress) { return; } } } UserManager::Instance()->RequestCharacterList(packet->systemAddress); break; } case MSG_WORLD_CLIENT_GAME_MSG: { RakNet::BitStream bitStream(packet->data, packet->length, false); uint64_t header; LWOOBJID objectID; uint16_t messageID; bitStream.Read(header); bitStream.Read(objectID); bitStream.Read(messageID); RakNet::BitStream dataStream; bitStream.Read(dataStream, bitStream.GetNumberOfUnreadBits()); GameMessageHandler::HandleMessage(&dataStream, packet->systemAddress, objectID, GAME_MSG(messageID)); break; } case MSG_WORLD_CLIENT_CHARACTER_CREATE_REQUEST: { UserManager::Instance()->CreateCharacter(packet->systemAddress, packet); break; } case MSG_WORLD_CLIENT_LOGIN_REQUEST: { RakNet::BitStream inStream(packet->data, packet->length, false); uint64_t header = inStream.Read(header); LWOOBJID playerID = 0; inStream.Read(playerID); playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_CHARACTER); playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_PERSISTENT); auto user = UserManager::Instance()->GetUser(packet->systemAddress); if (user) { auto lastCharacter = user->GetLoggedInChar(); // This means we swapped characters and we need to remove the previous player from the container. if (static_cast(lastCharacter) != playerID) { CBITSTREAM; PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION); bitStream.Write(lastCharacter); Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); } } UserManager::Instance()->LoginCharacter(packet->systemAddress, static_cast(playerID)); break; } case MSG_WORLD_CLIENT_CHARACTER_DELETE_REQUEST: { UserManager::Instance()->DeleteCharacter(packet->systemAddress, packet); UserManager::Instance()->RequestCharacterList(packet->systemAddress); break; } case MSG_WORLD_CLIENT_CHARACTER_RENAME_REQUEST: { UserManager::Instance()->RenameCharacter(packet->systemAddress, packet); break; } case MSG_WORLD_CLIENT_LEVEL_LOAD_COMPLETE: { Game::logger->Log("WorldServer", "Received level load complete from user."); User* user = UserManager::Instance()->GetUser(packet->systemAddress); if (user) { Character* c = user->GetLastUsedChar(); if (c != nullptr) { std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress); EntityInfo info{}; info.lot = 1; Entity* player = EntityManager::Instance()->CreateEntity(info, UserManager::Instance()->GetUser(packet->systemAddress)); WorldPackets::SendCreateCharacter(packet->systemAddress, player, c->GetXMLData(), username, c->GetGMLevel()); WorldPackets::SendServerState(packet->systemAddress); const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(dZoneManager::Instance()->GetZone()->GetWorldID()); EntityManager::Instance()->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS, true); if (respawnPoint != NiPoint3::ZERO) { GameMessages::SendPlayerReachedRespawnCheckpoint(player, respawnPoint, NiQuaternion::IDENTITY); } EntityManager::Instance()->ConstructAllEntities(packet->systemAddress); auto* characterComponent = player->GetComponent(); if (characterComponent) { player->GetComponent()->RocketUnEquip(player); } c->SetRetroactiveFlags(); player->RetroactiveVaultSize(); player->GetCharacter()->SetTargetScene(""); // Fix the destroyable component auto* destroyableComponent = player->GetComponent(); if (destroyableComponent != nullptr) { destroyableComponent->FixStats(); } //Tell the player to generate BBB models, if any: if (g_CloneID != 0) { const auto& worldId = dZoneManager::Instance()->GetZone()->GetZoneID(); const auto zoneId = Game::server->GetZoneID(); const auto cloneId = g_CloneID; auto query = CDClientDatabase::CreatePreppedStmt( "SELECT id FROM PropertyTemplate WHERE mapID = ?;"); query.bind(1, (int)zoneId); auto result = query.execQuery(); if (result.eof() || result.fieldIsNull(0)) { Game::logger->Log("WorldServer", "No property templates found for zone %d, not sending BBB", zoneId); goto noBBB; } //Check for BBB models: auto stmt = Database::CreatePreppedStmt("SELECT ugc_id FROM properties_contents WHERE lot=14 AND property_id=?"); int templateId = result.getIntField(0); result.finalize(); auto* propertyLookup = Database::CreatePreppedStmt("SELECT * FROM properties WHERE template_id = ? AND clone_id = ?;"); propertyLookup->setInt(1, templateId); propertyLookup->setInt64(2, g_CloneID); auto* propertyEntry = propertyLookup->executeQuery(); uint64_t propertyId = 0; if (propertyEntry->next()) { propertyId = propertyEntry->getUInt64(1); } delete propertyLookup; stmt->setUInt64(1, propertyId); auto res = stmt->executeQuery(); while (res->next()) { Game::logger->Log("UGC", "Getting lxfml ugcID: %u", res->getUInt(1)); //Get lxfml: auto stmtL = Database::CreatePreppedStmt("SELECT lxfml from ugc where id=?"); stmtL->setUInt(1, res->getUInt(1)); auto lxres = stmtL->executeQuery(); while (lxres->next()) { auto lxfml = lxres->getBlob(1); lxfml->seekg(0, std::ios::end); size_t lxfmlSize = lxfml->tellg(); lxfml->seekg(0); //Send message: { LWOOBJID blueprintID = res->getUInt(1); blueprintID = GeneralUtils::SetBit(blueprintID, OBJECT_BIT_CHARACTER); blueprintID = GeneralUtils::SetBit(blueprintID, OBJECT_BIT_PERSISTENT); CBITSTREAM; PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_BLUEPRINT_SAVE_RESPONSE); bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); bitStream.Write(1); bitStream.Write(blueprintID); bitStream.Write(lxfmlSize); for (size_t i = 0; i < lxfmlSize; ++i) bitStream.Write(lxfml->get()); SystemAddress sysAddr = packet->systemAddress; SEND_PACKET; PacketUtils::SavePacket("lxfml packet " + std::to_string(res->getUInt(1)) + ".bin", (char*)bitStream.GetData(), bitStream.GetNumberOfBytesUsed()); } } delete stmtL; delete lxres; } delete stmt; delete res; } noBBB: // Tell the client it's done loading: GameMessages::SendInvalidZoneTransferList(player, packet->systemAddress, GeneralUtils::ASCIIToUTF16(Game::config->GetValue("source")), u"", false, false); GameMessages::SendServerDoneLoadingAllObjects(player, packet->systemAddress); //Send the player it's mail count: //update: this might not be needed so im going to try disabling this here. //Mail::HandleNotificationRequest(packet->systemAddress, player->GetObjectID()); //Notify chat that a player has loaded: { const auto& playerName = player->GetCharacter()->GetName(); //RakNet::RakString playerName(player->GetCharacter()->GetName().c_str()); CBITSTREAM; PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_ADDED_NOTIFICATION); bitStream.Write(player->GetObjectID()); bitStream.Write(playerName.size()); for (size_t i = 0; i < playerName.size(); i++) { bitStream.Write(playerName[i]); } auto zone = dZoneManager::Instance()->GetZone()->GetZoneID(); bitStream.Write(zone.GetMapID()); bitStream.Write(zone.GetInstanceID()); bitStream.Write(zone.GetCloneID()); bitStream.Write(player->GetParentUser()->GetMuteExpire()); Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); } } else { Game::logger->Log("WorldServer", "Couldn't find character to log in with for user %s (%i)!", user->GetUsername().c_str(), user->GetAccountID()); Game::server->Disconnect(packet->systemAddress, SERVER_DISCON_CHARACTER_NOT_FOUND); } } else { Game::logger->Log("WorldServer", "Couldn't get user for level load complete!"); } break; } case MSG_WORLD_CLIENT_POSITION_UPDATE: { ClientPackets::HandleClientPositionUpdate(packet->systemAddress, packet); break; } case MSG_WORLD_CLIENT_MAIL: { RakNet::BitStream bitStream(packet->data, packet->length, false); LWOOBJID space; bitStream.Read(space); Mail::HandleMailStuff(&bitStream, packet->systemAddress, UserManager::Instance()->GetUser(packet->systemAddress)->GetLastUsedChar()->GetEntity()); break; } case MSG_WORLD_CLIENT_ROUTE_PACKET: { //Yeet to chat CINSTREAM; uint64_t header = 0; uint32_t size = 0; inStream.Read(header); inStream.Read(size); if (size > 20000) { Game::logger->Log("WorldServer", "Tried to route a packet with a read size > 20000, so likely a false packet."); return; } CBITSTREAM; PacketUtils::WriteHeader(bitStream, CHAT, packet->data[14]); //We need to insert the player's objectID so the chat server can find who originated this request: LWOOBJID objectID = 0; auto user = UserManager::Instance()->GetUser(packet->systemAddress); if (user) { objectID = user->GetLastUsedChar()->GetObjectID(); } bitStream.Write(objectID); //Now write the rest of the data: auto data = inStream.GetData(); for (uint32_t i = 0; i < size; ++i) { bitStream.Write(data[i + 23]); } Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE_ORDERED, 0, Game::chatSysAddr, false); break; } case MSG_WORLD_CLIENT_STRING_CHECK: { ClientPackets::HandleChatModerationRequest(packet->systemAddress, packet); break; } case MSG_WORLD_CLIENT_GENERAL_CHAT_MESSAGE: { if (chatDisabled) { ChatPackets::SendMessageFail(packet->systemAddress); } else { ClientPackets::HandleChatMessage(packet->systemAddress, packet); } break; } case MSG_WORLD_CLIENT_HANDLE_FUNNESS: { //This means the client is running slower or faster than it should. //Could be insane lag, but I'mma just YEET them as it's usually speedhacking. //This is updated to now count the amount of times we've been caught "speedhacking" to kick with a delay //This is hopefully going to fix the random disconnects people face sometimes. if (Game::config->GetValue("disable_anti_speedhack") == "1") { return; } User* user = UserManager::Instance()->GetUser(packet->systemAddress); if (user) { user->UserOutOfSync(); } else { Game::server->Disconnect(packet->systemAddress, SERVER_DISCON_KICK); } break; } default: Game::server->GetLogger()->Log("HandlePacket", "Unknown world packet received: %i", int(packet->data[3])); } } void WorldShutdownProcess(uint32_t zoneId) { Game::logger->Log("WorldServer", "Saving map %i instance %i", zoneId, instanceID); for (auto i = 0; i < Game::server->GetReplicaManager()->GetParticipantCount(); ++i) { const auto& player = Game::server->GetReplicaManager()->GetParticipantAtIndex(i); auto* entity = Player::GetPlayer(player); Game::logger->Log("WorldServer", "Saving data!"); if (entity != nullptr && entity->GetCharacter() != nullptr) { auto* skillComponent = entity->GetComponent(); if (skillComponent != nullptr) { skillComponent->Reset(); } Game::logger->Log("WorldServer", "Saving character %s...", entity->GetCharacter()->GetName().c_str()); entity->GetCharacter()->SaveXMLToDatabase(); Game::logger->Log("WorldServer", "Character data for %s was saved!", entity->GetCharacter()->GetName().c_str()); } } if (PropertyManagementComponent::Instance() != nullptr) { Game::logger->Log("WorldServer", "Saving ALL property data for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); PropertyManagementComponent::Instance()->Save(); Game::logger->Log("WorldServer", "ALL property data saved for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); } Game::logger->Log("WorldServer", "ALL DATA HAS BEEN SAVED FOR ZONE %i INSTANCE %i!", zoneId, instanceID); while (Game::server->GetReplicaManager()->GetParticipantCount() > 0) { const auto& player = Game::server->GetReplicaManager()->GetParticipantAtIndex(0); Game::server->Disconnect(player, SERVER_DISCON_KICK); } SendShutdownMessageToMaster(); } void WorldShutdownSequence() { if (worldShutdownSequenceStarted || worldShutdownSequenceComplete) { return; } worldShutdownSequenceStarted = true; Game::logger->Log("WorldServer", "Zone (%i) instance (%i) shutting down outside of main loop!", Game::server->GetZoneID(), instanceID); WorldShutdownProcess(Game::server->GetZoneID()); FinalizeShutdown(); } void FinalizeShutdown() { Game::logger->Log("WorldServer", "Shutdown complete, zone (%i), instance (%i)", Game::server->GetZoneID(), instanceID); //Delete our objects here: Metrics::Clear(); Database::Destroy("WorldServer"); if (Game::chatFilter) delete Game::chatFilter; if (Game::server) delete Game::server; if (Game::logger) delete Game::logger; if (Game::config) delete Game::config; worldShutdownSequenceComplete = true; exit(EXIT_SUCCESS); } void SendShutdownMessageToMaster() { CBITSTREAM; PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_SHUTDOWN_RESPONSE); Game::server->SendToMaster(&bitStream); }