From b489ee59f8072a30775537a113810e1e07a75cba Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 23:10:57 -0500 Subject: [PATCH] refactor: world packets as structs WorldPackets now holds what a client sends a world server, one struct per packet with Serialize/Deserialize: Validation, CharacterListRequest, CharacterCreateRequest, CharacterLoginRequest, GameMessage, CharacterDeleteRequest, CharacterRenameRequest, LevelLoadComplete, PositionUpdate, MailPacket, RoutePacket, StringCheck, GeneralChatMessage, HandleFunness, UIHelpTop5. WorldServer's switch became a dispatch map of handlers (each overrides Handle in WorldServer.cpp, logic unchanged: dashboard hooks, LoadPlayer, chat logging, migration checks, message inspector capture all still run); a packet that does not deserialize is logged and dropped. UserManager's create/delete/rename take the structs. The answers are ClientPackets (the CLIENT service): LoadStaticZone, CharacterListResponse, CharacterCreateResponse, CharacterRenameResponse, DeleteCharacterResponse, TransferToWorld, ServerStates, CreateCharacter, ChatModerationString, MakeGMResponse, HTTPMonitorInfoResponse and DebugOutput, built at the call sites (world server, UserManager, slash commands, dashboard actions, components, migration). The world -> chat forward of a routed packet is ChatPackets::RoutedFromClient. All WorldPackets::Send* functions, HTTPMonitorInfo and the ClientPackets parse functions are gone. The architecture doc now states the file rule: a packet lives in the file of the ServiceType in its header. No wire change. Verified against frozen verbatim copies of the old code (tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h): every response is sent through the old function and the struct over grids of inputs (all enum values, strings of every length class, IDs, >64 moderation segments, big XML) and must match byte for byte; every request is read by the old code and the struct and must give the same values; plus golden bytes, round trips, truncation checks and a field width mutation that made the tests fail. Only differences: malformed requests are dropped instead of handled with partial values, and LevelLoadComplete now reads the zone ID the client sends after it (lu_packets and captures show the 1.10.64 client always sends it; DLU ignored it). Co-Authored-By: Claude Opus 5.5 --- dGame/UserManager.cpp | 184 ++-- dGame/UserManager.h | 12 +- dGame/dBehaviors/PropertyTeleportBehavior.cpp | 8 +- dGame/dComponents/ActivityComponent.cpp | 8 +- dGame/dComponents/CharacterComponent.cpp | 8 +- dGame/dGameMessages/GameMessages.cpp | 8 +- dGame/dUtilities/DashboardActions.cpp | 15 +- .../SlashCommands/DEVGMCommands.cpp | 22 +- .../SlashCommands/GMZeroCommands.cpp | 14 +- dGame/dUtilities/WorldMigration.cpp | 8 +- dNet/ChatPackets.cpp | 12 + dNet/ChatPackets.h | 12 + dNet/ClientPackets.cpp | 460 +++++++-- dNet/ClientPackets.h | 196 +++- dNet/WorldPackets.cpp | 513 ++++++---- dNet/WorldPackets.h | 235 ++++- dWorldServer/WorldServer.cpp | 956 +++++++++--------- docs/PacketArchitecture.md | 11 +- tests/dGameTests/dNetTests/CMakeLists.txt | 3 +- .../dNetTests/Legacy/WorldPacketsLegacy.h | 543 ++++++++++ .../dNetTests/WorldPacketsTests.cpp | 719 +++++++++++++ 21 files changed, 3040 insertions(+), 907 deletions(-) create mode 100644 tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h create mode 100644 tests/dGameTests/dNetTests/WorldPacketsTests.cpp diff --git a/dGame/UserManager.cpp b/dGame/UserManager.cpp index e4b295342..368711872 100644 --- a/dGame/UserManager.cpp +++ b/dGame/UserManager.cpp @@ -10,6 +10,8 @@ #include "Logger.h" #include "User.h" #include "WorldPackets.h" +#include "ClientPackets.h" +#include "ClientPackets.h" #include "Character.h" #include "BitStream.h" #include "ObjectIDManager.h" @@ -244,89 +246,59 @@ void UserManager::RequestCharacterList(const SystemAddress& sysAddr) { chars.push_back(character); } - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE); + ClientPackets::CharacterListResponse response; + response.selectedCharacterIndex = 0; //TODO: Pick the most recent played index. character index in front, just picking 0 - std::vector characters = u->GetCharacters(); - bitStream.Write(characters.size()); - bitStream.Write(0); //TODO: Pick the most recent played index. character index in front, just picking 0 + for (const auto* const character : u->GetCharacters()) { + auto& entry = response.characters.emplace_back(); + entry.objectID = character->GetObjectID(); - for (uint32_t i = 0; i < characters.size(); ++i) { - bitStream.Write(characters[i]->GetObjectID()); - bitStream.Write(0); + entry.name = LUWString(character->GetName()); + entry.unapprovedName = LUWString(character->GetUnapprovedName()); - bitStream.Write(LUWString(characters[i]->GetName())); - bitStream.Write(LUWString(characters[i]->GetUnapprovedName())); + entry.nameRejected = character->GetNameRejected(); - bitStream.Write(characters[i]->GetNameRejected()); - bitStream.Write(false); + entry.shirtColor = character->GetShirtColor(); + entry.shirtStyle = character->GetShirtStyle(); + entry.pantsColor = character->GetPantsColor(); + entry.hairStyle = character->GetHairStyle(); + entry.hairColor = character->GetHairColor(); + entry.leftHand = character->GetLeftHand(); + entry.rightHand = character->GetRightHand(); + entry.eyebrows = character->GetEyebrows(); + entry.eyes = character->GetEyes(); + entry.mouth = character->GetMouth(); - bitStream.Write(LUString("", 10)); + entry.zoneID = character->GetZoneID(); + entry.zoneInstance = character->GetZoneInstance(); + entry.zoneClone = character->GetZoneClone(); - bitStream.Write(characters[i]->GetShirtColor()); - bitStream.Write(characters[i]->GetShirtStyle()); - bitStream.Write(characters[i]->GetPantsColor()); - bitStream.Write(characters[i]->GetHairStyle()); - bitStream.Write(characters[i]->GetHairColor()); - bitStream.Write(characters[i]->GetLeftHand()); - bitStream.Write(characters[i]->GetRightHand()); - bitStream.Write(characters[i]->GetEyebrows()); - bitStream.Write(characters[i]->GetEyes()); - bitStream.Write(characters[i]->GetMouth()); - bitStream.Write(0); + entry.lastLogin = character->GetLastLogin(); - bitStream.Write(characters[i]->GetZoneID()); - bitStream.Write(characters[i]->GetZoneInstance()); - bitStream.Write(characters[i]->GetZoneClone()); - - bitStream.Write(characters[i]->GetLastLogin()); - - const auto& equippedItems = characters[i]->GetEquippedItems(); - bitStream.Write(equippedItems.size()); - - for (uint32_t j = 0; j < equippedItems.size(); ++j) { - bitStream.Write(equippedItems[j]); - } + entry.equippedItems = character->GetEquippedItems(); } - SEND_PACKET; + response.Send(sysAddr); } -void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) { +void UserManager::CreateCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterCreateRequest& request) { User* u = GetUser(sysAddr); if (!u) return; - LUWString LUWStringName; - uint32_t firstNameIndex; - uint32_t middleNameIndex; - uint32_t lastNameIndex; - uint32_t shirtColor; - uint32_t shirtStyle; - uint32_t pantsColor; - uint32_t hairStyle; - uint32_t hairColor; - uint32_t lh; - uint32_t rh; - uint32_t eyebrows; - uint32_t eyes; - uint32_t mouth; - - CINSTREAM_SKIP_HEADER; - inStream.Read(LUWStringName); - inStream.Read(firstNameIndex); - inStream.Read(middleNameIndex); - inStream.Read(lastNameIndex); - inStream.IgnoreBytes(9); - inStream.Read(shirtColor); - inStream.Read(shirtStyle); - inStream.Read(pantsColor); - inStream.Read(hairStyle); - inStream.Read(hairColor); - inStream.Read(lh); - inStream.Read(rh); - inStream.Read(eyebrows); - inStream.Read(eyes); - inStream.Read(mouth); + const auto& LUWStringName = request.name; + const uint32_t firstNameIndex = request.firstNameIndex; + const uint32_t middleNameIndex = request.middleNameIndex; + const uint32_t lastNameIndex = request.lastNameIndex; + const uint32_t shirtColor = request.shirtColor; + const uint32_t shirtStyle = request.shirtStyle; + const uint32_t pantsColor = request.pantsColor; + const uint32_t hairStyle = request.hairStyle; + const uint32_t hairColor = request.hairColor; + const uint32_t lh = request.leftHand; + const uint32_t rh = request.rightHand; + const uint32_t eyebrows = request.eyebrows; + const uint32_t eyes = request.eyes; + const uint32_t mouth = request.mouth; const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted(); @@ -338,13 +310,17 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) if (!name.empty() && Database::Get()->IsNameInUse(name)) { LOG("AccountID: %i chose unavailable name: %s", u->GetAccountID(), name.c_str()); - WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::CUSTOM_NAME_IN_USE); + ClientPackets::CharacterCreateResponse response; + response.response = eCharacterCreationResponse::CUSTOM_NAME_IN_USE; + response.Send(sysAddr); return; } if (Database::Get()->IsNameInUse(predefinedName)) { LOG("AccountID: %i chose unavailable predefined name: %s", u->GetAccountID(), predefinedName.c_str()); - WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::PREDEFINED_NAME_IN_USE); + ClientPackets::CharacterCreateResponse response; + response.response = eCharacterCreationResponse::PREDEFINED_NAME_IN_USE; + response.Send(sysAddr); return; } @@ -370,7 +346,9 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) if (tries >= maxRetries) { LOG("Failed to get a unique objectID for new character after %i tries, aborting char creation for account %i", maxRetries, u->GetAccountID()); - WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE); + ClientPackets::CharacterCreateResponse response; + response.response = eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE; + response.Send(sysAddr); return; } @@ -430,20 +408,20 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet) //Now finally insert our character xml: Database::Get()->InsertCharacterXml(objectID, xml.str()); - WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::SUCCESS); + ClientPackets::CharacterCreateResponse response; + response.response = eCharacterCreationResponse::SUCCESS; + response.Send(sysAddr); UserManager::RequestCharacterList(sysAddr); } -void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet) { +void UserManager::DeleteCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterDeleteRequest& request) { User* u = GetUser(sysAddr); if (!u) { LOG("Couldn't get user to delete character"); return; } - CINSTREAM_SKIP_HEADER; - LWOOBJID objectID; - inStream.Read(objectID); + const LWOOBJID objectID = request.objectID; LOG("Received char delete req for ID: %llu", objectID); @@ -455,7 +433,9 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet) u->GetAccountID()); if (!hasCharacter) { - WorldPackets::SendCharacterDeleteResponse(sysAddr, false); + ClientPackets::DeleteCharacterResponse response; + response.success = false; + response.Send(sysAddr); } else { LOG("Deleting character %llu", objectID); Database::Get()->DeleteCharacter(objectID); @@ -466,26 +446,24 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, Packet* packet) bitStream.Write(objectID); Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); - WorldPackets::SendCharacterDeleteResponse(sysAddr, true); + ClientPackets::DeleteCharacterResponse response; + response.success = true; + response.Send(sysAddr); } } -void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) { +void UserManager::RenameCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterRenameRequest& request) { User* u = GetUser(sysAddr); if (!u) { LOG("Couldn't get user to delete character"); return; } - CINSTREAM_SKIP_HEADER; - LWOOBJID objectID; - inStream.Read(objectID); + const LWOOBJID objectID = request.objectID; LOG("Received char rename request for ID: %llu", objectID); - LUWString LUWStringName; - inStream.Read(LUWStringName); - auto newName = LUWStringName.GetAsString(); + auto newName = request.name.GetAsString(); Character* character = nullptr; const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted(); @@ -507,7 +485,9 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) }); if (!ownsCharacter || !character) { - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::UNKNOWN_ERROR; + response.Send(sysAddr); } else if (ownsCharacter && character) { if (autoRejectNames) { // Create a random preapproved name (fallback to default if none available) @@ -522,7 +502,9 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) } if (newName == character->GetName()) { - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::NAME_UNAVAILABLE; + response.Send(sysAddr); return; } @@ -531,28 +513,38 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) Database::Get()->SetCharacterName(objectID, newName); DashboardNotify::Changed("characters", objectID); LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str()); - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::SUCCESS; + response.Send(sysAddr); UserManager::RequestCharacterList(sysAddr); } else if (IsNamePreapproved(newName)) { Database::Get()->SetCharacterName(objectID, newName); DashboardNotify::Changed("characters", objectID); LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str()); - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::SUCCESS; + response.Send(sysAddr); UserManager::RequestCharacterList(sysAddr); } else { Database::Get()->SetPendingCharacterName(objectID, newName); DashboardNotify::Changed("characters", objectID); DashboardNotify::Changed("pending_names", objectID); LOG("Character %s has been renamed to %s and is pending approval by a moderator.", character->GetName().c_str(), newName.c_str()); - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::SUCCESS; + response.Send(sysAddr); UserManager::RequestCharacterList(sysAddr); } } else { - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_IN_USE); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::NAME_IN_USE; + response.Send(sysAddr); } } else { LOG("Unknown error occurred when renaming character, either hasCharacter or character variable != true."); - WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR); + ClientPackets::CharacterRenameResponse response; + response.response = eRenameResponse::UNKNOWN_ERROR; + response.Send(sysAddr); } } @@ -591,7 +583,11 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, LWOOBJID playerID character->SetZoneInstance(zoneInstance); character->SetZoneClone(zoneClone); } - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); return; }); } else { diff --git a/dGame/UserManager.h b/dGame/UserManager.h index dbc89f3a8..e5bfa7be3 100644 --- a/dGame/UserManager.h +++ b/dGame/UserManager.h @@ -9,6 +9,12 @@ class User; +namespace WorldPackets { + struct CharacterCreateRequest; + struct CharacterDeleteRequest; + struct CharacterRenameRequest; +} + class UserManager { public: static UserManager* Instance() { @@ -33,9 +39,9 @@ public: bool IsNamePreapproved(const std::string& requestedName); void RequestCharacterList(const SystemAddress& sysAddr); - void CreateCharacter(const SystemAddress& sysAddr, Packet* packet); - void DeleteCharacter(const SystemAddress& sysAddr, Packet* packet); - void RenameCharacter(const SystemAddress& sysAddr, Packet* packet); + void CreateCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterCreateRequest& request); + void DeleteCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterDeleteRequest& request); + void RenameCharacter(const SystemAddress& sysAddr, const WorldPackets::CharacterRenameRequest& request); void LoginCharacter(const SystemAddress& sysAddr, LWOOBJID playerID); void SaveAllActiveCharacters(); diff --git a/dGame/dBehaviors/PropertyTeleportBehavior.cpp b/dGame/dBehaviors/PropertyTeleportBehavior.cpp index bfbcd1634..f51fe3bdd 100644 --- a/dGame/dBehaviors/PropertyTeleportBehavior.cpp +++ b/dGame/dBehaviors/PropertyTeleportBehavior.cpp @@ -5,7 +5,7 @@ #include "Character.h" #include "CharacterComponent.h" #include "ChatPackets.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "EntityManager.h" #include "Game.h" #include "ZoneInstanceManager.h" @@ -55,7 +55,11 @@ void PropertyTeleportBehavior::Handle(BehaviorContext* context, RakNet::BitStrea entity->GetCharacter()->SaveXMLToDatabase(); - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); return; }); } diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index 7fa73f01d..cea28d83a 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -7,7 +7,7 @@ #include "ZoneInstanceManager.h" #include "Game.h" #include "Logger.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "EntityManager.h" #include "ChatPackets.h" #include "BitStreamUtils.h" @@ -477,7 +477,11 @@ void ActivityInstance::StartZone() { player->GetCharacter()->SetZoneClone(zoneClone); } - WorldPackets::SendTransferToWorld(player->GetSystemAddress(), serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(player->GetSystemAddress()); return; }); } diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index 4e7a506cb..1ee3ca961 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -22,7 +22,7 @@ #include "CDRewardCodesTable.h" #include "Mail.h" #include "ZoneInstanceManager.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "MessageType/Game.h" #include #include @@ -899,7 +899,11 @@ void CharacterComponent::SendToZone(LWOMAPID zoneId, LWOCLONEID cloneId) const { character->SaveXMLToDatabase(); } - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); Game::entityManager->DestructEntity(entity); }); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index e21ba21dc..aaaf77dd1 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -20,7 +20,7 @@ #include "CppScripts.h" #include "UserManager.h" #include "ZoneInstanceManager.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "Item.h" #include "ZCompression.h" #include "dConfig.h" @@ -4414,7 +4414,11 @@ void GameMessages::HandleFireEventServerSide(RakNet::BitStream& inStream, Entity character->SetZoneClone(zoneClone); } - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); return; }); } diff --git a/dGame/dUtilities/DashboardActions.cpp b/dGame/dUtilities/DashboardActions.cpp index d49295e27..c52dc5b48 100644 --- a/dGame/dUtilities/DashboardActions.cpp +++ b/dGame/dUtilities/DashboardActions.cpp @@ -14,7 +14,7 @@ #include "PlayerManager.h" #include "User.h" #include "UserManager.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "ZoneInstanceManager.h" #include "eGameMasterLevel.h" #include "eServerDisconnectIdentifiers.h" @@ -58,7 +58,12 @@ namespace { auto* entity = PlayerManager::GetPlayer(user->GetSystemAddress()); if (!entity || entity->GetGMLevel() <= info->maxGmLevel) continue; - WorldPackets::SendGMLevelChange(entity->GetSystemAddress(), true, info->maxGmLevel, entity->GetGMLevel(), info->maxGmLevel); + ClientPackets::MakeGMResponse response; + response.success = true; + response.highestLevel = info->maxGmLevel; + response.previousLevel = entity->GetGMLevel(); + response.newLevel = info->maxGmLevel; + response.Send(entity->GetSystemAddress()); GameMessages::SendChatModeUpdate(entity->GetObjectID(), info->maxGmLevel); entity->SetGMLevel(info->maxGmLevel); if (info->maxGmLevel == eGameMasterLevel::CIVILIAN) { @@ -138,7 +143,11 @@ namespace { character->SaveXMLToDatabase(); LOG("Dashboard rescue: transferring %llu to zone %u", objectId, zoneID); - WorldPackets::SendTransferToWorld(entity->GetSystemAddress(), serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(entity->GetSystemAddress()); }); return 1; } diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index f48ab0126..10e47107d 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -20,7 +20,7 @@ #include "UserManager.h" #include "User.h" #include "VanityUtilities.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "ZoneInstanceManager.h" // Database @@ -80,7 +80,12 @@ namespace DEVGMCommands { bool success = user->GetMaxGMLevel() >= level; if (success) { - WorldPackets::SendGMLevelChange(entity->GetSystemAddress(), success, user->GetMaxGMLevel(), entity->GetGMLevel(), level); + ClientPackets::MakeGMResponse response; + response.success = success; + response.highestLevel = user->GetMaxGMLevel(); + response.previousLevel = entity->GetGMLevel(); + response.newLevel = level; + response.Send(entity->GetSystemAddress()); GameMessages::SendChatModeUpdate(entity->GetObjectID(), level); entity->SetGMLevel(level); LOG("User %s (%i) has changed their GM level to %i for charID %llu", user->GetUsername().c_str(), user->GetAccountID(), level, entity->GetObjectID()); @@ -88,7 +93,12 @@ namespace DEVGMCommands { #ifndef DEVELOPER_SERVER if ((entity->GetGMLevel() > user->GetMaxGMLevel()) || (entity->GetGMLevel() > eGameMasterLevel::CIVILIAN && user->GetMaxGMLevel() == eGameMasterLevel::JUNIOR_DEVELOPER)) { - WorldPackets::SendGMLevelChange(entity->GetSystemAddress(), true, user->GetMaxGMLevel(), entity->GetGMLevel(), eGameMasterLevel::CIVILIAN); + ClientPackets::MakeGMResponse response; + response.success = true; + response.highestLevel = user->GetMaxGMLevel(); + response.previousLevel = entity->GetGMLevel(); + response.newLevel = eGameMasterLevel::CIVILIAN; + response.Send(entity->GetSystemAddress()); GameMessages::SendChatModeUpdate(entity->GetObjectID(), eGameMasterLevel::CIVILIAN); entity->SetGMLevel(eGameMasterLevel::CIVILIAN); @@ -1138,7 +1148,11 @@ namespace DEVGMCommands { entity->GetCharacter()->SaveXMLToDatabase(); - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); return; }); } else { diff --git a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp index 4ef1eb4e1..bb5291d3f 100644 --- a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp @@ -10,7 +10,7 @@ #include "PlayerManager.h" #include "SlashCommandHandler.h" #include "VanityUtilities.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "ZoneInstanceManager.h" #include "Database.h" @@ -179,7 +179,11 @@ namespace GMZeroCommands { entity->GetCharacter()->SaveXMLToDatabase(); - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); }); } @@ -210,7 +214,11 @@ namespace GMZeroCommands { entity->GetCharacter()->SaveXMLToDatabase(); - WorldPackets::SendTransferToWorld(sysAddr, serverIP, serverPort, mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(sysAddr); }); } diff --git a/dGame/dUtilities/WorldMigration.cpp b/dGame/dUtilities/WorldMigration.cpp index 5ee7f27ae..88c84db66 100644 --- a/dGame/dUtilities/WorldMigration.cpp +++ b/dGame/dUtilities/WorldMigration.cpp @@ -21,7 +21,7 @@ #include "PlayerManager.h" #include "ServiceType.h" #include "TradingManager.h" -#include "WorldPackets.h" +#include "ClientPackets.h" #include "dServer.h" #include "eServerDisconnectIdentifiers.h" @@ -158,7 +158,11 @@ namespace { // player is adopted by it. Same ordered channel as the transfer, so these arrive before it. if (order.seamless) Game::entityManager->DestructAllEntities(sysAddr); - WorldPackets::SendTransferToWorld(sysAddr, order.targetIp, order.targetPort, order.mythranShift); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(order.targetIp); + transfer.serverPort = order.targetPort; + transfer.mythranShift = order.mythranShift; + transfer.Send(sysAddr); LOG("Migration %u: sent %s (%llu) to instance %u", order.migrationId, character->GetName().c_str(), playerId, order.targetInstance); return true; } diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index b7875e762..08741477c 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -150,3 +150,15 @@ void ChatPackets::SendRoutedMsg(const LUBitStream& msg, const LWOOBJID targetID, msg.WritePacket(bitStream); Game::server->Send(bitStream, sysAddr, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); } + +void ChatPackets::RoutedFromClient::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(senderID); + for (const auto byte : data) bitStream.Write(byte); +} + +bool ChatPackets::RoutedFromClient::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(senderID)); + data.resize(BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits())); + if (!data.empty()) VALIDATE_READ(bitStream.ReadBits(data.data(), BYTES_TO_BITS(data.size()), true)); + return true; +} diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index 8139fb031..b661bd716 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -9,6 +9,7 @@ struct SystemAddress; #include +#include #include "dCommonVars.h" #include "MessageType/Chat.h" #include "BitStreamUtils.h" @@ -57,6 +58,17 @@ namespace ChatPackets { // No Deserialize needed on our end }; + // World -> chat. A chat packet the client sent through the world server (WorldPackets::RoutePacket): its id, + // the object ID of the player who sent it (the chat server has no other way to know), then the routed bytes. + struct RoutedFromClient : public LUBitStream { + LWOOBJID senderID{}; + std::vector data{}; + + RoutedFromClient(uint8_t routedMessageID = 0) : LUBitStream(ServiceType::CHAT, routedMessageID) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + void SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message); void SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, bool broadcast = false); void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, bool broadcast = false); diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index 5fff8f643..d83746a11 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -5,7 +5,15 @@ #include "ClientPackets.h" #include "dCommonVars.h" -#include "PositionUpdate.h" +#include "eCharacterCreationResponse.h" +#include "eGameMasterLevel.h" +#include "eRenameResponse.h" +#include "LDFFormat.h" +#include "ZCompression.h" + +#include +#include +#include #include "eLoginResponse.h" namespace ClientPackets { @@ -61,122 +69,374 @@ namespace ClientPackets { VALIDATE_READ(stamps.Deserialize(bitStream)); return true; } -} -ChatMessage ClientPackets::HandleChatMessage(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - ChatMessage message; - int32_t messageLength{}; - - inStream.Read(message.chatChannel); - inStream.Read(message.unknown); - inStream.Read(messageLength); - - if (messageLength > MAX_MESSAGE_LENGTH || messageLength < 0) return message; - - for (int32_t i = 0; i < (messageLength - 1); ++i) { - char16_t character; - inStream.Read(character); - message.message.push_back(character); + void LoadStaticZone::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(mapID); + bitStream.Write(instanceID); + bitStream.Write(cloneID); + bitStream.Write(mapChecksum); + bitStream.Write(editorEnabled); + bitStream.Write(editorLevel); + bitStream.Write(playerPosition.x); + bitStream.Write(playerPosition.y); + bitStream.Write(playerPosition.z); + bitStream.Write(instanceType); } - return message; -} - -PositionUpdate ClientPackets::HandleClientPositionUpdate(Packet* packet) { - PositionUpdate update; - CINSTREAM_SKIP_HEADER; - - inStream.Read(update.position.x); - inStream.Read(update.position.y); - inStream.Read(update.position.z); - - inStream.Read(update.rotation.x); - inStream.Read(update.rotation.y); - inStream.Read(update.rotation.z); - inStream.Read(update.rotation.w); - - inStream.Read(update.onGround); - inStream.Read(update.onRail); - - bool velocityFlag = false; - inStream.Read(velocityFlag); - if (velocityFlag) { - inStream.Read(update.velocity.x); - inStream.Read(update.velocity.y); - inStream.Read(update.velocity.z); + bool LoadStaticZone::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(mapID)); + VALIDATE_READ(bitStream.Read(instanceID)); + VALIDATE_READ(bitStream.Read(cloneID)); + VALIDATE_READ(bitStream.Read(mapChecksum)); + VALIDATE_READ(bitStream.Read(editorEnabled)); + VALIDATE_READ(bitStream.Read(editorLevel)); + VALIDATE_READ(bitStream.Read(playerPosition.x)); + VALIDATE_READ(bitStream.Read(playerPosition.y)); + VALIDATE_READ(bitStream.Read(playerPosition.z)); + VALIDATE_READ(bitStream.Read(instanceType)); + return true; } - bool angVelocityFlag = false; - inStream.Read(angVelocityFlag); - if (angVelocityFlag) { - inStream.Read(update.angularVelocity.x); - inStream.Read(update.angularVelocity.y); - inStream.Read(update.angularVelocity.z); - } + void CharacterListResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(characters.size()); + bitStream.Write(selectedCharacterIndex); - // TODO figure out how to use these. Ignoring for now, but reading in if they exist. - bool hasLocalSpaceInfo{}; - if (inStream.Read(hasLocalSpaceInfo) && hasLocalSpaceInfo) { - inStream.Read(update.localSpaceInfo.objectId); - inStream.Read(update.localSpaceInfo.position.x); - inStream.Read(update.localSpaceInfo.position.y); - inStream.Read(update.localSpaceInfo.position.z); - bool hasLinearVelocity = false; - if (inStream.Read(hasLinearVelocity) && hasLinearVelocity) { - inStream.Read(update.localSpaceInfo.linearVelocity.x); - inStream.Read(update.localSpaceInfo.linearVelocity.y); - inStream.Read(update.localSpaceInfo.linearVelocity.z); + for (const auto& character : characters) { + bitStream.Write(character.objectID); + bitStream.Write(character.unknown1); + + bitStream.Write(character.name); + bitStream.Write(character.unapprovedName); + + bitStream.Write(character.nameRejected); + bitStream.Write(character.isFreeToPlay); + + bitStream.Write(character.unknown2); + + bitStream.Write(character.shirtColor); + bitStream.Write(character.shirtStyle); + bitStream.Write(character.pantsColor); + bitStream.Write(character.hairStyle); + bitStream.Write(character.hairColor); + bitStream.Write(character.leftHand); + bitStream.Write(character.rightHand); + bitStream.Write(character.eyebrows); + bitStream.Write(character.eyes); + bitStream.Write(character.mouth); + bitStream.Write(character.unknown3); + + bitStream.Write(character.zoneID); + bitStream.Write(character.zoneInstance); + bitStream.Write(character.zoneClone); + + bitStream.Write(character.lastLogin); + + bitStream.Write(character.equippedItems.size()); + for (const auto item : character.equippedItems) bitStream.Write(item); } } - bool hasRemoteInputInfo{}; - if (inStream.Read(hasRemoteInputInfo) && hasRemoteInputInfo) { - inStream.Read(update.remoteInputInfo.m_RemoteInputX); - inStream.Read(update.remoteInputInfo.m_RemoteInputY); - inStream.Read(update.remoteInputInfo.m_IsPowersliding); - inStream.Read(update.remoteInputInfo.m_IsModified); + bool CharacterListResponse::Deserialize(RakNet::BitStream& bitStream) { + uint8_t count{}; + VALIDATE_READ(bitStream.Read(count)); + VALIDATE_READ(bitStream.Read(selectedCharacterIndex)); + + characters.resize(count); + for (auto& character : characters) { + VALIDATE_READ(bitStream.Read(character.objectID)); + VALIDATE_READ(bitStream.Read(character.unknown1)); + + VALIDATE_READ(bitStream.Read(character.name)); + VALIDATE_READ(bitStream.Read(character.unapprovedName)); + + uint8_t flag{}; + VALIDATE_READ(bitStream.Read(flag)); + character.nameRejected = flag != 0; + VALIDATE_READ(bitStream.Read(flag)); + character.isFreeToPlay = flag != 0; + + VALIDATE_READ(bitStream.Read(character.unknown2)); + + VALIDATE_READ(bitStream.Read(character.shirtColor)); + VALIDATE_READ(bitStream.Read(character.shirtStyle)); + VALIDATE_READ(bitStream.Read(character.pantsColor)); + VALIDATE_READ(bitStream.Read(character.hairStyle)); + VALIDATE_READ(bitStream.Read(character.hairColor)); + VALIDATE_READ(bitStream.Read(character.leftHand)); + VALIDATE_READ(bitStream.Read(character.rightHand)); + VALIDATE_READ(bitStream.Read(character.eyebrows)); + VALIDATE_READ(bitStream.Read(character.eyes)); + VALIDATE_READ(bitStream.Read(character.mouth)); + VALIDATE_READ(bitStream.Read(character.unknown3)); + + VALIDATE_READ(bitStream.Read(character.zoneID)); + VALIDATE_READ(bitStream.Read(character.zoneInstance)); + VALIDATE_READ(bitStream.Read(character.zoneClone)); + + VALIDATE_READ(bitStream.Read(character.lastLogin)); + + uint16_t itemCount{}; + VALIDATE_READ(bitStream.Read(itemCount)); + if (itemCount > BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()) / sizeof(LOT)) return false; + character.equippedItems.resize(itemCount); + for (auto& item : character.equippedItems) VALIDATE_READ(bitStream.Read(item)); + } + return true; } - return update; -} - -ChatModerationRequest ClientPackets::HandleChatModerationRequest(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - ChatModerationRequest request; - - inStream.Read(request.chatLevel); - inStream.Read(request.requestID); - - for (uint32_t i = 0; i < 42; ++i) { - uint16_t character; - inStream.Read(character); - request.receiver.push_back(static_cast(character)); + void CharacterCreateResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(response); } - if (!request.receiver.empty()) { - if (std::string(request.receiver.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are - request.receiver = std::string(request.receiver.c_str() + 4, request.receiver.size() - 4); + bool CharacterCreateResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(response)); + return true; + } + + void CharacterRenameResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(response); + } + + bool CharacterRenameResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(response)); + return true; + } + + void DeleteCharacterResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(success); + } + + bool DeleteCharacterResponse::Deserialize(RakNet::BitStream& bitStream) { + uint8_t flag{}; + VALIDATE_READ(bitStream.Read(flag)); + success = flag != 0; + return true; + } + + void TransferToWorld::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(serverIP); + bitStream.Write(serverPort); + bitStream.Write(mythranShift); + } + + bool TransferToWorld::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(serverIP)); + VALIDATE_READ(bitStream.Read(serverPort)); + uint8_t flag{}; + VALIDATE_READ(bitStream.Read(flag)); + mythranShift = flag != 0; + return true; + } + + void ServerStates::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(serverState); + } + + bool ServerStates::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(serverState)); + return true; + } + + void CreateCharacter::Serialize(RakNet::BitStream& bitStream) const { + using namespace std; + RakNet::BitStream data; + + LwoNameValue ldfData; + ldfData.Insert(u"objid", objectID); + ldfData.Insert(u"template", templateID); + ldfData.Insert(u"xmlData", xmlData); + ldfData.Insert(u"name", name); + ldfData.Insert(u"gmlevel", static_cast(gmLevel)); + ldfData.Insert(u"chatmode", chatMode); + ldfData.Insert(u"reputation", reputation); + ldfData.Insert(u"propertycloneid", propertyCloneID); + + data.Write(ldfData.values.size()); + for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data); + + //Compress the data before sending: + const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed()); + auto compressedData = std::make_unique(reservedSize); + + size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize); + + assert(size <= reservedSize); + + bitStream.Write(size + 9); //size of data + header bytes (8) + bitStream.Write(1); //compressed boolean, true + bitStream.Write(data.GetNumberOfBytesUsed()); + bitStream.Write(size); + + /** + * In practice, this warning serves no purpose for us. We allocate the max memory needed on the heap + * and then compress the data. In the off chance that the compression actually increases the size, + * an assertion is done to prevent bad data from being saved or sent. + */ +#pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'. + bitStream.WriteAlignedBytes(compressedData.get(), size); +#pragma warning(default:6385) + } + + namespace { + bool ReadLdfKey(RakNet::BitStream& bitStream, std::u16string& key) { + uint8_t keyBytes{}; + VALIDATE_READ(bitStream.Read(keyBytes)); + key.resize(keyBytes / sizeof(char16_t)); + for (auto& character : key) VALIDATE_READ(bitStream.Read(character)); + return true; } } - uint16_t messageLength; - inStream.Read(messageLength); - if (messageLength > MAX_MESSAGE_LENGTH) return request; - for (uint32_t i = 0; i < messageLength; ++i) { - uint16_t character; - inStream.Read(character); - request.message.push_back(static_cast(character)); + bool CreateCharacter::Deserialize(RakNet::BitStream& bitStream) { + uint32_t sizePlusHeader{}; + uint8_t compressed{}; + uint32_t uncompressedSize{}; + uint32_t compressedSize{}; + VALIDATE_READ(bitStream.Read(sizePlusHeader)); + VALIDATE_READ(bitStream.Read(compressed)); + VALIDATE_READ(bitStream.Read(uncompressedSize)); + VALIDATE_READ(bitStream.Read(compressedSize)); + if (compressed != 1 || sizePlusHeader != compressedSize + 9) return false; + if (compressedSize > BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()) || uncompressedSize > MAX_MESSAGE_LENGTH) return false; + + std::vector compressedData(compressedSize); + if (compressedSize > 0) VALIDATE_READ(bitStream.ReadAlignedBytes(compressedData.data(), compressedSize)); + std::vector uncompressed(uncompressedSize); + int32_t error{}; + const auto decompressedSize = ZCompression::Decompress(compressedData.data(), compressedSize, uncompressed.data(), uncompressedSize, error); + if (decompressedSize != static_cast(uncompressedSize)) return false; + + RakNet::BitStream data(uncompressed.data(), uncompressed.size(), false); + uint32_t count{}; + VALIDATE_READ(data.Read(count)); + for (uint32_t i = 0; i < count; i++) { + std::u16string key; + VALIDATE_READ(ReadLdfKey(data, key)); + uint8_t type{}; + VALIDATE_READ(data.Read(type)); + switch (type) { + case LDF_TYPE_S32: { + int32_t value{}; + VALIDATE_READ(data.Read(value)); + if (key == u"template") templateID = value; + else if (key == u"gmlevel") gmLevel = static_cast(value); + else if (key == u"chatmode") chatMode = value; + else if (key == u"propertycloneid") propertyCloneID = value; + break; + } + case LDF_TYPE_OBJID: { + int64_t value{}; + VALIDATE_READ(data.Read(value)); + if (key == u"objid") objectID = value; + else if (key == u"reputation") reputation = value; + break; + } + case LDF_TYPE_UTF_16: { + std::u16string value; + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(data, value)); + if (key == u"name") name = value; + break; + } + case LDF_TYPE_UTF_8: { + std::string value; + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(data, value)); + if (key == u"xmlData") xmlData = value; + break; + } + default: + return false; // CreateCharacter never uses other types + } + } + return true; } - return request; -} + void ChatModerationString::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(requestAccepted); // Is sentence ok? + bitStream.Write(sourceID); // Source ID, unknown -int32_t ClientPackets::SendTop5HelpIssues(Packet* packet) { - CINSTREAM_SKIP_HEADER; - int32_t language = 0; - inStream.Read(language); - return language; + bitStream.Write(requestID); // request ID + bitStream.Write(chatMode); // chat mode + + bitStream.Write(receiver); // receiver name + + for (const auto& [start, length] : rejectedSegments) { + bitStream.Write(start); // start index + bitStream.Write(length); // length + } + + for (auto i = rejectedSegments.size(); i < 64; i++) { + bitStream.Write(0); + } + } + + bool ChatModerationString::Deserialize(RakNet::BitStream& bitStream) { + uint8_t accepted{}; + VALIDATE_READ(bitStream.Read(accepted)); + requestAccepted = accepted != 0; + VALIDATE_READ(bitStream.Read(sourceID)); + VALIDATE_READ(bitStream.Read(requestID)); + VALIDATE_READ(bitStream.Read(chatMode)); + VALIDATE_READ(bitStream.Read(receiver)); + + rejectedSegments.clear(); + for (int i = 0; i < 64; i++) { + uint8_t start{}; + uint8_t length{}; + VALIDATE_READ(bitStream.Read(start)); + VALIDATE_READ(bitStream.Read(length)); + if (start != 0 || length != 0) rejectedSegments.emplace(start, length); + } + return true; + } + + void MakeGMResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(success); + bitStream.Write(static_cast(highestLevel)); + bitStream.Write(static_cast(previousLevel)); + bitStream.Write(static_cast(newLevel)); + } + + bool MakeGMResponse::Deserialize(RakNet::BitStream& bitStream) { + uint8_t flag{}; + uint16_t highest{}; + uint16_t previous{}; + uint16_t next{}; + VALIDATE_READ(bitStream.Read(flag)); + VALIDATE_READ(bitStream.Read(highest)); + VALIDATE_READ(bitStream.Read(previous)); + VALIDATE_READ(bitStream.Read(next)); + success = flag != 0; + highestLevel = static_cast(highest); + previousLevel = static_cast(previous); + newLevel = static_cast(next); + return true; + } + + void HTTPMonitorInfoResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(port); + bitStream.Write(openWeb); + bitStream.Write(supportsSum); + bitStream.Write(supportsDetail); + bitStream.Write(supportsWho); + bitStream.Write(supportsObjects); + } + + bool HTTPMonitorInfoResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(port)); + for (auto* flag : { &openWeb, &supportsSum, &supportsDetail, &supportsWho, &supportsObjects }) { + uint8_t value{}; + VALIDATE_READ(bitStream.Read(value)); + *flag = value != 0; + } + return true; + } + + void DebugOutput::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, data); + } + + bool DebugOutput::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, data)); + return true; + } } diff --git a/dNet/ClientPackets.h b/dNet/ClientPackets.h index 00d1dafdb..7ee271968 100644 --- a/dNet/ClientPackets.h +++ b/dNet/ClientPackets.h @@ -8,19 +8,23 @@ #include #include +#include #include +#include #include #include "BitStreamUtils.h" #include "MessageType/Client.h" #include "Stamps.h" +#include "dCommonVars.h" +#include "NiPoint3.h" + +enum class eCharacterCreationResponse : uint8_t; +enum class eGameMasterLevel : uint8_t; +enum class eRenameResponse : uint8_t; enum class eLoginResponse : uint8_t; -class PositionUpdate; - -struct Packet; - enum class Language : uint32_t { en_US, pl_US, @@ -28,19 +32,6 @@ enum class Language : uint32_t { en_GB, }; -struct ChatMessage { - uint8_t chatChannel = 0; - uint16_t unknown = 0; - std::u16string message; -}; - -struct ChatModerationRequest { - uint8_t chatLevel = 0; - uint8_t requestID = 0; - std::string receiver; - std::string message; -}; - namespace ClientPackets { // Server -> client. The auth server's answer to a login (the client's clientLoginResponsePacket). struct LoginResponse : public LUBitStream { @@ -73,10 +64,173 @@ namespace ClientPackets { bool Deserialize(RakNet::BitStream& bitStream) override; }; - ChatMessage HandleChatMessage(Packet* packet); - PositionUpdate HandleClientPositionUpdate(Packet* packet); - ChatModerationRequest HandleChatModerationRequest(Packet* packet); - int32_t SendTop5HelpIssues(Packet* packet); + // Server -> client. Tells the client which zone to load (ClientLoadStaticZonePacket in the client). + struct LoadStaticZone : public LUBitStream { + uint16_t mapID{}; + uint16_t instanceID{}; + uint32_t cloneID{}; // DLU has always sent 0 here + uint32_t mapChecksum{}; + uint8_t editorEnabled{}; + uint8_t editorLevel{}; + NiPoint3 playerPosition{}; + uint32_t instanceType{}; // Change this to eventually use 4 on activity worlds + + LoadStaticZone() : LUBitStream(ServiceType::CLIENT, MessageType::Client::LOAD_STATIC_ZONE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. The account's characters for the character select screen. + struct CharacterListResponse : public LUBitStream { + struct Character { + LWOOBJID objectID{}; + uint32_t unknown1{}; + LUWString name{ 33 }; + LUWString unapprovedName{ 33 }; + bool nameRejected{}; // Written as one byte + bool isFreeToPlay{}; // Written as one byte + LUString unknown2{ 10 }; + uint32_t shirtColor{}; + uint32_t shirtStyle{}; + uint32_t pantsColor{}; + uint32_t hairStyle{}; + uint32_t hairColor{}; + uint32_t leftHand{}; + uint32_t rightHand{}; + uint32_t eyebrows{}; + uint32_t eyes{}; + uint32_t mouth{}; + uint32_t unknown3{}; + uint16_t zoneID{}; + uint16_t zoneInstance{}; + uint32_t zoneClone{}; + uint64_t lastLogin{}; + std::vector equippedItems{}; // After a u16 count + }; + + // Written as a u8 count first + uint8_t selectedCharacterIndex{}; // TODO: Pick the most recent played index. DLU always sends 0 + std::vector characters{}; + + CharacterListResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. + struct CharacterCreateResponse : public LUBitStream { + eCharacterCreationResponse response{}; + + CharacterCreateResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. + struct CharacterRenameResponse : public LUBitStream { + eRenameResponse response{}; + + CharacterRenameResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHARACTER_RENAME_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. + struct DeleteCharacterResponse : public LUBitStream { + bool success{}; // Written as one byte + + DeleteCharacterResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::DELETE_CHARACTER_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. Sends the client to another world server. + struct TransferToWorld : public LUBitStream { + LUString serverIP{ 33 }; + uint16_t serverPort{}; + bool mythranShift{}; // Written as one byte + + TransferToWorld() : LUBitStream(ServiceType::CLIENT, MessageType::Client::TRANSFER_TO_WORLD) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. + struct ServerStates : public LUBitStream { + uint8_t serverState = 1; // If the server is receiving this request, it probably is ready anyway. + + ServerStates() : LUBitStream(ServiceType::CLIENT, MessageType::Client::SERVER_STATES) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. The player's own character: a compressed LDF list. + // On the wire: u32 compressed size + 9, u8 1 (compressed), u32 uncompressed size, u32 compressed size, the zlib data. + struct CreateCharacter : public LUBitStream { + LWOOBJID objectID{}; + LOT templateID = 1; + std::string xmlData{}; + std::u16string name{}; + eGameMasterLevel gmLevel{}; + int32_t chatMode{}; // DLU sends the GM level + int64_t reputation{}; + int32_t propertyCloneID{}; + + CreateCharacter() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. The answer to WorldPackets::StringCheck. + struct ChatModerationString : public LUBitStream { + bool requestAccepted{}; // Written as one byte + uint16_t sourceID = 0x16; // Unknown + uint8_t requestID{}; + uint8_t chatMode{}; + LUWString receiver{ 42 }; + // (start, length) of each part that was not accepted, in 64 two byte slots; unused slots are 0 + std::set> rejectedSegments{}; + + ChatModerationString() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. + struct MakeGMResponse : public LUBitStream { + bool success{}; // Written as one byte + eGameMasterLevel highestLevel{}; // Written as u16 + eGameMasterLevel previousLevel{}; // Written as u16 + eGameMasterLevel newLevel{}; // Written as u16 + + MakeGMResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::MAKE_GM_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. DLU does not send it. + struct HTTPMonitorInfoResponse : public LUBitStream { + uint16_t port = 80; + // Each written as one byte + bool openWeb = false; + bool supportsSum = false; + bool supportsDetail = false; + bool supportsWho = false; + bool supportsObjects = false; + + HTTPMonitorInfoResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::HTTP_MONITOR_INFO_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. Text for the client's debug output. DLU does not send it. + struct DebugOutput : public LUBitStream { + std::string data{}; // After a u32 length + + DebugOutput() : LUBitStream(ServiceType::CLIENT, MessageType::Client::DEBUG_OUTPUT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; }; #endif // CLIENTPACKETS_H diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index e963b10cb..aafc826fc 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -1,180 +1,351 @@ #include "WorldPackets.h" + #include "dCommonVars.h" -#include "BitStream.h" -#include "GeneralUtils.h" -#include "Logger.h" -#include "Game.h" -#include "LDFFormat.h" -#include "dServer.h" -#include "ZCompression.h" -#include "ServiceType.h" -#include "BitStreamUtils.h" -#include -#include +#include -void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const { - bitStream.Write(port); - bitStream.Write(openWeb); - bitStream.Write(supportsSum); - bitStream.Write(supportsDetail); - bitStream.Write(supportsWho); - bitStream.Write(supportsObjects); -} - -void WorldPackets::SendLoadStaticZone(const SystemAddress& sysAddr, float x, float y, float z, uint32_t checksum, LWOZONEID zone) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::LOAD_STATIC_ZONE); - - bitStream.Write(zone.GetMapID()); - bitStream.Write(zone.GetInstanceID()); - //bitStream.Write(zone.GetCloneID()); - bitStream.Write(0); - - bitStream.Write(checksum); - bitStream.Write(0); // ?? - - bitStream.Write(x); - bitStream.Write(y); - bitStream.Write(z); - - bitStream.Write(0); // Change this to eventually use 4 on activity worlds - - SEND_PACKET; -} - -void WorldPackets::SendCharacterCreationResponse(const SystemAddress& sysAddr, eCharacterCreationResponse response) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE); - bitStream.Write(response); - SEND_PACKET; -} - -void WorldPackets::SendCharacterRenameResponse(const SystemAddress& sysAddr, eRenameResponse response) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_RENAME_RESPONSE); - bitStream.Write(response); - SEND_PACKET; -} - -void WorldPackets::SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DELETE_CHARACTER_RESPONSE); - bitStream.Write(response); - SEND_PACKET; -} - -void WorldPackets::SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TRANSFER_TO_WORLD); - - bitStream.Write(LUString(serverIP)); - bitStream.Write(serverPort); - bitStream.Write(mythranShift); - - SEND_PACKET; -} - -void WorldPackets::SendServerState(const SystemAddress& sysAddr) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SERVER_STATES); - bitStream.Write(1); //If the server is receiving this request, it probably is ready anyway. - SEND_PACKET; -} - -void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID) { - using namespace std; - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER); - - RakNet::BitStream data; - - LwoNameValue ldfData; - ldfData.Insert(u"objid", player); - ldfData.Insert(u"template", 1); - ldfData.Insert(u"xmlData", xmlData); - ldfData.Insert(u"name", username); - ldfData.Insert(u"gmlevel", static_cast(gm)); - ldfData.Insert(u"chatmode", static_cast(gm)); - ldfData.Insert(u"reputation", reputation); - ldfData.Insert(u"propertycloneid", cloneID); - - data.Write(ldfData.values.size()); - for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data); - - //Compress the data before sending: - const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed()); - auto compressedData = std::make_unique(reservedSize); - - size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize); - - assert(size <= reservedSize); - - bitStream.Write(size + 9); //size of data + header bytes (8) - bitStream.Write(1); //compressed boolean, true - bitStream.Write(data.GetNumberOfBytesUsed()); - bitStream.Write(size); - - /** - * In practice, this warning serves no purpose for us. We allocate the max memory needed on the heap - * and then compress the data. In the off chance that the compression actually increases the size, - * an assertion is done to prevent bad data from being saved or sent. - */ -#pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'. - bitStream.WriteAlignedBytes(compressedData.get(), size); -#pragma warning(default:6385) - - SEND_PACKET; - LOG("Sent CreateCharacter for ID: %llu", player); -} - -void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set> unacceptedItems) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING); - - bitStream.Write(unacceptedItems.empty()); // Is sentence ok? - bitStream.Write(0x16); // Source ID, unknown - - bitStream.Write(requestID); // request ID - bitStream.Write(0); // chat mode - - bitStream.Write(LUWString(receiver, 42)); // receiver name - - for (auto it : unacceptedItems) { - bitStream.Write(it.first); // start index - bitStream.Write(it.second); // length +namespace { + // Copies every unread bit of from into to. + bool ReadRest(RakNet::BitStream& from, RakNet::BitStream& to) { + to.Reset(); + return from.Read(to, from.GetNumberOfUnreadBits()); } - for (int i = unacceptedItems.size(); 64 > i; i++) { - bitStream.Write(0); + void WriteAll(RakNet::BitStream& to, const RakNet::BitStream& from) { + auto& nonConst = const_cast(from); // GetData is not const in RakNet + to.WriteBits(nonConst.GetData(), nonConst.GetNumberOfBitsUsed(), false); + } +} + +namespace WorldPackets { + void Validation::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(username); + bitStream.Write(sessionKey); + bitStream.Write(fdbChecksum); } - SEND_PACKET; -} - -void WorldPackets::SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::MAKE_GM_RESPONSE); - - bitStream.Write(success); - bitStream.Write(static_cast(highestLevel)); - bitStream.Write(static_cast(prevLevel)); - bitStream.Write(static_cast(newLevel)); - - SEND_PACKET; -} - -void WorldPackets::SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::HTTP_MONITOR_INFO_RESPONSE); - info.Serialize(bitStream); - SEND_PACKET; -} - -void WorldPackets::SendDebugOuput(const SystemAddress& sysAddr, const std::string& data) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DEBUG_OUTPUT); - bitStream.Write(data.size()); - bitStream.Write(data); - SEND_PACKET; + bool Validation::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(username)); + // sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird + VALIDATE_READ(bitStream.Read(sessionKey)); + VALIDATE_READ(bitStream.Read(fdbChecksum)); + return true; + } + + void CharacterCreateRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(name); + bitStream.Write(firstNameIndex); + bitStream.Write(middleNameIndex); + bitStream.Write(lastNameIndex); + for (const auto byte : unknown) bitStream.Write(byte); + bitStream.Write(shirtColor); + bitStream.Write(shirtStyle); + bitStream.Write(pantsColor); + bitStream.Write(hairStyle); + bitStream.Write(hairColor); + bitStream.Write(leftHand); + bitStream.Write(rightHand); + bitStream.Write(eyebrows); + bitStream.Write(eyes); + bitStream.Write(mouth); + } + + bool CharacterCreateRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(name)); + VALIDATE_READ(bitStream.Read(firstNameIndex)); + VALIDATE_READ(bitStream.Read(middleNameIndex)); + VALIDATE_READ(bitStream.Read(lastNameIndex)); + for (auto& byte : unknown) VALIDATE_READ(bitStream.Read(byte)); + VALIDATE_READ(bitStream.Read(shirtColor)); + VALIDATE_READ(bitStream.Read(shirtStyle)); + VALIDATE_READ(bitStream.Read(pantsColor)); + VALIDATE_READ(bitStream.Read(hairStyle)); + VALIDATE_READ(bitStream.Read(hairColor)); + VALIDATE_READ(bitStream.Read(leftHand)); + VALIDATE_READ(bitStream.Read(rightHand)); + VALIDATE_READ(bitStream.Read(eyebrows)); + VALIDATE_READ(bitStream.Read(eyes)); + VALIDATE_READ(bitStream.Read(mouth)); + return true; + } + + void CharacterLoginRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + } + + bool CharacterLoginRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } + + void GameMessage::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(objectID); + bitStream.Write(messageID); + WriteAll(bitStream, data); + } + + bool GameMessage::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(objectID)); + VALIDATE_READ(bitStream.Read(messageID)); + VALIDATE_READ(ReadRest(bitStream, data)); + return true; + } + + void CharacterDeleteRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(objectID); + } + + bool CharacterDeleteRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(objectID)); + return true; + } + + void CharacterRenameRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(objectID); + bitStream.Write(name); + } + + bool CharacterRenameRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(objectID)); + VALIDATE_READ(bitStream.Read(name)); + return true; + } + + void LevelLoadComplete::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(mapID); + bitStream.Write(instanceID); + bitStream.Write(cloneID); + } + + bool LevelLoadComplete::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(mapID)); + VALIDATE_READ(bitStream.Read(instanceID)); + VALIDATE_READ(bitStream.Read(cloneID)); + return true; + } + + void PositionUpdate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(update.position.x); + bitStream.Write(update.position.y); + bitStream.Write(update.position.z); + + bitStream.Write(update.rotation.x); + bitStream.Write(update.rotation.y); + bitStream.Write(update.rotation.z); + bitStream.Write(update.rotation.w); + + bitStream.Write(update.onGround); + bitStream.Write(update.onRail); + + bitStream.Write(hasVelocity); + if (hasVelocity) { + bitStream.Write(update.velocity.x); + bitStream.Write(update.velocity.y); + bitStream.Write(update.velocity.z); + } + + bitStream.Write(hasAngularVelocity); + if (hasAngularVelocity) { + bitStream.Write(update.angularVelocity.x); + bitStream.Write(update.angularVelocity.y); + bitStream.Write(update.angularVelocity.z); + } + + bitStream.Write(hasLocalSpaceInfo); + if (hasLocalSpaceInfo) { + bitStream.Write(update.localSpaceInfo.objectId); + bitStream.Write(update.localSpaceInfo.position.x); + bitStream.Write(update.localSpaceInfo.position.y); + bitStream.Write(update.localSpaceInfo.position.z); + bitStream.Write(hasLinearVelocity); + if (hasLinearVelocity) { + bitStream.Write(update.localSpaceInfo.linearVelocity.x); + bitStream.Write(update.localSpaceInfo.linearVelocity.y); + bitStream.Write(update.localSpaceInfo.linearVelocity.z); + } + } + + bitStream.Write(hasRemoteInputInfo); + if (hasRemoteInputInfo) { + bitStream.Write(update.remoteInputInfo.m_RemoteInputX); + bitStream.Write(update.remoteInputInfo.m_RemoteInputY); + bitStream.Write(update.remoteInputInfo.m_IsPowersliding); + bitStream.Write(update.remoteInputInfo.m_IsModified); + } + } + + bool PositionUpdate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(update.position.x)); + VALIDATE_READ(bitStream.Read(update.position.y)); + VALIDATE_READ(bitStream.Read(update.position.z)); + + VALIDATE_READ(bitStream.Read(update.rotation.x)); + VALIDATE_READ(bitStream.Read(update.rotation.y)); + VALIDATE_READ(bitStream.Read(update.rotation.z)); + VALIDATE_READ(bitStream.Read(update.rotation.w)); + + VALIDATE_READ(bitStream.Read(update.onGround)); + VALIDATE_READ(bitStream.Read(update.onRail)); + + VALIDATE_READ(bitStream.Read(hasVelocity)); + if (hasVelocity) { + VALIDATE_READ(bitStream.Read(update.velocity.x)); + VALIDATE_READ(bitStream.Read(update.velocity.y)); + VALIDATE_READ(bitStream.Read(update.velocity.z)); + } + + VALIDATE_READ(bitStream.Read(hasAngularVelocity)); + if (hasAngularVelocity) { + VALIDATE_READ(bitStream.Read(update.angularVelocity.x)); + VALIDATE_READ(bitStream.Read(update.angularVelocity.y)); + VALIDATE_READ(bitStream.Read(update.angularVelocity.z)); + } + + // The last two sections may be missing entirely (DLU has always accepted that) + hasLocalSpaceInfo = false; + hasLinearVelocity = false; + if (bitStream.Read(hasLocalSpaceInfo) && hasLocalSpaceInfo) { + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.objectId)); + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.position.x)); + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.position.y)); + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.position.z)); + if (bitStream.Read(hasLinearVelocity) && hasLinearVelocity) { + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.linearVelocity.x)); + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.linearVelocity.y)); + VALIDATE_READ(bitStream.Read(update.localSpaceInfo.linearVelocity.z)); + } + } + + hasRemoteInputInfo = false; + if (bitStream.Read(hasRemoteInputInfo) && hasRemoteInputInfo) { + VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_RemoteInputX)); + VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_RemoteInputY)); + VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_IsPowersliding)); + VALIDATE_READ(bitStream.Read(update.remoteInputInfo.m_IsModified)); + } + + return true; + } + + void MailPacket::Serialize(RakNet::BitStream& bitStream) const { + WriteAll(bitStream, data); + } + + bool MailPacket::Deserialize(RakNet::BitStream& bitStream) { + return ReadRest(bitStream, data); + } + + void RoutePacket::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(size); + bitStream.Write(routedService); + bitStream.Write(routedMessageID); + bitStream.Write(padding); + for (const auto byte : routedData) bitStream.Write(byte); + } + + bool RoutePacket::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(size)); + VALIDATE_READ(bitStream.Read(routedService)); + VALIDATE_READ(bitStream.Read(routedMessageID)); + VALIDATE_READ(bitStream.Read(padding)); + routedData.resize(BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits())); + if (!routedData.empty()) VALIDATE_READ(bitStream.ReadBits(routedData.data(), BYTES_TO_BITS(routedData.size()), true)); + return true; + } + + ChatPackets::RoutedFromClient RoutePacket::ToChat(const LWOOBJID senderID) const { + constexpr size_t ROUTED_DATA_SKIP = 4; + ChatPackets::RoutedFromClient routed(static_cast(routedMessageID & 0xFF)); + routed.senderID = senderID; + for (size_t i = ROUTED_DATA_SKIP; i - ROUTED_DATA_SKIP < size && i < routedData.size(); ++i) { + routed.data.push_back(routedData[i]); + } + return routed; + } + + void StringCheck::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(chatLevel); + bitStream.Write(requestID); + auto fixedReceiver = receiver; + fixedReceiver.resize(42); + bitStream.Write(fixedReceiver); + bitStream.Write(message.size()); + bitStream.Write(message); + } + + bool StringCheck::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(chatLevel)); + VALIDATE_READ(bitStream.Read(requestID)); + receiver.resize(42); + VALIDATE_READ(bitStream.ReadBits(reinterpret_cast(receiver.data()), BYTES_TO_BITS(receiver.size() * sizeof(char16_t)), true)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, message)); + return true; + } + + std::string StringCheck::GetNarrowReceiver() const { + std::string narrow; + for (const auto character : receiver) narrow.push_back(static_cast(character)); + + if (!narrow.empty()) { + if (std::string(narrow.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are + narrow = std::string(narrow.c_str() + 4, narrow.size() - 4); + } + } + return narrow; + } + + std::string StringCheck::GetNarrowMessage() const { + std::string narrow; + for (const auto character : message) narrow.push_back(static_cast(character)); + return narrow; + } + + void GeneralChatMessage::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(chatChannel); + bitStream.Write(unknown); + bitStream.Write(message.size() + 1); + bitStream.Write(message); + bitStream.Write(u'\0'); + } + + bool GeneralChatMessage::Deserialize(RakNet::BitStream& bitStream) { + int32_t messageLength{}; + + VALIDATE_READ(bitStream.Read(chatChannel)); + VALIDATE_READ(bitStream.Read(unknown)); + VALIDATE_READ(bitStream.Read(messageLength)); + + if (messageLength < 0 || static_cast(messageLength) > MAX_MESSAGE_LENGTH) return false; + + message.clear(); + for (int32_t i = 0; i < (messageLength - 1); ++i) { + char16_t character; + VALIDATE_READ(bitStream.Read(character)); + message.push_back(character); + } + + // The null terminator the count includes; DLU never needed it + if (messageLength > 0) bitStream.IgnoreBits(std::min(bitStream.GetNumberOfUnreadBits(), BYTES_TO_BITS(sizeof(char16_t)))); + return true; + } + + void HandleFunness::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(cheatInfo); + bitStream.Write(cheatType); + } + + bool HandleFunness::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(cheatInfo)); + VALIDATE_READ(bitStream.Read(cheatType)); + return true; + } + + void UIHelpTop5::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(language); + } + + bool UIHelpTop5::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(language)); + return true; + } } diff --git a/dNet/WorldPackets.h b/dNet/WorldPackets.h index 1a48f5dfb..d37664e78 100644 --- a/dNet/WorldPackets.h +++ b/dNet/WorldPackets.h @@ -1,41 +1,214 @@ #ifndef WORLDPACKETS_H #define WORLDPACKETS_H +#include "BitStreamUtils.h" +#include "ChatPackets.h" #include "dCommonVars.h" -#include +#include "eFunnessTypes.h" +#include "MessageType/Game.h" +#include "MessageType/World.h" +#include "PositionUpdate.h" +#include "RakNetTypes.h" + +#include +#include #include +#include -class User; -struct SystemAddress; -enum class eGameMasterLevel : uint8_t; -enum class eCharacterCreationResponse : uint8_t; -enum class eRenameResponse : uint8_t; -namespace RakNet { - class BitStream; -}; - -struct HTTPMonitorInfo { - uint16_t port = 80; - bool openWeb = false; - bool supportsSum = false; - bool supportsDetail = false; - bool supportsWho = false; - bool supportsObjects = false; - void Serialize(RakNet::BitStream &bitstream) const; -}; - +// Packets of ServiceType::WORLD: what a client sends to a world server. The server's answers are ClientPackets. +// The structs only read and write bytes; the world server attaches the handlers (see WorldServer.cpp). namespace WorldPackets { - void SendLoadStaticZone(const SystemAddress& sysAddr, float x, float y, float z, uint32_t checksum, LWOZONEID zone); - void SendCharacterCreationResponse(const SystemAddress& sysAddr, eCharacterCreationResponse response); - void SendCharacterRenameResponse(const SystemAddress& sysAddr, eRenameResponse response); - void SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response); - void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift); - void SendServerState(const SystemAddress& sysAddr); - void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID); - void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set> unacceptedItems); - void SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel); - void SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info); - void SendDebugOuput(const SystemAddress& sysAddr, const std::string& data); + // Base of every world packet: the dispatcher sets sysAddr before Deserialize and Handle. + struct WorldLUBitStream : public LUBitStream { + SystemAddress sysAddr = UNASSIGNED_SYSTEM_ADDRESS; + + WorldLUBitStream() = default; + template + WorldLUBitStream(T internalPacketID) : LUBitStream(ServiceType::WORLD, internalPacketID) {} + }; + + // Client -> server. Who the client is and the session key auth gave it. + struct Validation : public WorldLUBitStream { + LUWString username{ 33 }; + LUWString sessionKey{ 33 }; + LUString fdbChecksum{ 32 }; + // The client may send one more byte after the checksum (a null terminator); it is ignored. + + Validation() : WorldLUBitStream(MessageType::World::VALIDATION) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. No payload. + struct CharacterListRequest : public WorldLUBitStream { + CharacterListRequest() : WorldLUBitStream(MessageType::World::CHARACTER_LIST_REQUEST) {} + }; + + // Client -> server. A new character from the character creator. + struct CharacterCreateRequest : public WorldLUBitStream { + LUWString name{ 33 }; + uint32_t firstNameIndex{}; + uint32_t middleNameIndex{}; + uint32_t lastNameIndex{}; + std::array unknown{}; // Not used by DLU + uint32_t shirtColor{}; + uint32_t shirtStyle{}; + uint32_t pantsColor{}; + uint32_t hairStyle{}; + uint32_t hairColor{}; + uint32_t leftHand{}; + uint32_t rightHand{}; + uint32_t eyebrows{}; + uint32_t eyes{}; + uint32_t mouth{}; + // The client sends one more byte; it is ignored. + + CharacterCreateRequest() : WorldLUBitStream(MessageType::World::CHARACTER_CREATE_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. The character the player picked to play. + struct CharacterLoginRequest : public WorldLUBitStream { + LWOOBJID playerID{}; + + CharacterLoginRequest() : WorldLUBitStream(MessageType::World::LOGIN_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. A game message; data holds its parameters for GameMessageHandler. + struct GameMessage : public WorldLUBitStream { + LWOOBJID objectID{}; + MessageType::Game messageID{}; + RakNet::BitStream data; + + GameMessage() : WorldLUBitStream(MessageType::World::GAME_MSG) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. + struct CharacterDeleteRequest : public WorldLUBitStream { + LWOOBJID objectID{}; + + CharacterDeleteRequest() : WorldLUBitStream(MessageType::World::CHARACTER_DELETE_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. + struct CharacterRenameRequest : public WorldLUBitStream { + LWOOBJID objectID{}; + LUWString name{ 33 }; + + CharacterRenameRequest() : WorldLUBitStream(MessageType::World::CHARACTER_RENAME_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. The client finished loading the zone from LoadStaticZone. + struct LevelLoadComplete : public WorldLUBitStream { + // The zone the client loaded; not used by DLU + uint16_t mapID{}; + uint16_t instanceID{}; + uint32_t cloneID{}; + + LevelLoadComplete() : WorldLUBitStream(MessageType::World::LEVEL_LOAD_COMPLETE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. Where the player's character is. + struct PositionUpdate : public WorldLUBitStream { + ::PositionUpdate update{}; + // Which optional sections are present + bool hasVelocity{}; + bool hasAngularVelocity{}; + bool hasLocalSpaceInfo{}; + bool hasLinearVelocity{}; // Inside the local space info + bool hasRemoteInputInfo{}; + + PositionUpdate() : WorldLUBitStream(MessageType::World::POSITION_UPDATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. A mail request; data holds it for Mail::HandleMail (which has its own sub-header). + struct MailPacket : public WorldLUBitStream { + RakNet::BitStream data; + + MailPacket() : WorldLUBitStream(MessageType::World::MAIL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. A packet for the chat server, wrapped: its size, then the chat packet's own header + // (service, id, pad) and body. + struct RoutePacket : public WorldLUBitStream { + uint32_t size{}; + ServiceType routedService{}; + uint32_t routedMessageID{}; + uint8_t padding{}; + std::vector routedData{}; // Everything after the routed header + + RoutePacket() : WorldLUBitStream(MessageType::World::ROUTE_PACKET) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + // What DLU forwards to the chat server: the first byte of the routed id, the sender, then at most size bytes + // of the routed data from 4 bytes in (the chat server skips 4 bytes after the sender ID it reads). + ChatPackets::RoutedFromClient ToChat(LWOOBJID senderID) const; + }; + + // Client -> server. Asks whether what the player is typing may be sent. + struct StringCheck : public WorldLUBitStream { + uint8_t chatLevel{}; + uint8_t requestID{}; + std::u16string receiver = std::u16string(42, u'\0'); // Always 42 characters on the wire + std::u16string message{}; // After a u16 character count + + StringCheck() : WorldLUBitStream(MessageType::World::STRING_CHECK) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + // The receiver as DLU has always used it: each character cut to one byte, all 42 of them (the text is + // followed by nulls and whatever else the client left in its buffer), without a leading "[GM]". + std::string GetNarrowReceiver() const; + // The message with each character cut to one byte. + std::string GetNarrowMessage() const; + }; + + // Client -> server. A message in the zone's chat. + struct GeneralChatMessage : public WorldLUBitStream { + uint8_t chatChannel{}; + uint16_t unknown{}; + // On the wire: an i32 count that includes a null terminator, the characters, then the terminator + std::u16string message{}; + + GeneralChatMessage() : WorldLUBitStream(MessageType::World::GENERAL_CHAT_MESSAGE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. The client's anti cheat noticed something. + struct HandleFunness : public WorldLUBitStream { + float cheatInfo{}; + eFunnessTypes cheatType{}; + + HandleFunness() : WorldLUBitStream(MessageType::World::HANDLE_FUNNESS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. The help screen wants its top 5 issues. + struct UIHelpTop5 : public WorldLUBitStream { + int32_t language{}; // 0: en_US, 1: pl_US, 2: de_DE, 3: en_GB + + UIHelpTop5() : WorldLUBitStream(MessageType::World::UI_HELP_TOP_5) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; } #endif // WORLDPACKETS_H diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 8022f086f..c759407ba 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "MD5.h" @@ -773,7 +775,14 @@ void HandleMasterPacket(Packet* packet) { z = pos.z; } - WorldPackets::SendLoadStaticZone(it->second.sysAddr, x, y, z, zone->GetChecksum(), Game::zoneManager->GetZoneID()); + ClientPackets::LoadStaticZone loadZone; + const auto zoneID = Game::zoneManager->GetZoneID(); + loadZone.mapID = zoneID.GetMapID(); + loadZone.instanceID = zoneID.GetInstanceID(); + loadZone.cloneID = 0; // DLU has always sent 0 here, whatever the clone + loadZone.mapChecksum = zone->GetChecksum(); + loadZone.playerPosition = NiPoint3(x, y, z); + loadZone.Send(it->second.sysAddr); } if (Game::server->GetZoneID() == 0) { @@ -1029,8 +1038,18 @@ void LoadPlayer(const SystemAddress& sysAddr) { destroyableComponent->FixStats(); } - WorldPackets::SendCreateCharacter(sysAddr, characterComponent->GetReputation(), player->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel(), c->GetPropertyCloneID()); - WorldPackets::SendServerState(sysAddr); + ClientPackets::CreateCharacter createCharacter; + createCharacter.objectID = player->GetObjectID(); + createCharacter.xmlData = c->GetXMLData(); + createCharacter.name = username; + createCharacter.gmLevel = c->GetGMLevel(); + createCharacter.chatMode = static_cast(c->GetGMLevel()); + createCharacter.reputation = characterComponent->GetReputation(); + createCharacter.propertyCloneID = c->GetPropertyCloneID(); + createCharacter.Send(sysAddr); + LOG("Sent CreateCharacter for ID: %llu", player->GetObjectID()); + ClientPackets::ServerStates serverStates; + serverStates.Send(sysAddr); const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID()); @@ -1192,6 +1211,463 @@ void CleanupDisconnectedUser(const SystemAddress& sysAddr) { Game::server->SendToMaster(bitStream); } +// The world server's handlers for what clients send (ServiceType::WORLD). WorldPackets has the structs; each one is +// handled by overriding Handle here, where the server's state is. +namespace { + struct ValidationPacket final : public WorldPackets::Validation { + void Handle() override { + const auto& clientDatabaseChecksum = fdbChecksum; + + // If the check is turned on, validate the client's database checksum. + if (Game::config->GetValue("check_fdb") == "1" && !g_DatabaseChecksum.empty()) { + auto accountInfo = Database::Get()->GetAccountInfo(username.GetAsString()); + if (!accountInfo) { + LOG("Client's account does not exist in the database, aborting connection."); + Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::CHARACTER_NOT_FOUND); + return; + } + + // Developers may skip this check + if (clientDatabaseChecksum.string != g_DatabaseChecksum) { + + if (accountInfo->maxGmLevel < eGameMasterLevel::DEVELOPER) { + LOG("Client's database checksum does not match the server's, aborting connection."); + Stamps stamps; + stamps.Add(eStamps::PASSPORT_AUTH_ERROR, 1); + + // Using the LoginResponse here since the UI is still in the login screen state + // and we have a way to send a message about the client mismatch. + AuthPackets::SendLoginResponse( + Game::server, sysAddr, eLoginResponse::PERMISSIONS_NOT_HIGH_ENOUGH, + Game::config->GetValue("cdclient_mismatch_message"), "", 0, "", stamps); + return; + } else { + AMFArrayValue args; + + args.Insert("title", Game::config->GetValue("cdclient_mismatch_title")); + args.Insert("message", Game::config->GetValue("cdclient_mismatch_message")); + + GameMessages::SendUIMessageServerToSingleClient("ToggleAnnounce", args, sysAddr); + LOG("Account (%s) with GmLevel (%s) does not have a matching FDB, but is a developer and will skip this check." + , username.GetAsString().c_str(), StringifiedEnum::ToString(accountInfo->maxGmLevel).data()); + } + } + } + + //Request the session info from Master: + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_SESSION_KEY); + bitStream.Write(username); + Game::server->SendToMaster(bitStream); + + //Insert info into our pending list + TempSessionInfo info; + info.sysAddr = sysAddr; + info.hash = sessionKey.GetAsString(); + g_PendingUsers[username.GetAsString()] = info; + } + }; + + struct CharacterListRequestPacket final : public WorldPackets::CharacterListRequest { + void Handle() override { + //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(sysAddr); + if (!user || !user->GetLastUsedChar()) return; + Game::entityManager->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 (g_PendingUsers.size() > 0) { + for (const auto& it : g_PendingUsers) { + if (it.second.sysAddr == sysAddr) { + return; + } + } + } + + UserManager::Instance()->RequestCharacterList(sysAddr); + } + }; + + struct GameMessagePacket final : public WorldPackets::GameMessage { + void Handle() override { + auto isSender = CheatDetection::VerifyLwoobjidIsSender( + objectID, + sysAddr, + CheckType::Entity, + "Sending GM with a sending player that does not match their own. GM ID: %i", + static_cast(messageID) + ); + + if (isSender) GameMessageHandler::HandleMessage(data, sysAddr, objectID, messageID); + } + }; + + struct CharacterCreateRequestPacket final : public WorldPackets::CharacterCreateRequest { + void Handle() override { + UserManager::Instance()->CreateCharacter(sysAddr, *this); + } + }; + + struct CharacterLoginRequestPacket final : public WorldPackets::CharacterLoginRequest { + void Handle() override { + LOG("User is requesting to login with character %llu", playerID); + bool valid = CheatDetection::VerifyLwoobjidIsSender( + playerID, + sysAddr, + CheckType::User, + "Sending login request with a sending player that does not match their own. Player ID: %llu", + playerID + ); + LOG("Login request for player %llu is %s", playerID, valid ? "valid" : "invalid"); + if (!valid) return; + + auto user = UserManager::Instance()->GetUser(sysAddr); + + if (user) { + auto lastCharacter = user->GetLoggedInChar(); + // This means we swapped characters and we need to remove the previous player from the container. + if (lastCharacter != playerID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); + bitStream.Write(lastCharacter); + Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + } + } + + UserManager::Instance()->LoginCharacter(sysAddr, playerID); + } + }; + + struct CharacterDeleteRequestPacket final : public WorldPackets::CharacterDeleteRequest { + void Handle() override { + UserManager::Instance()->DeleteCharacter(sysAddr, *this); + } + }; + + struct CharacterRenameRequestPacket final : public WorldPackets::CharacterRenameRequest { + void Handle() override { + UserManager::Instance()->RenameCharacter(sysAddr, *this); + } + }; + + struct LevelLoadCompletePacket final : public WorldPackets::LevelLoadComplete { + void Handle() override { + LOG("Received level load complete from user."); + LoadPlayer(sysAddr); + } + }; + + struct PositionUpdatePacket final : public WorldPackets::PositionUpdate { + void Handle() override { + auto positionUpdate = update; + + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) { + LOG("Unable to get user to parse position update"); + return; + } + + if (const auto* const lastChar = user->GetLastUsedChar()) { + if (auto* const entity = Game::entityManager->GetEntity(lastChar->GetObjectID())) { + entity->ProcessPositionUpdate(positionUpdate); + } + } + } + }; + + struct MailPacket final : public WorldPackets::MailPacket { + void Handle() override { + if (auto* const user = UserManager::Instance()->GetUser(sysAddr)) { + if (auto* const lastChar = user->GetLastUsedChar()) { + if (auto* const entity = lastChar->GetEntity()) { + Mail::HandleMail(data, sysAddr, entity); + } + } + } + } + }; + + struct RoutePacket final : public WorldPackets::RoutePacket { + void Handle() override { + //Yeet to chat + if (size > 20000) { + LOG("Tried to route a packet with a read size > 20000, so likely a false packet."); + return; + } + + //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(sysAddr); + if (user) { + const auto* const lastChar = user->GetLastUsedChar(); + if (lastChar) objectID = lastChar->GetObjectID(); + } + + const auto routed = ToChat(objectID); + RakNet::BitStream bitStream; + routed.WritePacket(bitStream); + Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE_ORDERED, 0, Game::chatSysAddr, false); + } + }; + + struct StringCheckPacket final : public WorldPackets::StringCheck { + void Handle() override { + const auto receiver = GetNarrowReceiver(); + const auto message = GetNarrowMessage(); + + // TODO: Find a good home for the logic in this case. + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) { + LOG("Unable to get user to parse chat moderation request"); + return; + } + + auto* entity = PlayerManager::GetPlayer(sysAddr); + + if (entity == nullptr) { + LOG("Unable to get player to parse chat moderation request"); + return; + } + + // Check if the player has restricted chat access + auto* character = entity->GetCharacter(); + + if (character->HasPermission(ePermissionMap::RestrictedChatAccess)) { + // Send a message to the player + ChatPackets::SendSystemMessage( + sysAddr, + u"This character has restricted chat access." + ); + + return; + } + + bool isBestFriend = false; + + if (chatLevel == 1) { + // Private chat + LWOOBJID idOfReceiver = LWOOBJID_EMPTY; + + { + auto characterIdFetch = Database::Get()->GetCharacterInfo(receiver); + + if (characterIdFetch) { + idOfReceiver = characterIdFetch->id; + } + } + const auto& bffMap = user->GetIsBestFriendMap(); + if (bffMap.find(receiver) == bffMap.end() && idOfReceiver != LWOOBJID_EMPTY) { + auto bffInfo = Database::Get()->GetBestFriendStatus(entity->GetObjectID(), idOfReceiver); + + if (bffInfo) { + isBestFriend = bffInfo->bestFriendStatus == 3; + } + + if (isBestFriend) { + user->UpdateBestFriendValue(receiver, true); + } + } else if (bffMap.find(receiver) != bffMap.end()) { + isBestFriend = true; + } + } + + const auto segments = Game::chatFilter->IsSentenceOkay(message, entity->GetGMLevel(), !(isBestFriend && chatLevel == 1)); + + bool bAllClean = segments.empty(); + + if (user->GetIsMuted()) { + bAllClean = false; + } + + user->SetLastChatMessageApproved(bAllClean); + + ClientPackets::ChatModerationString response; + response.requestAccepted = segments.empty(); // What DLU has always sent, even when bAllClean is false + response.requestID = requestID; + response.receiver = LUWString(receiver, 42); + response.rejectedSegments = segments; + response.Send(sysAddr); + } + }; + + struct GeneralChatMessagePacket final : public WorldPackets::GeneralChatMessage { + void Handle() override { + if (g_ChatDisabled) { + ChatPackets::SendMessageFail(sysAddr); + } else { + // TODO: Find a good home for the logic in this case. + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) { + LOG("Unable to get user to parse chat message"); + return; + } + + const auto* const lastChar = user->GetLastUsedChar(); + if (!lastChar) { + LOG("No last used character for chat message %i", user->GetAccountID()); + return; + } + + if (user->GetIsMuted()) { + lastChar->SendMuteNotice(); + return; + } + std::string playerName = lastChar->GetName(); + bool isMythran = lastChar->GetGMLevel() > eGameMasterLevel::CIVILIAN; + bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(message), lastChar->GetGMLevel()).empty(); + LOG_DEBUG("Msg: %s was approved previously? %i", GeneralUtils::UTF16ToWTF8(message).c_str(), user->GetLastChatMessageApproved()); + // Kept for moderation and chat bridges, including what the filter stopped (log_chat=0 turns it off) + if (Game::config->GetValue("log_chat") != "0") { + IChatLog::ChatMessage entry; + entry.time = static_cast(std::time(nullptr)); + entry.channel = "zone"; + entry.senderId = user->GetLoggedInChar(); + entry.senderName = lastChar->GetName(); + entry.accountId = user->GetAccountID(); + entry.zoneId = Game::server->GetZoneID(); + entry.instanceId = static_cast(Game::server->GetInstanceID()); + entry.cloneId = Game::zoneManager->GetZoneID().GetCloneID(); + entry.message = GeneralUtils::UTF16ToWTF8(message); + entry.blocked = !isOk; + try { + Database::Get()->InsertChatMessage(entry); + } catch (const std::exception& ex) { + LOG("Couldn't log a chat message: %s", ex.what()); + } + } + if (!isOk) return; + if (!isOk && !isMythran) return; + + std::string sMessage = GeneralUtils::UTF16ToWTF8(message); + LOG("%s: %s", playerName.c_str(), sMessage.c_str()); + ChatPackets::SendChatMessage(sysAddr, chatChannel, playerName, user->GetLoggedInChar(), isMythran, message); + if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage); + } + } + }; + + struct HandleFunnessPacket final : public WorldPackets::HandleFunness { + void Handle() override { + //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. + + CaughtFunness funness; + funness.cheatInfo = this->cheatInfo; + funness.cheatType = this->cheatType; + const auto [cheatType, cheatInfo] = funness; + LOG_DEBUG("Received cheat type %s with info %f", StringifiedEnum::ToString(cheatType).data(), cheatInfo); + const auto disabledCheats = Game::config->GetValue("disable_anti_speedhack") == "1"; + + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) { + Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::KICK); + return; + } + + const auto* const character = user->GetLastUsedChar(); + if (character) { + const auto dcUser = [](const User& user, const SystemAddress& sysAddr) { + if (user.GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) { + Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::KICK); + } + }; + if (cheatType == eFunnessTypes::DebuggerActive) { + LOG("Player %s was detected to be using a debugger (funness thread was %f seconds longer than expected deviation).", character->GetName().c_str(), cheatInfo); + // Immediately kick unless they are a gm + if (!disabledCheats) dcUser(*user, sysAddr); + } else if (cheatType == eFunnessTypes::FdbFailedChecksum) { + LOG("Player %s has failed the fdb checksum after passing it to sign into this server, highly likely cheating.", character->GetName().c_str()); + // Immedately kick unless they are a gm + if (!disabledCheats) dcUser(*user, sysAddr); + } else if (cheatType == eFunnessTypes::RacingBoostTimeTooLong) { + if (cheatInfo == 0.0f) LOG("Player %s has enabled a speedboost for far too long (> 3.501 seconds).", character->GetName().c_str()); + else if (cheatInfo == 1.0f) LOG("Unknown racing boost/speed variable was tampered with."); + else if (cheatInfo == 2.0f) LOG("Player vehicle top speed was tampered with."); + if (!disabledCheats) dcUser(*user, sysAddr); + } else if (cheatType == eFunnessTypes::SomeRacingManipCheat) { + if (cheatInfo >= 0.0f && cheatInfo <= 6.0f) LOG("Cheat RNG value A does not match what it should be %f.", cheatInfo); + else if (cheatInfo >= 7.0f && cheatInfo <= 10.0f) LOG("Cheat RNG value B does not match what it should be %f.", cheatInfo); + } else if (cheatType == eFunnessTypes::Unknown_9) { + LOG("Racing cheat 9 detected with value %f.", cheatInfo); + } else if (cheatType == eFunnessTypes::Unknown_10) { + LOG("Racing cheat 10 detected with value %f.", cheatInfo); + } else if (cheatType == eFunnessTypes::CharacterPosLength) { + LOG("Detected pos length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo); + } else if (cheatType == eFunnessTypes::CharacterVelLength) { + LOG("Detected vel length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo); + } else if (cheatType == eFunnessTypes::CharacterGravityScale) { + LOG("Detected gravity scale difference of %f which is greater than the expected value of 0.0f, not normal!", cheatInfo); + } else if (cheatType == eFunnessTypes::CharacterRunMultiplier) { + LOG("Detected run multiplier difference of %f which is greater than the expected value 0.0f, not normal!", cheatInfo); + } + + if (!disabledCheats && user->GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) user->UserOutOfSync(funness); + } + } + }; + + struct UIHelpTop5Packet final : public WorldPackets::UIHelpTop5 { + void Handle() override { + // TODO: Handle different languages in a nice way + // 0: en_US + // 1: pl_US + // 2: de_DE + // 3: en_GB + + // TODO: Find a good home for the logic in this case. + auto* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + auto* character = user->GetLastUsedChar(); + if (!character) return; + auto* entity = character->GetEntity(); + if (!entity) return; + + AMFArrayValue data; + // Summaries + data.Insert("Summary0", Game::config->GetValue("help_0_summary")); + data.Insert("Summary1", Game::config->GetValue("help_1_summary")); + data.Insert("Summary2", Game::config->GetValue("help_2_summary")); + data.Insert("Summary3", Game::config->GetValue("help_3_summary")); + data.Insert("Summary4", Game::config->GetValue("help_4_summary")); + + // Descriptions + data.Insert("Description0", Game::config->GetValue("help_0_description")); + data.Insert("Description1", Game::config->GetValue("help_1_description")); + data.Insert("Description2", Game::config->GetValue("help_2_description")); + data.Insert("Description3", Game::config->GetValue("help_3_description")); + data.Insert("Description4", Game::config->GetValue("help_4_description")); + + GameMessages::SendUIMessageServerToSingleClient(entity, sysAddr, "UIHelpTop5", data); + } + }; + + template + std::unique_ptr Create() { return std::make_unique(); } + + const std::map()>> g_WorldHandlers = { + { MessageType::World::VALIDATION, Create }, + { MessageType::World::CHARACTER_LIST_REQUEST, Create }, + { MessageType::World::GAME_MSG, Create }, + { MessageType::World::CHARACTER_CREATE_REQUEST, Create }, + { MessageType::World::LOGIN_REQUEST, Create }, + { MessageType::World::CHARACTER_DELETE_REQUEST, Create }, + { MessageType::World::CHARACTER_RENAME_REQUEST, Create }, + { MessageType::World::LEVEL_LOAD_COMPLETE, Create }, + { MessageType::World::POSITION_UPDATE, Create }, + { MessageType::World::MAIL, Create }, + { MessageType::World::ROUTE_PACKET, Create }, + { MessageType::World::STRING_CHECK, Create }, + { MessageType::World::GENERAL_CHAT_MESSAGE, Create }, + { MessageType::World::HANDLE_FUNNESS, Create }, + { MessageType::World::UI_HELP_TOP_5, Create }, + }; +} + void HandlePacket(Packet* packet) { if (packet->length < 1) return; if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) { @@ -1205,476 +1681,30 @@ void HandlePacket(Packet* packet) { CINSTREAM; LUBitStream luBitStream; - luBitStream.ReadHeader(inStream); + if (!luBitStream.ReadHeader(inStream)) return; if (luBitStream.connectionType == ServiceType::COMMON) { CommonPackets::Handle(inStream, packet->systemAddress, luBitStream.internalPacketID); } if (luBitStream.connectionType != ServiceType::WORLD) return; - LOG_DEBUG("Got world packet %s", StringifiedEnum::ToString(static_cast(luBitStream.internalPacketID)).data()); - switch (static_cast(luBitStream.internalPacketID)) { - case MessageType::World::VALIDATION: { - CINSTREAM_SKIP_HEADER; - LUWString username; - inStream.Read(username); + const auto messageId = static_cast(luBitStream.internalPacketID); + LOG_DEBUG("Got world packet %s", StringifiedEnum::ToString(messageId).data()); - LUWString sessionKey; - // sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird - inStream.Read(sessionKey); - LUString clientDatabaseChecksum(32); - inStream.Read(clientDatabaseChecksum); - - // If the check is turned on, validate the client's database checksum. - if (Game::config->GetValue("check_fdb") == "1" && !g_DatabaseChecksum.empty()) { - auto accountInfo = Database::Get()->GetAccountInfo(username.GetAsString()); - if (!accountInfo) { - LOG("Client's account does not exist in the database, aborting connection."); - Game::server->Disconnect(packet->systemAddress, eServerDisconnectIdentifiers::CHARACTER_NOT_FOUND); - return; - } - - // Developers may skip this check - if (clientDatabaseChecksum.string != g_DatabaseChecksum) { - - if (accountInfo->maxGmLevel < eGameMasterLevel::DEVELOPER) { - LOG("Client's database checksum does not match the server's, aborting connection."); - Stamps stamps; - stamps.Add(eStamps::PASSPORT_AUTH_ERROR, 1); - - // Using the LoginResponse here since the UI is still in the login screen state - // and we have a way to send a message about the client mismatch. - AuthPackets::SendLoginResponse( - Game::server, packet->systemAddress, eLoginResponse::PERMISSIONS_NOT_HIGH_ENOUGH, - Game::config->GetValue("cdclient_mismatch_message"), "", 0, "", stamps); - return; - } else { - AMFArrayValue args; - - args.Insert("title", Game::config->GetValue("cdclient_mismatch_title")); - args.Insert("message", Game::config->GetValue("cdclient_mismatch_message")); - - GameMessages::SendUIMessageServerToSingleClient("ToggleAnnounce", args, packet->systemAddress); - LOG("Account (%s) with GmLevel (%s) does not have a matching FDB, but is a developer and will skip this check." - , username.GetAsString().c_str(), StringifiedEnum::ToString(accountInfo->maxGmLevel).data()); - } - } - } - - //Request the session info from Master: - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::REQUEST_SESSION_KEY); - bitStream.Write(username); - Game::server->SendToMaster(bitStream); - - //Insert info into our pending list - TempSessionInfo info; - info.sysAddr = packet->systemAddress; - info.hash = sessionKey.GetAsString(); - g_PendingUsers[username.GetAsString()] = info; - - break; - } - - case MessageType::World::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 || !user->GetLastUsedChar()) return; - Game::entityManager->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 (g_PendingUsers.size() > 0) { - for (const auto& it : g_PendingUsers) { - if (it.second.sysAddr == packet->systemAddress) { - return; - } - } - } - - UserManager::Instance()->RequestCharacterList(packet->systemAddress); - break; - } - - case MessageType::World::GAME_MSG: { - RakNet::BitStream bitStream(packet->data, packet->length, false); - - uint64_t header; - LWOOBJID objectID; - MessageType::Game messageID; - - bitStream.Read(header); - bitStream.Read(objectID); - bitStream.Read(messageID); - - RakNet::BitStream dataStream; - bitStream.Read(dataStream, bitStream.GetNumberOfUnreadBits()); - - auto isSender = CheatDetection::VerifyLwoobjidIsSender( - objectID, - packet->systemAddress, - CheckType::Entity, - "Sending GM with a sending player that does not match their own. GM ID: %i", - static_cast(messageID) - ); - - if (isSender) GameMessageHandler::HandleMessage(dataStream, packet->systemAddress, objectID, messageID); - break; - } - - case MessageType::World::CHARACTER_CREATE_REQUEST: { - UserManager::Instance()->CreateCharacter(packet->systemAddress, packet); - break; - } - - case MessageType::World::LOGIN_REQUEST: { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = 0; - inStream.Read(playerID); - LOG("User is requesting to login with character %llu", playerID); - bool valid = CheatDetection::VerifyLwoobjidIsSender( - playerID, - packet->systemAddress, - CheckType::User, - "Sending login request with a sending player that does not match their own. Player ID: %llu", - playerID - ); - LOG("Login request for player %llu is %s", playerID, valid ? "valid" : "invalid"); - if (!valid) return; - - 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 (lastCharacter != playerID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); - bitStream.Write(lastCharacter); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); - } - } - - UserManager::Instance()->LoginCharacter(packet->systemAddress, playerID); - break; - } - - case MessageType::World::CHARACTER_DELETE_REQUEST: { - UserManager::Instance()->DeleteCharacter(packet->systemAddress, packet); - break; - } - - case MessageType::World::CHARACTER_RENAME_REQUEST: { - UserManager::Instance()->RenameCharacter(packet->systemAddress, packet); - break; - } - - case MessageType::World::LEVEL_LOAD_COMPLETE: { - LOG("Received level load complete from user."); - LoadPlayer(packet->systemAddress); - break; - } - - case MessageType::World::POSITION_UPDATE: { - auto positionUpdate = ClientPackets::HandleClientPositionUpdate(packet); - - User* user = UserManager::Instance()->GetUser(packet->systemAddress); - if (!user) { - LOG("Unable to get user to parse position update"); - return; - } - - if (const auto* const lastChar = user->GetLastUsedChar()) { - if (auto* const entity = Game::entityManager->GetEntity(lastChar->GetObjectID())) { - entity->ProcessPositionUpdate(positionUpdate); - } - } - break; - } - - case MessageType::World::MAIL: { - if (auto* const user = UserManager::Instance()->GetUser(packet->systemAddress)) { - if (auto* const lastChar = user->GetLastUsedChar()) { - if (auto* const entity = lastChar->GetEntity()) { - Mail::HandleMail(inStream, packet->systemAddress, entity); - } - } - } - break; - } - - case MessageType::World::ROUTE_PACKET: { - //Yeet to chat - CINSTREAM_SKIP_HEADER; - uint32_t size = 0; - inStream.Read(size); - - if (size > 20000) { - LOG("Tried to route a packet with a read size > 20000, so likely a false packet."); - return; - } - - CBITSTREAM; - - BitStreamUtils::WriteHeader(bitStream, ServiceType::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) { - const auto* const lastChar = user->GetLastUsedChar(); - if (lastChar) objectID = lastChar->GetObjectID(); - } - - bitStream.Write(objectID); - - //Now write the rest of the data: - auto data = inStream.GetData(); - for (uint32_t i = 23; i - 23 < size && i < packet->length; ++i) { - bitStream.Write(data[i]); - } - - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE_ORDERED, 0, Game::chatSysAddr, false); - break; - } - - case MessageType::World::STRING_CHECK: { - auto request = ClientPackets::HandleChatModerationRequest(packet); - - // TODO: Find a good home for the logic in this case. - User* user = UserManager::Instance()->GetUser(packet->systemAddress); - if (!user) { - LOG("Unable to get user to parse chat moderation request"); - return; - } - - auto* entity = PlayerManager::GetPlayer(packet->systemAddress); - - if (entity == nullptr) { - LOG("Unable to get player to parse chat moderation request"); - return; - } - - // Check if the player has restricted chat access - auto* character = entity->GetCharacter(); - - if (character->HasPermission(ePermissionMap::RestrictedChatAccess)) { - // Send a message to the player - ChatPackets::SendSystemMessage( - packet->systemAddress, - u"This character has restricted chat access." - ); - - return; - } - - bool isBestFriend = false; - - if (request.chatLevel == 1) { - // Private chat - LWOOBJID idOfReceiver = LWOOBJID_EMPTY; - - { - auto characterIdFetch = Database::Get()->GetCharacterInfo(request.receiver); - - if (characterIdFetch) { - idOfReceiver = characterIdFetch->id; - } - } - const auto& bffMap = user->GetIsBestFriendMap(); - if (bffMap.find(request.receiver) == bffMap.end() && idOfReceiver != LWOOBJID_EMPTY) { - auto bffInfo = Database::Get()->GetBestFriendStatus(entity->GetObjectID(), idOfReceiver); - - if (bffInfo) { - isBestFriend = bffInfo->bestFriendStatus == 3; - } - - if (isBestFriend) { - user->UpdateBestFriendValue(request.receiver, true); - } - } else if (bffMap.find(request.receiver) != bffMap.end()) { - isBestFriend = true; - } - } - - const auto segments = Game::chatFilter->IsSentenceOkay(request.message, entity->GetGMLevel(), !(isBestFriend && request.chatLevel == 1)); - - bool bAllClean = segments.empty(); - - if (user->GetIsMuted()) { - bAllClean = false; - } - - user->SetLastChatMessageApproved(bAllClean); - WorldPackets::SendChatModerationResponse(packet->systemAddress, bAllClean, request.requestID, request.receiver, segments); - break; - } - - case MessageType::World::GENERAL_CHAT_MESSAGE: { - if (g_ChatDisabled) { - ChatPackets::SendMessageFail(packet->systemAddress); - } else { - auto chatMessage = ClientPackets::HandleChatMessage(packet); - - // TODO: Find a good home for the logic in this case. - User* user = UserManager::Instance()->GetUser(packet->systemAddress); - if (!user) { - LOG("Unable to get user to parse chat message"); - return; - } - - const auto* const lastChar = user->GetLastUsedChar(); - if (!lastChar) { - LOG("No last used character for chat message %i", user->GetAccountID()); - return; - } - - if (user->GetIsMuted()) { - lastChar->SendMuteNotice(); - return; - } - std::string playerName = lastChar->GetName(); - bool isMythran = lastChar->GetGMLevel() > eGameMasterLevel::CIVILIAN; - bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(chatMessage.message), lastChar->GetGMLevel()).empty(); - LOG_DEBUG("Msg: %s was approved previously? %i", GeneralUtils::UTF16ToWTF8(chatMessage.message).c_str(), user->GetLastChatMessageApproved()); - // Kept for moderation and chat bridges, including what the filter stopped (log_chat=0 turns it off) - if (Game::config->GetValue("log_chat") != "0") { - IChatLog::ChatMessage entry; - entry.time = static_cast(std::time(nullptr)); - entry.channel = "zone"; - entry.senderId = user->GetLoggedInChar(); - entry.senderName = lastChar->GetName(); - entry.accountId = user->GetAccountID(); - entry.zoneId = Game::server->GetZoneID(); - entry.instanceId = static_cast(Game::server->GetInstanceID()); - entry.cloneId = Game::zoneManager->GetZoneID().GetCloneID(); - entry.message = GeneralUtils::UTF16ToWTF8(chatMessage.message); - entry.blocked = !isOk; - try { - Database::Get()->InsertChatMessage(entry); - } catch (const std::exception& ex) { - LOG("Couldn't log a chat message: %s", ex.what()); - } - } - if (!isOk) return; - if (!isOk && !isMythran) return; - - std::string sMessage = GeneralUtils::UTF16ToWTF8(chatMessage.message); - LOG("%s: %s", playerName.c_str(), sMessage.c_str()); - ChatPackets::SendChatMessage(packet->systemAddress, chatMessage.chatChannel, playerName, user->GetLoggedInChar(), isMythran, chatMessage.message); - if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage); - } - - break; - } - - case MessageType::World::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. - - CINSTREAM_SKIP_HEADER; - CaughtFunness funness; - inStream.Read(funness.cheatInfo); - inStream.Read(funness.cheatType); - const auto [cheatType, cheatInfo] = funness; - LOG_DEBUG("Received cheat type %s with info %f", StringifiedEnum::ToString(cheatType).data(), cheatInfo); - const auto disabledCheats = Game::config->GetValue("disable_anti_speedhack") == "1"; - - User* user = UserManager::Instance()->GetUser(packet->systemAddress); - if (!user) { - Game::server->Disconnect(packet->systemAddress, eServerDisconnectIdentifiers::KICK); - return; - } - - const auto* const character = user->GetLastUsedChar(); - if (character) { - const auto dcUser = [](const User& user, const SystemAddress& sysAddr) { - if (user.GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) { - Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::KICK); - } - }; - if (cheatType == eFunnessTypes::DebuggerActive) { - LOG("Player %s was detected to be using a debugger (funness thread was %f seconds longer than expected deviation).", character->GetName().c_str(), cheatInfo); - // Immediately kick unless they are a gm - if (!disabledCheats) dcUser(*user, packet->systemAddress); - } else if (cheatType == eFunnessTypes::FdbFailedChecksum) { - LOG("Player %s has failed the fdb checksum after passing it to sign into this server, highly likely cheating.", character->GetName().c_str()); - // Immedately kick unless they are a gm - if (!disabledCheats) dcUser(*user, packet->systemAddress); - } else if (cheatType == eFunnessTypes::RacingBoostTimeTooLong) { - if (cheatInfo == 0.0f) LOG("Player %s has enabled a speedboost for far too long (> 3.501 seconds).", character->GetName().c_str()); - else if (cheatInfo == 1.0f) LOG("Unknown racing boost/speed variable was tampered with."); - else if (cheatInfo == 2.0f) LOG("Player vehicle top speed was tampered with."); - if (!disabledCheats) dcUser(*user, packet->systemAddress); - } else if (cheatType == eFunnessTypes::SomeRacingManipCheat) { - if (cheatInfo >= 0.0f && cheatInfo <= 6.0f) LOG("Cheat RNG value A does not match what it should be %f.", cheatInfo); - else if (cheatInfo >= 7.0f && cheatInfo <= 10.0f) LOG("Cheat RNG value B does not match what it should be %f.", cheatInfo); - } else if (cheatType == eFunnessTypes::Unknown_9) { - LOG("Racing cheat 9 detected with value %f.", cheatInfo); - } else if (cheatType == eFunnessTypes::Unknown_10) { - LOG("Racing cheat 10 detected with value %f.", cheatInfo); - } else if (cheatType == eFunnessTypes::CharacterPosLength) { - LOG("Detected pos length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo); - } else if (cheatType == eFunnessTypes::CharacterVelLength) { - LOG("Detected vel length of %f which is greater than the expected value 1.0f, not normal!", cheatInfo); - } else if (cheatType == eFunnessTypes::CharacterGravityScale) { - LOG("Detected gravity scale difference of %f which is greater than the expected value of 0.0f, not normal!", cheatInfo); - } else if (cheatType == eFunnessTypes::CharacterRunMultiplier) { - LOG("Detected run multiplier difference of %f which is greater than the expected value 0.0f, not normal!", cheatInfo); - } - - if (!disabledCheats && user->GetMaxGMLevel() < eGameMasterLevel::DEVELOPER) user->UserOutOfSync(funness); - } - - break; - } - - - case MessageType::World::UI_HELP_TOP_5: { - auto language = ClientPackets::SendTop5HelpIssues(packet); - // TODO: Handle different languages in a nice way - // 0: en_US - // 1: pl_US - // 2: de_DE - // 3: en_GB - - // TODO: Find a good home for the logic in this case. - auto* user = UserManager::Instance()->GetUser(packet->systemAddress); - if (!user) return; - auto* character = user->GetLastUsedChar(); - if (!character) return; - auto* entity = character->GetEntity(); - if (!entity) return; - - AMFArrayValue data; - // Summaries - data.Insert("Summary0", Game::config->GetValue("help_0_summary")); - data.Insert("Summary1", Game::config->GetValue("help_1_summary")); - data.Insert("Summary2", Game::config->GetValue("help_2_summary")); - data.Insert("Summary3", Game::config->GetValue("help_3_summary")); - data.Insert("Summary4", Game::config->GetValue("help_4_summary")); - - // Descriptions - data.Insert("Description0", Game::config->GetValue("help_0_description")); - data.Insert("Description1", Game::config->GetValue("help_1_description")); - data.Insert("Description2", Game::config->GetValue("help_2_description")); - data.Insert("Description3", Game::config->GetValue("help_3_description")); - data.Insert("Description4", Game::config->GetValue("help_4_description")); - - GameMessages::SendUIMessageServerToSingleClient(entity, packet->systemAddress, "UIHelpTop5", data); - break; - } - - default: - const auto messageId = *reinterpret_cast(&packet->data[3]); + const auto handler = g_WorldHandlers.find(messageId); + if (handler == g_WorldHandlers.end()) { const std::string_view messageIdString = StringifiedEnum::ToString(messageId); LOG("Unknown world packet received: %4i, %s", messageId, messageIdString.data()); + return; } + + auto request = handler->second(); + request->sysAddr = packet->systemAddress; + if (!request->Deserialize(inStream)) { + LOG("Failed to read world packet %s from %s", StringifiedEnum::ToString(messageId).data(), packet->systemAddress.ToString()); + return; + } + request->Handle(); } void WorldShutdownProcess(uint32_t zoneId) { diff --git a/docs/PacketArchitecture.md b/docs/PacketArchitecture.md index 7d1a26358..a9eec0db2 100644 --- a/docs/PacketArchitecture.md +++ b/docs/PacketArchitecture.md @@ -159,9 +159,14 @@ Every non-game-message packet is an `LUBitStream` subclass. The base owns the fu - Each service has one dispatch function with a `std::map`, exactly like `Mail::HandleMail`: read the id, create, set context, `Deserialize` (log and drop on failure), `Handle`. Each server's `HandlePacket` only routes by `ServiceType`. -- Structs live in their service's namespace and file: `AuthPackets`, `ChatPackets`, `ClientPackets`, - `CommonPackets` (new, for `ServiceType::COMMON`), `MasterPackets`, `WorldPackets` in `dNet/`; Mail stays in - `dGame/dUtilities/Mail.*`. Handlers that need game or server state stay in the server that owns that state +- **One file pair per `ServiceType`; a packet goes in the file of the `ServiceType` written in its header**, in + that namespace: `CommonPackets` (COMMON), `AuthPackets` (AUTH), `ChatPackets` (CHAT), `WorldPackets` (WORLD: + what a client sends to a world server), `ClientPackets` (CLIENT: what any server sends to the client), + `MasterPackets` (MASTER), all in `dNet/`. So the auth server's login response is in `ClientPackets`, and a + world server's message to chat is in `ChatPackets`. Anything used by several services (for example `Stamps`, + which rides in auth, master and client packets, or generic helpers) gets its own small file named for what it + is (`dNet/Stamps.h`), not one service's file. A packet found in the wrong file is moved (a pure move, same + bytes). Mail stays in `dGame/dUtilities/Mail.*`. Handlers that need game or server state stay in the server that owns that state (`dChatServer`, `dMasterServer`, `dWorldServer`, `dAuthServer`) and are attached to the struct by overriding `Handle` in that server's translation unit. - Fixed-width strings use `LUString`/`LUWString` with the width spelled out (`LUWString password(41)`). diff --git a/tests/dGameTests/dNetTests/CMakeLists.txt b/tests/dGameTests/dNetTests/CMakeLists.txt index 6c8a5d642..2af0b3d4c 100644 --- a/tests/dGameTests/dNetTests/CMakeLists.txt +++ b/tests/dGameTests/dNetTests/CMakeLists.txt @@ -1,5 +1,6 @@ SET(DNET_TESTS - "CommonAuthPacketsTests.cpp") + "CommonAuthPacketsTests.cpp" + "WorldPacketsTests.cpp") # Get the folder name and prepend it to the files above get_filename_component(thisFolderName ${CMAKE_CURRENT_SOURCE_DIR} NAME) diff --git a/tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h b/tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h new file mode 100644 index 000000000..15fa7bec9 --- /dev/null +++ b/tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h @@ -0,0 +1,543 @@ +#ifndef WORLDPACKETSLEGACY_H +#define WORLDPACKETSLEGACY_H + +// FROZEN ORACLE - DO NOT EDIT. +// Verbatim copies of the hand written code that WorldPackets / the ClientPackets world responses replaced, from +// before the conversion: every WorldPackets::Send* function and HTTPMonitorInfo (dNet/WorldPackets.cpp), the four +// ClientPackets parse functions (dNet/ClientPackets.cpp), and, below them, the parts of UserManager.cpp and +// WorldServer.cpp's HandlePacket that read or wrote packets inline (each copied verbatim into a function that +// returns what was read, or the packet that was built, instead of acting on it). +// Only the namespace changed. The byte-equality tests run the same inputs through these and the new structs. + +#include "BitStreamUtils.h" +#include "dCommonVars.h" +#include "dServer.h" +#include "eCharacterCreationResponse.h" +#include "eFunnessTypes.h" +#include "eGameMasterLevel.h" +#include "eRenameResponse.h" +#include "Game.h" +#include "LDFFormat.h" +#include "Logger.h" +#include "MessageType/Chat.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" +#include "PositionUpdate.h" +#include "ServiceType.h" +#include "ZCompression.h" + +#include +#include +#include +#include +#include +#include + +namespace LegacyWorldPackets { +struct HTTPMonitorInfo { + uint16_t port = 80; + bool openWeb = false; + bool supportsSum = false; + bool supportsDetail = false; + bool supportsWho = false; + bool supportsObjects = false; + void Serialize(RakNet::BitStream &bitstream) const; +}; + +inline void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(port); + bitStream.Write(openWeb); + bitStream.Write(supportsSum); + bitStream.Write(supportsDetail); + bitStream.Write(supportsWho); + bitStream.Write(supportsObjects); +} + +inline void SendLoadStaticZone(const SystemAddress& sysAddr, float x, float y, float z, uint32_t checksum, LWOZONEID zone) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::LOAD_STATIC_ZONE); + + bitStream.Write(zone.GetMapID()); + bitStream.Write(zone.GetInstanceID()); + //bitStream.Write(zone.GetCloneID()); + bitStream.Write(0); + + bitStream.Write(checksum); + bitStream.Write(0); // ?? + + bitStream.Write(x); + bitStream.Write(y); + bitStream.Write(z); + + bitStream.Write(0); // Change this to eventually use 4 on activity worlds + + SEND_PACKET; +} + +inline void SendCharacterCreationResponse(const SystemAddress& sysAddr, eCharacterCreationResponse response) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE); + bitStream.Write(response); + SEND_PACKET; +} + +inline void SendCharacterRenameResponse(const SystemAddress& sysAddr, eRenameResponse response) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_RENAME_RESPONSE); + bitStream.Write(response); + SEND_PACKET; +} + +inline void SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DELETE_CHARACTER_RESPONSE); + bitStream.Write(response); + SEND_PACKET; +} + +inline void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TRANSFER_TO_WORLD); + + bitStream.Write(LUString(serverIP)); + bitStream.Write(serverPort); + bitStream.Write(mythranShift); + + SEND_PACKET; +} + +inline void SendServerState(const SystemAddress& sysAddr) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SERVER_STATES); + bitStream.Write(1); //If the server is receiving this request, it probably is ready anyway. + SEND_PACKET; +} + +inline void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID) { + using namespace std; + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER); + + RakNet::BitStream data; + + LwoNameValue ldfData; + ldfData.Insert(u"objid", player); + ldfData.Insert(u"template", 1); + ldfData.Insert(u"xmlData", xmlData); + ldfData.Insert(u"name", username); + ldfData.Insert(u"gmlevel", static_cast(gm)); + ldfData.Insert(u"chatmode", static_cast(gm)); + ldfData.Insert(u"reputation", reputation); + ldfData.Insert(u"propertycloneid", cloneID); + + data.Write(ldfData.values.size()); + for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data); + + //Compress the data before sending: + const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed()); + auto compressedData = std::make_unique(reservedSize); + + size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize); + + assert(size <= reservedSize); + + bitStream.Write(size + 9); //size of data + header bytes (8) + bitStream.Write(1); //compressed boolean, true + bitStream.Write(data.GetNumberOfBytesUsed()); + bitStream.Write(size); + + /** + * In practice, this warning serves no purpose for us. We allocate the max memory needed on the heap + * and then compress the data. In the off chance that the compression actually increases the size, + * an assertion is done to prevent bad data from being saved or sent. + */ +#pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'. + bitStream.WriteAlignedBytes(compressedData.get(), size); +#pragma warning(default:6385) + + SEND_PACKET; + LOG("Sent CreateCharacter for ID: %llu", player); +} + +inline void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set> unacceptedItems) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING); + + bitStream.Write(unacceptedItems.empty()); // Is sentence ok? + bitStream.Write(0x16); // Source ID, unknown + + bitStream.Write(requestID); // request ID + bitStream.Write(0); // chat mode + + bitStream.Write(LUWString(receiver, 42)); // receiver name + + for (auto it : unacceptedItems) { + bitStream.Write(it.first); // start index + bitStream.Write(it.second); // length + } + + for (int i = unacceptedItems.size(); 64 > i; i++) { + bitStream.Write(0); + } + + SEND_PACKET; +} + +inline void SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::MAKE_GM_RESPONSE); + + bitStream.Write(success); + bitStream.Write(static_cast(highestLevel)); + bitStream.Write(static_cast(prevLevel)); + bitStream.Write(static_cast(newLevel)); + + SEND_PACKET; +} + +inline void SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::HTTP_MONITOR_INFO_RESPONSE); + info.Serialize(bitStream); + SEND_PACKET; +} + +inline void SendDebugOuput(const SystemAddress& sysAddr, const std::string& data) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::DEBUG_OUTPUT); + bitStream.Write(data.size()); + bitStream.Write(data); + SEND_PACKET; +} + +struct ChatMessage { + uint8_t chatChannel = 0; + uint16_t unknown = 0; + std::u16string message; +}; + +struct ChatModerationRequest { + uint8_t chatLevel = 0; + uint8_t requestID = 0; + std::string receiver; + std::string message; +}; + +inline ChatMessage HandleChatMessage(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + ChatMessage message; + int32_t messageLength{}; + + inStream.Read(message.chatChannel); + inStream.Read(message.unknown); + inStream.Read(messageLength); + + if (messageLength > MAX_MESSAGE_LENGTH || messageLength < 0) return message; + + for (int32_t i = 0; i < (messageLength - 1); ++i) { + char16_t character; + inStream.Read(character); + message.message.push_back(character); + } + + return message; +} + +inline PositionUpdate HandleClientPositionUpdate(Packet* packet) { + PositionUpdate update; + CINSTREAM_SKIP_HEADER; + + inStream.Read(update.position.x); + inStream.Read(update.position.y); + inStream.Read(update.position.z); + + inStream.Read(update.rotation.x); + inStream.Read(update.rotation.y); + inStream.Read(update.rotation.z); + inStream.Read(update.rotation.w); + + inStream.Read(update.onGround); + inStream.Read(update.onRail); + + bool velocityFlag = false; + inStream.Read(velocityFlag); + if (velocityFlag) { + inStream.Read(update.velocity.x); + inStream.Read(update.velocity.y); + inStream.Read(update.velocity.z); + } + + bool angVelocityFlag = false; + inStream.Read(angVelocityFlag); + if (angVelocityFlag) { + inStream.Read(update.angularVelocity.x); + inStream.Read(update.angularVelocity.y); + inStream.Read(update.angularVelocity.z); + } + + // TODO figure out how to use these. Ignoring for now, but reading in if they exist. + bool hasLocalSpaceInfo{}; + if (inStream.Read(hasLocalSpaceInfo) && hasLocalSpaceInfo) { + inStream.Read(update.localSpaceInfo.objectId); + inStream.Read(update.localSpaceInfo.position.x); + inStream.Read(update.localSpaceInfo.position.y); + inStream.Read(update.localSpaceInfo.position.z); + bool hasLinearVelocity = false; + if (inStream.Read(hasLinearVelocity) && hasLinearVelocity) { + inStream.Read(update.localSpaceInfo.linearVelocity.x); + inStream.Read(update.localSpaceInfo.linearVelocity.y); + inStream.Read(update.localSpaceInfo.linearVelocity.z); + } + } + + bool hasRemoteInputInfo{}; + if (inStream.Read(hasRemoteInputInfo) && hasRemoteInputInfo) { + inStream.Read(update.remoteInputInfo.m_RemoteInputX); + inStream.Read(update.remoteInputInfo.m_RemoteInputY); + inStream.Read(update.remoteInputInfo.m_IsPowersliding); + inStream.Read(update.remoteInputInfo.m_IsModified); + } + + return update; +} + +inline ChatModerationRequest HandleChatModerationRequest(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + ChatModerationRequest request; + + inStream.Read(request.chatLevel); + inStream.Read(request.requestID); + + for (uint32_t i = 0; i < 42; ++i) { + uint16_t character; + inStream.Read(character); + request.receiver.push_back(static_cast(character)); + } + + if (!request.receiver.empty()) { + if (std::string(request.receiver.c_str(), 4) == "[GM]") { // Shift the string forward if we are speaking to a GM as the client appends "[GM]" if they are + request.receiver = std::string(request.receiver.c_str() + 4, request.receiver.size() - 4); + } + } + + uint16_t messageLength; + inStream.Read(messageLength); + if (messageLength > MAX_MESSAGE_LENGTH) return request; + for (uint32_t i = 0; i < messageLength; ++i) { + uint16_t character; + inStream.Read(character); + request.message.push_back(static_cast(character)); + } + + return request; +} + +inline int32_t SendTop5HelpIssues(Packet* packet) { + CINSTREAM_SKIP_HEADER; + int32_t language = 0; + inStream.Read(language); + return language; +} + + // ---- UserManager::RequestCharacterList, the part that wrote the packet (characters[i] is any type with + // Character's getters) ---- + template + inline void SendCharacterList(const SystemAddress& sysAddr, std::vector characters) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE); + + bitStream.Write(characters.size()); + bitStream.Write(0); //TODO: Pick the most recent played index. character index in front, just picking 0 + + for (uint32_t i = 0; i < characters.size(); ++i) { + bitStream.Write(characters[i]->GetObjectID()); + bitStream.Write(0); + + bitStream.Write(LUWString(characters[i]->GetName())); + bitStream.Write(LUWString(characters[i]->GetUnapprovedName())); + + bitStream.Write(characters[i]->GetNameRejected()); + bitStream.Write(false); + + bitStream.Write(LUString("", 10)); + + bitStream.Write(characters[i]->GetShirtColor()); + bitStream.Write(characters[i]->GetShirtStyle()); + bitStream.Write(characters[i]->GetPantsColor()); + bitStream.Write(characters[i]->GetHairStyle()); + bitStream.Write(characters[i]->GetHairColor()); + bitStream.Write(characters[i]->GetLeftHand()); + bitStream.Write(characters[i]->GetRightHand()); + bitStream.Write(characters[i]->GetEyebrows()); + bitStream.Write(characters[i]->GetEyes()); + bitStream.Write(characters[i]->GetMouth()); + bitStream.Write(0); + + bitStream.Write(characters[i]->GetZoneID()); + bitStream.Write(characters[i]->GetZoneInstance()); + bitStream.Write(characters[i]->GetZoneClone()); + + bitStream.Write(characters[i]->GetLastLogin()); + + const auto& equippedItems = characters[i]->GetEquippedItems(); + bitStream.Write(equippedItems.size()); + + for (uint32_t j = 0; j < equippedItems.size(); ++j) { + bitStream.Write(equippedItems[j]); + } + } + + SEND_PACKET; + } + + // ---- UserManager::CreateCharacter, the reads ---- + struct CreateCharacterRead { + LUWString LUWStringName; + uint32_t firstNameIndex; + uint32_t middleNameIndex; + uint32_t lastNameIndex; + uint32_t shirtColor; + uint32_t shirtStyle; + uint32_t pantsColor; + uint32_t hairStyle; + uint32_t hairColor; + uint32_t lh; + uint32_t rh; + uint32_t eyebrows; + uint32_t eyes; + uint32_t mouth; + }; + + inline CreateCharacterRead ReadCreateCharacter(Packet* packet) { + CreateCharacterRead r{}; + auto& [LUWStringName, firstNameIndex, middleNameIndex, lastNameIndex, shirtColor, shirtStyle, pantsColor, hairStyle, hairColor, lh, rh, eyebrows, eyes, mouth] = r; + CINSTREAM_SKIP_HEADER; + inStream.Read(LUWStringName); + inStream.Read(firstNameIndex); + inStream.Read(middleNameIndex); + inStream.Read(lastNameIndex); + inStream.IgnoreBytes(9); + inStream.Read(shirtColor); + inStream.Read(shirtStyle); + inStream.Read(pantsColor); + inStream.Read(hairStyle); + inStream.Read(hairColor); + inStream.Read(lh); + inStream.Read(rh); + inStream.Read(eyebrows); + inStream.Read(eyes); + inStream.Read(mouth); + return r; + } + + // ---- UserManager::DeleteCharacter, the read ---- + inline LWOOBJID ReadDeleteCharacter(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID objectID; + inStream.Read(objectID); + return objectID; + } + + // ---- UserManager::RenameCharacter, the reads ---- + inline std::pair ReadRenameCharacter(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID objectID; + inStream.Read(objectID); + + LUWString LUWStringName; + inStream.Read(LUWStringName); + auto newName = LUWStringName.GetAsString(); + return { objectID, newName }; + } + + // ---- WorldServer HandlePacket VALIDATION, the reads ---- + struct ValidationRead { + std::string username; + std::string sessionKey; + std::string checksum; + }; + + inline ValidationRead ReadValidation(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LUWString username; + inStream.Read(username); + + LUWString sessionKey; + // sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird + inStream.Read(sessionKey); + LUString clientDatabaseChecksum(32); + inStream.Read(clientDatabaseChecksum); + return { username.GetAsString(), sessionKey.GetAsString(), clientDatabaseChecksum.string }; + } + + // ---- WorldServer HandlePacket LOGIN_REQUEST, the read ---- + inline LWOOBJID ReadLoginRequest(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID = 0; + inStream.Read(playerID); + return playerID; + } + + // ---- WorldServer HandlePacket GAME_MSG, the reads ---- + struct GameMessageRead { + LWOOBJID objectID; + MessageType::Game messageID; + std::vector data; + uint32_t dataBits; + }; + + inline GameMessageRead ReadGameMessage(Packet* packet) { + RakNet::BitStream bitStream(packet->data, packet->length, false); + + uint64_t header; + LWOOBJID objectID; + MessageType::Game messageID; + + bitStream.Read(header); + bitStream.Read(objectID); + bitStream.Read(messageID); + + RakNet::BitStream dataStream; + bitStream.Read(dataStream, bitStream.GetNumberOfUnreadBits()); + return { objectID, messageID, { dataStream.GetData(), dataStream.GetData() + dataStream.GetNumberOfBytesUsed() }, static_cast(dataStream.GetNumberOfBitsUsed()) }; + } + + // ---- WorldServer HandlePacket ROUTE_PACKET: the packet it built for the chat server (it then sent it with + // Game::chatServer->Send); objectID is what it looked up for the sender. Returns false where it returned. ---- + inline bool BuildRoutePacket(Packet* packet, LWOOBJID objectID, RakNet::BitStream& bitStream) { + //Yeet to chat + CINSTREAM_SKIP_HEADER; + uint32_t size = 0; + inStream.Read(size); + + if (size > 20000) { + LOG("Tried to route a packet with a read size > 20000, so likely a false packet."); + return false; + } + + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, packet->data[14]); + + bitStream.Write(objectID); + + //Now write the rest of the data: + auto data = inStream.GetData(); + for (uint32_t i = 23; i - 23 < size && i < packet->length; ++i) { + bitStream.Write(data[i]); + } + return true; + } + + // ---- WorldServer HandlePacket HANDLE_FUNNESS, the reads ---- + inline CaughtFunness ReadFunness(Packet* packet) { + CINSTREAM_SKIP_HEADER; + CaughtFunness funness; + inStream.Read(funness.cheatInfo); + inStream.Read(funness.cheatType); + return funness; + } +} + +#endif // WORLDPACKETSLEGACY_H diff --git a/tests/dGameTests/dNetTests/WorldPacketsTests.cpp b/tests/dGameTests/dNetTests/WorldPacketsTests.cpp new file mode 100644 index 000000000..bef02e965 --- /dev/null +++ b/tests/dGameTests/dNetTests/WorldPacketsTests.cpp @@ -0,0 +1,719 @@ +#include "ChatPackets.h" +#include "ClientPackets.h" +#include "GameDependencies.h" +#include "PacketTestUtils.h" +#include "WorldPackets.h" +#include "Legacy/WorldPacketsLegacy.h" + +#include "eCharacterCreationResponse.h" +#include "eGameMasterLevel.h" +#include "eRenameResponse.h" +#include "magic_enum.hpp" + +#include +#include +#include +#include + +#include + +using namespace PacketTestUtils; + +namespace { + SystemAddress TestAddress() { + SystemAddress address; + address.binaryAddress = 0x0100007F; + address.port = 1234; + return address; + } + + void ExpectSameSend(const std::function& legacy, const LUBitStream& packet) { + const auto sysAddr = TestAddress(); + const auto expected = Capture([&] { legacy(sysAddr); }); + const auto actual = Capture([&] { packet.Send(sysAddr); }); + ASSERT_EQ(expected.size(), 1); + ASSERT_EQ(actual.size(), 1); + EXPECT_PACKET_EQ(FromCapture(expected[0]), FromCapture(actual[0])); + EXPECT_EQ(expected[0].sysAddr, actual[0].sysAddr); + EXPECT_EQ(expected[0].broadcast, actual[0].broadcast); + } + + template + T RoundTrip(const T& packet) { + RakNet::BitStream first; + packet.WritePacket(first); + T copy; + EXPECT_TRUE(copy.ReadHeader(first)); + EXPECT_TRUE(copy.Deserialize(first)); + EXPECT_EQ(first.GetNumberOfUnreadBits(), 0); + RakNet::BitStream second; + copy.WritePacket(second); + EXPECT_PACKET_EQ(FromBitStream(first), FromBitStream(second)); + return copy; + } + + template + void ExpectTruncatedFails(const T& packet, uint32_t keepAtLeast = 8) { + RakNet::BitStream full; + packet.WritePacket(full); + const auto bytes = full.GetNumberOfBytesUsed(); + for (uint32_t cut = keepAtLeast; cut < bytes; cut++) { + RakNet::BitStream truncated(full.GetData(), cut, true); + T copy; + ASSERT_TRUE(copy.ReadHeader(truncated)); + EXPECT_FALSE(copy.Deserialize(truncated)) << "cut at " << cut << " of " << bytes; + } + } + + // A received packet as the servers get it, from bytes written by a struct. + struct ReceivedPacket { + RakNet::BitStream bytes; + Packet packet{}; + + explicit ReceivedPacket(const LUBitStream& from) { + from.WritePacket(bytes); + packet.systemAddress = TestAddress(); + packet.data = bytes.GetData(); + packet.length = bytes.GetNumberOfBytesUsed(); + packet.bitSize = bytes.GetNumberOfBitsUsed(); + } + }; + + // Reads a struct from a received packet the way the dispatchers do. + template + bool Read(ReceivedPacket& received, T& out) { + RakNet::BitStream inStream(received.packet.data, received.packet.length, false); + if (!out.ReadHeader(inStream)) return false; + return out.Deserialize(inStream); + } + + const std::vector g_Strings = { "", "a", "Hello World", "127.0.0.1", std::string(32, 'x'), std::string(33, 'y'), std::string(300, 'z') }; + const std::vector g_ObjectIDs = { 0, 1, 1152921508901814000, -1, std::numeric_limits::max() }; + + struct FakeCharacter { + LWOOBJID objectID{}; + std::string name; + std::string unapprovedName; + bool nameRejected{}; + uint32_t shirtColor{}, shirtStyle{}, pantsColor{}, hairStyle{}, hairColor{}, leftHand{}, rightHand{}, eyebrows{}, eyes{}, mouth{}; + uint32_t zoneID{}, zoneInstance{}, zoneClone{}; + uint64_t lastLogin{}; + std::vector equippedItems; + + LWOOBJID GetObjectID() const { return objectID; } + const std::string& GetName() const { return name; } + const std::string& GetUnapprovedName() const { return unapprovedName; } + bool GetNameRejected() const { return nameRejected; } + uint32_t GetShirtColor() const { return shirtColor; } + uint32_t GetShirtStyle() const { return shirtStyle; } + uint32_t GetPantsColor() const { return pantsColor; } + uint32_t GetHairStyle() const { return hairStyle; } + uint32_t GetHairColor() const { return hairColor; } + uint32_t GetLeftHand() const { return leftHand; } + uint32_t GetRightHand() const { return rightHand; } + uint32_t GetEyebrows() const { return eyebrows; } + uint32_t GetEyes() const { return eyes; } + uint32_t GetMouth() const { return mouth; } + uint32_t GetZoneID() const { return zoneID; } + uint32_t GetZoneInstance() const { return zoneInstance; } + uint32_t GetZoneClone() const { return zoneClone; } + uint64_t GetLastLogin() const { return lastLogin; } + const std::vector& GetEquippedItems() const { return equippedItems; } + }; +} + +class WorldPacketsTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +// ---- Server -> client ---- + +TEST_F(WorldPacketsTests, LoadStaticZoneMatchesLegacy) { + for (const auto& zone : { LWOZONEID(1000, 0, 0), LWOZONEID(1100, 7, 1234), LWOZONEID(0xFFFF, 0xFFFF, 0xFFFFFFFF) }) { + for (const auto& [x, y, z] : { std::tuple{ 0.0f, 0.0f, 0.0f }, std::tuple{ -1.5f, 200.25f, 1e30f } }) { + for (const uint32_t checksum : { 0u, 0x20b8087cu, 0xFFFFFFFFu }) { + ClientPackets::LoadStaticZone packet; + packet.mapID = zone.GetMapID(); + packet.instanceID = zone.GetInstanceID(); + packet.cloneID = 0; // what the world server sends + packet.mapChecksum = checksum; + packet.playerPosition = NiPoint3(x, y, z); + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendLoadStaticZone(a, x, y, z, checksum, zone); }, packet); + EXPECT_EQ(RoundTrip(packet).mapChecksum, checksum); + } + } + } + + ClientPackets::LoadStaticZone packet; + packet.mapID = 1100; + packet.instanceID = 2; + packet.mapChecksum = 0x11223344; + packet.playerPosition = NiPoint3(1.0f, 2.0f, 3.0f); + RakNet::BitStream bytes; + packet.WritePacket(bytes); + // 0x53 | CLIENT (5) | LOAD_STATIC_ZONE (2) | pad | map | instance | clone | checksum | editor u8 u8 | x y z | instance type + EXPECT_PACKET_EQ(FromHex("53 05 00 02 00 00 00 00 4c 04 02 00 00 00 00 00 44 33 22 11 00 00 00 00 80 3f 00 00 00 40 00 00 40 40 00 00 00 00"), FromBitStream(bytes)); + ExpectTruncatedFails(packet); +} + +TEST_F(WorldPacketsTests, SmallResponsesMatchLegacy) { + for (const auto response : magic_enum::enum_values()) { + ClientPackets::CharacterCreateResponse packet; + packet.response = response; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterCreationResponse(a, response); }, packet); + EXPECT_EQ(RoundTrip(packet).response, response); + } + for (const auto response : magic_enum::enum_values()) { + ClientPackets::CharacterRenameResponse packet; + packet.response = response; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterRenameResponse(a, response); }, packet); + EXPECT_EQ(RoundTrip(packet).response, response); + } + for (const bool success : { false, true }) { + ClientPackets::DeleteCharacterResponse packet; + packet.success = success; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterDeleteResponse(a, success); }, packet); + EXPECT_EQ(RoundTrip(packet).success, success); + } + { + ClientPackets::ServerStates packet; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendServerState(a); }, packet); + RoundTrip(packet); + } + + ClientPackets::CharacterCreateResponse created; + created.response = eCharacterCreationResponse::CUSTOM_NAME_IN_USE; + RakNet::BitStream bytes; + created.WritePacket(bytes); + EXPECT_PACKET_EQ(FromHex("53 05 00 07 00 00 00 00 04"), FromBitStream(bytes)); +} + +TEST_F(WorldPacketsTests, TransferToWorldMatchesLegacy) { + for (const auto& ip : g_Strings) { + for (const uint32_t port : { 0u, 2001u, 0xFFFFu, 0x12345u }) { + for (const bool mythranShift : { false, true }) { + ClientPackets::TransferToWorld packet; + packet.serverIP = LUString(ip); + packet.serverPort = port; + packet.mythranShift = mythranShift; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendTransferToWorld(a, ip, port, mythranShift); }, packet); + const auto copy = RoundTrip(packet); + EXPECT_EQ(copy.serverPort, static_cast(port)); + EXPECT_EQ(copy.mythranShift, mythranShift); + } + } + } + + ClientPackets::TransferToWorld packet; + packet.serverIP = LUString("1.2"); + packet.serverPort = 0x0102; + packet.mythranShift = true; + RakNet::BitStream bytes; + packet.WritePacket(bytes); + std::string zeros; + for (int i = 0; i < 30; i++) zeros += " 00"; + EXPECT_PACKET_EQ(FromHex("53 05 00 0e 00 00 00 00 31 2e 32" + zeros + " 02 01 01"), FromBitStream(bytes)); + ExpectTruncatedFails(packet); +} + +TEST_F(WorldPacketsTests, MakeGMResponseMatchesLegacy) { + const auto levels = magic_enum::enum_values(); + for (const bool success : { false, true }) { + for (const auto highest : levels) { + for (const auto previous : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) { + ClientPackets::MakeGMResponse packet; + packet.success = success; + packet.highestLevel = highest; + packet.previousLevel = previous; + packet.newLevel = highest; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendGMLevelChange(a, success, highest, previous, highest); }, packet); + EXPECT_EQ(RoundTrip(packet).highestLevel, highest); + } + } + } + + ClientPackets::MakeGMResponse packet; + packet.success = true; + packet.highestLevel = eGameMasterLevel::OPERATOR; + packet.previousLevel = eGameMasterLevel::CIVILIAN; + packet.newLevel = eGameMasterLevel::DEVELOPER; + RakNet::BitStream bytes; + packet.WritePacket(bytes); + EXPECT_PACKET_EQ(FromHex("53 05 00 10 00 00 00 00 01 09 00 00 00 08 00"), FromBitStream(bytes)); + ExpectTruncatedFails(packet); +} + +TEST_F(WorldPacketsTests, HTTPMonitorInfoAndDebugOutputMatchLegacy) { + for (int bits = 0; bits < 32; bits++) { + LegacyWorldPackets::HTTPMonitorInfo info; + info.port = static_cast(80 + bits); + info.openWeb = bits & 1; + info.supportsSum = bits & 2; + info.supportsDetail = bits & 4; + info.supportsWho = bits & 8; + info.supportsObjects = bits & 16; + ClientPackets::HTTPMonitorInfoResponse packet; + packet.port = info.port; + packet.openWeb = info.openWeb; + packet.supportsSum = info.supportsSum; + packet.supportsDetail = info.supportsDetail; + packet.supportsWho = info.supportsWho; + packet.supportsObjects = info.supportsObjects; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendHTTPMonitorInfo(a, info); }, packet); + EXPECT_EQ(RoundTrip(packet).supportsWho, info.supportsWho); + } + + for (const auto& text : g_Strings) { + ClientPackets::DebugOutput packet; + packet.data = text; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendDebugOuput(a, text); }, packet); + EXPECT_EQ(RoundTrip(packet).data, text); + } +} + +TEST_F(WorldPacketsTests, ChatModerationStringMatchesLegacy) { + using Segments = std::set>; + Segments many; + for (uint8_t i = 0; i < 70; i++) many.emplace(i, static_cast(i + 1)); + for (const auto& segments : { Segments{}, Segments{ { 0, 4 } }, Segments{ { 2, 3 }, { 10, 255 } }, many }) { + for (const uint32_t requestID : { 0u, 7u, 255u, 256u }) { + for (const auto& receiver : g_Strings) { + for (const bool accepted : { false, true }) { + ClientPackets::ChatModerationString packet; + packet.requestAccepted = segments.empty(); // what the world server sends + packet.requestID = requestID; + packet.receiver = LUWString(receiver, 42); + packet.rejectedSegments = segments; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendChatModerationResponse(a, accepted, requestID, receiver, segments); }, packet); + if (segments.size() <= 64) EXPECT_EQ(RoundTrip(packet).rejectedSegments, segments); + } + } + } + } +} + +TEST_F(WorldPacketsTests, CreateCharacterMatchesLegacy) { + for (const auto objectID : g_ObjectIDs) { + for (const auto& xml : { std::string(), std::string(""), std::string(20000, 'x') }) { + for (const auto gm : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) { + for (const int64_t reputation : { int64_t{ 0 }, int64_t{ -5 }, std::numeric_limits::max() }) { + const std::u16string name = u"Some Name"; + const LWOCLONEID cloneID = 1234; + ClientPackets::CreateCharacter packet; + packet.objectID = objectID; + packet.xmlData = xml; + packet.name = name; + packet.gmLevel = gm; + packet.chatMode = static_cast(gm); + packet.reputation = reputation; + packet.propertyCloneID = cloneID; + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCreateCharacter(a, reputation, objectID, xml, name, gm, cloneID); }, packet); + + const auto copy = RoundTrip(packet); + EXPECT_EQ(copy.objectID, objectID); + EXPECT_EQ(copy.xmlData, xml); + EXPECT_EQ(copy.name, name); + EXPECT_EQ(copy.gmLevel, gm); + EXPECT_EQ(copy.reputation, reputation); + EXPECT_EQ(copy.propertyCloneID, 1234); + EXPECT_EQ(copy.templateID, 1); + } + } + } + } + + ClientPackets::CreateCharacter packet; + packet.xmlData = ""; + ExpectTruncatedFails(packet); +} + +TEST_F(WorldPacketsTests, CharacterListMatchesLegacy) { + FakeCharacter first; + FakeCharacter second; + second.objectID = 1152921508901814000; + second.name = "SomeName"; + second.unapprovedName = std::string(40, 'u'); + second.nameRejected = true; + second.shirtColor = 1; second.shirtStyle = 2; second.pantsColor = 3; second.hairStyle = 4; second.hairColor = 5; + second.leftHand = 6; second.rightHand = 7; second.eyebrows = 8; second.eyes = 9; second.mouth = 10; + second.zoneID = 1100; second.zoneInstance = 0x12345; second.zoneClone = 99; + second.lastLogin = 1790000000; + second.equippedItems = { 1, -1, 7000 }; + + const std::vector> lists = { {}, { &first }, { &first, &second, &second, &second } }; + for (const auto& list : lists) { + ClientPackets::CharacterListResponse packet; + for (const auto* character : list) { + auto& entry = packet.characters.emplace_back(); + entry.objectID = character->GetObjectID(); + entry.name = LUWString(character->GetName()); + entry.unapprovedName = LUWString(character->GetUnapprovedName()); + entry.nameRejected = character->GetNameRejected(); + entry.shirtColor = character->GetShirtColor(); + entry.shirtStyle = character->GetShirtStyle(); + entry.pantsColor = character->GetPantsColor(); + entry.hairStyle = character->GetHairStyle(); + entry.hairColor = character->GetHairColor(); + entry.leftHand = character->GetLeftHand(); + entry.rightHand = character->GetRightHand(); + entry.eyebrows = character->GetEyebrows(); + entry.eyes = character->GetEyes(); + entry.mouth = character->GetMouth(); + entry.zoneID = character->GetZoneID(); + entry.zoneInstance = character->GetZoneInstance(); + entry.zoneClone = character->GetZoneClone(); + entry.lastLogin = character->GetLastLogin(); + entry.equippedItems = character->GetEquippedItems(); + } + ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCharacterList(a, list); }, packet); + const auto copy = RoundTrip(packet); + ASSERT_EQ(copy.characters.size(), list.size()); + if (list.size() > 1) { + EXPECT_EQ(copy.characters[1].name.GetAsString(), "SomeName"); + EXPECT_EQ(copy.characters[1].equippedItems, second.equippedItems); + EXPECT_TRUE(copy.characters[1].nameRejected); + } + if (!list.empty()) ExpectTruncatedFails(packet); + } +} + +// ---- Client -> server ---- + +TEST_F(WorldPacketsTests, ValidationMatchesLegacy) { + for (const auto& username : g_Strings) { + for (const auto& checksum : { std::string(), std::string("0123456789abcdef0123456789abcdef") }) { + WorldPackets::Validation packet; + packet.username = LUWString(username); + packet.sessionKey = LUWString(std::string("fdce6d87c07128819c3a13cec99de30a")); + packet.fdbChecksum = LUString(checksum, 32); + ReceivedPacket received(packet); + const auto legacy = LegacyWorldPackets::ReadValidation(&received.packet); + WorldPackets::Validation read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.username.GetAsString(), legacy.username); + EXPECT_EQ(read.sessionKey.GetAsString(), legacy.sessionKey); + EXPECT_EQ(read.fdbChecksum.string, legacy.checksum); + RoundTrip(packet); + ExpectTruncatedFails(packet); + } + } + + // The optional trailing byte some clients send is ignored + WorldPackets::Validation packet; + packet.username = LUWString(std::string("user")); + RakNet::BitStream bytes; + packet.WritePacket(bytes); + bytes.Write(0); + WorldPackets::Validation read; + ASSERT_TRUE(read.ReadHeader(bytes)); + ASSERT_TRUE(read.Deserialize(bytes)); + EXPECT_EQ(read.username.GetAsString(), "user"); + + // Golden: header then the username's first characters + RakNet::BitStream golden; + packet.WritePacket(golden); + const auto all = FromBitStream(golden); + EXPECT_PACKET_EQ(FromHex("53 04 00 01 00 00 00 00 75 00 73 00 65 00 72 00 00 00"), (PacketBytes{ { all.bytes.begin(), all.bytes.begin() + 18 }, 18 * 8 })); + EXPECT_EQ(all.bytes.size(), 8 + 66 + 66 + 32); +} + +TEST_F(WorldPacketsTests, CharacterRequestsMatchLegacy) { + for (const auto objectID : g_ObjectIDs) { + WorldPackets::CharacterLoginRequest login; + login.playerID = objectID; + ReceivedPacket loginReceived(login); + WorldPackets::CharacterLoginRequest loginRead; + ASSERT_TRUE(Read(loginReceived, loginRead)); + EXPECT_EQ(loginRead.playerID, LegacyWorldPackets::ReadLoginRequest(&loginReceived.packet)); + ExpectTruncatedFails(login); + + WorldPackets::CharacterDeleteRequest deletion; + deletion.objectID = objectID; + ReceivedPacket deleteReceived(deletion); + WorldPackets::CharacterDeleteRequest deleteRead; + ASSERT_TRUE(Read(deleteReceived, deleteRead)); + EXPECT_EQ(deleteRead.objectID, LegacyWorldPackets::ReadDeleteCharacter(&deleteReceived.packet)); + ExpectTruncatedFails(deletion); + + for (const auto& name : g_Strings) { + WorldPackets::CharacterRenameRequest rename; + rename.objectID = objectID; + rename.name = LUWString(name); + ReceivedPacket renameReceived(rename); + WorldPackets::CharacterRenameRequest renameRead; + ASSERT_TRUE(Read(renameReceived, renameRead)); + const auto [legacyID, legacyName] = LegacyWorldPackets::ReadRenameCharacter(&renameReceived.packet); + EXPECT_EQ(renameRead.objectID, legacyID); + EXPECT_EQ(renameRead.name.GetAsString(), legacyName); + RoundTrip(rename); + } + } + + for (const auto& name : g_Strings) { + WorldPackets::CharacterCreateRequest create; + create.name = LUWString(name); + create.firstNameIndex = 1; create.middleNameIndex = 2; create.lastNameIndex = 0xFFFFFFFF; + create.unknown = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + create.shirtColor = 10; create.shirtStyle = 11; create.pantsColor = 12; create.hairStyle = 13; create.hairColor = 14; + create.leftHand = 15; create.rightHand = 16; create.eyebrows = 17; create.eyes = 18; create.mouth = 19; + ReceivedPacket received(create); + const auto legacy = LegacyWorldPackets::ReadCreateCharacter(&received.packet); + WorldPackets::CharacterCreateRequest read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.name.GetAsString(), legacy.LUWStringName.GetAsString()); + EXPECT_EQ(read.firstNameIndex, legacy.firstNameIndex); + EXPECT_EQ(read.middleNameIndex, legacy.middleNameIndex); + EXPECT_EQ(read.lastNameIndex, legacy.lastNameIndex); + EXPECT_EQ(read.shirtColor, legacy.shirtColor); + EXPECT_EQ(read.shirtStyle, legacy.shirtStyle); + EXPECT_EQ(read.pantsColor, legacy.pantsColor); + EXPECT_EQ(read.hairStyle, legacy.hairStyle); + EXPECT_EQ(read.hairColor, legacy.hairColor); + EXPECT_EQ(read.leftHand, legacy.lh); + EXPECT_EQ(read.rightHand, legacy.rh); + EXPECT_EQ(read.eyebrows, legacy.eyebrows); + EXPECT_EQ(read.eyes, legacy.eyes); + EXPECT_EQ(read.mouth, legacy.mouth); + RoundTrip(create); + ExpectTruncatedFails(create); + } + + WorldPackets::CharacterListRequest list; + RakNet::BitStream bytes; + list.WritePacket(bytes); + EXPECT_PACKET_EQ(FromHex("53 04 00 02 00 00 00 00"), FromBitStream(bytes)); +} + +TEST_F(WorldPacketsTests, GameMessageMatchesLegacy) { + for (const auto objectID : g_ObjectIDs) { + for (const uint32_t payloadBits : { 0u, 1u, 8u, 13u, 800u }) { + WorldPackets::GameMessage packet; + packet.objectID = objectID; + packet.messageID = MessageType::Game::REQUEST_USE; + for (uint32_t i = 0; i < payloadBits; i++) packet.data.Write(i % 3 == 0); + ReceivedPacket received(packet); + const auto legacy = LegacyWorldPackets::ReadGameMessage(&received.packet); + WorldPackets::GameMessage read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.objectID, legacy.objectID); + EXPECT_EQ(read.messageID, legacy.messageID); + // A packet is whole bytes, so the data read is the payload padded to a byte, as before + EXPECT_EQ(read.data.GetNumberOfBitsUsed(), legacy.dataBits); + EXPECT_PACKET_EQ((PacketBytes{ legacy.data, legacy.dataBits }), FromBitStream(read.data)); + } + } + + WorldPackets::GameMessage packet; + packet.objectID = 0x0102030405060708; + packet.messageID = static_cast(0x0A0B); + RakNet::BitStream bytes; + packet.WritePacket(bytes); + EXPECT_PACKET_EQ(FromHex("53 04 00 05 00 00 00 00 08 07 06 05 04 03 02 01 0b 0a"), FromBitStream(bytes)); + ExpectTruncatedFails(packet); +} + +TEST_F(WorldPacketsTests, PositionUpdateMatchesLegacy) { + for (int flags = 0; flags < 32; flags++) { + WorldPackets::PositionUpdate packet; + auto& update = packet.update; + update.position = NiPoint3(1.0f, -2.0f, 3.5f); + update.rotation = NiQuaternion(0.5f, 0.1f, 0.2f, 0.3f); + update.onGround = flags & 1; + update.onRail = !(flags & 1); + packet.hasVelocity = flags & 2; + update.velocity = packet.hasVelocity ? NiPoint3(4.0f, 5.0f, 6.0f) : NiPoint3Constant::ZERO; + packet.hasAngularVelocity = flags & 4; + update.angularVelocity = packet.hasAngularVelocity ? NiPoint3(7.0f, 8.0f, 9.0f) : NiPoint3Constant::ZERO; + packet.hasLocalSpaceInfo = flags & 8; + packet.hasLinearVelocity = packet.hasLocalSpaceInfo && (flags & 1); + if (packet.hasLocalSpaceInfo) { + update.localSpaceInfo.objectId = 1234; + update.localSpaceInfo.position = NiPoint3(10.0f, 11.0f, 12.0f); + if (packet.hasLinearVelocity) update.localSpaceInfo.linearVelocity = NiPoint3(13.0f, 14.0f, 15.0f); + } + packet.hasRemoteInputInfo = flags & 16; + if (packet.hasRemoteInputInfo) { + update.remoteInputInfo.m_RemoteInputX = 0.25f; + update.remoteInputInfo.m_RemoteInputY = -0.75f; + update.remoteInputInfo.m_IsPowersliding = true; + update.remoteInputInfo.m_IsModified = flags & 1; + } + + ReceivedPacket received(packet); + const auto legacy = LegacyWorldPackets::HandleClientPositionUpdate(&received.packet); + WorldPackets::PositionUpdate read; + ASSERT_TRUE(Read(received, read)); + const auto& got = read.update; + EXPECT_EQ(got.position, legacy.position); + EXPECT_EQ(got.rotation, legacy.rotation); + EXPECT_EQ(got.onGround, legacy.onGround); + EXPECT_EQ(got.onRail, legacy.onRail); + EXPECT_EQ(got.velocity, legacy.velocity); + EXPECT_EQ(got.angularVelocity, legacy.angularVelocity); + EXPECT_EQ(got.localSpaceInfo.objectId, legacy.localSpaceInfo.objectId); + EXPECT_EQ(got.localSpaceInfo.position, legacy.localSpaceInfo.position); + EXPECT_EQ(got.localSpaceInfo.linearVelocity, legacy.localSpaceInfo.linearVelocity); + auto remote = got.remoteInputInfo; + EXPECT_TRUE(remote == legacy.remoteInputInfo); + RoundTrip(packet); + } + + // Like before, the last two sections may be left out entirely + WorldPackets::PositionUpdate packet; + RakNet::BitStream bytes; + packet.WritePacket(bytes); + RakNet::BitStream shortened(bytes.GetData(), bytes.GetNumberOfBytesUsed(), true); + shortened.SetWriteOffset(bytes.GetNumberOfBitsUsed() - 2); // drop the two trailing flags + WorldPackets::PositionUpdate read; + ASSERT_TRUE(read.ReadHeader(shortened)); + EXPECT_TRUE(read.Deserialize(shortened)); + EXPECT_FALSE(read.hasLocalSpaceInfo); + EXPECT_FALSE(read.hasRemoteInputInfo); +} + +TEST_F(WorldPacketsTests, StringCheckMatchesLegacy) { + const std::vector receivers = { u"", u"Bob", u"[GM]Mythran", u"[GM]", std::u16string(42, u'r') }; + const std::vector messages = { u"", u"hi", u"hello there", std::u16string(300, u'm') }; + for (const auto& receiver : receivers) { + for (const auto& message : messages) { + for (const uint8_t chatLevel : { uint8_t{ 0 }, uint8_t{ 1 } }) { + WorldPackets::StringCheck packet; + packet.chatLevel = chatLevel; + packet.requestID = 0xa2; + packet.receiver = receiver; + packet.receiver.resize(42); + // what clients leave after the name + if (receiver.size() < 40) packet.receiver[receiver.size() + 1] = u'\x1894'; + packet.message = message; + ReceivedPacket received(packet); + const auto legacy = LegacyWorldPackets::HandleChatModerationRequest(&received.packet); + WorldPackets::StringCheck read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.chatLevel, legacy.chatLevel); + EXPECT_EQ(read.requestID, legacy.requestID); + EXPECT_EQ(read.GetNarrowReceiver(), legacy.receiver); + EXPECT_EQ(read.GetNarrowMessage(), legacy.message); + RoundTrip(packet); + ExpectTruncatedFails(packet); + } + } + } +} + +TEST_F(WorldPacketsTests, GeneralChatMessageMatchesLegacy) { + for (const auto& message : { std::u16string(), std::u16string(u"how long are you going to stay there?"), std::u16string(2000, u'x') }) { + for (const uint8_t channel : { uint8_t{ 0 }, uint8_t{ 4 } }) { + WorldPackets::GeneralChatMessage packet; + packet.chatChannel = channel; + packet.unknown = 0x0900; + packet.message = message; + ReceivedPacket received(packet); + const auto legacy = LegacyWorldPackets::HandleChatMessage(&received.packet); + WorldPackets::GeneralChatMessage read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.chatChannel, legacy.chatChannel); + EXPECT_EQ(read.unknown, legacy.unknown); + EXPECT_EQ(read.message, legacy.message); + RoundTrip(packet); + } + } + + WorldPackets::GeneralChatMessage packet; + packet.chatChannel = 4; + packet.message = u"hi"; + RakNet::BitStream bytes; + packet.WritePacket(bytes); + // channel | unknown u16 | count 3 (includes the terminator) | "hi" | terminator + EXPECT_PACKET_EQ(FromHex("53 04 00 0e 00 00 00 00 04 00 00 03 00 00 00 68 00 69 00 00 00"), FromBitStream(bytes)); + + // A negative or huge count is dropped + for (const int32_t count : { -1, static_cast(MAX_MESSAGE_LENGTH) + 1 }) { + RakNet::BitStream bad; + LUBitStream(ServiceType::WORLD, MessageType::World::GENERAL_CHAT_MESSAGE).WriteHeader(bad); + bad.Write(0); + bad.Write(0); + bad.Write(count); + WorldPackets::GeneralChatMessage read; + ASSERT_TRUE(read.ReadHeader(bad)); + EXPECT_FALSE(read.Deserialize(bad)); + } +} + +TEST_F(WorldPacketsTests, RoutePacketMatchesLegacy) { + for (const uint32_t bodySize : { 0u, 1u, 4u, 20u }) { + for (const uint32_t declaredSize : { 0u, 2u, 11u, 100u, 20001u }) { + for (const LWOOBJID sender : { LWOOBJID{ 0 }, LWOOBJID{ 1152921508901814000 } }) { + WorldPackets::RoutePacket packet; + packet.size = declaredSize; + packet.routedService = ServiceType::CHAT; + packet.routedMessageID = 0x10A; + for (uint32_t i = 0; i < bodySize; i++) packet.routedData.push_back(static_cast(i + 1)); + ReceivedPacket received(packet); + + RakNet::BitStream legacy; + const bool legacyForwarded = LegacyWorldPackets::BuildRoutePacket(&received.packet, sender, legacy); + + WorldPackets::RoutePacket read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.routedData, packet.routedData); + EXPECT_EQ(legacyForwarded, read.size <= 20000); // the world server's size check + if (!legacyForwarded) continue; + + RakNet::BitStream converted; + read.ToChat(sender).WritePacket(converted); + EXPECT_PACKET_EQ(FromBitStream(legacy), FromBitStream(converted)); + RoundTrip(packet); + } + } + } + + WorldPackets::RoutePacket packet; + ExpectTruncatedFails(packet); +} + +TEST_F(WorldPacketsTests, SmallRequestsMatchLegacy) { + for (const auto type : magic_enum::enum_values()) { + for (const float info : { 0.0f, 1.0f, 6.5f, -3.0f }) { + WorldPackets::HandleFunness packet; + packet.cheatType = type; + packet.cheatInfo = info; + ReceivedPacket received(packet); + const auto legacy = LegacyWorldPackets::ReadFunness(&received.packet); + WorldPackets::HandleFunness read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.cheatType, legacy.cheatType); + EXPECT_EQ(read.cheatInfo, legacy.cheatInfo); + ExpectTruncatedFails(packet); + } + } + + for (const int32_t language : { 0, 1, 2, 3, -1 }) { + WorldPackets::UIHelpTop5 packet; + packet.language = language; + ReceivedPacket received(packet); + WorldPackets::UIHelpTop5 read; + ASSERT_TRUE(Read(received, read)); + EXPECT_EQ(read.language, LegacyWorldPackets::SendTop5HelpIssues(&received.packet)); + ExpectTruncatedFails(packet); + } + + WorldPackets::LevelLoadComplete level; + level.mapID = 1101; + level.instanceID = 46671; + RakNet::BitStream bytes; + level.WritePacket(bytes); + EXPECT_PACKET_EQ(FromHex("53 04 00 13 00 00 00 00 4d 04 4f b6 00 00 00 00"), FromBitStream(bytes)); + EXPECT_EQ(RoundTrip(level).instanceID, 46671); + ExpectTruncatedFails(level); + + WorldPackets::MailPacket mail; + mail.data.Write(0x01020304); + RakNet::BitStream mailBytes; + mail.WritePacket(mailBytes); + WorldPackets::MailPacket mailRead; + ASSERT_TRUE(mailRead.ReadHeader(mailBytes)); + ASSERT_TRUE(mailRead.Deserialize(mailBytes)); + EXPECT_PACKET_EQ(FromBitStream(mail.data), FromBitStream(mailRead.data)); +}