From 796fd1b9418dd51f8b51b205d7df63957ee656d7 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 19:08:13 -0500 Subject: [PATCH] refactor: chat packets as structs Every packet of the chat service is now an LUBitStream struct in dNet/ChatPackets.h (docs/PacketArchitecture.md, PR 13), and nothing in the chat server or the chat side of the world server reads or writes a packet by hand any more. - World <-> chat: LoginSessionNotify, UnexpectedDisconnect, GMLevelUpdate, GMMute, Announcement (GM announce), CreateTeam, TeamUpdate (TEAM_GET_STATUS to worlds), AchievementNotify, ShowAllRequest, FindPlayerRequest. - Client -> world -> chat (friends, ignore list, teams, general and private chat): GetFriendsList, AddFriendRequest, AddFriendResponse, RemoveFriend, GetIgnoreList, AddIgnore, RemoveIgnore, GeneralChatMessage, PrivateChatMessage, TeamInvite, TeamInviteResponse, TeamLeave, TeamKick, TeamSetLeader, TeamSetLoot, TeamGetStatus. The 77 bytes the handlers skipped are named fields now (the sender block the client fills in); the 4 unused bytes after the player ID are kept as `unknown`. - What the client receives. Chat service (ChatPackets::Client): GeneralChatMessage (replaces SendChatMessage; SendSystemMessage stays as a helper built on it), PrivateChatMessage. Client service (ClientPackets, as structs go in the file of the ServiceType in their header): SendCannedText (replaces SendMessageFail), GetFriendsListResponse, AddFriendRequest, AddFriendResponse, RemoveFriendResponse, UpdateFriendNotify, WhoResponse, ShowAllResponse, Get/Add/RemoveIgnoreResponse, TeamInvite, TeamInviteInitialResponse, and the team game messages chat writes (TeamInviteConfirm, TeamGetStatusResponse, TeamSetLeader, TeamAddPlayer, TeamRemovePlayer, TeamSetOffWorldFlag) as TeamGameMsg structs, since chat doesn't link dGame's NetGameMsg. - WORLD_ROUTE_PACKET is ChatPackets::WorldRoutePacket (its own file, dNet/WorldRoutePacket.h, since it carries packets of other services): the target and the inner packet. ChatPacketHandler::SendRouted replaces the per-function route headers and SendRoutedMsg. Dispatch: dNet/PacketDispatcher.h is a dispatch map from packet ID to (struct, handler function); packets that fail to Deserialize are logged and dropped. The chat server's switch and the world's HandlePacketChat switch are now maps. The handlers take the structs; their logic is unchanged. World code sends to chat with ChatServerLink::Send (dGame) instead of writing to Game::chatServer by hand. eChatChannel, eChatMessageResponseCode and eAddIgnoreResponse moved to dCommon/dEnums so the structs can use them. Verified: tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h is a verbatim copy of the old senders and readers; ChatPacketsTests (25 tests) sends a grid of inputs through both and requires identical bits, bytes and destination, checks the old readers read what the structs write, round trips every struct, checks truncated packets are refused, and has hand written golden packets for general chat, canned text, GM mute and a routed team game message. Breaking one field width (UpdateFriendNotify) and the TeamAddPlayer zone flag made the tests fail. No wire bytes changed. Co-Authored-By: Claude Opus 5.5 --- dChatServer/ChatIgnoreList.cpp | 82 +- dChatServer/ChatIgnoreList.h | 17 +- dChatServer/ChatPacketHandler.cpp | 321 ++---- dChatServer/ChatPacketHandler.h | 65 +- dChatServer/ChatServer.cpp | 214 ++-- dChatServer/PlayerContainer.cpp | 56 +- dChatServer/PlayerContainer.h | 7 +- dChatServer/TeamContainer.cpp | 309 ++---- dChatServer/TeamContainer.h | 19 +- dCommon/dEnums/eAddIgnoreResponse.h | 13 + dCommon/dEnums/eChatChannel.h | 32 + dCommon/dEnums/eChatMessageResponseCode.h | 16 + dGame/Entity.cpp | 12 +- dGame/User.cpp | 11 +- dGame/UserManager.cpp | 9 +- dGame/dComponents/ActivityComponent.cpp | 15 +- dGame/dMission/Mission.cpp | 8 +- dGame/dPropertyBehaviors/Strip.cpp | 9 +- dGame/dUtilities/CMakeLists.txt | 1 + dGame/dUtilities/ChatServerLink.cpp | 13 + dGame/dUtilities/ChatServerLink.h | 14 + dGame/dUtilities/DashboardActions.cpp | 6 +- dGame/dUtilities/SlashCommandHandler.cpp | 20 +- .../GMGreaterThanZeroCommands.cpp | 38 +- dNet/CMakeLists.txt | 1 + dNet/ChatPackets.cpp | 590 ++++++++--- dNet/ChatPackets.h | 372 ++++++- dNet/ClientPackets.cpp | 385 ++++++++ dNet/ClientPackets.h | 246 +++++ dNet/PacketDispatcher.h | 77 ++ dNet/WorldRoutePacket.cpp | 17 + dNet/WorldRoutePacket.h | 31 + dWorldServer/WorldServer.cpp | 234 ++--- docs/PacketArchitecture.md | 10 + tests/dGameTests/CMakeLists.txt | 1 + tests/dGameTests/dNetTests/CMakeLists.txt | 1 + .../dGameTests/dNetTests/ChatPacketsTests.cpp | 900 +++++++++++++++++ .../dNetTests/Legacy/ChatPacketsLegacy.h | 913 ++++++++++++++++++ 38 files changed, 3944 insertions(+), 1141 deletions(-) create mode 100644 dCommon/dEnums/eAddIgnoreResponse.h create mode 100644 dCommon/dEnums/eChatChannel.h create mode 100644 dCommon/dEnums/eChatMessageResponseCode.h create mode 100644 dGame/dUtilities/ChatServerLink.cpp create mode 100644 dGame/dUtilities/ChatServerLink.h create mode 100644 dNet/PacketDispatcher.h create mode 100644 dNet/WorldRoutePacket.cpp create mode 100644 dNet/WorldRoutePacket.h create mode 100644 tests/dGameTests/dNetTests/ChatPacketsTests.cpp create mode 100644 tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h diff --git a/dChatServer/ChatIgnoreList.cpp b/dChatServer/ChatIgnoreList.cpp index 661e74f99..1b315e658 100644 --- a/dChatServer/ChatIgnoreList.cpp +++ b/dChatServer/ChatIgnoreList.cpp @@ -8,23 +8,14 @@ #include "eObjectBits.h" #include "Database.h" +#include "ChatPacketHandler.h" // A note to future readers, The client handles all the actual ignoring logic: // not allowing teams, rejecting DMs, friends requets etc. // The only thing not auto-handled is instance activities force joining the team on the server. -void WriteOutgoingReplyHeader(RakNet::BitStream& bitStream, const LWOOBJID& receivingPlayer, const MessageType::Client type) { - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receivingPlayer); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, type); -} - -void ChatIgnoreList::GetIgnoreList(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerId; - inStream.Read(playerId); +void ChatIgnoreList::GetIgnoreList(const ChatPackets::GetIgnoreList& request, const SystemAddress& sysAddr) { + const LWOOBJID playerId = request.playerID; auto& receiver = Game::playerContainer.GetPlayerDataMutable(playerId); if (!receiver) { @@ -47,25 +38,17 @@ void ChatIgnoreList::GetIgnoreList(Packet* packet) { } } - CBITSTREAM; - WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::GET_IGNORE_LIST_RESPONSE); - - bitStream.Write(false); // Is Free Trial, but we don't care about that - bitStream.Write(0); // literally spacing due to struct alignment - - bitStream.Write(receiver.ignoredPlayers.size()); + ClientPackets::GetIgnoreListResponse response; + response.isFreeTrial = false; // Is Free Trial, but we don't care about that for (const auto& ignoredPlayer : receiver.ignoredPlayers) { - bitStream.Write(ignoredPlayer.playerId); - bitStream.Write(LUWString(ignoredPlayer.playerName, 36)); + response.ignored.push_back({ ignoredPlayer.playerId, LUWString(ignoredPlayer.playerName, 36) }); } - Game::server->Send(bitStream, packet->systemAddress, false); + ChatPacketHandler::SendRouted(receiver.playerID, sysAddr, response); } -void ChatIgnoreList::AddIgnore(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerId; - inStream.Read(playerId); +void ChatIgnoreList::AddIgnore(const ChatPackets::AddIgnore& request, const SystemAddress& sysAddr) { + const LWOOBJID playerId = request.playerID; auto& receiver = Game::playerContainer.GetPlayerDataMutable(playerId); if (!receiver) { @@ -79,25 +62,20 @@ void ChatIgnoreList::AddIgnore(Packet* packet) { return; } - inStream.IgnoreBytes(4); // ignore some garbage zeros idk + std::string toIgnoreStr = request.playerName.GetAsString(); - LUWString toIgnoreName; - inStream.Read(toIgnoreName); - std::string toIgnoreStr = toIgnoreName.GetAsString(); - - CBITSTREAM; - WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::ADD_IGNORE_RESPONSE); + ClientPackets::AddIgnoreResponse response; // Check if the player exists LWOOBJID ignoredPlayerId = LWOOBJID_EMPTY; if (toIgnoreStr == receiver.playerName || toIgnoreStr.find("[GM]") == 0) { LOG_DEBUG("Player %llu tried to ignore themselves", playerId); - bitStream.Write(ChatIgnoreList::AddResponse::GENERAL_ERROR); + response.responseCode = eAddIgnoreResponse::GENERAL_ERROR; } else if (std::count(receiver.ignoredPlayers.begin(), receiver.ignoredPlayers.end(), toIgnoreStr) > 0) { LOG_DEBUG("Player %llu is already ignoring %s", playerId, toIgnoreStr.c_str()); - bitStream.Write(ChatIgnoreList::AddResponse::ALREADY_IGNORED); + response.responseCode = eAddIgnoreResponse::ALREADY_IGNORED; } else { // Get the playerId falling back to query if not online const auto& playerData = Game::playerContainer.GetPlayerData(toIgnoreStr); @@ -120,23 +98,20 @@ void ChatIgnoreList::AddIgnore(Packet* packet) { receiver.ignoredPlayers.emplace_back(toIgnoreStr, ignoredPlayerId); LOG_DEBUG("Player %llu is ignoring %s", playerId, toIgnoreStr.c_str()); - bitStream.Write(ChatIgnoreList::AddResponse::SUCCESS); + response.responseCode = eAddIgnoreResponse::SUCCESS; } else { - bitStream.Write(ChatIgnoreList::AddResponse::PLAYER_NOT_FOUND); + response.responseCode = eAddIgnoreResponse::PLAYER_NOT_FOUND; } } - LUWString playerNameSend(toIgnoreStr, 33); - bitStream.Write(playerNameSend); - bitStream.Write(ignoredPlayerId); + response.playerName = LUWString(toIgnoreStr, 33); + response.playerID = ignoredPlayerId; - Game::server->Send(bitStream, packet->systemAddress, false); + ChatPacketHandler::SendRouted(receiver.playerID, sysAddr, response); } -void ChatIgnoreList::RemoveIgnore(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerId; - inStream.Read(playerId); +void ChatIgnoreList::RemoveIgnore(const ChatPackets::RemoveIgnore& request, const SystemAddress& sysAddr) { + const LWOOBJID playerId = request.playerID; auto& receiver = Game::playerContainer.GetPlayerDataMutable(playerId); if (!receiver) { @@ -144,11 +119,7 @@ void ChatIgnoreList::RemoveIgnore(Packet* packet) { return; } - inStream.IgnoreBytes(4); // ignore some garbage zeros idk - - LUWString removedIgnoreName; - inStream.Read(removedIgnoreName); - std::string removedIgnoreStr = removedIgnoreName.GetAsString(); + std::string removedIgnoreStr = request.playerName.GetAsString(); auto toRemove = std::remove(receiver.ignoredPlayers.begin(), receiver.ignoredPlayers.end(), removedIgnoreStr); if (toRemove == receiver.ignoredPlayers.end()) { @@ -159,12 +130,9 @@ void ChatIgnoreList::RemoveIgnore(Packet* packet) { Database::Get()->RemoveIgnore(playerId, toRemove->playerId); receiver.ignoredPlayers.erase(toRemove, receiver.ignoredPlayers.end()); - CBITSTREAM; - WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::REMOVE_IGNORE_RESPONSE); + ClientPackets::RemoveIgnoreResponse response; + response.responseCode = 0; + response.playerName = LUWString(removedIgnoreStr, 33); - bitStream.Write(0); - LUWString playerNameSend(removedIgnoreStr, 33); - bitStream.Write(playerNameSend); - - Game::server->Send(bitStream, packet->systemAddress, false); + ChatPacketHandler::SendRouted(receiver.playerID, sysAddr, response); } diff --git a/dChatServer/ChatIgnoreList.h b/dChatServer/ChatIgnoreList.h index ea20deb51..ca8911b0d 100644 --- a/dChatServer/ChatIgnoreList.h +++ b/dChatServer/ChatIgnoreList.h @@ -1,26 +1,21 @@ #ifndef __CHATIGNORELIST__H__ #define __CHATIGNORELIST__H__ -struct Packet; +struct SystemAddress; #include +#include "ChatPackets.h" + /** * @brief The ignore list allows players to ignore someone silently. Requests will generally be blocked by the client, but they should also be checked * on the server as well so the sender can get a generic error code in response. * */ namespace ChatIgnoreList { - void GetIgnoreList(Packet* packet); - void AddIgnore(Packet* packet); - void RemoveIgnore(Packet* packet); - - enum class AddResponse : uint8_t { - SUCCESS, - ALREADY_IGNORED, - PLAYER_NOT_FOUND, - GENERAL_ERROR, - }; + void GetIgnoreList(const ChatPackets::GetIgnoreList& request, const SystemAddress& sysAddr); + void AddIgnore(const ChatPackets::AddIgnore& request, const SystemAddress& sysAddr); + void RemoveIgnore(const ChatPackets::RemoveIgnore& request, const SystemAddress& sysAddr); }; #endif //!__CHATIGNORELIST__H__ diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index e5d512820..a48ff0eaa 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -22,11 +22,19 @@ #include "ChatPackets.h" #include "TeamContainer.h" -void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) { +void ChatPacketHandler::SendRouted(const LWOOBJID target, const SystemAddress& world, const LUBitStream& msg, const bool broadcast) { + ChatPackets::WorldRoutePacket route; + route.targetID = target; + route.routed = &msg; + + RakNet::BitStream bitStream; + route.WritePacket(bitStream); + Game::server->Send(bitStream, world, broadcast); +} + +void ChatPacketHandler::HandleFriendlistRequest(const ChatPackets::GetFriendsList& request, const SystemAddress& sysAddr) { //Get from the packet which player we want to do something with: - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = 0; - inStream.Read(playerID); + const LWOOBJID playerID = request.playerID; auto& player = Game::playerContainer.GetPlayerDataMutable(playerID); if (!player) return; @@ -60,37 +68,16 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) { } //Now, we need to send the friendlist to the server they came from: - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::GET_FRIENDS_LIST_RESPONSE); - bitStream.Write(0); - bitStream.Write(1); //Length of packet -- just writing one as it doesn't matter, client skips it. - bitStream.Write(player.friends.size()); - - for (const auto& data : player.friends) { - data.Serialize(bitStream); - } - - SystemAddress sysAddr = player.worldServerSysAddr; - SEND_PACKET; + ClientPackets::GetFriendsListResponse response; + response.friends = player.friends; + SendRouted(playerID, player.worldServerSysAddr, response); } -void ChatPacketHandler::HandleFriendRequest(Packet* packet) { - CINSTREAM_SKIP_HEADER; +void ChatPacketHandler::HandleFriendRequest(const ChatPackets::AddFriendRequest& request, const SystemAddress& sysAddr) { + const LWOOBJID requestorPlayerID = request.playerID; + const char isBestFriendRequest = request.isBestFriendRequest; - LWOOBJID requestorPlayerID; - LUWString LUplayerName; - char isBestFriendRequest{}; - - inStream.Read(requestorPlayerID); - inStream.IgnoreBytes(4); - inStream.Read(LUplayerName); - inStream.Read(isBestFriendRequest); - - auto playerName = LUplayerName.GetAsString(); + auto playerName = request.friendName.GetAsString(); auto& requestor = Game::playerContainer.GetPlayerDataMutable(requestorPlayerID); if (!requestor) { @@ -225,17 +212,10 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) { } } -void ChatPacketHandler::HandleFriendResponse(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID; - eAddFriendResponseCode clientResponseCode; - LUWString friendName; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(clientResponseCode); - inStream.Read(friendName); +void ChatPacketHandler::HandleFriendResponse(const ChatPackets::AddFriendResponse& response, const SystemAddress& sysAddr) { + const LWOOBJID playerID = response.playerID; + const eAddFriendResponseCode clientResponseCode = response.responseCode; + const LUWString& friendName = response.friendName; //Now to try and find both of these: auto& requestor = Game::playerContainer.GetPlayerDataMutable(playerID); @@ -298,14 +278,9 @@ void ChatPacketHandler::HandleFriendResponse(Packet* packet) { if (serverResponseCode != eAddFriendResponseType::ALREADYFRIEND) SendFriendResponse(requestee, requestor, serverResponseCode, isAlreadyBestFriends); } -void ChatPacketHandler::HandleRemoveFriend(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - LUWString LUFriendName; - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(LUFriendName); - auto friendName = LUFriendName.GetAsString(); +void ChatPacketHandler::HandleRemoveFriend(const ChatPackets::RemoveFriend& request, const SystemAddress& sysAddr) { + const LWOOBJID playerID = request.playerID; + auto friendName = request.friendName.GetAsString(); //we'll have to query the db here to find the user, since you can delete them while they're offline. //First, we need to find their ID: @@ -350,20 +325,14 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) { SendRemoveFriend(goonB, goonAName, true); } -void ChatPacketHandler::HandleGMLevelUpdate(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); - auto& player = Game::playerContainer.GetPlayerData(playerID); +void ChatPacketHandler::HandleGMLevelUpdate(const ChatPackets::GMLevelUpdate& update, const SystemAddress& sysAddr) { + auto& player = Game::playerContainer.GetPlayerDataMutable(update.playerID); if (!player) return; - inStream.Read(player.gmLevel); + player.gmLevel = update.gmLevel; } -void ChatPacketHandler::HandleWho(Packet* packet) { - CINSTREAM_SKIP_HEADER; - FindPlayerRequest request; - request.Deserialize(inStream); +void ChatPacketHandler::HandleWho(const ChatPackets::FindPlayerRequest& request, const SystemAddress& sysAddr) { const auto& sender = Game::playerContainer.GetPlayerData(request.requestor); if (!sender) return; @@ -371,53 +340,29 @@ void ChatPacketHandler::HandleWho(Packet* packet) { const auto& player = Game::playerContainer.GetPlayerData(request.playerName.GetAsString()); bool online = player; - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(request.requestor); - - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::WHO_RESPONSE); - bitStream.Write(online); - bitStream.Write(player.zoneID.GetMapID()); - bitStream.Write(player.zoneID.GetInstanceID()); - bitStream.Write(player.zoneID.GetCloneID()); - bitStream.Write(request.playerName); - - SystemAddress sysAddr = sender.worldServerSysAddr; - SEND_PACKET; + ClientPackets::WhoResponse response; + response.isOnline = online; + response.zoneID = player.zoneID; + response.playerName = request.playerName; + SendRouted(request.requestor, sender.worldServerSysAddr, response); } -void ChatPacketHandler::HandleShowAll(Packet* packet) { - CINSTREAM_SKIP_HEADER; - ShowAllRequest request; - request.Deserialize(inStream); - +void ChatPacketHandler::HandleShowAll(const ChatPackets::ShowAllRequest& request, const SystemAddress& sysAddr) { const auto& sender = Game::playerContainer.GetPlayerData(request.requestor); if (!sender) return; - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(request.requestor); - - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SHOW_ALL_RESPONSE); - bitStream.Write(!request.displayZoneData && !request.displayIndividualPlayers); - bitStream.Write(Game::playerContainer.GetPlayerCount()); - bitStream.Write(Game::playerContainer.GetSimCount()); - bitStream.Write(request.displayIndividualPlayers); - bitStream.Write(request.displayZoneData); + ClientPackets::ShowAllResponse response; + response.playerCount = Game::playerContainer.GetPlayerCount(); + response.simCount = Game::playerContainer.GetSimCount(); + response.displayIndividualPlayers = request.displayIndividualPlayers; + response.displayZoneData = request.displayZoneData; if (request.displayZoneData || request.displayIndividualPlayers) { for (auto& [playerID, playerData] : Game::playerContainer.GetAllPlayers()) { if (!playerData) continue; - bitStream.Write(0); // structure packing - if (request.displayIndividualPlayers) bitStream.Write(LUWString(playerData.playerName)); - if (request.displayZoneData) { - bitStream.Write(playerData.zoneID.GetMapID()); - bitStream.Write(playerData.zoneID.GetInstanceID()); - bitStream.Write(playerData.zoneID.GetCloneID()); - } + response.players.push_back({ playerData.playerName, playerData.zoneID }); } } - SystemAddress sysAddr = sender.worldServerSysAddr; - SEND_PACKET; + SendRouted(request.requestor, sender.worldServerSysAddr, response); } // the structure the client uses to send this packet is shared in many chat messages @@ -446,29 +391,15 @@ namespace { } } -void ChatPacketHandler::HandleChatMessage(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); +void ChatPacketHandler::HandleChatMessage(const ChatPackets::GeneralChatMessage& chatMessage, const SystemAddress& sysAddr) { + const LWOOBJID playerID = chatMessage.playerID; const auto& sender = Game::playerContainer.GetPlayerData(playerID); if (!sender || sender.GetIsMuted()) return; - eChatChannel channel; - uint32_t size; - - inStream.IgnoreBytes(4); - inStream.Read(channel); - inStream.Read(size); - if (size > MAX_MESSAGE_LENGTH) { - LOG("Received a probably spoofed chat message, ignoring msg"); - return; - } - - inStream.IgnoreBytes(77); - - LUWString message(size); - inStream.Read(message); + // Messages longer than MAX_MESSAGE_LENGTH were dropped when read + const eChatChannel channel = chatMessage.chatChannel; + const LUWString& message = chatMessage.message; LOG("Got a message from (%s) via [%s]: %s", sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str()); @@ -493,36 +424,18 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { // the structure the client uses to send this packet is shared in many chat messages // that are sent to the server. Because of this, there are large gaps of unused data in chat messages -void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); +void ChatPacketHandler::HandlePrivateChatMessage(const ChatPackets::PrivateChatMessage& chatMessage, const SystemAddress& sysAddr) { + const LWOOBJID playerID = chatMessage.playerID; const auto& sender = Game::playerContainer.GetPlayerData(playerID); if (!sender || sender.GetIsMuted()) return; - eChatChannel channel; - uint32_t size; - LUWString LUReceiverName; - - inStream.IgnoreBytes(4); - inStream.Read(channel); + const eChatChannel channel = chatMessage.chatChannel; if (channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!"); - inStream.Read(size); - if (size > MAX_MESSAGE_LENGTH) { - LOG("Received a probably spoofed chat message, ignoring msg"); - return; - } - - inStream.IgnoreBytes(77); - - inStream.Read(LUReceiverName); - auto receiverName = LUReceiverName.GetAsString(); - inStream.IgnoreBytes(2); - - LUWString message(size); - inStream.Read(message); + // Messages longer than MAX_MESSAGE_LENGTH were dropped when read + auto receiverName = chatMessage.receiverName.GetAsString(); + const LUWString& message = chatMessage.message; LOG("Got a message from (%s) via [%s]: %s to %s", sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str(), receiverName.c_str()); @@ -553,9 +466,7 @@ void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) { SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::GENERAL, eChatMessageResponseCode::NOTFRIENDS); } -void ChatPacketHandler::OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr) { - ChatPackets::AchievementNotify notify{}; - notify.Deserialize(bitstream); +void ChatPacketHandler::OnAchievementNotify(ChatPackets::AchievementNotify& notify, const SystemAddress& sysAddr) { const auto& playerData = Game::playerContainer.GetPlayerData(notify.earnerName.GetAsString()); if (!playerData) return; @@ -565,36 +476,23 @@ void ChatPacketHandler::OnAchievementNotify(RakNet::BitStream& bitstream, const notify.targetPlayerName.string = GeneralUtils::ASCIIToUTF16(friendData.playerName); LOG_DEBUG("Sending achievement notify to %s", notify.targetPlayerName.GetAsString().c_str()); - RakNet::BitStream worldStream; - BitStreamUtils::WriteHeader(worldStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - worldStream.Write(friendData.playerID); - notify.WriteHeader(worldStream); - notify.Serialize(worldStream); - Game::server->Send(worldStream, friendData.worldServerSysAddr, false); + SendRouted(friendData.playerID, friendData.worldServerSysAddr, notify); } } } void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(routeTo.playerID); - - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE); - bitStream.Write(sender.playerID); - bitStream.Write(channel); - bitStream.Write(0); // not used - bitStream.Write(LUWString(sender.playerName)); - bitStream.Write(sender.playerID); - bitStream.Write(0); // sourceID - bitStream.Write(sender.gmLevel); - bitStream.Write(LUWString(receiver.playerName)); - bitStream.Write(receiver.gmLevel); - bitStream.Write(responseCode); - bitStream.Write(message); - - SystemAddress sysAddr = routeTo.worldServerSysAddr; - SEND_PACKET; + ChatPackets::Client::PrivateChatMessage chatMessage; + chatMessage.playerID = sender.playerID; + chatMessage.chatChannel = channel; + chatMessage.senderName = LUWString(sender.playerName); + chatMessage.senderID = sender.playerID; + chatMessage.senderGMLevel = sender.gmLevel; + chatMessage.receiverName = LUWString(receiver.playerName); + chatMessage.receiverGMLevel = receiver.gmLevel; + chatMessage.responseCode = responseCode; + chatMessage.message = message; + SendRouted(routeTo.playerID, routeTo.worldServerSysAddr, chatMessage); } void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend) { @@ -611,32 +509,21 @@ void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const Pla [bool] - is best friend [bool] - is FTP*/ - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(friendData.playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::UPDATE_FRIEND_NOTIFY); - bitStream.Write(notifyType); + ClientPackets::UpdateFriendNotify notify; + notify.notifyType = notifyType; std::string playerName = playerData.playerName.c_str(); - bitStream.Write(LUWString(playerName)); + notify.friendName = LUWString(playerName); - bitStream.Write(playerData.zoneID.GetMapID()); - bitStream.Write(playerData.zoneID.GetInstanceID()); + const auto& zoneID = playerData.zoneID; + const LWOCLONEID cloneID = zoneID.GetCloneID() == friendData.zoneID.GetCloneID() ? 0 : zoneID.GetCloneID(); + notify.zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), cloneID); - if (playerData.zoneID.GetCloneID() == friendData.zoneID.GetCloneID()) { - bitStream.Write(0); - } else { - bitStream.Write(playerData.zoneID.GetCloneID()); - } + notify.isBestFriend = isBestFriend; //isBFF + notify.isFreeTrial = 0; //isFTP - bitStream.Write(isBestFriend); //isBFF - bitStream.Write(0); //isFTP - - SystemAddress sysAddr = friendData.worldServerSysAddr; - SEND_PACKET; + SendRouted(friendData.playerID, friendData.worldServerSysAddr, notify); } void ChatPacketHandler::SendFriendRequest(const PlayerData& receiver, const PlayerData& sender) { @@ -648,52 +535,30 @@ void ChatPacketHandler::SendFriendRequest(const PlayerData& receiver, const Play } } - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_REQUEST); - bitStream.Write(LUWString(sender.playerName)); - bitStream.Write(0); // This is a BFF flag however this is unused in live and does not have an implementation client side. - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::AddFriendRequest request; + request.requestorName = LUWString(sender.playerName); + request.isBestFriendRequest = 0; // This is a BFF flag however this is unused in live and does not have an implementation client side. + SendRouted(receiver.playerID, receiver.worldServerSysAddr, request); } void ChatPacketHandler::SendFriendResponse(const PlayerData& receiver, const PlayerData& sender, eAddFriendResponseType responseCode, uint8_t isBestFriendsAlready, uint8_t isBestFriendRequest) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - // Portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_RESPONSE); - bitStream.Write(responseCode); + ClientPackets::AddFriendResponse response; + response.responseCode = responseCode; // For all requests besides accepted, write a flag that says whether or not we are already best friends with the receiver. - bitStream.Write(responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS); + response.isOnlineOrBestFriend = responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS; // Then write the player name - bitStream.Write(LUWString(sender.playerName)); + response.friendName = LUWString(sender.playerName); // Then if this is an acceptance code, write the following extra info. - if (responseCode == eAddFriendResponseType::ACCEPTED) { - bitStream.Write(sender.playerID); - bitStream.Write(sender.zoneID); - bitStream.Write(isBestFriendRequest); //isBFF - bitStream.Write(0); //isFTP - } - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + response.friendID = sender.playerID; + response.zoneID = sender.zoneID; + response.isBestFriend = isBestFriendRequest; //isBFF + response.isFreeTrial = 0; //isFTP + SendRouted(receiver.playerID, receiver.worldServerSysAddr, response); } void ChatPacketHandler::SendRemoveFriend(const PlayerData& receiver, std::string& personToRemove, bool isSuccessful) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::REMOVE_FRIEND_RESPONSE); - bitStream.Write(isSuccessful); //isOnline - bitStream.Write(LUWString(personToRemove)); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::RemoveFriendResponse response; + response.isSuccessful = isSuccessful; //isOnline + response.friendName = LUWString(personToRemove); + SendRouted(receiver.playerID, receiver.worldServerSysAddr, response); } diff --git a/dChatServer/ChatPacketHandler.h b/dChatServer/ChatPacketHandler.h index 29a205874..fe48f357c 100644 --- a/dChatServer/ChatPacketHandler.h +++ b/dChatServer/ChatPacketHandler.h @@ -2,60 +2,31 @@ #include "dCommonVars.h" #include "dNetCommon.h" #include "BitStream.h" +#include "ChatPackets.h" +#include "ClientPackets.h" +#include "WorldRoutePacket.h" +#include "eChatChannel.h" +#include "eChatMessageResponseCode.h" struct PlayerData; +struct LUBitStream; enum class eAddFriendResponseType : uint8_t; -enum class eChatChannel : uint8_t { - SYSTEMNOTIFY = 0, - SYSTEMWARNING, - SYSTEMERROR, - BROADCAST, - LOCAL, - LOCALNOANIM, - EMOTE, - PRIVATE_CHAT, - TEAM, - TEAMLOCAL, - GUILD, - GUILDNOTIFY, - PROPERTY, - ADMIN, - COMBATDAMAGE, - COMBATHEALING, - COMBATLOOT, - COMBATEXP, - COMBATDEATH, - GENERAL, - TRADE, - LFG, - USER -}; - - -enum class eChatMessageResponseCode : uint8_t { - SENT = 0, - NOTONLINE, - GENERALERROR, - RECEIVEDNEWWHISPER, - NOTFRIENDS, - SENDERFREETRIAL, - RECEIVERFREETRIAL, -}; - namespace ChatPacketHandler { - void HandleFriendlistRequest(Packet* packet); - void HandleFriendRequest(Packet* packet); - void HandleFriendResponse(Packet* packet); - void HandleRemoveFriend(Packet* packet); - void HandleGMLevelUpdate(Packet* packet); - void HandleWho(Packet* packet); - void HandleShowAll(Packet* packet); - void HandleChatMessage(Packet* packet); - void HandlePrivateChatMessage(Packet* packet); + // Sends msg to the world server `world`, which passes it on to the client of `target` + void SendRouted(const LWOOBJID target, const SystemAddress& world, const LUBitStream& msg, const bool broadcast = false); - void OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr); + void HandleFriendlistRequest(const ChatPackets::GetFriendsList& request, const SystemAddress& sysAddr); + void HandleFriendRequest(const ChatPackets::AddFriendRequest& request, const SystemAddress& sysAddr); + void HandleFriendResponse(const ChatPackets::AddFriendResponse& response, const SystemAddress& sysAddr); + void HandleRemoveFriend(const ChatPackets::RemoveFriend& request, const SystemAddress& sysAddr); + void HandleGMLevelUpdate(const ChatPackets::GMLevelUpdate& update, const SystemAddress& sysAddr); + void HandleWho(const ChatPackets::FindPlayerRequest& request, const SystemAddress& sysAddr); + void HandleShowAll(const ChatPackets::ShowAllRequest& request, const SystemAddress& sysAddr); + void HandleChatMessage(const ChatPackets::GeneralChatMessage& chatMessage, const SystemAddress& sysAddr); + void HandlePrivateChatMessage(const ChatPackets::PrivateChatMessage& chatMessage, const SystemAddress& sysAddr); + void OnAchievementNotify(ChatPackets::AchievementNotify& notify, const SystemAddress& sysAddr); //FriendData is the player we're SENDING this stuff to. Player is the friend that changed state. void SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend); diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index 83146e565..a38ccb1ec 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -24,6 +24,8 @@ #include "ChatIgnoreList.h" #include "StringifiedEnum.h" #include "TeamContainer.h" +#include "PacketDispatcher.h" +#include "ChatPackets.h" #include "Game.h" #include "Server.h" @@ -224,6 +226,55 @@ int main(int argc, char** argv) { return EXIT_SUCCESS; } +namespace { + // Packets from world servers + const PacketDispatcher& ChatHandlers() { + static const auto handlers = [] { + PacketDispatcher handlers; + using namespace ChatPackets; + using MessageType::Chat; + handlers.On(Chat::GM_MUTE, [](const GMMute& mute, const SystemAddress& sysAddr) { Game::playerContainer.MuteUpdate(mute, sysAddr); }); + handlers.On(Chat::CREATE_TEAM, TeamContainer::CreateTeamServer); + handlers.On(Chat::GET_FRIENDS_LIST, ChatPacketHandler::HandleFriendlistRequest); + handlers.On(Chat::GET_IGNORE_LIST, ChatIgnoreList::GetIgnoreList); + handlers.On(Chat::ADD_IGNORE, ChatIgnoreList::AddIgnore); + handlers.On(Chat::REMOVE_IGNORE, ChatIgnoreList::RemoveIgnore); + handlers.On(Chat::TEAM_GET_STATUS, TeamContainer::HandleTeamStatusRequest); + //this involves someone sending the initial request, the response is below, response as in from the other player. + //We basically just check to see if this player is online or not and route the packet. + handlers.On(Chat::ADD_FRIEND_REQUEST, ChatPacketHandler::HandleFriendRequest); + //This isn't the response a server sent, rather it is a player's response to a received request. + //Here, we'll actually have to add them to eachother's friend lists depending on the response code. + handlers.On(Chat::ADD_FRIEND_RESPONSE, ChatPacketHandler::HandleFriendResponse); + handlers.On(Chat::REMOVE_FRIEND, ChatPacketHandler::HandleRemoveFriend); + handlers.On(Chat::GENERAL_CHAT_MESSAGE, ChatPacketHandler::HandleChatMessage); + //This message is supposed to be echo'd to both the sender and the receiver + //BUT: they have to have different responseCodes, so we'll do some of the ol hacky wacky to fix that right up. + handlers.On(Chat::PRIVATE_CHAT_MESSAGE, ChatPacketHandler::HandlePrivateChatMessage); + handlers.On(Chat::TEAM_INVITE, TeamContainer::HandleTeamInvite); + handlers.On(Chat::TEAM_INVITE_RESPONSE, TeamContainer::HandleTeamInviteResponse); + handlers.On(Chat::TEAM_LEAVE, TeamContainer::HandleTeamLeave); + handlers.On(Chat::TEAM_SET_LEADER, TeamContainer::HandleTeamPromote); + handlers.On(Chat::TEAM_KICK, TeamContainer::HandleTeamKick); + handlers.On(Chat::TEAM_SET_LOOT, TeamContainer::HandleTeamLootOption); + handlers.On(Chat::GMLEVEL_UPDATE, ChatPacketHandler::HandleGMLevelUpdate); + handlers.On(Chat::LOGIN_SESSION_NOTIFY, [](const LoginSessionNotify& notify, const SystemAddress& sysAddr) { Game::playerContainer.InsertPlayer(notify, sysAddr); }); + // we just forward this packet to every connected server + handlers.On(Chat::GM_ANNOUNCE, [](const Announcement& announcement, const SystemAddress& sysAddr) { + RakNet::BitStream bitStream; + announcement.WritePacket(bitStream); + Game::server->Send(bitStream, sysAddr, true); // send to everyone except origin + }); + handlers.On(Chat::UNEXPECTED_DISCONNECT, [](const UnexpectedDisconnect& notify, const SystemAddress& sysAddr) { Game::playerContainer.ScheduleRemovePlayer(notify, sysAddr); }); + handlers.On(Chat::WHO, ChatPacketHandler::HandleWho); + handlers.On(Chat::SHOW_ALL, ChatPacketHandler::HandleShowAll); + handlers.On(Chat::ACHIEVEMENT_NOTIFY, ChatPacketHandler::OnAchievementNotify); + return handlers; + }(); + return handlers; + } +} + // Messages from master that dServer doesn't handle itself void HandleMasterPacket(Packet* packet) { if (packet->length < 4 || static_cast(packet->data[1]) != ServiceType::MASTER) return; @@ -246,166 +297,15 @@ void HandlePacket(Packet* packet) { LOG("A server is connecting, awaiting user list."); } else if (packet->length < 4 || packet->data[0] != ID_USER_PACKET_ENUM) return; // Nothing left to process or not the right packet type - CINSTREAM; - inStream.SetReadOffset(BYTES_TO_BITS(1)); - - ServiceType connection; - inStream.Read(connection); - if (connection != ServiceType::CHAT) return; - - MessageType::Chat chatMessageID; - inStream.Read(chatMessageID); + RakNet::BitStream inStream(packet->data, packet->length, false); + LUBitStream header; + if (!header.ReadHeader(inStream) || header.connectionType != ServiceType::CHAT) return; // Our packing byte wasnt there? Probably a false packet - if (inStream.GetNumberOfUnreadBits() < 8) return; - inStream.IgnoreBytes(1); + if (packet->length < 8) return; - switch (chatMessageID) { - case MessageType::Chat::GM_MUTE: - Game::playerContainer.MuteUpdate(packet); - break; - - case MessageType::Chat::CREATE_TEAM: - TeamContainer::CreateTeamServer(packet); - break; - - case MessageType::Chat::GET_FRIENDS_LIST: - ChatPacketHandler::HandleFriendlistRequest(packet); - break; - - case MessageType::Chat::GET_IGNORE_LIST: - ChatIgnoreList::GetIgnoreList(packet); - break; - - case MessageType::Chat::ADD_IGNORE: - ChatIgnoreList::AddIgnore(packet); - break; - - case MessageType::Chat::REMOVE_IGNORE: - ChatIgnoreList::RemoveIgnore(packet); - break; - - case MessageType::Chat::TEAM_GET_STATUS: - TeamContainer::HandleTeamStatusRequest(packet); - break; - - case MessageType::Chat::ADD_FRIEND_REQUEST: - //this involves someone sending the initial request, the response is below, response as in from the other player. - //We basically just check to see if this player is online or not and route the packet. - ChatPacketHandler::HandleFriendRequest(packet); - break; - - case MessageType::Chat::ADD_FRIEND_RESPONSE: - //This isn't the response a server sent, rather it is a player's response to a received request. - //Here, we'll actually have to add them to eachother's friend lists depending on the response code. - ChatPacketHandler::HandleFriendResponse(packet); - break; - - case MessageType::Chat::REMOVE_FRIEND: - ChatPacketHandler::HandleRemoveFriend(packet); - break; - - case MessageType::Chat::GENERAL_CHAT_MESSAGE: - ChatPacketHandler::HandleChatMessage(packet); - break; - - case MessageType::Chat::PRIVATE_CHAT_MESSAGE: - //This message is supposed to be echo'd to both the sender and the receiver - //BUT: they have to have different responseCodes, so we'll do some of the ol hacky wacky to fix that right up. - ChatPacketHandler::HandlePrivateChatMessage(packet); - break; - - case MessageType::Chat::TEAM_INVITE: - TeamContainer::HandleTeamInvite(packet); - break; - - case MessageType::Chat::TEAM_INVITE_RESPONSE: - TeamContainer::HandleTeamInviteResponse(packet); - break; - - case MessageType::Chat::TEAM_LEAVE: - TeamContainer::HandleTeamLeave(packet); - break; - - case MessageType::Chat::TEAM_SET_LEADER: - TeamContainer::HandleTeamPromote(packet); - break; - - case MessageType::Chat::TEAM_KICK: - TeamContainer::HandleTeamKick(packet); - break; - - case MessageType::Chat::TEAM_SET_LOOT: - TeamContainer::HandleTeamLootOption(packet); - break; - case MessageType::Chat::GMLEVEL_UPDATE: - ChatPacketHandler::HandleGMLevelUpdate(packet); - break; - case MessageType::Chat::LOGIN_SESSION_NOTIFY: - Game::playerContainer.InsertPlayer(packet); - break; - case MessageType::Chat::GM_ANNOUNCE: - // we just forward this packet to every connected server - inStream.ResetReadPointer(); - Game::server->Send(inStream, packet->systemAddress, true); // send to everyone except origin - break; - case MessageType::Chat::UNEXPECTED_DISCONNECT: - Game::playerContainer.ScheduleRemovePlayer(packet); - break; - case MessageType::Chat::WHO: - ChatPacketHandler::HandleWho(packet); - break; - case MessageType::Chat::SHOW_ALL: - ChatPacketHandler::HandleShowAll(packet); - break; - case MessageType::Chat::ACHIEVEMENT_NOTIFY: - ChatPacketHandler::OnAchievementNotify(inStream, packet->systemAddress); - break; - case MessageType::Chat::USER_CHANNEL_CHAT_MESSAGE: - case MessageType::Chat::WORLD_DISCONNECT_REQUEST: - case MessageType::Chat::WORLD_PROXIMITY_RESPONSE: - case MessageType::Chat::WORLD_PARCEL_RESPONSE: - case MessageType::Chat::TEAM_MISSED_INVITE_CHECK: - case MessageType::Chat::GUILD_CREATE: - case MessageType::Chat::GUILD_INVITE: - case MessageType::Chat::GUILD_INVITE_RESPONSE: - case MessageType::Chat::GUILD_LEAVE: - case MessageType::Chat::GUILD_KICK: - case MessageType::Chat::GUILD_GET_STATUS: - case MessageType::Chat::GUILD_GET_ALL: - case MessageType::Chat::BLUEPRINT_MODERATED: - case MessageType::Chat::BLUEPRINT_MODEL_READY: - case MessageType::Chat::PROPERTY_READY_FOR_APPROVAL: - case MessageType::Chat::PROPERTY_MODERATION_CHANGED: - case MessageType::Chat::PROPERTY_BUILDMODE_CHANGED: - case MessageType::Chat::PROPERTY_BUILDMODE_CHANGED_REPORT: - case MessageType::Chat::MAIL: - case MessageType::Chat::WORLD_INSTANCE_LOCATION_REQUEST: - case MessageType::Chat::REPUTATION_UPDATE: - case MessageType::Chat::SEND_CANNED_TEXT: - case MessageType::Chat::CHARACTER_NAME_CHANGE_REQUEST: - case MessageType::Chat::CSR_REQUEST: - case MessageType::Chat::CSR_REPLY: - case MessageType::Chat::GM_KICK: - case MessageType::Chat::WORLD_ROUTE_PACKET: - case MessageType::Chat::GET_ZONE_POPULATIONS: - case MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE: - case MessageType::Chat::MATCH_REQUEST: - case MessageType::Chat::UGCMANIFEST_REPORT_MISSING_FILE: - case MessageType::Chat::UGCMANIFEST_REPORT_DONE_FILE: - case MessageType::Chat::UGCMANIFEST_REPORT_DONE_BLUEPRINT: - case MessageType::Chat::UGCC_REQUEST: - case MessageType::Chat::WORLD_PLAYERS_PET_MODERATED_ACKNOWLEDGE: - case MessageType::Chat::GM_CLOSE_PRIVATE_CHAT_WINDOW: - case MessageType::Chat::PLAYER_READY: - case MessageType::Chat::GET_DONATION_TOTAL: - case MessageType::Chat::UPDATE_DONATION: - case MessageType::Chat::PRG_CSR_COMMAND: - case MessageType::Chat::HEARTBEAT_REQUEST_FROM_WORLD: - case MessageType::Chat::UPDATE_FREE_TRIAL_STATUS: + const auto chatMessageID = static_cast(header.internalPacketID); + if (!ChatHandlers().Dispatch(chatMessageID, inStream, packet->systemAddress)) { LOG("Unhandled CHAT Message id: %s (%i)", StringifiedEnum::ToString(chatMessageID).data(), chatMessageID); - break; - default: - LOG("Unknown CHAT Message id: %i", chatMessageID); } } diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 56fc8974f..5c9c1cd70 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -26,13 +26,9 @@ TeamData::TeamData() { lootFlag = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1; } -void PlayerContainer::InsertPlayer(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerId; - if (!inStream.Read(playerId)) { - LOG("Failed to read player ID"); - return; - } +void PlayerContainer::InsertPlayer(const ChatPackets::LoginSessionNotify& notify, const SystemAddress& sysAddr) { + // Names longer than ChatPackets::LoginSessionNotify::MAX_NAME_LENGTH were dropped when read + const LWOOBJID playerId = notify.playerID; auto isLogin = !m_Players.contains(playerId); auto& data = m_Players[playerId]; @@ -40,21 +36,11 @@ void PlayerContainer::InsertPlayer(Packet* packet) { data.isLogin = isLogin; data.playerID = playerId; - uint32_t len; - if (!inStream.Read(len)) return; - - if (len > 33) { - LOG("Received a really long player name, probably a fake packet %i.", len); - return; - } - - data.playerName.resize(len); - inStream.ReadAlignedBytes(reinterpret_cast(data.playerName.data()), len); - - if (!inStream.Read(data.zoneID)) return; - if (!inStream.Read(data.muteExpire)) return; - if (!inStream.Read(data.gmLevel)) return; - data.worldServerSysAddr = packet->systemAddress; + data.playerName = notify.playerName; + data.zoneID = notify.zoneID; + data.muteExpire = static_cast(notify.muteExpire); + data.gmLevel = notify.gmLevel; + data.worldServerSysAddr = sysAddr; m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName); m_PlayerCount++; @@ -66,10 +52,8 @@ void PlayerContainer::InsertPlayer(Packet* packet) { m_PlayersToRemove.erase(playerId); } -void PlayerContainer::ScheduleRemovePlayer(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID{ LWOOBJID_EMPTY }; - inStream.Read(playerID); +void PlayerContainer::ScheduleRemovePlayer(const ChatPackets::UnexpectedDisconnect& notify, const SystemAddress& sysAddr) { + const LWOOBJID playerID = notify.playerID; constexpr float updatePlayerOnLogoutTime = 20.0f; if (playerID != LWOOBJID_EMPTY) m_PlayersToRemove.insert_or_assign(playerID, updatePlayerOnLogoutTime); } @@ -117,12 +101,9 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { Database::Get()->UpdateActivityLog(playerID, eActivityType::PlayerLoggedOut, player.zoneID.GetMapID()); } -void PlayerContainer::MuteUpdate(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); - time_t expire = 0; - inStream.Read(expire); +void PlayerContainer::MuteUpdate(const ChatPackets::GMMute& mute, const SystemAddress& sysAddr) { + const LWOOBJID playerID = mute.playerID; + const time_t expire = static_cast(mute.expire); auto& player = this->GetPlayerDataMutable(playerID); @@ -138,13 +119,10 @@ void PlayerContainer::MuteUpdate(Packet* packet) { } void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE); - - bitStream.Write(player); - bitStream.Write(time); - - Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); + ChatPackets::GMMute mute; + mute.playerID = player; + mute.expire = time; + mute.Broadcast(); } std::u16string PlayerContainer::GetName(LWOOBJID playerID) { diff --git a/dChatServer/PlayerContainer.h b/dChatServer/PlayerContainer.h index 207857470..569465aea 100644 --- a/dChatServer/PlayerContainer.h +++ b/dChatServer/PlayerContainer.h @@ -6,6 +6,7 @@ #include "Game.h" #include "dServer.h" #include +#include "ChatPackets.h" enum class eGameMasterLevel : uint8_t; @@ -65,10 +66,10 @@ struct TeamData { class PlayerContainer { public: void Initialize(); - void InsertPlayer(Packet* packet); - void ScheduleRemovePlayer(Packet* packet); + void InsertPlayer(const ChatPackets::LoginSessionNotify& notify, const SystemAddress& sysAddr); + void ScheduleRemovePlayer(const ChatPackets::UnexpectedDisconnect& notify, const SystemAddress& sysAddr); void RemovePlayer(const LWOOBJID playerID); - void MuteUpdate(Packet* packet); + void MuteUpdate(const ChatPackets::GMMute& mute, const SystemAddress& sysAddr); void BroadcastMuteUpdate(LWOOBJID player, time_t time); void Shutdown(); diff --git a/dChatServer/TeamContainer.cpp b/dChatServer/TeamContainer.cpp index 536cc57f9..c542e39f4 100644 --- a/dChatServer/TeamContainer.cpp +++ b/dChatServer/TeamContainer.cpp @@ -29,15 +29,9 @@ void TeamContainer::Shutdown() { for (auto* team : g_TeamContainer.mTeams) if (team) delete team; } -void TeamContainer::HandleTeamInvite(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID; - LUWString invitedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(invitedPlayer); +void TeamContainer::HandleTeamInvite(const ChatPackets::TeamInvite& invite, const SystemAddress& sysAddr) { + const LWOOBJID playerID = invite.playerID; + const LUWString& invitedPlayer = invite.invitedPlayer; const auto& player = Game::playerContainer.GetPlayerData(playerID); @@ -76,22 +70,16 @@ void TeamContainer::HandleTeamInvite(Packet* packet) { } } - ChatPackets::TeamInviteInitialResponse response{}; + ClientPackets::TeamInviteInitialResponse response{}; response.inviteFailedToSend = failed; response.playerName = invitedPlayer.string; - ChatPackets::SendRoutedMsg(response, playerID, player.worldServerSysAddr); + ChatPacketHandler::SendRouted(playerID, player.worldServerSysAddr, response, player.worldServerSysAddr == UNASSIGNED_SYSTEM_ADDRESS); } -void TeamContainer::HandleTeamInviteResponse(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - char declined = 0; - inStream.Read(declined); - LWOOBJID leaderID = LWOOBJID_EMPTY; - inStream.Read(leaderID); +void TeamContainer::HandleTeamInviteResponse(const ChatPackets::TeamInviteResponse& response, const SystemAddress& sysAddr) { + const LWOOBJID playerID = response.playerID; + const char declined = response.declined; + const LWOOBJID leaderID = response.leaderID; LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined); @@ -115,12 +103,8 @@ void TeamContainer::HandleTeamInviteResponse(Packet* packet) { AddMember(team, playerID); } -void TeamContainer::HandleTeamLeave(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); +void TeamContainer::HandleTeamLeave(const ChatPackets::TeamLeave& request, const SystemAddress& sysAddr) { + const LWOOBJID playerID = request.playerID; auto* team = GetTeam(playerID); @@ -131,15 +115,9 @@ void TeamContainer::HandleTeamLeave(Packet* packet) { } } -void TeamContainer::HandleTeamKick(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = LWOOBJID_EMPTY; - LUWString kickedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(kickedPlayer); +void TeamContainer::HandleTeamKick(const ChatPackets::TeamKick& request, const SystemAddress& sysAddr) { + const LWOOBJID playerID = request.playerID; + const LUWString& kickedPlayer = request.kickedPlayer; LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str()); @@ -165,15 +143,9 @@ void TeamContainer::HandleTeamKick(Packet* packet) { } } -void TeamContainer::HandleTeamPromote(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = LWOOBJID_EMPTY; - LUWString promotedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(promotedPlayer); +void TeamContainer::HandleTeamPromote(const ChatPackets::TeamSetLeader& request, const SystemAddress& sysAddr) { + const LWOOBJID playerID = request.playerID; + const LUWString& promotedPlayer = request.promotedPlayer; LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str()); @@ -190,15 +162,9 @@ void TeamContainer::HandleTeamPromote(Packet* packet) { } } -void TeamContainer::HandleTeamLootOption(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - - char option; - inStream.Read(option); +void TeamContainer::HandleTeamLootOption(const ChatPackets::TeamSetLoot& request, const SystemAddress& sysAddr) { + const LWOOBJID playerID = request.playerID; + const char option = request.lootFlag; auto* team = GetTeam(playerID); @@ -213,10 +179,8 @@ void TeamContainer::HandleTeamLootOption(Packet* packet) { } } -void TeamContainer::HandleTeamStatusRequest(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); +void TeamContainer::HandleTeamStatusRequest(const ChatPackets::TeamGetStatus& request, const SystemAddress& sysAddr) { + const LWOOBJID playerID = request.playerID; auto* team = GetTeam(playerID); const auto& data = Game::playerContainer.GetPlayerData(playerID); @@ -263,190 +227,86 @@ void TeamContainer::HandleTeamStatusRequest(Packet* packet) { } void TeamContainer::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TEAM_INVITE); - - bitStream.Write(LUWString(sender.playerName.c_str())); - bitStream.Write(sender.playerID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::TeamInvite invite; + invite.senderName = LUWString(sender.playerName.c_str()); + invite.senderID = sender.playerID; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, invite); } void TeamContainer::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM); - - bitStream.Write(bLeaderIsFreeTrial); - bitStream.Write(i64LeaderID); - bitStream.Write(i64LeaderZoneID); - bitStream.Write(0); // BinaryBuffe, no clue what's in here - bitStream.Write(ucLootFlag); - bitStream.Write(ucNumOfOtherPlayers); - bitStream.Write(ucResponseCode); - bitStream.Write(wsLeaderName.size()); - for (const auto character : wsLeaderName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::TeamInviteConfirm msg; + msg.target = receiver.playerID; + msg.bLeaderIsFreeTrial = bLeaderIsFreeTrial; + msg.i64LeaderID = i64LeaderID; + msg.i64LeaderZoneID = i64LeaderZoneID; + msg.ucLootFlag = ucLootFlag; + msg.ucNumOfOtherPlayers = ucNumOfOtherPlayers; + msg.ucResponseCode = ucResponseCode; + msg.wsLeaderName = wsLeaderName; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, msg); } void TeamContainer::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE); - - bitStream.Write(i64LeaderID); - bitStream.Write(i64LeaderZoneID); - bitStream.Write(0); // BinaryBuffe, no clue what's in here - bitStream.Write(ucLootFlag); - bitStream.Write(ucNumOfOtherPlayers); - bitStream.Write(wsLeaderName.size()); - for (const auto character : wsLeaderName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::TeamGetStatusResponse msg; + msg.target = receiver.playerID; + msg.i64LeaderID = i64LeaderID; + msg.i64LeaderZoneID = i64LeaderZoneID; + msg.ucLootFlag = ucLootFlag; + msg.ucNumOfOtherPlayers = ucNumOfOtherPlayers; + msg.wsLeaderName = wsLeaderName; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, msg); } void TeamContainer::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_SET_LEADER); - - bitStream.Write(i64PlayerID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::TeamSetLeader msg; + msg.target = receiver.playerID; + msg.i64PlayerID = i64PlayerID; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, msg); } void TeamContainer::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER); - - bitStream.Write(bIsFreeTrial); - bitStream.Write(bLocal); - bitStream.Write(bNoLootOnDeath); - bitStream.Write(i64PlayerID); - bitStream.Write(wsPlayerName.size()); - for (const auto character : wsPlayerName) { - bitStream.Write(character); - } - bitStream.Write1(); + ClientPackets::TeamAddPlayer msg; + msg.target = receiver.playerID; + msg.bIsFreeTrial = bIsFreeTrial; + msg.bLocal = bLocal; + msg.bNoLootOnDeath = bNoLootOnDeath; + msg.i64PlayerID = i64PlayerID; + msg.wsPlayerName = wsPlayerName; if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); } - bitStream.Write(zoneID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + msg.zoneID = zoneID; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, msg); } void TeamContainer::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER); - - bitStream.Write(bDisband); - bitStream.Write(bIsKicked); - bitStream.Write(bIsLeaving); - bitStream.Write(bLocal); - bitStream.Write(i64LeaderID); - bitStream.Write(i64PlayerID); - bitStream.Write(wsPlayerName.size()); - for (const auto character : wsPlayerName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + ClientPackets::TeamRemovePlayer msg; + msg.target = receiver.playerID; + msg.bDisband = bDisband; + msg.bIsKicked = bIsKicked; + msg.bIsLeaving = bIsLeaving; + msg.bLocal = bLocal; + msg.i64LeaderID = i64LeaderID; + msg.i64PlayerID = i64PlayerID; + msg.wsPlayerName = wsPlayerName; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, msg); } void TeamContainer::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG); - - bitStream.Write(i64PlayerID); + ClientPackets::TeamSetOffWorldFlag msg; + msg.target = receiver.playerID; + msg.i64PlayerID = i64PlayerID; if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); } - bitStream.Write(zoneID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; + msg.zoneID = zoneID; + ChatPacketHandler::SendRouted(receiver.playerID, receiver.worldServerSysAddr, msg); } -void TeamContainer::CreateTeamServer(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); - size_t membersSize = 0; - inStream.Read(membersSize); - - if (membersSize >= 4) { - LOG("Tried to create a team with more than 4 players"); - return; - } - - std::vector members; - - members.reserve(membersSize); - - for (size_t i = 0; i < membersSize; i++) { - LWOOBJID member; - inStream.Read(member); - members.push_back(member); - } - - LWOZONEID zoneId; - - inStream.Read(zoneId); +void TeamContainer::CreateTeamServer(const ChatPackets::CreateTeam& request, const SystemAddress& sysAddr) { + // Requests for more than 4 players (ChatPackets::CreateTeam::MAX_MEMBERS) were dropped when read + const std::vector& members = request.members; + const LWOZONEID zoneId = request.zoneID; auto* team = CreateLocalTeam(members); @@ -651,19 +511,14 @@ void TeamContainer::TeamStatusUpdate(TeamData* team) { } void TeamContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::TEAM_GET_STATUS); - - bitStream.Write(team->teamID); - bitStream.Write(deleteTeam); + ChatPackets::TeamUpdate update; + update.teamID = team->teamID; + update.deleteTeam = deleteTeam; if (!deleteTeam) { - bitStream.Write(team->lootFlag); - bitStream.Write(team->memberIDs.size()); - for (const auto memberID : team->memberIDs) { - bitStream.Write(memberID); - } + update.lootFlag = team->lootFlag; + update.members = team->memberIDs; } - Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); + update.Broadcast(); } diff --git a/dChatServer/TeamContainer.h b/dChatServer/TeamContainer.h index a316cf717..a879061a2 100644 --- a/dChatServer/TeamContainer.h +++ b/dChatServer/TeamContainer.h @@ -9,8 +9,9 @@ #include #include "dCommonVars.h" +#include "ChatPackets.h" -struct Packet; +struct SystemAddress; struct PlayerData; struct TeamData; @@ -21,13 +22,13 @@ namespace TeamContainer { void Shutdown(); - void HandleTeamInvite(Packet* packet); - void HandleTeamInviteResponse(Packet* packet); - void HandleTeamLeave(Packet* packet); - void HandleTeamKick(Packet* packet); - void HandleTeamPromote(Packet* packet); - void HandleTeamLootOption(Packet* packet); - void HandleTeamStatusRequest(Packet* packet); + void HandleTeamInvite(const ChatPackets::TeamInvite& invite, const SystemAddress& sysAddr); + void HandleTeamInviteResponse(const ChatPackets::TeamInviteResponse& response, const SystemAddress& sysAddr); + void HandleTeamLeave(const ChatPackets::TeamLeave& request, const SystemAddress& sysAddr); + void HandleTeamKick(const ChatPackets::TeamKick& request, const SystemAddress& sysAddr); + void HandleTeamPromote(const ChatPackets::TeamSetLeader& request, const SystemAddress& sysAddr); + void HandleTeamLootOption(const ChatPackets::TeamSetLoot& request, const SystemAddress& sysAddr); + void HandleTeamStatusRequest(const ChatPackets::TeamGetStatus& request, const SystemAddress& sysAddr); void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender); void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName); @@ -39,7 +40,7 @@ namespace TeamContainer { void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName); void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID); - void CreateTeamServer(Packet* packet); + void CreateTeamServer(const ChatPackets::CreateTeam& request, const SystemAddress& sysAddr); TeamData* CreateLocalTeam(std::vector members); TeamData* CreateTeam(LWOOBJID leader, bool local = false); diff --git a/dCommon/dEnums/eAddIgnoreResponse.h b/dCommon/dEnums/eAddIgnoreResponse.h new file mode 100644 index 000000000..554092dfd --- /dev/null +++ b/dCommon/dEnums/eAddIgnoreResponse.h @@ -0,0 +1,13 @@ +#ifndef EADDIGNORERESPONSE_H +#define EADDIGNORERESPONSE_H + +#include + +enum class eAddIgnoreResponse : uint8_t { + SUCCESS, + ALREADY_IGNORED, + PLAYER_NOT_FOUND, + GENERAL_ERROR, +}; + +#endif // EADDIGNORERESPONSE_H diff --git a/dCommon/dEnums/eChatChannel.h b/dCommon/dEnums/eChatChannel.h new file mode 100644 index 000000000..56c52be80 --- /dev/null +++ b/dCommon/dEnums/eChatChannel.h @@ -0,0 +1,32 @@ +#ifndef ECHATCHANNEL_H +#define ECHATCHANNEL_H + +#include + +enum class eChatChannel : uint8_t { + SYSTEMNOTIFY = 0, + SYSTEMWARNING, + SYSTEMERROR, + BROADCAST, + LOCAL, + LOCALNOANIM, + EMOTE, + PRIVATE_CHAT, + TEAM, + TEAMLOCAL, + GUILD, + GUILDNOTIFY, + PROPERTY, + ADMIN, + COMBATDAMAGE, + COMBATHEALING, + COMBATLOOT, + COMBATEXP, + COMBATDEATH, + GENERAL, + TRADE, + LFG, + USER +}; + +#endif // ECHATCHANNEL_H diff --git a/dCommon/dEnums/eChatMessageResponseCode.h b/dCommon/dEnums/eChatMessageResponseCode.h new file mode 100644 index 000000000..fb949af79 --- /dev/null +++ b/dCommon/dEnums/eChatMessageResponseCode.h @@ -0,0 +1,16 @@ +#ifndef ECHATMESSAGERESPONSECODE_H +#define ECHATMESSAGERESPONSECODE_H + +#include + +enum class eChatMessageResponseCode : uint8_t { + SENT = 0, + NOTONLINE, + GENERALERROR, + RECEIVEDNEWWHISPER, + NOTFRIENDS, + SENDERFREETRIAL, + RECEIVERFREETRIAL, +}; + +#endif // ECHATMESSAGERESPONSECODE_H diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index ce573824b..622bbf1cd 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -1,4 +1,6 @@ #include "dCommonVars.h" +#include "ChatServerLink.h" +#include "ChatPackets.h" #include "Entity.h" #include "EconomyLedger.h" #include "CDClientManager.h" @@ -945,12 +947,10 @@ void Entity::SetGMLevel(eGameMasterLevel value) { // Update the chat server of our GM Level { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GMLEVEL_UPDATE); - bitStream.Write(m_ObjectID); - bitStream.Write(m_GMLevel); - - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::GMLevelUpdate update; + update.playerID = m_ObjectID; + update.gmLevel = m_GMLevel; + ChatServerLink::Send(update); } } diff --git a/dGame/User.cpp b/dGame/User.cpp index 42e0196c6..42b7376df 100644 --- a/dGame/User.cpp +++ b/dGame/User.cpp @@ -1,4 +1,6 @@ #include "User.h" +#include "ChatServerLink.h" +#include "ChatPackets.h" #include "Database.h" #include "Character.h" #include "dServer.h" @@ -108,11 +110,10 @@ bool User::GetIsMuted() { m_MuteExpire = expire; if (Game::chatServer && m_LoggedInCharID != 0) { - RakNet::BitStream bitStream; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE); - bitStream.Write(m_LoggedInCharID); - bitStream.Write(m_MuteExpire); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::GMMute mute; + mute.playerID = m_LoggedInCharID; + mute.expire = m_MuteExpire; + ChatServerLink::Send(mute); } } } diff --git a/dGame/UserManager.cpp b/dGame/UserManager.cpp index 368711872..7b54407b5 100644 --- a/dGame/UserManager.cpp +++ b/dGame/UserManager.cpp @@ -1,4 +1,6 @@ #include "UserManager.h" +#include "ChatServerLink.h" +#include "ChatPackets.h" #include "DashboardNotify.h" #include #include @@ -441,10 +443,9 @@ void UserManager::DeleteCharacter(const SystemAddress& sysAddr, const WorldPacke Database::Get()->DeleteCharacter(objectID); DashboardNotify::Changed("characters", objectID); - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); - bitStream.Write(objectID); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::UnexpectedDisconnect notify; + notify.playerID = objectID; + ChatServerLink::Send(notify); ClientPackets::DeleteCharacterResponse response; response.success = true; diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index cea28d83a..9a3b45153 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -1,4 +1,6 @@ #include "ActivityComponent.h" +#include "ChatServerLink.h" +#include "ChatPackets.h" #include "GameMessages.h" #include "CDClientManager.h" #include "MissionComponent.h" @@ -441,19 +443,16 @@ void ActivityInstance::StartZone() { // only make a team if we have more than one participant if (participants.size() > 1) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::CREATE_TEAM); - - bitStream.Write(leader->GetObjectID()); - bitStream.Write(m_Participants.size()); + ChatPackets::CreateTeam createTeam; + createTeam.leaderID = leader->GetObjectID(); for (const auto& participant : m_Participants) { - bitStream.Write(participant); + createTeam.members.push_back(participant); } - bitStream.Write(zoneId); + createTeam.zoneID = zoneId; - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatServerLink::Send(createTeam); } const auto cloneId = GeneralUtils::GenerateRandomNumber(1, UINT32_MAX); diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 6f9409f4d..69c6593f6 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -1,4 +1,5 @@ #include "Mission.h" +#include "ChatServerLink.h" #include @@ -364,11 +365,8 @@ void Mission::Complete(const bool yieldRewards) { notify.earningPlayerID = entity->GetObjectID(); notify.earnerName.string = character ? GeneralUtils::ASCIIToUTF16(character->GetName()) : u""; - // Manual write since it's sent to chat server and not a game client - RakNet::BitStream bitstream; - notify.WriteHeader(bitstream); - notify.Serialize(bitstream); - Game::chatServer->Send(&bitstream, HIGH_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + // Sent to the chat server, not a game client + ChatServerLink::Send(notify, HIGH_PRIORITY); } } } diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index 258fd1f20..82a937282 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -214,7 +214,14 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up bool isOk = Game::chatFilter->IsSentenceOkay(valueStr.data(), eGameMasterLevel::CIVILIAN).empty(); // In case a word is removed from the whitelist after it was approved const auto modelName = "%[Objects_" + std::to_string(entity.GetLOT()) + "_name]"; - if (isOk) ChatPackets::SendChatMessage(UNASSIGNED_SYSTEM_ADDRESS, 12, modelName, entity.GetObjectID(), false, GeneralUtils::ASCIIToUTF16(valueStr)); + if (isOk) { + ChatPackets::Client::GeneralChatMessage chatMessage; + chatMessage.chatChannel = 12; + chatMessage.senderName = LUWString(modelName); + chatMessage.senderID = entity.GetObjectID(); + chatMessage.message = GeneralUtils::ASCIIToUTF16(valueStr); + chatMessage.Broadcast(); + } PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data()); } else if (nextActionType == Actions::PRIVATE_MESSAGE) { PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data()); diff --git a/dGame/dUtilities/CMakeLists.txt b/dGame/dUtilities/CMakeLists.txt index 785df213a..0b7370805 100644 --- a/dGame/dUtilities/CMakeLists.txt +++ b/dGame/dUtilities/CMakeLists.txt @@ -1,4 +1,5 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp" + "ChatServerLink.cpp" "CheatDetection.cpp" "DashboardActions.cpp" "EconomyLedger.cpp" diff --git a/dGame/dUtilities/ChatServerLink.cpp b/dGame/dUtilities/ChatServerLink.cpp new file mode 100644 index 000000000..2cd56248d --- /dev/null +++ b/dGame/dUtilities/ChatServerLink.cpp @@ -0,0 +1,13 @@ +#include "ChatServerLink.h" + +#include "BitStream.h" +#include "BitStreamUtils.h" +#include "Game.h" +#include "RakPeerInterface.h" + +void ChatServerLink::Send(const LUBitStream& msg, const PacketPriority priority, const PacketReliability reliability) { + if (!Game::chatServer) return; + RakNet::BitStream bitStream; + msg.WritePacket(bitStream); + Game::chatServer->Send(&bitStream, priority, reliability, 0, Game::chatSysAddr, false); +} diff --git a/dGame/dUtilities/ChatServerLink.h b/dGame/dUtilities/ChatServerLink.h new file mode 100644 index 000000000..df47bc980 --- /dev/null +++ b/dGame/dUtilities/ChatServerLink.h @@ -0,0 +1,14 @@ +#ifndef CHATSERVERLINK_H +#define CHATSERVERLINK_H + +#include "PacketPriority.h" + +struct LUBitStream; + +// A world's connection to the chat server +namespace ChatServerLink { + // World -> chat: sends msg (a ChatPackets struct) over the world's chat connection + void Send(const LUBitStream& msg, PacketPriority priority = SYSTEM_PRIORITY, PacketReliability reliability = RELIABLE); +} + +#endif // CHATSERVERLINK_H diff --git a/dGame/dUtilities/DashboardActions.cpp b/dGame/dUtilities/DashboardActions.cpp index c52dc5b48..b698c3dc0 100644 --- a/dGame/dUtilities/DashboardActions.cpp +++ b/dGame/dUtilities/DashboardActions.cpp @@ -266,7 +266,11 @@ uint32_t DashboardActions::Apply(const PlayerActionRequest& request) { if (request.instanceId >= 0 && request.instanceId != Game::server->GetInstanceID()) return 0; if (Game::config->GetValue("chat_bridge_filter") != "0" && !Game::chatFilter->IsSentenceOkay(request.text, eGameMasterLevel::CIVILIAN).empty()) return 0; const auto players = PlayerManager::GetAllPlayers().size(); - ChatPackets::SendChatMessage(UNASSIGNED_SYSTEM_ADDRESS, static_cast(4), request.name, LWOOBJID_EMPTY, false, GeneralUtils::UTF8ToUTF16(request.text)); + ChatPackets::Client::GeneralChatMessage chatMessage; + chatMessage.chatChannel = 4; + chatMessage.senderName = LUWString(request.name); + chatMessage.message = GeneralUtils::UTF8ToUTF16(request.text); + chatMessage.Broadcast(); return static_cast(players); } case ePlayerAction::RELOAD_VANITY: diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index f7f0bdc62..14b13761f 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -6,6 +6,8 @@ #include "SlashCommandHandler.h" +#include "ChatServerLink.h" +#include "ChatPackets.h" #include "WorldMigration.h" #include @@ -339,20 +341,10 @@ void SlashCommandHandler::SendAnnouncement(const std::string& title, const std:: GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", args); //Notify chat about it - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_ANNOUNCE); - - bitStream.Write(title.size()); - for (auto character : title) { - bitStream.Write(character); - } - - bitStream.Write(message.size()); - for (auto character : message) { - bitStream.Write(character); - } - - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::Announcement announcement; + announcement.title = title; + announcement.message = message; + ChatServerLink::Send(announcement); } void SlashCommandHandler::Startup() { diff --git a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp index e948e29b3..9754ade79 100644 --- a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp @@ -1,4 +1,5 @@ #include "GMGreaterThanZeroCommands.h" +#include "ChatServerLink.h" #include "DashboardNotify.h" #include "SlashCommandHandler.h" @@ -180,13 +181,10 @@ namespace GMGreaterThanZeroCommands { ChatPackets::SendSystemMessage(sysAddr, u"Muted: " + GeneralUtils::UTF8ToUTF16(splitArgs[0]) + u" until " + timeStr); //Notify chat about it - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE); - - bitStream.Write(characterId); - bitStream.Write(expire); - - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::GMMute mute; + mute.playerID = characterId; + mute.expire = expire; + ChatServerLink::Send(mute); } else { ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /mute "); } @@ -281,15 +279,11 @@ namespace GMGreaterThanZeroCommands { if (!splitArgs.empty() && !splitArgs.at(0).empty()) displayZoneData = splitArgs.at(0) == "1"; if (splitArgs.size() > 1) displayIndividualPlayers = splitArgs.at(1) == "1"; - ShowAllRequest request { - .requestor = entity->GetObjectID(), - .displayZoneData = displayZoneData, - .displayIndividualPlayers = displayIndividualPlayers - }; - - CBITSTREAM; - request.Serialize(bitStream); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::ShowAllRequest request; + request.requestor = entity->GetObjectID(); + request.displayZoneData = displayZoneData; + request.displayIndividualPlayers = displayIndividualPlayers; + ChatServerLink::Send(request); } void FindPlayer(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -298,14 +292,10 @@ namespace GMGreaterThanZeroCommands { return; } - FindPlayerRequest request { - .requestor = entity->GetObjectID(), - .playerName = LUWString(args) - }; - - CBITSTREAM; - request.Serialize(bitStream); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::FindPlayerRequest request; + request.requestor = entity->GetObjectID(); + request.playerName = LUWString(args); + ChatServerLink::Send(request); } void Spectate(Entity* entity, const SystemAddress& sysAddr, const std::string args) { diff --git a/dNet/CMakeLists.txt b/dNet/CMakeLists.txt index 14ab3dc35..71105f096 100644 --- a/dNet/CMakeLists.txt +++ b/dNet/CMakeLists.txt @@ -9,6 +9,7 @@ set(DNET_SOURCES "AuthPackets.cpp" "PacketUtils.cpp" "Stamps.cpp" "WorldPackets.cpp" + "WorldRoutePacket.cpp" "ZoneInstanceManager.cpp") add_library(dNet STATIC ${DNET_SOURCES}) diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index 08741477c..224e4e21c 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -12,50 +12,465 @@ #include "ServiceType.h" #include "MessageType/Chat.h" -void ShowAllRequest::Serialize(RakNet::BitStream& bitStream) { - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::SHOW_ALL); - bitStream.Write(this->requestor); - bitStream.Write(this->displayZoneData); - bitStream.Write(this->displayIndividualPlayers); -} - -void ShowAllRequest::Deserialize(RakNet::BitStream& inStream) { - inStream.Read(this->requestor); - inStream.Read(this->displayZoneData); - inStream.Read(this->displayIndividualPlayers); -} - -void FindPlayerRequest::Serialize(RakNet::BitStream& bitStream) { - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WHO); - bitStream.Write(this->requestor); - bitStream.Write(this->playerName); -} - -void FindPlayerRequest::Deserialize(RakNet::BitStream& inStream) { - inStream.Read(this->requestor); - inStream.Read(this->playerName); -} - -void ChatPackets::SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE); - - bitStream.Write(0); - bitStream.Write(chatChannel); - - bitStream.Write(message.size()); - bitStream.Write(LUWString(senderName)); - - bitStream.Write(playerObjectID); - bitStream.Write(0); - bitStream.Write(0); - - for (uint32_t i = 0; i < message.size(); ++i) { - bitStream.Write(message[i]); +namespace { + // A zone ID as three fields: map (u16), instance (u16), clone (u32) + void WriteZone(RakNet::BitStream& bitStream, const LWOZONEID& zoneID) { + bitStream.Write(zoneID.GetMapID()); + bitStream.Write(zoneID.GetInstanceID()); + bitStream.Write(zoneID.GetCloneID()); } - bitStream.Write(0); - SEND_PACKET_BROADCAST; + bool ReadZone(RakNet::BitStream& bitStream, LWOZONEID& zoneID) { + LWOMAPID mapID{}; + LWOINSTANCEID instanceID{}; + LWOCLONEID cloneID{}; + VALIDATE_READ(bitStream.Read(mapID)); + VALIDATE_READ(bitStream.Read(instanceID)); + VALIDATE_READ(bitStream.Read(cloneID)); + zoneID = LWOZONEID(mapID, instanceID, cloneID); + return true; + } + + // The LUWString fills the rest of the stream (a message whose length isn't in the packet) + bool ReadRemainingWString(RakNet::BitStream& bitStream, LUWString& value) { + value.size = static_cast(BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()) / sizeof(char16_t)); + return value.size == 0 || bitStream.Read(value); + } + + // Player and team packets from the client all start with the player's object ID and 4 unused bytes + bool ReadPlayerHeader(RakNet::BitStream& bitStream, LWOOBJID& playerID, uint32_t& unknown) { + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(bitStream.Read(unknown)); + return true; + } +} + +namespace ChatPackets { + void LoginSessionNotify::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + BitStreamUtils::WriteLengthPrefixed(bitStream, playerName); + WriteZone(bitStream, zoneID); + bitStream.Write(muteExpire); + bitStream.Write(gmLevel); + } + + bool LoginSessionNotify::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, playerName, MAX_NAME_LENGTH)); + VALIDATE_READ(ReadZone(bitStream, zoneID)); + VALIDATE_READ(bitStream.Read(muteExpire)); + VALIDATE_READ(bitStream.Read(gmLevel)); + return true; + } + + void UnexpectedDisconnect::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + } + + bool UnexpectedDisconnect::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } + + void GMLevelUpdate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(gmLevel); + } + + bool GMLevelUpdate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(bitStream.Read(gmLevel)); + return true; + } + + void GMMute::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(expire); + } + + bool GMMute::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(bitStream.Read(expire)); + return true; + } + + void Announcement::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, title); + BitStreamUtils::WriteLengthPrefixed(bitStream, message); + } + + bool Announcement::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, title)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, message)); + return true; + } + + void CreateTeam::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(leaderID); + bitStream.Write(members.size()); + for (const auto member : members) bitStream.Write(member); + WriteZone(bitStream, zoneID); + } + + bool CreateTeam::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(leaderID)); + uint64_t count{}; + VALIDATE_READ(bitStream.Read(count)); + if (count > MAX_MEMBERS) return false; + members.resize(count); + for (auto& member : members) VALIDATE_READ(bitStream.Read(member)); + VALIDATE_READ(ReadZone(bitStream, zoneID)); + return true; + } + + void TeamUpdate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(teamID); + bitStream.Write(deleteTeam); + if (deleteTeam) return; + bitStream.Write(lootFlag); + bitStream.Write(members.size()); + for (const auto member : members) bitStream.Write(member); + } + + bool TeamUpdate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(teamID)); + VALIDATE_READ(bitStream.Read(deleteTeam)); + if (deleteTeam) return true; + VALIDATE_READ(bitStream.Read(lootFlag)); + uint8_t count{}; + VALIDATE_READ(bitStream.Read(count)); + members.resize(count); + for (auto& member : members) VALIDATE_READ(bitStream.Read(member)); + return true; + } + + void AchievementNotify::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(0); // Packing + bitStream.Write(0); // Packing + bitStream.Write(0); // Packing + bitStream.Write(earnerName); + bitStream.Write(0); // Packing / No way to know meaning because of not enough data. + bitStream.Write(0); // Packing / No way to know meaning because of not enough data. + bitStream.Write(0); // Packing / No way to know meaning because of not enough data. + bitStream.Write(0); // Packing / No way to know meaning because of not enough data. + bitStream.Write(missionEmailID); + bitStream.Write(earningPlayerID); + bitStream.Write(targetPlayerName); + } + + bool AchievementNotify::Deserialize(RakNet::BitStream& bitStream) { + bitStream.IgnoreBytes(13); + VALIDATE_READ(bitStream.Read(earnerName)); + bitStream.IgnoreBytes(15); + VALIDATE_READ(bitStream.Read(missionEmailID)); + VALIDATE_READ(bitStream.Read(earningPlayerID)); + VALIDATE_READ(bitStream.Read(targetPlayerName)); + return true; + } + + void ShowAllRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(requestor); + bitStream.Write(displayZoneData); + bitStream.Write(displayIndividualPlayers); + } + + bool ShowAllRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(requestor)); + VALIDATE_READ(bitStream.Read(displayZoneData)); + VALIDATE_READ(bitStream.Read(displayIndividualPlayers)); + return true; + } + + void FindPlayerRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(requestor); + bitStream.Write(playerName); + } + + bool FindPlayerRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(requestor)); + VALIDATE_READ(bitStream.Read(playerName)); + return true; + } + + void GetFriendsList::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + } + + bool GetFriendsList::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } + + void AddFriendRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(friendName); + bitStream.Write(isBestFriendRequest); + } + + bool AddFriendRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(friendName)); + VALIDATE_READ(bitStream.Read(isBestFriendRequest)); + return true; + } + + void AddFriendResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(responseCode); + bitStream.Write(friendName); + } + + bool AddFriendResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(bitStream.Read(friendName)); + return true; + } + + void RemoveFriend::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(friendName); + } + + bool RemoveFriend::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(friendName)); + return true; + } + + void GetIgnoreList::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + } + + bool GetIgnoreList::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } + + void AddIgnore::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(playerName); + } + + bool AddIgnore::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(playerName)); + return true; + } + + void RemoveIgnore::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(playerName); + } + + bool RemoveIgnore::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(playerName)); + return true; + } + + void GeneralChatMessage::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(chatChannel); + bitStream.Write(messageLength); + bitStream.Write(senderName); + bitStream.Write(senderID); + bitStream.Write(sourceID); + bitStream.Write(senderGMLevel); + bitStream.Write(message); + } + + bool GeneralChatMessage::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(chatChannel)); + VALIDATE_READ(bitStream.Read(messageLength)); + if (messageLength > MAX_MESSAGE_LENGTH) return false; + VALIDATE_READ(bitStream.Read(senderName)); + VALIDATE_READ(bitStream.Read(senderID)); + VALIDATE_READ(bitStream.Read(sourceID)); + VALIDATE_READ(bitStream.Read(senderGMLevel)); + message = LUWString(messageLength); + if (messageLength != 0) VALIDATE_READ(bitStream.Read(message)); + return true; + } + + void PrivateChatMessage::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(chatChannel); + bitStream.Write(messageLength); + bitStream.Write(senderName); + bitStream.Write(senderID); + bitStream.Write(sourceID); + bitStream.Write(senderGMLevel); + bitStream.Write(receiverName); + bitStream.Write(receiverGMLevel); + bitStream.Write(responseCode); + bitStream.Write(message); + } + + bool PrivateChatMessage::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(chatChannel)); + VALIDATE_READ(bitStream.Read(messageLength)); + if (messageLength > MAX_MESSAGE_LENGTH) return false; + VALIDATE_READ(bitStream.Read(senderName)); + VALIDATE_READ(bitStream.Read(senderID)); + VALIDATE_READ(bitStream.Read(sourceID)); + VALIDATE_READ(bitStream.Read(senderGMLevel)); + VALIDATE_READ(bitStream.Read(receiverName)); + VALIDATE_READ(bitStream.Read(receiverGMLevel)); + VALIDATE_READ(bitStream.Read(responseCode)); + message = LUWString(messageLength); + if (messageLength != 0) VALIDATE_READ(bitStream.Read(message)); + return true; + } + + void TeamInvite::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(invitedPlayer); + } + + bool TeamInvite::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(invitedPlayer)); + return true; + } + + void TeamInviteResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(declined); + bitStream.Write(leaderID); + } + + bool TeamInviteResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(declined)); + VALIDATE_READ(bitStream.Read(leaderID)); + return true; + } + + void TeamLeave::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + } + + bool TeamLeave::Deserialize(RakNet::BitStream& bitStream) { + return ReadPlayerHeader(bitStream, playerID, unknown); + } + + void TeamKick::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(kickedPlayer); + } + + bool TeamKick::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(kickedPlayer)); + return true; + } + + void TeamSetLeader::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(promotedPlayer); + } + + bool TeamSetLeader::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(promotedPlayer)); + return true; + } + + void TeamSetLoot::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(lootFlag); + } + + bool TeamSetLoot::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(lootFlag)); + return true; + } + + void TeamGetStatus::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + } + + bool TeamGetStatus::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } +} + +namespace ChatPackets::Client { + void GeneralChatMessage::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(unknown); + bitStream.Write(chatChannel); + bitStream.Write(message.size()); + bitStream.Write(senderName); + bitStream.Write(senderID); + bitStream.Write(sourceID); + bitStream.Write(senderGMLevel); + bitStream.Write(message); + bitStream.Write(0); // terminator + } + + bool GeneralChatMessage::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(unknown)); + VALIDATE_READ(bitStream.Read(chatChannel)); + uint32_t length{}; + VALIDATE_READ(bitStream.Read(length)); + if (length > MAX_MESSAGE_LENGTH) return false; + VALIDATE_READ(bitStream.Read(senderName)); + VALIDATE_READ(bitStream.Read(senderID)); + VALIDATE_READ(bitStream.Read(sourceID)); + VALIDATE_READ(bitStream.Read(senderGMLevel)); + message.resize(length); + if (length != 0) VALIDATE_READ(bitStream.ReadBits(reinterpret_cast(message.data()), BYTES_TO_BITS(length * sizeof(char16_t)), true)); + uint16_t terminator{}; + VALIDATE_READ(bitStream.Read(terminator)); + return true; + } + + void PrivateChatMessage::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(chatChannel); + bitStream.Write(messageLength); + bitStream.Write(senderName); + bitStream.Write(senderID); + bitStream.Write(sourceID); + bitStream.Write(senderGMLevel); + bitStream.Write(receiverName); + bitStream.Write(receiverGMLevel); + bitStream.Write(responseCode); + bitStream.Write(message); + } + + bool PrivateChatMessage::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(bitStream.Read(chatChannel)); + VALIDATE_READ(bitStream.Read(messageLength)); + VALIDATE_READ(bitStream.Read(senderName)); + VALIDATE_READ(bitStream.Read(senderID)); + VALIDATE_READ(bitStream.Read(sourceID)); + VALIDATE_READ(bitStream.Read(senderGMLevel)); + VALIDATE_READ(bitStream.Read(receiverName)); + VALIDATE_READ(bitStream.Read(receiverGMLevel)); + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(ReadRemainingWString(bitStream, message)); + return true; + } } void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, const bool broadcast) { @@ -63,91 +478,14 @@ void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::str } void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, const bool broadcast) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE); + Client::GeneralChatMessage chatMessage; + chatMessage.chatChannel = 4; + chatMessage.senderName = LUWString("", 33); + chatMessage.message = message; - bitStream.Write(0); - bitStream.Write(4); - - bitStream.Write(message.size()); - bitStream.Write(LUWString("", 33)); - - bitStream.Write(0); - bitStream.Write(0); - bitStream.Write(0); - - for (uint32_t i = 0; i < message.size(); ++i) { - bitStream.Write(message[i]); - } - - bitStream.Write(0); - - //This is so Wincent's announcement works: - if (sysAddr != UNASSIGNED_SYSTEM_ADDRESS) { - SEND_PACKET; - return; - } - - SEND_PACKET_BROADCAST; -} - -void ChatPackets::SendMessageFail(const SystemAddress& sysAddr) { - //0x00 - "Chat is currently disabled." - //0x01 - "Upgrade to a full LEGO Universe Membership to chat with other players." - - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SEND_CANNED_TEXT); - bitStream.Write(0); //response type, options above ^ - //docs say there's a wstring here-- no idea what it's for, or if it's even needed so leaving it as is for now. - SEND_PACKET; -} - -namespace ChatPackets { - void Announcement::Serialize(RakNet::BitStream& bitStream) const { - bitStream.Write(title.size()); - bitStream.Write(title); - bitStream.Write(message.size()); - bitStream.Write(message); - } -} - -void ChatPackets::AchievementNotify::Serialize(RakNet::BitStream& bitstream) const { - bitstream.Write(0); // Packing - bitstream.Write(0); // Packing - bitstream.Write(0); // Packing - bitstream.Write(earnerName); - bitstream.Write(0); // Packing / No way to know meaning because of not enough data. - bitstream.Write(0); // Packing / No way to know meaning because of not enough data. - bitstream.Write(0); // Packing / No way to know meaning because of not enough data. - bitstream.Write(0); // Packing / No way to know meaning because of not enough data. - bitstream.Write(missionEmailID); - bitstream.Write(earningPlayerID); - bitstream.Write(targetPlayerName); -} - -bool ChatPackets::AchievementNotify::Deserialize(RakNet::BitStream& bitstream) { - bitstream.IgnoreBytes(13); - VALIDATE_READ(bitstream.Read(earnerName)); - bitstream.IgnoreBytes(15); - VALIDATE_READ(bitstream.Read(missionEmailID)); - VALIDATE_READ(bitstream.Read(earningPlayerID)); - VALIDATE_READ(bitstream.Read(targetPlayerName)); - - return true; -} - -void ChatPackets::TeamInviteInitialResponse::Serialize(RakNet::BitStream& bitstream) const { - bitstream.Write(inviteFailedToSend); - bitstream.Write(playerName); -} - -void ChatPackets::SendRoutedMsg(const LUBitStream& msg, const LWOOBJID targetID, const SystemAddress& sysAddr) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(targetID); - - // Now write the actual packet - msg.WritePacket(bitStream); + RakNet::BitStream bitStream; + chatMessage.WritePacket(bitStream); + // A message with an address goes only to that client (so Wincent's announcement works) Game::server->Send(bitStream, sysAddr, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); } diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index b661bd716..90a732f82 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -12,50 +12,139 @@ struct SystemAddress; #include #include "dCommonVars.h" #include "MessageType/Chat.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" #include "BitStreamUtils.h" +#include "eAddFriendResponseCode.h" +#include "eAddFriendResponseType.h" +#include "eAddIgnoreResponse.h" +#include "eChatChannel.h" +#include "eChatMessageResponseCode.h" +#include "eGameMasterLevel.h" -struct ShowAllRequest{ - LWOOBJID requestor = LWOOBJID_EMPTY; - bool displayZoneData = true; - bool displayIndividualPlayers = true; - void Serialize(RakNet::BitStream& bitStream); - void Deserialize(RakNet::BitStream& inStream); -}; - -struct FindPlayerRequest{ - LWOOBJID requestor = LWOOBJID_EMPTY; - LUWString playerName; - void Serialize(RakNet::BitStream& bitStream); - void Deserialize(RakNet::BitStream& inStream); -}; - +/** + * Packets of the chat service (docs/PacketArchitecture.md). + * + * Top level: packets between world servers and the chat server (MessageType::Chat). The ones a client sends go to its + * world as a ROUTE_PACKET; the world forwards them to chat with the player's object ID in front, so every such + * struct starts with `playerID`. + * + * ChatPackets::Client: the chat-service packets the client receives. What the chat server sends a client in other + * services (friends, ignore list and team responses, team game messages) is in ClientPackets.h. The chat server wraps + * them in a WorldRoutePacket (WorldRoutePacket.h) to the world the player is in, which passes the inner packet on to + * the client unchanged. + */ namespace ChatPackets { + // World -> chat: a player finished loading into a world (also sent when they change worlds) + struct LoginSessionNotify : public LUBitStream { + static constexpr uint32_t MAX_NAME_LENGTH = 33; - struct Announcement : public LUBitStream { - std::string title; - std::string message; + LWOOBJID playerID{}; + std::string playerName; // u32 length, then 1 byte per character + LWOZONEID zoneID{}; + int64_t muteExpire{}; // time_t + eGameMasterLevel gmLevel{}; - Announcement() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GM_ANNOUNCE) {}; - virtual void Serialize(RakNet::BitStream& bitStream) const override; + LoginSessionNotify() : LUBitStream(ServiceType::CHAT, MessageType::Chat::LOGIN_SESSION_NOTIFY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; }; + // World -> chat: a player left (or switched characters, or a character was deleted) + struct UnexpectedDisconnect : public LUBitStream { + LWOOBJID playerID{}; + + UnexpectedDisconnect() : LUBitStream(ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // World -> chat: a player's GM level changed + struct GMLevelUpdate : public LUBitStream { + LWOOBJID playerID{}; + eGameMasterLevel gmLevel{}; + + GMLevelUpdate() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GMLEVEL_UPDATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // World -> chat, and chat -> every world: a player was muted until `expire` (time_t; 1 means until reviewed) + struct GMMute : public LUBitStream { + LWOOBJID playerID{}; + int64_t expire{}; + + GMMute() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GM_MUTE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // World -> chat -> every other world: a GM's /announce + struct Announcement : public LUBitStream { + std::string title; // u32 length, then 1 byte per character + std::string message; // u32 length, then 1 byte per character + + Announcement() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GM_ANNOUNCE) {}; + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // World -> chat (custom DLU message): make a team of the players starting an activity together + struct CreateTeam : public LUBitStream { + // More can't be a team; bigger requests are dropped + static constexpr uint64_t MAX_MEMBERS = 3; + + LWOOBJID leaderID{}; + std::vector members; // u64 count + LWOZONEID zoneID{}; + + CreateTeam() : LUBitStream(ServiceType::CHAT, MessageType::Chat::CREATE_TEAM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Chat -> every world: a team changed (or was deleted), so worlds can share loot and credit + struct TeamUpdate : public LUBitStream { + LWOOBJID teamID{}; + bool deleteTeam{}; // one bit; nothing follows when set + uint8_t lootFlag{}; + std::vector members; // u8 count + + TeamUpdate() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_GET_STATUS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // World -> chat -> the earner's online friends: a mission announcement (routed to the client as is) struct AchievementNotify : public LUBitStream { LUWString targetPlayerName{}; uint32_t missionEmailID{}; LWOOBJID earningPlayerID{}; LUWString earnerName{}; AchievementNotify() : LUBitStream(ServiceType::CHAT, MessageType::Chat::ACHIEVEMENT_NOTIFY) {} - void Serialize(RakNet::BitStream& bitstream) const override; - bool Deserialize(RakNet::BitStream& bitstream) override; + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; }; - struct TeamInviteInitialResponse : public LUBitStream { - bool inviteFailedToSend{}; - LUWString playerName{}; - TeamInviteInitialResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::TEAM_INVITE_INITIAL_RESPONSE) {} + // World -> chat: /showall + struct ShowAllRequest : public LUBitStream { + LWOOBJID requestor = LWOOBJID_EMPTY; + bool displayZoneData = true; // one bit + bool displayIndividualPlayers = true; // one bit - void Serialize(RakNet::BitStream& bitstream) const override; - // No Deserialize needed on our end + ShowAllRequest() : LUBitStream(ServiceType::CHAT, MessageType::Chat::SHOW_ALL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // World -> chat: /findplayer + struct FindPlayerRequest : public LUBitStream { + LWOOBJID requestor = LWOOBJID_EMPTY; + LUWString playerName; + + FindPlayerRequest() : LUBitStream(ServiceType::CHAT, MessageType::Chat::WHO) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; }; // World -> chat. A chat packet the client sent through the world server (WorldPackets::RoutePacket): its id, @@ -69,11 +158,232 @@ namespace ChatPackets { 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); + // Client -> world -> chat. Every packet below starts with the player's object ID (put there by the world). + // `unknown` is 4 bytes of the client's packet the server doesn't use. + + // A player's friends list (sent when they load into a world) + struct GetFriendsList : public LUBitStream { + LWOOBJID playerID{}; + + GetFriendsList() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GET_FRIENDS_LIST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct AddFriendRequest : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString friendName; + uint8_t isBestFriendRequest{}; + + AddFriendRequest() : LUBitStream(ServiceType::CHAT, MessageType::Chat::ADD_FRIEND_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // The answer of the player who was asked to be friends + struct AddFriendResponse : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + eAddFriendResponseCode responseCode{}; + LUWString friendName; + + AddFriendResponse() : LUBitStream(ServiceType::CHAT, MessageType::Chat::ADD_FRIEND_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct RemoveFriend : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString friendName; + + RemoveFriend() : LUBitStream(ServiceType::CHAT, MessageType::Chat::REMOVE_FRIEND) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct GetIgnoreList : public LUBitStream { + LWOOBJID playerID{}; + + GetIgnoreList() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GET_IGNORE_LIST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct AddIgnore : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString playerName; + + AddIgnore() : LUBitStream(ServiceType::CHAT, MessageType::Chat::ADD_IGNORE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct RemoveIgnore : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString playerName; + + RemoveIgnore() : LUBitStream(ServiceType::CHAT, MessageType::Chat::REMOVE_IGNORE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Chat in a channel the chat server handles (team chat) + struct GeneralChatMessage : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + eChatChannel chatChannel{}; + uint32_t messageLength{}; // characters in message; more than MAX_MESSAGE_LENGTH is refused + // What the client fills in about itself (the server uses its own records) + LUWString senderName; + LWOOBJID senderID{}; + uint16_t sourceID{}; + uint8_t senderGMLevel{}; + LUWString message{ 0u }; // messageLength characters + + GeneralChatMessage() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // A whisper + struct PrivateChatMessage : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + eChatChannel chatChannel{}; + uint32_t messageLength{}; // characters in message; more than MAX_MESSAGE_LENGTH is refused + // What the client fills in about itself (the server uses its own records) + LUWString senderName; + LWOOBJID senderID{}; + uint16_t sourceID{}; + uint8_t senderGMLevel{}; + LUWString receiverName; + uint8_t receiverGMLevel{}; + uint8_t responseCode{}; + LUWString message{ 0u }; // messageLength characters + + PrivateChatMessage() : LUBitStream(ServiceType::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamInvite : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString invitedPlayer; + + TeamInvite() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_INVITE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamInviteResponse : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + uint8_t declined{}; + LWOOBJID leaderID{}; + + TeamInviteResponse() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_INVITE_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamLeave : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + + TeamLeave() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_LEAVE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamKick : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString kickedPlayer; + + TeamKick() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_KICK) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Make another member the team's leader + struct TeamSetLeader : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + LUWString promotedPlayer; + + TeamSetLeader() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_SET_LEADER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamSetLoot : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + uint8_t lootFlag{}; + + TeamSetLoot() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_SET_LOOT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // A player's team, again (sent when they load into a world) + struct TeamGetStatus : public LUBitStream { + LWOOBJID playerID{}; + + TeamGetStatus() : LUBitStream(ServiceType::CHAT, MessageType::Chat::TEAM_GET_STATUS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + /** + * Chat-service packets the client receives, from the chat server (routed through the player's world) or from its world. + */ + namespace Client { + // Chat in a channel (world -> client; sent to everyone) + struct GeneralChatMessage : public LUBitStream { + uint64_t unknown{}; // always 0 from the server + uint8_t chatChannel{}; + LUWString senderName; + LWOOBJID senderID{}; + uint16_t sourceID{}; + uint8_t senderGMLevel{}; // DLU always sends 0 + std::u16string message; // u32 length (without the terminator), the characters, then a 0 terminator + + GeneralChatMessage() : LUBitStream(ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // A whisper, or team chat, or why a whisper didn't go out + struct PrivateChatMessage : public LUBitStream { + LWOOBJID playerID{}; // the sender again + eChatChannel chatChannel{}; + uint32_t messageLength{}; // DLU always sends 0 + LUWString senderName; + LWOOBJID senderID{}; + uint16_t sourceID{}; + eGameMasterLevel senderGMLevel{}; + LUWString receiverName; + eGameMasterLevel receiverGMLevel{}; + eChatMessageResponseCode responseCode{}; + // Everything that is left. The sender's message as it came in, with its padding. + LUWString message{ 0u }; + + PrivateChatMessage() : LUBitStream(ServiceType::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + } + + // A system message (chat channel 4) to one client, or everyone when sysAddr is UNASSIGNED_SYSTEM_ADDRESS. + // `broadcast` is not used: a message with an address only goes to that client. void SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, bool broadcast = false); void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, bool broadcast = false); - void SendMessageFail(const SystemAddress& sysAddr); - void SendRoutedMsg(const LUBitStream& msg, const LWOOBJID targetID, const SystemAddress& sysAddr); }; #endif // CHATPACKETS_H diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index 322ac2162..087e15034 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -483,3 +483,388 @@ bool ClientPackets::BlueprintLoadItemResponse::Deserialize(RakNet::BitStream& bi VALIDATE_READ(bitStream.Read(destItemId)); return true; } + +namespace { + // A zone ID as three fields: map (u16), instance (u16), clone (u32) + void WriteZone(RakNet::BitStream& bitStream, const LWOZONEID& zoneID) { + bitStream.Write(zoneID.GetMapID()); + bitStream.Write(zoneID.GetInstanceID()); + bitStream.Write(zoneID.GetCloneID()); + } + + bool ReadZone(RakNet::BitStream& bitStream, LWOZONEID& zoneID) { + LWOMAPID mapID{}; + LWOINSTANCEID instanceID{}; + LWOCLONEID cloneID{}; + VALIDATE_READ(bitStream.Read(mapID)); + VALIDATE_READ(bitStream.Read(instanceID)); + VALIDATE_READ(bitStream.Read(cloneID)); + zoneID = LWOZONEID(mapID, instanceID, cloneID); + return true; + } +} + +namespace ClientPackets { + void SendCannedText::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(responseType); + } + + bool SendCannedText::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(responseType)); + return true; + } + + void GetFriendsListResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(responseCode); + bitStream.Write(packetLength); + bitStream.Write(friends.size()); + for (const auto& data : friends) data.Serialize(bitStream); + } + + bool GetFriendsListResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(bitStream.Read(packetLength)); + uint16_t count{}; + VALIDATE_READ(bitStream.Read(count)); + friends.resize(count); + // The layout FriendData::Serialize writes + for (auto& data : friends) { + uint8_t isOnline{}, isBestFriend{}, isFTP{}, unknown1{}; + uint32_t unknown4{}; + VALIDATE_READ(bitStream.Read(isOnline)); + VALIDATE_READ(bitStream.Read(isBestFriend)); + VALIDATE_READ(bitStream.Read(isFTP)); + VALIDATE_READ(bitStream.Read(unknown4)); + VALIDATE_READ(bitStream.Read(unknown1)); + VALIDATE_READ(ReadZone(bitStream, data.zoneID)); + VALIDATE_READ(bitStream.Read(data.friendID)); + LUWString name; + VALIDATE_READ(bitStream.Read(name)); + data.friendName = name.GetAsString(); + uint16_t unknown2{}; + VALIDATE_READ(bitStream.Read(unknown4)); + VALIDATE_READ(bitStream.Read(unknown2)); + data.isOnline = isOnline != 0; + data.isBestFriend = isBestFriend != 0; + data.isFTP = isFTP != 0; + } + return true; + } + + void AddFriendRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(requestorName); + bitStream.Write(isBestFriendRequest); + } + + bool AddFriendRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(requestorName)); + VALIDATE_READ(bitStream.Read(isBestFriendRequest)); + return true; + } + + void AddFriendResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(responseCode); + bitStream.Write(isOnlineOrBestFriend); + bitStream.Write(friendName); + if (responseCode != eAddFriendResponseType::ACCEPTED) return; + bitStream.Write(friendID); + WriteZone(bitStream, zoneID); + bitStream.Write(isBestFriend); + bitStream.Write(isFreeTrial); + } + + bool AddFriendResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(bitStream.Read(isOnlineOrBestFriend)); + VALIDATE_READ(bitStream.Read(friendName)); + if (responseCode != eAddFriendResponseType::ACCEPTED) return true; + VALIDATE_READ(bitStream.Read(friendID)); + VALIDATE_READ(ReadZone(bitStream, zoneID)); + VALIDATE_READ(bitStream.Read(isBestFriend)); + VALIDATE_READ(bitStream.Read(isFreeTrial)); + return true; + } + + void RemoveFriendResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(isSuccessful); + bitStream.Write(friendName); + } + + bool RemoveFriendResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(isSuccessful)); + VALIDATE_READ(bitStream.Read(friendName)); + return true; + } + + void UpdateFriendNotify::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(notifyType); + bitStream.Write(friendName); + WriteZone(bitStream, zoneID); + bitStream.Write(isBestFriend); + bitStream.Write(isFreeTrial); + } + + bool UpdateFriendNotify::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(notifyType)); + VALIDATE_READ(bitStream.Read(friendName)); + VALIDATE_READ(ReadZone(bitStream, zoneID)); + VALIDATE_READ(bitStream.Read(isBestFriend)); + VALIDATE_READ(bitStream.Read(isFreeTrial)); + return true; + } + + void WhoResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(isOnline); + WriteZone(bitStream, zoneID); + bitStream.Write(playerName); + } + + bool WhoResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(isOnline)); + VALIDATE_READ(ReadZone(bitStream, zoneID)); + VALIDATE_READ(bitStream.Read(playerName)); + return true; + } + + void ShowAllResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(!displayZoneData && !displayIndividualPlayers); + bitStream.Write(playerCount); + bitStream.Write(simCount); + bitStream.Write(displayIndividualPlayers); + bitStream.Write(displayZoneData); + if (!displayZoneData && !displayIndividualPlayers) return; + for (const auto& player : players) { + bitStream.Write(0); // structure packing + if (displayIndividualPlayers) bitStream.Write(LUWString(player.name)); + if (displayZoneData) WriteZone(bitStream, player.zoneID); + } + } + + bool ShowAllResponse::Deserialize(RakNet::BitStream& bitStream) { + uint8_t nothingShown{}, individualPlayers{}, zoneData{}; + VALIDATE_READ(bitStream.Read(nothingShown)); + VALIDATE_READ(bitStream.Read(playerCount)); + VALIDATE_READ(bitStream.Read(simCount)); + VALIDATE_READ(bitStream.Read(individualPlayers)); + VALIDATE_READ(bitStream.Read(zoneData)); + displayIndividualPlayers = individualPlayers != 0; + displayZoneData = zoneData != 0; + players.clear(); + if (!displayZoneData && !displayIndividualPlayers) return true; + while (bitStream.GetNumberOfUnreadBits() > 0) { + auto& player = players.emplace_back(); + uint8_t packing{}; + VALIDATE_READ(bitStream.Read(packing)); + if (displayIndividualPlayers) { + LUWString name; + VALIDATE_READ(bitStream.Read(name)); + player.name = name.GetAsString(); + } + if (displayZoneData) VALIDATE_READ(ReadZone(bitStream, player.zoneID)); + } + return true; + } + + void GetIgnoreListResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(isFreeTrial); + bitStream.Write(padding); + bitStream.Write(ignored.size()); + for (const auto& player : ignored) { + bitStream.Write(player.playerID); + bitStream.Write(player.playerName); + } + } + + bool GetIgnoreListResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(isFreeTrial)); + VALIDATE_READ(bitStream.Read(padding)); + uint16_t count{}; + VALIDATE_READ(bitStream.Read(count)); + ignored.resize(count); + for (auto& player : ignored) { + VALIDATE_READ(bitStream.Read(player.playerID)); + VALIDATE_READ(bitStream.Read(player.playerName)); + } + return true; + } + + void AddIgnoreResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(responseCode); + bitStream.Write(playerName); + bitStream.Write(playerID); + } + + bool AddIgnoreResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(bitStream.Read(playerName)); + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } + + void RemoveIgnoreResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(responseCode); + bitStream.Write(playerName); + } + + bool RemoveIgnoreResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(bitStream.Read(playerName)); + return true; + } + + void TeamInvite::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(senderName); + bitStream.Write(senderID); + } + + bool TeamInvite::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(senderName)); + VALIDATE_READ(bitStream.Read(senderID)); + return true; + } + + void TeamInviteInitialResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(inviteFailedToSend); + bitStream.Write(playerName); + } + + bool TeamInviteInitialResponse::Deserialize(RakNet::BitStream& bitStream) { + uint8_t failed{}; + VALIDATE_READ(bitStream.Read(failed)); + inviteFailedToSend = failed != 0; + VALIDATE_READ(bitStream.Read(playerName)); + return true; + } + + void TeamGameMsg::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(target); + bitStream.Write(msgId); + } + + bool TeamGameMsg::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(target)); + MessageType::Game readId{}; + VALIDATE_READ(bitStream.Read(readId)); + return readId == msgId; + } + + void TeamInviteConfirm::Serialize(RakNet::BitStream& bitStream) const { + TeamGameMsg::Serialize(bitStream); + bitStream.Write(bLeaderIsFreeTrial); + bitStream.Write(i64LeaderID); + WriteZone(bitStream, i64LeaderZoneID); + bitStream.Write(binaryBufferLength); + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(ucResponseCode); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsLeaderName); + } + + bool TeamInviteConfirm::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(TeamGameMsg::Deserialize(bitStream)); + VALIDATE_READ(bitStream.Read(bLeaderIsFreeTrial)); + VALIDATE_READ(bitStream.Read(i64LeaderID)); + VALIDATE_READ(ReadZone(bitStream, i64LeaderZoneID)); + VALIDATE_READ(bitStream.Read(binaryBufferLength)); + if (binaryBufferLength != 0) return false; // never filled + VALIDATE_READ(bitStream.Read(ucLootFlag)); + VALIDATE_READ(bitStream.Read(ucNumOfOtherPlayers)); + VALIDATE_READ(bitStream.Read(ucResponseCode)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsLeaderName)); + return true; + } + + void TeamGetStatusResponse::Serialize(RakNet::BitStream& bitStream) const { + TeamGameMsg::Serialize(bitStream); + bitStream.Write(i64LeaderID); + WriteZone(bitStream, i64LeaderZoneID); + bitStream.Write(binaryBufferLength); + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsLeaderName); + } + + bool TeamGetStatusResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(TeamGameMsg::Deserialize(bitStream)); + VALIDATE_READ(bitStream.Read(i64LeaderID)); + VALIDATE_READ(ReadZone(bitStream, i64LeaderZoneID)); + VALIDATE_READ(bitStream.Read(binaryBufferLength)); + if (binaryBufferLength != 0) return false; // never filled + VALIDATE_READ(bitStream.Read(ucLootFlag)); + VALIDATE_READ(bitStream.Read(ucNumOfOtherPlayers)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsLeaderName)); + return true; + } + + void TeamSetLeader::Serialize(RakNet::BitStream& bitStream) const { + TeamGameMsg::Serialize(bitStream); + bitStream.Write(i64PlayerID); + } + + bool TeamSetLeader::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(TeamGameMsg::Deserialize(bitStream)); + VALIDATE_READ(bitStream.Read(i64PlayerID)); + return true; + } + + void TeamAddPlayer::Serialize(RakNet::BitStream& bitStream) const { + TeamGameMsg::Serialize(bitStream); + bitStream.Write(bIsFreeTrial); + bitStream.Write(bLocal); + bitStream.Write(bNoLootOnDeath); + bitStream.Write(i64PlayerID); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsPlayerName); + bitStream.Write1(); // zoneID is always written + WriteZone(bitStream, zoneID); + } + + bool TeamAddPlayer::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(TeamGameMsg::Deserialize(bitStream)); + VALIDATE_READ(bitStream.Read(bIsFreeTrial)); + VALIDATE_READ(bitStream.Read(bLocal)); + VALIDATE_READ(bitStream.Read(bNoLootOnDeath)); + VALIDATE_READ(bitStream.Read(i64PlayerID)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsPlayerName)); + bool hasZone{}; + VALIDATE_READ(bitStream.Read(hasZone)); + if (!hasZone) return false; // always written + VALIDATE_READ(ReadZone(bitStream, zoneID)); + return true; + } + + void TeamRemovePlayer::Serialize(RakNet::BitStream& bitStream) const { + TeamGameMsg::Serialize(bitStream); + bitStream.Write(bDisband); + bitStream.Write(bIsKicked); + bitStream.Write(bIsLeaving); + bitStream.Write(bLocal); + bitStream.Write(i64LeaderID); + bitStream.Write(i64PlayerID); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsPlayerName); + } + + bool TeamRemovePlayer::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(TeamGameMsg::Deserialize(bitStream)); + VALIDATE_READ(bitStream.Read(bDisband)); + VALIDATE_READ(bitStream.Read(bIsKicked)); + VALIDATE_READ(bitStream.Read(bIsLeaving)); + VALIDATE_READ(bitStream.Read(bLocal)); + VALIDATE_READ(bitStream.Read(i64LeaderID)); + VALIDATE_READ(bitStream.Read(i64PlayerID)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsPlayerName)); + return true; + } + + void TeamSetOffWorldFlag::Serialize(RakNet::BitStream& bitStream) const { + TeamGameMsg::Serialize(bitStream); + bitStream.Write(i64PlayerID); + WriteZone(bitStream, zoneID); + } + + bool TeamSetOffWorldFlag::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(TeamGameMsg::Deserialize(bitStream)); + VALIDATE_READ(bitStream.Read(i64PlayerID)); + VALIDATE_READ(ReadZone(bitStream, zoneID)); + return true; + } +} diff --git a/dNet/ClientPackets.h b/dNet/ClientPackets.h index cc07fb32e..c85c83ebb 100644 --- a/dNet/ClientPackets.h +++ b/dNet/ClientPackets.h @@ -15,8 +15,11 @@ #include "BitStreamUtils.h" #include "dCommonVars.h" +#include "eAddFriendResponseType.h" +#include "eAddIgnoreResponse.h" #include "eBlueprintSaveResponseType.h" #include "MessageType/Client.h" +#include "MessageType/Game.h" #include "NiPoint3.h" #include "Stamps.h" @@ -263,4 +266,247 @@ namespace ClientPackets { }; }; +/** + * What the chat server sends the client (routed through the player's world in a WorldRoutePacket): friends, ignore + * list and team responses, and the team game messages. SendCannedText comes from the world. + */ +namespace ClientPackets { + // World -> client: chat is off (0: "Chat is currently disabled.", 1: "Upgrade to a full LEGO Universe + // Membership to chat with other players.") + struct SendCannedText : public LUBitStream { + uint8_t responseType{}; + + SendCannedText() : LUBitStream(ServiceType::CLIENT, MessageType::Client::SEND_CANNED_TEXT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct GetFriendsListResponse : public LUBitStream { + uint8_t responseCode{}; + uint16_t packetLength{ 1 }; // the client skips it + std::vector friends; // u16 count + + GetFriendsListResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::GET_FRIENDS_LIST_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Someone wants to be friends + struct AddFriendRequest : public LUBitStream { + LUWString requestorName; + uint8_t isBestFriendRequest{}; // unused in live, and the client does nothing with it + + AddFriendRequest() : LUBitStream(ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct AddFriendResponse : public LUBitStream { + eAddFriendResponseType responseCode{}; + // ACCEPTED: whether the friend is online; anything else: whether they are best friends already + uint8_t isOnlineOrBestFriend{}; + LUWString friendName; + // ACCEPTED only + LWOOBJID friendID{}; + LWOZONEID zoneID{}; + uint8_t isBestFriend{}; + uint8_t isFreeTrial{}; + + AddFriendResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct RemoveFriendResponse : public LUBitStream { + uint8_t isSuccessful{}; + LUWString friendName; + + RemoveFriendResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::REMOVE_FRIEND_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // A friend logged in (1), out (0) or changed worlds (2) + struct UpdateFriendNotify : public LUBitStream { + uint8_t notifyType{}; + LUWString friendName; + LWOZONEID zoneID{}; // clone 0 when it is the receiver's clone + uint8_t isBestFriend{}; + uint8_t isFreeTrial{}; + + UpdateFriendNotify() : LUBitStream(ServiceType::CLIENT, MessageType::Client::UPDATE_FRIEND_NOTIFY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Answer to /findplayer + struct WhoResponse : public LUBitStream { + uint8_t isOnline{}; + LWOZONEID zoneID{}; + LUWString playerName; + + WhoResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::WHO_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Answer to /showall + struct ShowAllResponse : public LUBitStream { + struct Player { + std::string name; // written when displayIndividualPlayers + LWOZONEID zoneID{}; // written when displayZoneData + }; + + uint32_t playerCount{}; + uint32_t simCount{}; + bool displayIndividualPlayers{}; + bool displayZoneData{}; + // Only written when either display flag is set; there is no count, they fill the rest of the packet + std::vector players; + + ShowAllResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::SHOW_ALL_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct GetIgnoreListResponse : public LUBitStream { + struct Ignored { + LWOOBJID playerID{}; + LUWString playerName{ 36u }; + }; + + uint8_t isFreeTrial{}; + uint16_t padding{}; // struct alignment + std::vector ignored; // u16 count + + GetIgnoreListResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::GET_IGNORE_LIST_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct AddIgnoreResponse : public LUBitStream { + eAddIgnoreResponse responseCode{}; + LUWString playerName; + LWOOBJID playerID{}; + + AddIgnoreResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::ADD_IGNORE_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct RemoveIgnoreResponse : public LUBitStream { + int8_t responseCode{}; + LUWString playerName; + + RemoveIgnoreResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::REMOVE_IGNORE_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Someone invites the player to their team + struct TeamInvite : public LUBitStream { + LUWString senderName; + LWOOBJID senderID{}; + + TeamInvite() : LUBitStream(ServiceType::CLIENT, MessageType::Client::TEAM_INVITE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Whether the player's team invite went out + struct TeamInviteInitialResponse : public LUBitStream { + bool inviteFailedToSend{}; // one byte + LUWString playerName{}; + TeamInviteInitialResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::TEAM_INVITE_INITIAL_RESPONSE) {} + + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + /** + * Team game messages, written by the chat server: the CLIENT/GAME_MSG header, the object the message is for, + * the game message ID, then the message's own fields (like GameMessages::NetGameMsg, which chat doesn't link). + */ + struct TeamGameMsg : public LUBitStream { + LWOOBJID target{}; + MessageType::Game msgId{}; + + TeamGameMsg(MessageType::Game id) : LUBitStream(ServiceType::CLIENT, MessageType::Client::GAME_MSG), msgId{ id } {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamInviteConfirm : public TeamGameMsg { + bool bLeaderIsFreeTrial{}; + LWOOBJID i64LeaderID{}; + LWOZONEID i64LeaderZoneID{}; + uint32_t binaryBufferLength{}; // always empty + uint8_t ucLootFlag{}; + uint8_t ucNumOfOtherPlayers{}; + uint8_t ucResponseCode{}; + std::u16string wsLeaderName; // u32 length + + TeamInviteConfirm() : TeamGameMsg(MessageType::Game::TEAM_INVITE_CONFIRM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamGetStatusResponse : public TeamGameMsg { + LWOOBJID i64LeaderID{}; + LWOZONEID i64LeaderZoneID{}; + uint32_t binaryBufferLength{}; // always empty + uint8_t ucLootFlag{}; + uint8_t ucNumOfOtherPlayers{}; + std::u16string wsLeaderName; // u32 length + + TeamGetStatusResponse() : TeamGameMsg(MessageType::Game::TEAM_GET_STATUS_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamSetLeader : public TeamGameMsg { + LWOOBJID i64PlayerID{}; + + TeamSetLeader() : TeamGameMsg(MessageType::Game::TEAM_SET_LEADER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamAddPlayer : public TeamGameMsg { + bool bIsFreeTrial{}; + bool bLocal{}; + bool bNoLootOnDeath{}; + LWOOBJID i64PlayerID{}; + std::u16string wsPlayerName; // u32 length + LWOZONEID zoneID{}; // always written (its default flag is always set); clone 0 when it's the receiver's clone + + TeamAddPlayer() : TeamGameMsg(MessageType::Game::TEAM_ADD_PLAYER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamRemovePlayer : public TeamGameMsg { + bool bDisband{}; + bool bIsKicked{}; + bool bIsLeaving{}; + bool bLocal{}; + LWOOBJID i64LeaderID{}; // not empty: the client makes this player the leader + LWOOBJID i64PlayerID{}; + std::u16string wsPlayerName; // u32 length + + TeamRemovePlayer() : TeamGameMsg(MessageType::Game::TEAM_REMOVE_PLAYER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + struct TeamSetOffWorldFlag : public TeamGameMsg { + LWOOBJID i64PlayerID{}; + LWOZONEID zoneID{}; // clone 0 when it's the receiver's clone + + TeamSetOffWorldFlag() : TeamGameMsg(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; +} + #endif // CLIENTPACKETS_H diff --git a/dNet/PacketDispatcher.h b/dNet/PacketDispatcher.h new file mode 100644 index 000000000..ea8046fb6 --- /dev/null +++ b/dNet/PacketDispatcher.h @@ -0,0 +1,77 @@ +#ifndef PACKETDISPATCHER_H +#define PACKETDISPATCHER_H + +#include +#include +#include +#include + +#include "BitStream.h" +#include "BitStreamUtils.h" +#include "Logger.h" +#include "RakNetTypes.h" +#include "StringifiedEnum.h" + +/** + * One dispatch map per service a server receives (see docs/PacketArchitecture.md, section 2.2). + * + * Each entry names the struct a packet ID is read into and the function that handles it, so the same struct can be + * handled differently by each server that receives it (WORLD_READY means one thing to master and another to the + * dashboard). Dispatch reads the packet into a fresh struct; a packet that fails to Deserialize is logged and + * dropped before its handler runs. + * + * PacketDispatcher handlers; + * handlers.On(MessageType::Master::PLAYER_ADDED, &OnPlayerAdded); + * ... + * handlers.Dispatch(packet); // reads the header and routes by packet ID + */ +template +class PacketDispatcher { +public: + template + using Handler = std::function; + + // Registers the struct packets with this ID are read into, and what to do with them + template + void On(const IdT id, Handler handler) { + static_assert(std::is_base_of_v, "packets are LUBitStream structs"); + m_Handlers[id] = [handler = std::move(handler)](RakNet::BitStream& bitStream, const SystemAddress& sysAddr) { + Msg msg; + if (!msg.Deserialize(bitStream)) return false; + handler(msg, sysAddr); + return true; + }; + } + + bool Has(const IdT id) const { return m_Handlers.contains(id); } + + /** + * Routes a payload (the stream positioned right after the packet header) to its handler. + * Returns false if the ID has no handler. Packets that fail to Deserialize are logged and dropped. + */ + bool Dispatch(const IdT id, RakNet::BitStream& bitStream, const SystemAddress& sysAddr) const { + const auto it = m_Handlers.find(id); + if (it == m_Handlers.end()) return false; + if (!it->second(bitStream, sysAddr)) { + LOG("Dropped a %s packet (%u) from %s that failed to read", StringifiedEnum::ToString(id).data(), static_cast(id), sysAddr.ToString()); + } + return true; + } + + /** + * Reads the packet's header and routes the rest to the handler for its ID. Returns false if the packet is not an + * LU packet of this dispatcher's service or has no handler. + */ + bool Dispatch(const Packet* packet, const ServiceType service) const { + if (!packet || packet->length < 1) return false; + RakNet::BitStream bitStream(packet->data, packet->length, false); + LUBitStream header; + if (!header.ReadHeader(bitStream) || header.connectionType != service) return false; + return Dispatch(static_cast(header.internalPacketID), bitStream, packet->systemAddress); + } + +private: + std::map> m_Handlers; +}; + +#endif // PACKETDISPATCHER_H diff --git a/dNet/WorldRoutePacket.cpp b/dNet/WorldRoutePacket.cpp new file mode 100644 index 000000000..10bf8ac2c --- /dev/null +++ b/dNet/WorldRoutePacket.cpp @@ -0,0 +1,17 @@ +#include "WorldRoutePacket.h" + +namespace ChatPackets { + void WorldRoutePacket::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(targetID); + if (routed) routed->WritePacket(bitStream); + else if (!routedData.empty()) bitStream.WriteAlignedBytes(routedData.data(), static_cast(routedData.size())); + } + + bool WorldRoutePacket::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(targetID)); + // Whole bytes only, as they are passed on + routedData.resize(BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits())); + if (bitStream.GetNumberOfUnreadBits() % 8 != 0) routedData.pop_back(); + return routedData.empty() || bitStream.ReadAlignedBytes(routedData.data(), static_cast(routedData.size())); + } +} diff --git a/dNet/WorldRoutePacket.h b/dNet/WorldRoutePacket.h new file mode 100644 index 000000000..e08df3478 --- /dev/null +++ b/dNet/WorldRoutePacket.h @@ -0,0 +1,31 @@ +#ifndef WORLDROUTEPACKET_H +#define WORLDROUTEPACKET_H + +#include +#include + +#include "BitStreamUtils.h" +#include "dCommonVars.h" +#include "MessageType/Chat.h" + +/** + * WORLD_ROUTE_PACKET (chat service), in its own file because it carries a packet of another service: the chat server + * wraps what it sends a client (ClientPackets, ChatPackets::Client) in one of these to the world the player is in. + */ +namespace ChatPackets { + // Chat -> world: pass `routed` to the client of targetID. The world forwards everything after targetID as is. + struct WorldRoutePacket : public LUBitStream { + LWOOBJID targetID{}; + // Serialize: the packet to route (its header included). Not owned. + const LUBitStream* routed{}; + // Deserialize: the routed packet's bytes, header included. Serialize writes these when routed is null. + std::vector routedData; + + WorldRoutePacket() : LUBitStream(ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + +} + +#endif // WORLDROUTEPACKET_H diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 4260bbf32..cb010c1d0 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -59,6 +59,9 @@ #include "Entity.h" #include "Character.h" #include "ChatPackets.h" +#include "WorldRoutePacket.h" +#include "ChatServerLink.h" +#include "PacketDispatcher.h" #include "GameMessageHandler.h" #include "GameMessages.h" #include "Mail.h" @@ -590,6 +593,69 @@ int main(int argc, char** argv) { return EXIT_SUCCESS; } +namespace { + // Packets from the chat server + const PacketDispatcher& ChatHandlers() { + static const auto handlers = [] { + PacketDispatcher handlers; + // A packet for one of our players' clients + handlers.On(MessageType::Chat::WORLD_ROUTE_PACKET, [](const ChatPackets::WorldRoutePacket& route, const SystemAddress&) { + auto player = Game::entityManager->GetEntity(route.targetID); + if (!player) return; + + auto sysAddr = player->GetSystemAddress(); + + //Write our stream outwards: + RakNet::BitStream bitStream; + if (!route.routedData.empty()) bitStream.WriteAlignedBytes(route.routedData.data(), static_cast(route.routedData.size())); + Game::server->Send(bitStream, sysAddr, false); //send routed packet to player + }); + + handlers.On(MessageType::Chat::GM_ANNOUNCE, [](const ChatPackets::Announcement& announcement, const SystemAddress&) { + //Send to our clients: + AMFArrayValue args; + + args.Insert("title", announcement.title); + args.Insert("message", announcement.message); + + GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", args); + }); + + handlers.On(MessageType::Chat::GM_MUTE, [](const ChatPackets::GMMute& mute, const SystemAddress&) { + auto* entity = Game::entityManager->GetEntity(mute.playerID); + auto* character = entity != nullptr ? entity->GetCharacter() : nullptr; + auto* user = character != nullptr ? character->GetParentUser() : nullptr; + if (user) { + user->SetMuteExpire(static_cast(mute.expire)); + + entity->GetCharacter()->SendMuteNotice(); + } + }); + + handlers.On(MessageType::Chat::TEAM_GET_STATUS, [](const ChatPackets::TeamUpdate& update, const SystemAddress&) { + const LWOOBJID teamID = update.teamID; + + if (update.deleteTeam) { + TeamManager::Instance()->DeleteTeam(teamID); + + LOG("Deleting team (%llu)", teamID); + + return; + } + + LOG("Updating team ID:(%llu), Loot:(%i), #Members:(%i)", teamID, update.lootFlag, static_cast(update.members.size())); + for (const auto member : update.members) { + LOG("Added member (%llu) to the team", member); + } + + TeamManager::Instance()->UpdateTeam(teamID, update.lootFlag, update.members); + }); + return handlers; + }(); + return handlers; + } +} + void HandlePacketChat(Packet* packet) { if (packet->length < 1) return; if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) { @@ -606,118 +672,12 @@ void HandlePacketChat(Packet* packet) { } if (packet->data[0] == ID_USER_PACKET_ENUM && packet->length >= 4) { - if (static_cast(packet->data[1]) == ServiceType::CHAT) { - switch (static_cast(packet->data[3])) { - case MessageType::Chat::WORLD_ROUTE_PACKET: { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); - - auto player = Game::entityManager->GetEntity(playerID); - if (!player) return; - - auto sysAddr = player->GetSystemAddress(); - - //Write our stream outwards: - CBITSTREAM; - unsigned char data; - while (inStream.Read(data)) { - bitStream.Write(data); - } - - SEND_PACKET; //send routed packet to player - break; - } - - case MessageType::Chat::GM_ANNOUNCE: { - CINSTREAM_SKIP_HEADER; - - std::string title; - std::string msg; - - uint32_t len; - inStream.Read(len); - for (uint32_t i = 0; len > i; i++) { - char character; - inStream.Read(character); - title += character; - } - - len = 0; - inStream.Read(len); - for (uint32_t i = 0; len > i; i++) { - char character; - inStream.Read(character); - msg += character; - } - - //Send to our clients: - AMFArrayValue args; - - args.Insert("title", title); - args.Insert("message", msg); - - GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", args); - - break; - } - - case MessageType::Chat::GM_MUTE: { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerId; - time_t expire = 0; - inStream.Read(playerId); - inStream.Read(expire); - - auto* entity = Game::entityManager->GetEntity(playerId); - auto* character = entity != nullptr ? entity->GetCharacter() : nullptr; - auto* user = character != nullptr ? character->GetParentUser() : nullptr; - if (user) { - user->SetMuteExpire(expire); - - entity->GetCharacter()->SendMuteNotice(); - } - - break; - } - - case MessageType::Chat::TEAM_GET_STATUS: { - CINSTREAM_SKIP_HEADER; - - LWOOBJID teamID = 0; - char lootOption = 0; - char memberCount = 0; - std::vector members; - - inStream.Read(teamID); - bool deleteTeam = inStream.ReadBit(); - - if (deleteTeam) { - TeamManager::Instance()->DeleteTeam(teamID); - - LOG("Deleting team (%llu)", teamID); - - break; - } - - inStream.Read(lootOption); - inStream.Read(memberCount); - LOG("Updating team ID:(%llu), Loot:(%i), #Members:(%i)", teamID, lootOption, memberCount); - for (char i = 0; i < memberCount; i++) { - LWOOBJID member = LWOOBJID_EMPTY; - inStream.Read(member); - members.push_back(member); - - LOG("Added member (%llu) to the team", member); - } - - TeamManager::Instance()->UpdateTeam(teamID, lootOption, members); - - break; - } - default: - LOG("Received an unknown chat: %s", StringifiedEnum::ToString(static_cast(packet->data[3])).data()); - } + RakNet::BitStream inStream(packet->data, packet->length, false); + LUBitStream header; + if (!header.ReadHeader(inStream) || header.connectionType != ServiceType::CHAT) return; + const auto messageID = static_cast(header.internalPacketID); + if (!ChatHandlers().Dispatch(messageID, inStream, packet->systemAddress)) { + LOG("Received an unknown chat: %s", StringifiedEnum::ToString(messageID).data()); } } } @@ -1125,22 +1085,13 @@ void LoadPlayer(const SystemAddress& sysAddr) { if (user) { const auto& playerName = character->GetName(); - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::LOGIN_SESSION_NOTIFY); - bitStream.Write(player->GetObjectID()); - bitStream.Write(playerName.size()); - for (size_t i = 0; i < playerName.size(); i++) { - bitStream.Write(playerName[i]); - } - - auto zone = Game::zoneManager->GetZone()->GetZoneID(); - bitStream.Write(zone.GetMapID()); - bitStream.Write(zone.GetInstanceID()); - bitStream.Write(zone.GetCloneID()); - bitStream.Write(user->GetMuteExpire()); - bitStream.Write(player->GetGMLevel()); - - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::LoginSessionNotify notify; + notify.playerID = player->GetObjectID(); + notify.playerName = playerName; + notify.zoneID = Game::zoneManager->GetZone()->GetZoneID(); + notify.muteExpire = user->GetMuteExpire(); + notify.gmLevel = player->GetGMLevel(); + ChatServerLink::Send(notify); } } else { LOG("Couldn't find character to log in with for user %s (%i)!", user->GetUsername().c_str(), user->GetAccountID()); @@ -1189,10 +1140,9 @@ void CleanupDisconnectedUser(const SystemAddress& sysAddr) { } { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); - bitStream.Write(user->GetLoggedInChar()); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + ChatPackets::UnexpectedDisconnect notify; + notify.playerID = user->GetLoggedInChar(); + ChatServerLink::Send(notify); } UserManager::Instance()->DeleteUser(sysAddr); @@ -1327,10 +1277,9 @@ namespace { 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); + ChatPackets::UnexpectedDisconnect notify; + notify.playerID = lastCharacter; + ChatServerLink::Send(notify); } } @@ -1493,7 +1442,11 @@ namespace { struct GeneralChatMessagePacket final : public WorldPackets::GeneralChatMessage { void Handle() override { if (g_ChatDisabled) { - ChatPackets::SendMessageFail(sysAddr); + //0x00 - "Chat is currently disabled." + //0x01 - "Upgrade to a full LEGO Universe Membership to chat with other players." + ClientPackets::SendCannedText cannedText; + cannedText.responseType = 0; + cannedText.Send(sysAddr); } else { // TODO: Find a good home for the logic in this case. User* user = UserManager::Instance()->GetUser(sysAddr); @@ -1540,7 +1493,12 @@ namespace { std::string sMessage = GeneralUtils::UTF16ToWTF8(message); LOG("%s: %s", playerName.c_str(), sMessage.c_str()); - ChatPackets::SendChatMessage(sysAddr, chatChannel, playerName, user->GetLoggedInChar(), isMythran, message); + ChatPackets::Client::GeneralChatMessage generalChat; + generalChat.chatChannel = chatChannel; + generalChat.senderName = LUWString(playerName); + generalChat.senderID = user->GetLoggedInChar(); + generalChat.message = message; + generalChat.Broadcast(); if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage); } } diff --git a/docs/PacketArchitecture.md b/docs/PacketArchitecture.md index a9eec0db2..b8443cce6 100644 --- a/docs/PacketArchitecture.md +++ b/docs/PacketArchitecture.md @@ -170,6 +170,16 @@ Every non-game-message packet is an `LUBitStream` subclass. The base owns the fu (`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)`). +- `PacketDispatcher` (`dNet/PacketDispatcher.h`) is the dispatch map for servers where the same + struct is handled differently depending on who receives it (chat, master, the dashboard): each entry names the + struct and a handler function `(const Msg&, const SystemAddress&)`. `Dispatch(packet, service)` reads the header, + reads the struct and drops (and logs) packets that fail to `Deserialize`. +- Structs go in the file of the `ServiceType` in their header (`ChatPackets` for CHAT, `ClientPackets` for CLIENT, + `MasterPackets` for MASTER, ...), whoever sends them. What chat sends a client in the CLIENT service (friends, + ignore list and team responses, and the team game messages it writes as `ClientPackets::TeamGameMsg`, since chat + doesn't link `dGame`) is in `ClientPackets`; chat-service packets the client receives are in `ChatPackets::Client`. +- Chat -> client packets are wrapped in `ChatPackets::WorldRoutePacket` (`dNet/WorldRoutePacket.h`: target object ID + + the inner packet of any service); the world passes the inner bytes on unchanged. ```cpp // dNet/ChatPackets.h diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index ef417264c..983697571 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -21,6 +21,7 @@ file(COPY ${COMPONENT_TEST_DATA} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) # Add the executable. Remember to add all tests above this! add_executable(dGameTests ${DGAMETEST_SOURCES}) +target_include_directories(dGameTests PRIVATE "." "${PROJECT_SOURCE_DIR}/dChatServer") if(APPLE) add_custom_target(dGameTestsLink diff --git a/tests/dGameTests/dNetTests/CMakeLists.txt b/tests/dGameTests/dNetTests/CMakeLists.txt index 2af0b3d4c..25e09445f 100644 --- a/tests/dGameTests/dNetTests/CMakeLists.txt +++ b/tests/dGameTests/dNetTests/CMakeLists.txt @@ -1,4 +1,5 @@ SET(DNET_TESTS + "ChatPacketsTests.cpp" "CommonAuthPacketsTests.cpp" "WorldPacketsTests.cpp") diff --git a/tests/dGameTests/dNetTests/ChatPacketsTests.cpp b/tests/dGameTests/dNetTests/ChatPacketsTests.cpp new file mode 100644 index 000000000..4cf147037 --- /dev/null +++ b/tests/dGameTests/dNetTests/ChatPacketsTests.cpp @@ -0,0 +1,900 @@ +#include "ChatPackets.h" +#include "ClientPackets.h" +#include "WorldRoutePacket.h" +#include "GameDependencies.h" +#include "PacketTestUtils.h" +#include "dGameMessagesTests/GameMessageTestUtils.h" +#include "Legacy/ChatPacketsLegacy.h" + +#include +#include +#include + +#include + +using namespace PacketTestUtils; +using GameMessageTestUtils::RoundTrip; +using GameMessageTestUtils::ExpectTruncatedFails; + +namespace { + SystemAddress WorldAddress() { + SystemAddress address; + address.binaryAddress = 0x0100007f; + address.port = 3000; + return address; + } + + const std::array g_Ids = { LWOOBJID_EMPTY, 0x1000000000000001LL, 0x0102030405060708LL }; + const std::array g_Names = { "", "Bob", "A name with 33 characters in it!!", "Th\xc3\xa9o" }; + const std::array g_Messages = { u"", u"hello", u"café ☃", std::u16string(300, u'x') }; + const std::array g_Zones = { LWOZONEID(), LWOZONEID(1100, 3, 0), LWOZONEID(1150, 65535, 0xDEADBEEF) }; + + PacketBytes StructPacket(const LUBitStream& msg) { + RakNet::BitStream bitStream; + msg.WritePacket(bitStream); + return FromBitStream(bitStream); + } + + // What ChatPacketHandler::SendRouted puts on the wire + PacketBytes Routed(LWOOBJID target, const LUBitStream& msg) { + ChatPackets::WorldRoutePacket route; + route.targetID = target; + route.routed = &msg; + return StructPacket(route); + } + + // The one packet the legacy function sent + CapturedPacket LegacyPacket(const std::function& legacySend) { + const auto packets = Capture(legacySend); + EXPECT_EQ(packets.size(), 1); + return packets.empty() ? CapturedPacket{} : packets[0]; + } + + PacketBytes Written(const std::function& write) { + RakNet::BitStream bitStream; + write(bitStream); + return FromBitStream(bitStream); + } + + // A stream positioned after the header of msg's packet + void LoadPayload(RakNet::BitStream& bitStream, const LUBitStream& msg) { + msg.WritePacket(bitStream); + bitStream.IgnoreBytes(8); + } + + PlayerData MakePlayer(LWOOBJID id, const std::string& name, LWOZONEID zone, eGameMasterLevel gmLevel = eGameMasterLevel::CIVILIAN) { + PlayerData player; + player.playerID = id; + player.playerName = name; + player.zoneID = zone; + player.gmLevel = gmLevel; + player.worldServerSysAddr = WorldAddress(); + return player; + } +} + +class ChatPacketsTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +// Client-facing: general chat and system messages + +TEST_F(ChatPacketsTests, GeneralChatMessageMatchesLegacySendChatMessage) { + for (const uint8_t channel : { 0, 4, 12, 255 }) { + for (const auto& name : g_Names) { + for (const auto id : g_Ids) { + for (const auto& message : g_Messages) { + for (const bool mythran : { false, true }) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendChatMessage(WorldAddress(), static_cast(channel), name, id, mythran, message); }); + + ChatPackets::Client::GeneralChatMessage chatMessage; + chatMessage.chatChannel = channel; + chatMessage.senderName = LUWString(name); + chatMessage.senderID = id; + chatMessage.message = message; + const auto sent = LegacyPacket([&] { chatMessage.Broadcast(); }); + + EXPECT_PACKET_EQ(FromCapture(legacy), FromCapture(sent)); + EXPECT_EQ(legacy.broadcast, sent.broadcast); + EXPECT_EQ(legacy.sysAddr, sent.sysAddr); + + const auto copy = RoundTrip(chatMessage); + EXPECT_EQ(copy.message, message); + EXPECT_EQ(copy.senderID, id); + EXPECT_EQ(copy.chatChannel, channel); + } + } + } + } + } +} + +TEST_F(ChatPacketsTests, SendSystemMessageMatchesLegacy) { + for (const auto& address : { WorldAddress(), UNASSIGNED_SYSTEM_ADDRESS }) { + for (const bool broadcast : { false, true }) { + for (const auto& message : g_Messages) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendSystemMessage(address, message, broadcast); }); + const auto sent = LegacyPacket([&] { ChatPackets::SendSystemMessage(address, message, broadcast); }); + EXPECT_PACKET_EQ(FromCapture(legacy), FromCapture(sent)); + EXPECT_EQ(legacy.broadcast, sent.broadcast); + EXPECT_EQ(legacy.sysAddr, sent.sysAddr); + } + const auto legacy = LegacyPacket([&] { LegacyChat::SendSystemMessage(address, std::string("ascii text"), broadcast); }); + const auto sent = LegacyPacket([&] { ChatPackets::SendSystemMessage(address, std::string("ascii text"), broadcast); }); + EXPECT_PACKET_EQ(FromCapture(legacy), FromCapture(sent)); + } + } +} + +TEST_F(ChatPacketsTests, GeneralChatMessageGolden) { + ChatPackets::Client::GeneralChatMessage chatMessage; + chatMessage.chatChannel = 4; + chatMessage.senderName = LUWString("", 33); + chatMessage.message = u"hi"; + const auto packet = StructPacket(chatMessage); + // 53 | CHAT (0x0002) | GENERAL_CHAT_MESSAGE (1) | pad | u64 0 | channel 4 | u32 length 2 | 66 zero bytes of name | + // u64 sender 0 | u16 source 0 | u8 gm level 0 | 'h' 'i' | u16 0 + std::string hex = "53 02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 04 02 00 00 00"; + for (int i = 0; i < 66; i++) hex += " 00"; + hex += " 00 00 00 00 00 00 00 00 00 00 00 68 00 69 00 00 00"; + EXPECT_PACKET_EQ(FromHex(hex), packet); +} + +TEST_F(ChatPacketsTests, SendCannedTextMatchesLegacySendMessageFail) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendMessageFail(WorldAddress()); }); + ClientPackets::SendCannedText cannedText; + const auto sent = LegacyPacket([&] { cannedText.Send(WorldAddress()); }); + EXPECT_PACKET_EQ(FromCapture(legacy), FromCapture(sent)); + EXPECT_EQ(legacy.broadcast, sent.broadcast); + EXPECT_EQ(legacy.sysAddr, sent.sysAddr); + EXPECT_PACKET_EQ(FromHex("53 05 00 35 00 00 00 00 00"), StructPacket(cannedText)); + RoundTrip(cannedText); +} + +// Routed packets from the chat server + +TEST_F(ChatPacketsTests, WorldRoutePacketMatchesLegacySendRoutedMsg) { + for (const auto target : g_Ids) { + for (const bool failed : { false, true }) { + ClientPackets::TeamInviteInitialResponse response; + response.inviteFailedToSend = failed; + response.playerName = LUWString("Bob"); + + RakNet::BitStream inner; + LegacyChat::WriteTeamInviteInitialResponse(inner, failed, LUWString("Bob")); + EXPECT_PACKET_EQ(FromBitStream(inner), StructPacket(response)); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendRoutedMsg(inner, target, WorldAddress()); }); + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(target, response)); + EXPECT_FALSE(legacy.broadcast); + + const auto copy = RoundTrip(response); + EXPECT_EQ(copy.inviteFailedToSend, failed); + } + } +} + +TEST_F(ChatPacketsTests, WorldRoutePacketGoldenAndWorldForwarding) { + ClientPackets::TeamSetLeader msg; + msg.target = 0x0102030405060708LL; + msg.i64PlayerID = 0x1112131415161718LL; + const auto packet = Routed(0x0102030405060708LL, msg); + // WORLD_ROUTE_PACKET (0x30) | target | CLIENT GAME_MSG (0x0c) | target | TEAM_SET_LEADER (1557 = 0x0615) | player + EXPECT_PACKET_EQ(FromHex( + "53 02 00 30 00 00 00 00 08 07 06 05 04 03 02 01 " + "53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 15 06 18 17 16 15 14 13 12 11"), packet); + + // The world reads the target and passes everything after it on unchanged + RakNet::BitStream bitStream; + bitStream.WriteBits(packet.bytes.data(), packet.bits); + RakNet::BitStream legacyStream; + legacyStream.WriteBits(packet.bytes.data(), packet.bits); + bitStream.IgnoreBytes(8); + legacyStream.IgnoreBytes(8); + + ChatPackets::WorldRoutePacket route; + ASSERT_TRUE(route.Deserialize(bitStream)); + const auto [legacyTarget, legacyBytes] = LegacyChat::ReadWorldRoute(legacyStream); + EXPECT_EQ(route.targetID, legacyTarget); + EXPECT_EQ(route.routedData, legacyBytes); + EXPECT_EQ(route.routedData, StructPacket(msg).bytes); + + // And serializing what was read gives the same packet + EXPECT_PACKET_EQ(packet, StructPacket(route)); +} + +TEST_F(ChatPacketsTests, GetFriendsListResponseMatchesLegacy) { + for (const auto target : g_Ids) { + for (size_t count = 0; count <= 3; count++) { + PlayerData player = MakePlayer(target, "Me", g_Zones[1]); + for (size_t i = 0; i < count; i++) { + FriendData data; + data.isOnline = i % 2 == 0; + data.isBestFriend = i == 1; + data.isFTP = i == 2; + data.zoneID = g_Zones[i % g_Zones.size()]; + data.friendID = g_Ids[i % g_Ids.size()]; + data.friendName = g_Names[i % 3]; // names of up to 33 characters + player.friends.push_back(data); + } + + const auto legacy = LegacyPacket([&] { LegacyChat::SendFriendsList(target, player); }); + ClientPackets::GetFriendsListResponse response; + response.friends = player.friends; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(target, response)); + EXPECT_EQ(legacy.sysAddr, WorldAddress()); + EXPECT_FALSE(legacy.broadcast); + + const auto copy = RoundTrip(response); + ASSERT_EQ(copy.friends.size(), count); + for (size_t i = 0; i < count; i++) { + EXPECT_EQ(copy.friends[i].friendName, player.friends[i].friendName); + EXPECT_EQ(copy.friends[i].zoneID, player.friends[i].zoneID); + } + } + } +} + +TEST_F(ChatPacketsTests, WhoResponseMatchesLegacy) { + for (const auto requestor : g_Ids) { + for (const auto& name : g_Names) { + for (const bool online : { false, true }) { + LegacyChat::FindPlayerRequest legacyRequest{ requestor, LUWString(name) }; + const PlayerData sender = MakePlayer(requestor, "Asker", g_Zones[1]); + const PlayerData player = online ? MakePlayer(0x55, name, g_Zones[2]) : PlayerData(); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendWhoResponse(legacyRequest, sender, player); }); + ClientPackets::WhoResponse response; + response.isOnline = static_cast(player); + response.zoneID = player.zoneID; + response.playerName = LUWString(name); + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(requestor, response)); + RoundTrip(response); + ExpectTruncatedFails(response); + } + } + } +} + +TEST_F(ChatPacketsTests, ShowAllResponseMatchesLegacy) { + std::map players; + players[LWOOBJID_EMPTY] = PlayerData(); // the container's empty entry is skipped + players[1] = MakePlayer(1, "One", g_Zones[1]); + players[2] = MakePlayer(2, "Two", g_Zones[2]); + players[3] = MakePlayer(3, "", g_Zones[0]); + + for (const bool zoneData : { false, true }) { + for (const bool individualPlayers : { false, true }) { + for (const uint32_t playerCount : { 0u, 3u, 0xFFFFFFFFu }) { + LegacyChat::ShowAllRequest legacyRequest{ 0x42, zoneData, individualPlayers }; + const PlayerData sender = MakePlayer(0x42, "Asker", g_Zones[1]); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendShowAllResponse(legacyRequest, sender, playerCount, 7, players); }); + + ClientPackets::ShowAllResponse response; + response.playerCount = playerCount; + response.simCount = 7; + response.displayIndividualPlayers = individualPlayers; + response.displayZoneData = zoneData; + if (zoneData || individualPlayers) { + for (auto& [playerID, playerData] : players) { + if (!playerData) continue; + response.players.push_back({ playerData.playerName, playerData.zoneID }); + } + } + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(0x42, response)); + + const auto copy = RoundTrip(response); + EXPECT_EQ(copy.players.size(), response.players.size()); + } + } + } +} + +TEST_F(ChatPacketsTests, PrivateChatMessageMatchesLegacy) { + for (const auto& senderName : g_Names) { + for (const auto gmLevel : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) { + for (const auto responseCode : { eChatMessageResponseCode::SENT, eChatMessageResponseCode::NOTFRIENDS, eChatMessageResponseCode::RECEIVERFREETRIAL }) { + for (const auto channel : { eChatChannel::PRIVATE_CHAT, eChatChannel::TEAM, eChatChannel::GENERAL }) { + for (const uint32_t size : { 0u, 5u, 40u }) { + const PlayerData sender = MakePlayer(0x0102030405060708LL, senderName, g_Zones[1], gmLevel); + const PlayerData receiver = MakePlayer(0x77, "Receiver", g_Zones[2], eGameMasterLevel::DEVELOPER); + LUWString message(u"hello", size); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendPrivateChatMessage(sender, receiver, receiver, message, channel, responseCode); }); + + ChatPackets::Client::PrivateChatMessage chatMessage; + chatMessage.playerID = sender.playerID; + chatMessage.chatChannel = channel; + chatMessage.senderName = LUWString(sender.playerName); + chatMessage.senderID = sender.playerID; + chatMessage.senderGMLevel = sender.gmLevel; + chatMessage.receiverName = LUWString(receiver.playerName); + chatMessage.receiverGMLevel = receiver.gmLevel; + chatMessage.responseCode = responseCode; + chatMessage.message = message; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, chatMessage)); + + const auto copy = RoundTrip(chatMessage); + EXPECT_EQ(copy.message.size, size); + EXPECT_EQ(copy.responseCode, responseCode); + } + } + } + } + } +} + +TEST_F(ChatPacketsTests, UpdateFriendNotifyMatchesLegacy) { + for (const auto& zone : g_Zones) { + for (const auto& friendZone : g_Zones) { + for (const uint8_t notifyType : { 0, 1, 2 }) { + for (const uint8_t isBestFriend : { 0, 1 }) { + const PlayerData friendData = MakePlayer(0x10, "Friend", friendZone); + const PlayerData playerData = MakePlayer(0x20, "Player", zone); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendFriendUpdate(friendData, playerData, notifyType, isBestFriend); }); + + // As ChatPacketHandler::SendFriendUpdate fills it in + ClientPackets::UpdateFriendNotify notify; + notify.notifyType = notifyType; + notify.friendName = LUWString(playerData.playerName); + const LWOCLONEID cloneID = zone.GetCloneID() == friendData.zoneID.GetCloneID() ? 0 : zone.GetCloneID(); + notify.zoneID = LWOZONEID(zone.GetMapID(), zone.GetInstanceID(), cloneID); + notify.isBestFriend = isBestFriend; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(friendData.playerID, notify)); + RoundTrip(notify); + ExpectTruncatedFails(notify); + } + } + } + } +} + +TEST_F(ChatPacketsTests, AddFriendRequestAndResponseMatchLegacy) { + for (const auto& name : g_Names) { + const PlayerData receiver = MakePlayer(0x10, "Receiver", g_Zones[1]); + PlayerData sender = MakePlayer(0x20, name, g_Zones[2]); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendFriendRequest(receiver, sender); }); + ClientPackets::AddFriendRequest request; + request.requestorName = LUWString(sender.playerName); + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, request)); + RoundTrip(request); + + for (int code = 0; code <= static_cast(eAddFriendResponseType::FRIENDISFREETRIAL); code++) { + const auto responseCode = static_cast(code); + for (const bool senderOnline : { false, true }) { + for (const uint8_t alreadyBest : { 0, 1 }) { + for (const uint8_t bestRequest : { 0, 1 }) { + sender.worldServerSysAddr = senderOnline ? WorldAddress() : UNASSIGNED_SYSTEM_ADDRESS; + const auto legacyResponse = LegacyPacket([&] { LegacyChat::SendFriendResponse(receiver, sender, responseCode, alreadyBest, bestRequest); }); + + // As ChatPacketHandler::SendFriendResponse fills it in + ClientPackets::AddFriendResponse response; + response.responseCode = responseCode; + response.isOnlineOrBestFriend = responseCode != eAddFriendResponseType::ACCEPTED ? alreadyBest : sender.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS; + response.friendName = LUWString(sender.playerName); + response.friendID = sender.playerID; + response.zoneID = sender.zoneID; + response.isBestFriend = bestRequest; + EXPECT_PACKET_EQ(FromCapture(legacyResponse), Routed(receiver.playerID, response)); + RoundTrip(response); + ExpectTruncatedFails(response); + } + } + } + } + } +} + +TEST_F(ChatPacketsTests, RemoveFriendResponseMatchesLegacy) { + for (auto name : g_Names) { + for (const bool success : { false, true }) { + const PlayerData receiver = MakePlayer(0x10, "Receiver", g_Zones[1]); + const auto legacy = LegacyPacket([&] { LegacyChat::SendRemoveFriend(receiver, name, success); }); + ClientPackets::RemoveFriendResponse response; + response.isSuccessful = success; + response.friendName = LUWString(name); + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, response)); + RoundTrip(response); + } + } +} + +TEST_F(ChatPacketsTests, IgnoreListResponsesMatchLegacy) { + for (size_t count = 0; count <= 3; count++) { + PlayerData receiver = MakePlayer(0x10, "Receiver", g_Zones[1]); + for (size_t i = 0; i < count; i++) receiver.ignoredPlayers.emplace_back(g_Names[i], g_Ids[i]); + + const auto legacy = LegacyPacket([&] { LegacyChat::SendIgnoreList(receiver, WorldAddress()); }); + ClientPackets::GetIgnoreListResponse response; + for (const auto& ignoredPlayer : receiver.ignoredPlayers) { + response.ignored.push_back({ ignoredPlayer.playerId, LUWString(ignoredPlayer.playerName, 36) }); + } + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, response)); + EXPECT_EQ(legacy.sysAddr, WorldAddress()); + EXPECT_FALSE(legacy.broadcast); + RoundTrip(response); + } + + const PlayerData receiver = MakePlayer(0x10, "Receiver", g_Zones[1]); + for (int code = 0; code <= 3; code++) { + for (const auto& name : g_Names) { + for (const auto id : g_Ids) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendAddIgnoreResponse(receiver, static_cast(code), name, id, WorldAddress()); }); + ClientPackets::AddIgnoreResponse response; + response.responseCode = static_cast(code); + response.playerName = LUWString(name, 33); + response.playerID = id; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, response)); + RoundTrip(response); + ExpectTruncatedFails(response); + } + } + } + + for (const auto& name : g_Names) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendRemoveIgnoreResponse(receiver, name, WorldAddress()); }); + ClientPackets::RemoveIgnoreResponse response; + response.playerName = LUWString(name, 33); + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, response)); + RoundTrip(response); + } +} + +// Team packets and team game messages + +TEST_F(ChatPacketsTests, TeamInviteMatchesLegacy) { + for (const auto& name : g_Names) { + for (const auto id : g_Ids) { + const PlayerData receiver = MakePlayer(0x10, "Receiver", g_Zones[1]); + const PlayerData sender = MakePlayer(id, name, g_Zones[2]); + const auto legacy = LegacyPacket([&] { LegacyChat::SendTeamInvite(receiver, sender); }); + ClientPackets::TeamInvite invite; + invite.senderName = LUWString(sender.playerName.c_str()); + invite.senderID = sender.playerID; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, invite)); + RoundTrip(invite); + ExpectTruncatedFails(invite); + } + } +} + +TEST_F(ChatPacketsTests, TeamGameMessagesMatchLegacy) { + const std::array names = { u"", u"Leader", u"Léader with a much longer name than usual" }; + for (const auto receiverId : g_Ids) { + for (const auto& zone : g_Zones) { + const PlayerData receiver = MakePlayer(receiverId, "Receiver", g_Zones[1]); + for (const auto& name : names) { + for (const bool flag : { false, true }) { + for (const uint8_t byte : { 0, 1, 255 }) { + { + const auto legacy = LegacyPacket([&] { LegacyChat::SendTeamInviteConfirm(receiver, flag, 0x42, zone, byte, byte, byte, name); }); + ClientPackets::TeamInviteConfirm msg; + msg.target = receiver.playerID; + msg.bLeaderIsFreeTrial = flag; + msg.i64LeaderID = 0x42; + msg.i64LeaderZoneID = zone; + msg.ucLootFlag = byte; + msg.ucNumOfOtherPlayers = byte; + msg.ucResponseCode = byte; + msg.wsLeaderName = name; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, msg)); + RoundTrip(msg); + ExpectTruncatedFails(msg); + } + { + const auto legacy = LegacyPacket([&] { LegacyChat::SendTeamStatus(receiver, 0x42, zone, byte, byte, name); }); + ClientPackets::TeamGetStatusResponse msg; + msg.target = receiver.playerID; + msg.i64LeaderID = 0x42; + msg.i64LeaderZoneID = zone; + msg.ucLootFlag = byte; + msg.ucNumOfOtherPlayers = byte; + msg.wsLeaderName = name; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, msg)); + RoundTrip(msg); + ExpectTruncatedFails(msg); + } + } + for (const bool local : { false, true }) { + for (const bool noLoot : { false, true }) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendTeamAddPlayer(receiver, flag, local, noLoot, 0x42, name, zone); }); + // As TeamContainer::SendTeamAddPlayer fills it in + ClientPackets::TeamAddPlayer msg; + msg.target = receiver.playerID; + msg.bIsFreeTrial = flag; + msg.bLocal = local; + msg.bNoLootOnDeath = noLoot; + msg.i64PlayerID = 0x42; + msg.wsPlayerName = name; + msg.zoneID = receiver.zoneID.GetCloneID() == zone.GetCloneID() ? LWOZONEID(zone.GetMapID(), zone.GetInstanceID(), 0) : zone; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, msg)); + RoundTrip(msg); + ExpectTruncatedFails(msg); + + for (const bool leaving : { false, true }) { + const auto legacyRemove = LegacyPacket([&] { LegacyChat::SendTeamRemovePlayer(receiver, flag, noLoot, leaving, local, 0x99, 0x42, name); }); + ClientPackets::TeamRemovePlayer remove; + remove.target = receiver.playerID; + remove.bDisband = flag; + remove.bIsKicked = noLoot; + remove.bIsLeaving = leaving; + remove.bLocal = local; + remove.i64LeaderID = 0x99; + remove.i64PlayerID = 0x42; + remove.wsPlayerName = name; + EXPECT_PACKET_EQ(FromCapture(legacyRemove), Routed(receiver.playerID, remove)); + RoundTrip(remove); + ExpectTruncatedFails(remove); + } + } + } + } + } + for (const auto playerId : g_Ids) { + const auto legacy = LegacyPacket([&] { LegacyChat::SendTeamSetLeader(receiver, playerId); }); + ClientPackets::TeamSetLeader msg; + msg.target = receiver.playerID; + msg.i64PlayerID = playerId; + EXPECT_PACKET_EQ(FromCapture(legacy), Routed(receiver.playerID, msg)); + RoundTrip(msg); + ExpectTruncatedFails(msg); + + const auto legacyFlag = LegacyPacket([&] { LegacyChat::SendTeamSetOffWorldFlag(receiver, playerId, zone); }); + // As TeamContainer::SendTeamSetOffWorldFlag fills it in + ClientPackets::TeamSetOffWorldFlag flagMsg; + flagMsg.target = receiver.playerID; + flagMsg.i64PlayerID = playerId; + flagMsg.zoneID = receiver.zoneID.GetCloneID() == zone.GetCloneID() ? LWOZONEID(zone.GetMapID(), zone.GetInstanceID(), 0) : zone; + EXPECT_PACKET_EQ(FromCapture(legacyFlag), Routed(receiver.playerID, flagMsg)); + RoundTrip(flagMsg); + ExpectTruncatedFails(flagMsg); + } + } + } +} + +TEST_F(ChatPacketsTests, TeamUpdateMatchesLegacyAndWorldReader) { + for (const bool deleteTeam : { false, true }) { + for (size_t count = 0; count <= 4; count++) { + const std::vector members(g_Ids.begin(), g_Ids.begin() + std::min(count, g_Ids.size())); + const auto legacy = LegacyPacket([&] { LegacyChat::UpdateTeamsOnWorld(0x1234, 1, members, deleteTeam); }); + ChatPackets::TeamUpdate update; + update.teamID = 0x1234; + update.deleteTeam = deleteTeam; + if (!deleteTeam) { + update.lootFlag = 1; + update.members = members; + } + const auto sent = LegacyPacket([&] { update.Broadcast(); }); + EXPECT_PACKET_EQ(FromCapture(legacy), FromCapture(sent)); + EXPECT_TRUE(sent.broadcast); + + RakNet::BitStream stream; LoadPayload(stream, update); + const auto read = LegacyChat::ReadTeamStatus(stream); + const auto copy = RoundTrip(update); + EXPECT_EQ(read.teamID, copy.teamID); + EXPECT_EQ(read.deleteTeam, copy.deleteTeam); + EXPECT_EQ(static_cast(read.lootOption), copy.lootFlag); + EXPECT_EQ(read.members, copy.members); + ExpectTruncatedFails(update); + } + } +} + +TEST_F(ChatPacketsTests, GMMuteMatchesLegacy) { + for (const auto id : g_Ids) { + for (const time_t expire : { static_cast(0), static_cast(1), static_cast(1893456000), std::numeric_limits::max() }) { + const auto legacy = LegacyPacket([&] { LegacyChat::BroadcastMuteUpdate(id, expire); }); + ChatPackets::GMMute mute; + mute.playerID = id; + mute.expire = expire; + const auto sent = LegacyPacket([&] { mute.Broadcast(); }); + EXPECT_PACKET_EQ(FromCapture(legacy), FromCapture(sent)); + EXPECT_TRUE(sent.broadcast); + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteGMMute(b, id, expire); }), StructPacket(mute)); + RoundTrip(mute); + ExpectTruncatedFails(mute); + } + } + ChatPackets::GMMute mute; + mute.playerID = 0x0102030405060708LL; + mute.expire = 1; + EXPECT_PACKET_EQ(FromHex("53 02 00 2e 00 00 00 00 08 07 06 05 04 03 02 01 01 00 00 00 00 00 00 00"), StructPacket(mute)); +} + +// World -> chat + +TEST_F(ChatPacketsTests, WorldToChatMatchesLegacy) { + for (const auto id : g_Ids) { + for (const auto& name : g_Names) { + for (const auto& zone : g_Zones) { + for (const auto gmLevel : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) { + ChatPackets::LoginSessionNotify notify; + notify.playerID = id; + notify.playerName = name; + notify.zoneID = zone; + notify.muteExpire = 1893456000; + notify.gmLevel = gmLevel; + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteLoginSessionNotify(b, id, name, zone, 1893456000, gmLevel); }), StructPacket(notify)); + const auto copy = RoundTrip(notify); + EXPECT_EQ(copy.playerName, name); + ExpectTruncatedFails(notify); + + // What PlayerContainer::InsertPlayer read + RakNet::BitStream stream; LoadPayload(stream, notify); + PlayerData data; + ASSERT_TRUE(LegacyChat::ReadLoginSessionNotify(stream, data)); + EXPECT_EQ(data.playerID, copy.playerID); + EXPECT_EQ(data.playerName, copy.playerName); + EXPECT_EQ(data.zoneID, copy.zoneID); + EXPECT_EQ(data.muteExpire, copy.muteExpire); + EXPECT_EQ(data.gmLevel, copy.gmLevel); + + ChatPackets::GMLevelUpdate update; + update.playerID = id; + update.gmLevel = gmLevel; + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteGMLevelUpdate(b, id, gmLevel); }), StructPacket(update)); + RoundTrip(update); + } + } + } + + ChatPackets::UnexpectedDisconnect disconnect; + disconnect.playerID = id; + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteUnexpectedDisconnect(b, id); }), StructPacket(disconnect)); + RoundTrip(disconnect); + ExpectTruncatedFails(disconnect); + } + + // Names longer than 33 characters were refused + ChatPackets::LoginSessionNotify tooLong; + tooLong.playerName = std::string(34, 'x'); + RakNet::BitStream stream; LoadPayload(stream, tooLong); + ChatPackets::LoginSessionNotify read; + EXPECT_FALSE(read.Deserialize(stream)); +} + +TEST_F(ChatPacketsTests, CreateTeamMatchesLegacy) { + for (size_t count = 0; count <= 3; count++) { + for (const auto& zone : g_Zones) { + const std::vector members(g_Ids.begin(), g_Ids.begin() + count); + ChatPackets::CreateTeam createTeam; + createTeam.leaderID = 0x42; + createTeam.members = members; + createTeam.zoneID = zone; + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteCreateTeam(b, 0x42, members, zone); }), StructPacket(createTeam)); + + RakNet::BitStream stream; LoadPayload(stream, createTeam); + const auto read = LegacyChat::ReadCreateTeam(stream); + const auto copy = RoundTrip(createTeam); + EXPECT_EQ(read.playerID, copy.leaderID); + EXPECT_EQ(read.members, copy.members); + EXPECT_EQ(read.zoneId, copy.zoneID); + ExpectTruncatedFails(createTeam); + } + } + // 4 or more players can't be a team (CreateTeamServer refused them) + ChatPackets::CreateTeam tooBig; + tooBig.members = { 1, 2, 3, 4 }; + RakNet::BitStream stream; LoadPayload(stream, tooBig); + ChatPackets::CreateTeam read; + EXPECT_FALSE(read.Deserialize(stream)); +} + +TEST_F(ChatPacketsTests, AnnouncementMatchesLegacy) { + for (const auto& title : g_Names) { + for (const auto& message : g_Names) { + ChatPackets::Announcement announcement; + announcement.title = title; + announcement.message = message; + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteGMAnnounce(b, title, message); }), StructPacket(announcement)); + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyChat::WriteAnnouncement(b, title, message); }), StructPacket(announcement)); + + RakNet::BitStream stream; LoadPayload(stream, announcement); + const auto [legacyTitle, legacyMessage] = LegacyChat::ReadGMAnnounce(stream); + const auto copy = RoundTrip(announcement); + EXPECT_EQ(legacyTitle, copy.title); + EXPECT_EQ(legacyMessage, copy.message); + ExpectTruncatedFails(announcement); + } + } +} + +TEST_F(ChatPacketsTests, ShowAllAndWhoRequestsMatchLegacy) { + for (const auto id : g_Ids) { + for (const bool zoneData : { false, true }) { + for (const bool individualPlayers : { false, true }) { + LegacyChat::ShowAllRequest legacy{ id, zoneData, individualPlayers }; + ChatPackets::ShowAllRequest request; + request.requestor = id; + request.displayZoneData = zoneData; + request.displayIndividualPlayers = individualPlayers; + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { legacy.Serialize(b); }), StructPacket(request)); + + RakNet::BitStream stream; LoadPayload(stream, request); + LegacyChat::ShowAllRequest legacyRead; + legacyRead.Deserialize(stream); + const auto copy = RoundTrip(request); + EXPECT_EQ(legacyRead.requestor, copy.requestor); + EXPECT_EQ(legacyRead.displayZoneData, copy.displayZoneData); + EXPECT_EQ(legacyRead.displayIndividualPlayers, copy.displayIndividualPlayers); + ExpectTruncatedFails(request); + } + } + for (const auto& name : g_Names) { + LegacyChat::FindPlayerRequest legacy{ id, LUWString(name) }; + ChatPackets::FindPlayerRequest request; + request.requestor = id; + request.playerName = LUWString(name); + EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { legacy.Serialize(b); }), StructPacket(request)); + + RakNet::BitStream stream; LoadPayload(stream, request); + LegacyChat::FindPlayerRequest legacyRead; + legacyRead.Deserialize(stream); + const auto copy = RoundTrip(request); + EXPECT_EQ(legacyRead.playerName.string, copy.playerName.string); + ExpectTruncatedFails(request); + } + } +} + +// Client -> world -> chat: the structs read what the old handlers read + +TEST_F(ChatPacketsTests, FriendAndIgnoreRequestsReadLikeLegacy) { + for (const auto id : g_Ids) { + for (const auto& name : g_Names) { + for (const uint8_t extra : { 0, 1, 3 }) { + ChatPackets::AddFriendRequest request; + request.playerID = id; + request.unknown = 0xAABBCCDD; + request.friendName = LUWString(name); + request.isBestFriendRequest = extra; + RakNet::BitStream stream; LoadPayload(stream, request); + const auto legacy = LegacyChat::ReadFriendRequest(stream); + const auto copy = RoundTrip(request); + EXPECT_EQ(legacy.playerID, copy.playerID); + EXPECT_EQ(legacy.name.string, copy.friendName.string); + EXPECT_EQ(static_cast(legacy.extra), copy.isBestFriendRequest); + ExpectTruncatedFails(request); + + ChatPackets::AddFriendResponse response; + response.playerID = id; + response.responseCode = static_cast(extra); + response.friendName = LUWString(name); + RakNet::BitStream responseStream; LoadPayload(responseStream, response); + const auto legacyResponse = LegacyChat::ReadFriendResponse(responseStream); + const auto responseCopy = RoundTrip(response); + EXPECT_EQ(legacyResponse.playerID, responseCopy.playerID); + EXPECT_EQ(legacyResponse.name.string, responseCopy.friendName.string); + EXPECT_EQ(static_cast(legacyResponse.extra), static_cast(responseCopy.responseCode)); + ExpectTruncatedFails(response); + } + + const auto checkPlayerAndName = [&](const auto& msg, const LUWString& name) { + RakNet::BitStream stream; LoadPayload(stream, msg); + const auto legacy = LegacyChat::ReadPlayerAndName(stream); + const auto copy = RoundTrip(msg); + EXPECT_EQ(legacy.playerID, copy.playerID); + EXPECT_EQ(legacy.name.string, name.string); + ExpectTruncatedFails(msg); + }; + ChatPackets::RemoveFriend removeFriend; + removeFriend.playerID = id; + removeFriend.friendName = LUWString(name); + checkPlayerAndName(removeFriend, removeFriend.friendName); + ChatPackets::AddIgnore addIgnore; + addIgnore.playerID = id; + addIgnore.playerName = LUWString(name); + checkPlayerAndName(addIgnore, addIgnore.playerName); + ChatPackets::RemoveIgnore removeIgnore; + removeIgnore.playerID = id; + removeIgnore.playerName = LUWString(name); + checkPlayerAndName(removeIgnore, removeIgnore.playerName); + ChatPackets::TeamInvite teamInvite; + teamInvite.playerID = id; + teamInvite.invitedPlayer = LUWString(name); + checkPlayerAndName(teamInvite, teamInvite.invitedPlayer); + ChatPackets::TeamKick teamKick; + teamKick.playerID = id; + teamKick.kickedPlayer = LUWString(name); + checkPlayerAndName(teamKick, teamKick.kickedPlayer); + ChatPackets::TeamSetLeader teamSetLeader; + teamSetLeader.playerID = id; + teamSetLeader.promotedPlayer = LUWString(name); + checkPlayerAndName(teamSetLeader, teamSetLeader.promotedPlayer); + } + + ChatPackets::TeamInviteResponse inviteResponse; + inviteResponse.playerID = id; + inviteResponse.declined = 1; + inviteResponse.leaderID = 0x1234; + RakNet::BitStream stream; LoadPayload(stream, inviteResponse); + const auto legacy = LegacyChat::ReadTeamInviteResponse(stream); + EXPECT_EQ(legacy.playerID, id); + EXPECT_EQ(legacy.declined, 1); + EXPECT_EQ(legacy.leaderID, 0x1234); + RoundTrip(inviteResponse); + ExpectTruncatedFails(inviteResponse); + + ChatPackets::TeamSetLoot setLoot; + setLoot.playerID = id; + setLoot.lootFlag = 1; + RakNet::BitStream lootStream; LoadPayload(lootStream, setLoot); + const auto [lootPlayer, lootOption] = LegacyChat::ReadTeamLootOption(lootStream); + EXPECT_EQ(lootPlayer, id); + EXPECT_EQ(lootOption, 1); + RoundTrip(setLoot); + ExpectTruncatedFails(setLoot); + + for (const auto& msg : std::initializer_list>{ + [&] { ChatPackets::GetFriendsList m; m.playerID = id; RoundTrip(m); ExpectTruncatedFails(m); }, + [&] { ChatPackets::GetIgnoreList m; m.playerID = id; RoundTrip(m); ExpectTruncatedFails(m); }, + [&] { ChatPackets::TeamGetStatus m; m.playerID = id; RoundTrip(m); ExpectTruncatedFails(m); }, + [&] { ChatPackets::TeamLeave m; m.playerID = id; RoundTrip(m); ExpectTruncatedFails(m); }, + }) msg(); + } +} + +TEST_F(ChatPacketsTests, ChatMessagesReadLikeLegacy) { + for (const auto channel : { eChatChannel::TEAM, eChatChannel::PRIVATE_CHAT, eChatChannel::LOCAL }) { + for (const uint32_t size : { 0u, 1u, 6u, 50u }) { + ChatPackets::GeneralChatMessage chatMessage; + chatMessage.playerID = 0x0102030405060708LL; + chatMessage.chatChannel = channel; + chatMessage.messageLength = size; + chatMessage.senderName = LUWString("Sender"); + chatMessage.senderID = 0x42; + chatMessage.message = LUWString(u"hello", size); + RakNet::BitStream stream; LoadPayload(stream, chatMessage); + const auto legacy = LegacyChat::ReadChatMessage(stream); + const auto copy = RoundTrip(chatMessage); + EXPECT_EQ(legacy.playerID, copy.playerID); + EXPECT_EQ(legacy.channel, copy.chatChannel); + EXPECT_EQ(legacy.size, copy.messageLength); + EXPECT_EQ(legacy.message.string, copy.message.string); + ExpectTruncatedFails(chatMessage); + + ChatPackets::PrivateChatMessage privateMessage; + privateMessage.playerID = 0x0102030405060708LL; + privateMessage.chatChannel = channel; + privateMessage.messageLength = size; + privateMessage.receiverName = LUWString("Receiver"); + privateMessage.receiverGMLevel = 3; + privateMessage.responseCode = 4; + privateMessage.message = LUWString(u"hello", size); + RakNet::BitStream privateStream; LoadPayload(privateStream, privateMessage); + const auto legacyPrivate = LegacyChat::ReadPrivateChatMessage(privateStream); + const auto privateCopy = RoundTrip(privateMessage); + EXPECT_EQ(legacyPrivate.playerID, privateCopy.playerID); + EXPECT_EQ(legacyPrivate.channel, privateCopy.chatChannel); + EXPECT_EQ(legacyPrivate.receiverName.string, privateCopy.receiverName.string); + EXPECT_EQ(legacyPrivate.message.string, privateCopy.message.string); + ExpectTruncatedFails(privateMessage); + } + } + + // Longer than MAX_MESSAGE_LENGTH: the old handlers ignored the message, the structs refuse it + ChatPackets::GeneralChatMessage tooLong; + tooLong.messageLength = MAX_MESSAGE_LENGTH + 1; + RakNet::BitStream stream; LoadPayload(stream, tooLong); + ChatPackets::GeneralChatMessage read; + EXPECT_FALSE(read.Deserialize(stream)); +} + +TEST_F(ChatPacketsTests, AchievementNotifyRoundTrips) { + ChatPackets::AchievementNotify notify; + notify.targetPlayerName = LUWString("Friend"); + notify.missionEmailID = 1234; + notify.earningPlayerID = 0x0102030405060708LL; + notify.earnerName = LUWString("Earner"); + const auto copy = RoundTrip(notify); + EXPECT_EQ(copy.missionEmailID, 1234u); + EXPECT_EQ(copy.earnerName.string, u"Earner"); + EXPECT_EQ(copy.targetPlayerName.string, u"Friend"); + ExpectTruncatedFails(notify); +} diff --git a/tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h b/tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h new file mode 100644 index 000000000..d1633d9d5 --- /dev/null +++ b/tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h @@ -0,0 +1,913 @@ +#ifndef CHATPACKETSLEGACY_H +#define CHATPACKETSLEGACY_H + +// FROZEN ORACLE - DO NOT EDIT. +// Verbatim copies of the hand written chat packet code that dNet/ChatPackets.h replaced (dNet/ChatPackets.cpp, +// dChatServer/{ChatPacketHandler,ChatIgnoreList,TeamContainer,PlayerContainer}.cpp, dWorldServer/WorldServer.cpp and +// the dGame senders at f8f977f8). Only the namespace changed, except: +// - senders that wrote to Game::chatServer (a RakPeer the tests don't have) write into `bitStream` instead; +// - the parts of handlers that write or read a packet are copied on their own, with the state they used passed in; +// - old readers return what they read in a struct instead of acting on it. +// The byte-equality tests send the same inputs through these and through the new structs and require identical +// bytes, so the wire format is pinned even after the production code is deleted. + +#include "BitStreamUtils.h" +#include "dCommonVars.h" +#include "dServer.h" +#include "Game.h" +#include "MessageType/Chat.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" +#include "ServiceType.h" +#include "PlayerContainer.h" +#include "eAddFriendResponseCode.h" +#include "eAddFriendResponseType.h" +#include "eChatChannel.h" +#include "eChatMessageResponseCode.h" +#include "eGameMasterLevel.h" + +#include +#include +#include + +namespace LegacyChat { + // dNet/ChatPackets.h + struct ShowAllRequest{ + LWOOBJID requestor = LWOOBJID_EMPTY; + bool displayZoneData = true; + bool displayIndividualPlayers = true; + void Serialize(RakNet::BitStream& bitStream); + void Deserialize(RakNet::BitStream& inStream); + }; + + struct FindPlayerRequest{ + LWOOBJID requestor = LWOOBJID_EMPTY; + LUWString playerName; + void Serialize(RakNet::BitStream& bitStream); + void Deserialize(RakNet::BitStream& inStream); + }; + + // dNet/ChatPackets.cpp + inline void ShowAllRequest::Serialize(RakNet::BitStream& bitStream) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::SHOW_ALL); + bitStream.Write(this->requestor); + bitStream.Write(this->displayZoneData); + bitStream.Write(this->displayIndividualPlayers); + } + + inline void ShowAllRequest::Deserialize(RakNet::BitStream& inStream) { + inStream.Read(this->requestor); + inStream.Read(this->displayZoneData); + inStream.Read(this->displayIndividualPlayers); + } + + inline void FindPlayerRequest::Serialize(RakNet::BitStream& bitStream) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WHO); + bitStream.Write(this->requestor); + bitStream.Write(this->playerName); + } + + inline void FindPlayerRequest::Deserialize(RakNet::BitStream& inStream) { + inStream.Read(this->requestor); + inStream.Read(this->playerName); + } + + inline void SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE); + + bitStream.Write(0); + bitStream.Write(chatChannel); + + bitStream.Write(message.size()); + bitStream.Write(LUWString(senderName)); + + bitStream.Write(playerObjectID); + bitStream.Write(0); + bitStream.Write(0); + + for (uint32_t i = 0; i < message.size(); ++i) { + bitStream.Write(message[i]); + } + bitStream.Write(0); + + SEND_PACKET_BROADCAST; + } + + inline void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, const bool broadcast = false) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE); + + bitStream.Write(0); + bitStream.Write(4); + + bitStream.Write(message.size()); + bitStream.Write(LUWString("", 33)); + + bitStream.Write(0); + bitStream.Write(0); + bitStream.Write(0); + + for (uint32_t i = 0; i < message.size(); ++i) { + bitStream.Write(message[i]); + } + + bitStream.Write(0); + + //This is so Wincent's announcement works: + if (sysAddr != UNASSIGNED_SYSTEM_ADDRESS) { + SEND_PACKET; + return; + } + + SEND_PACKET_BROADCAST; + } + + inline void SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, const bool broadcast = false) { + LegacyChat::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(message), broadcast); + } + + inline void SendMessageFail(const SystemAddress& sysAddr) { + //0x00 - "Chat is currently disabled." + //0x01 - "Upgrade to a full LEGO Universe Membership to chat with other players." + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SEND_CANNED_TEXT); + bitStream.Write(0); //response type, options above ^ + //docs say there's a wstring here-- no idea what it's for, or if it's even needed so leaving it as is for now. + SEND_PACKET; + } + + // ChatPackets::Announcement::Serialize, with its header + inline void WriteAnnouncement(RakNet::BitStream& bitStream, const std::string& title, const std::string& message) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_ANNOUNCE); + bitStream.Write(title.size()); + bitStream.Write(title); + bitStream.Write(message.size()); + bitStream.Write(message); + } + + // ChatPackets::TeamInviteInitialResponse::Serialize, with its header + inline void WriteTeamInviteInitialResponse(RakNet::BitStream& bitstream, bool inviteFailedToSend, const LUWString& playerName) { + BitStreamUtils::WriteHeader(bitstream, ServiceType::CLIENT, MessageType::Client::TEAM_INVITE_INITIAL_RESPONSE); + bitstream.Write(inviteFailedToSend); + bitstream.Write(playerName); + } + + // ChatPackets::SendRoutedMsg, with the routed message's bytes (msg.WritePacket) given + inline void SendRoutedMsg(RakNet::BitStream& msgPacket, const LWOOBJID targetID, const SystemAddress& sysAddr) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(targetID); + + // Now write the actual packet + bitStream.WriteBits(msgPacket.GetData(), msgPacket.GetNumberOfBitsUsed()); + Game::server->Send(bitStream, sysAddr, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); + } + + // dChatServer/ChatPacketHandler.cpp + + // The end of HandleFriendlistRequest + inline void SendFriendsList(const LWOOBJID playerID, const PlayerData& player) { + //Now, we need to send the friendlist to the server they came from: + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::GET_FRIENDS_LIST_RESPONSE); + bitStream.Write(0); + bitStream.Write(1); //Length of packet -- just writing one as it doesn't matter, client skips it. + bitStream.Write(player.friends.size()); + + for (const auto& data : player.friends) { + data.Serialize(bitStream); + } + + SystemAddress sysAddr = player.worldServerSysAddr; + SEND_PACKET; + } + + // The end of HandleWho + inline void SendWhoResponse(const FindPlayerRequest& request, const PlayerData& sender, const PlayerData& player) { + bool online = player; + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(request.requestor); + + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::WHO_RESPONSE); + bitStream.Write(online); + bitStream.Write(player.zoneID.GetMapID()); + bitStream.Write(player.zoneID.GetInstanceID()); + bitStream.Write(player.zoneID.GetCloneID()); + bitStream.Write(request.playerName); + + SystemAddress sysAddr = sender.worldServerSysAddr; + SEND_PACKET; + } + + // The end of HandleShowAll, with Game::playerContainer's counts and players passed in + inline void SendShowAllResponse(const ShowAllRequest& request, const PlayerData& sender, uint32_t playerCount, uint32_t simCount, const std::map& allPlayers) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(request.requestor); + + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SHOW_ALL_RESPONSE); + bitStream.Write(!request.displayZoneData && !request.displayIndividualPlayers); + bitStream.Write(playerCount); + bitStream.Write(simCount); + bitStream.Write(request.displayIndividualPlayers); + bitStream.Write(request.displayZoneData); + if (request.displayZoneData || request.displayIndividualPlayers) { + for (auto& [playerID, playerData] : allPlayers) { + if (!playerData) continue; + bitStream.Write(0); // structure packing + if (request.displayIndividualPlayers) bitStream.Write(LUWString(playerData.playerName)); + if (request.displayZoneData) { + bitStream.Write(playerData.zoneID.GetMapID()); + bitStream.Write(playerData.zoneID.GetInstanceID()); + bitStream.Write(playerData.zoneID.GetCloneID()); + } + } + } + SystemAddress sysAddr = sender.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(routeTo.playerID); + + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE); + bitStream.Write(sender.playerID); + bitStream.Write(channel); + bitStream.Write(0); // not used + bitStream.Write(LUWString(sender.playerName)); + bitStream.Write(sender.playerID); + bitStream.Write(0); // sourceID + bitStream.Write(sender.gmLevel); + bitStream.Write(LUWString(receiver.playerName)); + bitStream.Write(receiver.gmLevel); + bitStream.Write(responseCode); + bitStream.Write(message); + + SystemAddress sysAddr = routeTo.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(friendData.playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::UPDATE_FRIEND_NOTIFY); + bitStream.Write(notifyType); + + std::string playerName = playerData.playerName.c_str(); + + bitStream.Write(LUWString(playerName)); + + bitStream.Write(playerData.zoneID.GetMapID()); + bitStream.Write(playerData.zoneID.GetInstanceID()); + + if (playerData.zoneID.GetCloneID() == friendData.zoneID.GetCloneID()) { + bitStream.Write(0); + } else { + bitStream.Write(playerData.zoneID.GetCloneID()); + } + + bitStream.Write(isBestFriend); //isBFF + bitStream.Write(0); //isFTP + + SystemAddress sysAddr = friendData.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendFriendResponse(const PlayerData& receiver, const PlayerData& sender, eAddFriendResponseType responseCode, uint8_t isBestFriendsAlready = 0U, uint8_t isBestFriendRequest = 0U) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + // Portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_RESPONSE); + bitStream.Write(responseCode); + // For all requests besides accepted, write a flag that says whether or not we are already best friends with the receiver. + bitStream.Write(responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS); + // Then write the player name + bitStream.Write(LUWString(sender.playerName)); + // Then if this is an acceptance code, write the following extra info. + if (responseCode == eAddFriendResponseType::ACCEPTED) { + bitStream.Write(sender.playerID); + bitStream.Write(sender.zoneID); + bitStream.Write(isBestFriendRequest); //isBFF + bitStream.Write(0); //isFTP + } + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendFriendRequest(const PlayerData& receiver, const PlayerData& sender) { + //Make sure people aren't requesting people that they're already friends with: + for (const auto& fr : receiver.friends) { + if (fr.friendID == sender.playerID) { + SendFriendResponse(sender, receiver, eAddFriendResponseType::ALREADYFRIEND, fr.isBestFriend); + return; //we have this player as a friend, yeet this function so it doesn't send another request. + } + } + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_REQUEST); + bitStream.Write(LUWString(sender.playerName)); + bitStream.Write(0); // This is a BFF flag however this is unused in live and does not have an implementation client side. + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendRemoveFriend(const PlayerData& receiver, std::string& personToRemove, bool isSuccessful) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::REMOVE_FRIEND_RESPONSE); + bitStream.Write(isSuccessful); //isOnline + bitStream.Write(LUWString(personToRemove)); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + // dChatServer/ChatIgnoreList.cpp + enum class AddResponse : uint8_t { + SUCCESS, + ALREADY_IGNORED, + PLAYER_NOT_FOUND, + GENERAL_ERROR, + }; + + inline void WriteOutgoingReplyHeader(RakNet::BitStream& bitStream, const LWOOBJID& receivingPlayer, const MessageType::Client type) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receivingPlayer); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, type); + } + + // The end of GetIgnoreList + inline void SendIgnoreList(const PlayerData& receiver, const SystemAddress& packetSystemAddress) { + CBITSTREAM; + WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::GET_IGNORE_LIST_RESPONSE); + + bitStream.Write(false); // Is Free Trial, but we don't care about that + bitStream.Write(0); // literally spacing due to struct alignment + + bitStream.Write(receiver.ignoredPlayers.size()); + for (const auto& ignoredPlayer : receiver.ignoredPlayers) { + bitStream.Write(ignoredPlayer.playerId); + bitStream.Write(LUWString(ignoredPlayer.playerName, 36)); + } + + Game::server->Send(bitStream, packetSystemAddress, false); + } + + // The response AddIgnore writes, with the outcome passed in + inline void SendAddIgnoreResponse(const PlayerData& receiver, AddResponse response, const std::string& toIgnoreStr, LWOOBJID ignoredPlayerId, const SystemAddress& packetSystemAddress) { + CBITSTREAM; + WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::ADD_IGNORE_RESPONSE); + + bitStream.Write(response); + + LUWString playerNameSend(toIgnoreStr, 33); + bitStream.Write(playerNameSend); + bitStream.Write(ignoredPlayerId); + + Game::server->Send(bitStream, packetSystemAddress, false); + } + + // The end of RemoveIgnore + inline void SendRemoveIgnoreResponse(const PlayerData& receiver, const std::string& removedIgnoreStr, const SystemAddress& packetSystemAddress) { + CBITSTREAM; + WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::REMOVE_IGNORE_RESPONSE); + + bitStream.Write(0); + LUWString playerNameSend(removedIgnoreStr, 33); + bitStream.Write(playerNameSend); + + Game::server->Send(bitStream, packetSystemAddress, false); + } + + // dChatServer/TeamContainer.cpp + inline void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TEAM_INVITE); + + bitStream.Write(LUWString(sender.playerName.c_str())); + bitStream.Write(sender.playerID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM); + + bitStream.Write(bLeaderIsFreeTrial); + bitStream.Write(i64LeaderID); + bitStream.Write(i64LeaderZoneID); + bitStream.Write(0); // BinaryBuffe, no clue what's in here + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(ucResponseCode); + bitStream.Write(wsLeaderName.size()); + for (const auto character : wsLeaderName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE); + + bitStream.Write(i64LeaderID); + bitStream.Write(i64LeaderZoneID); + bitStream.Write(0); // BinaryBuffe, no clue what's in here + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(wsLeaderName.size()); + for (const auto character : wsLeaderName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_SET_LEADER); + + bitStream.Write(i64PlayerID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER); + + bitStream.Write(bIsFreeTrial); + bitStream.Write(bLocal); + bitStream.Write(bNoLootOnDeath); + bitStream.Write(i64PlayerID); + bitStream.Write(wsPlayerName.size()); + for (const auto character : wsPlayerName) { + bitStream.Write(character); + } + bitStream.Write1(); + if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { + zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); + } + bitStream.Write(zoneID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER); + + bitStream.Write(bDisband); + bitStream.Write(bIsKicked); + bitStream.Write(bIsLeaving); + bitStream.Write(bLocal); + bitStream.Write(i64LeaderID); + bitStream.Write(i64PlayerID); + bitStream.Write(wsPlayerName.size()); + for (const auto character : wsPlayerName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + inline void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG); + + bitStream.Write(i64PlayerID); + if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { + zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); + } + bitStream.Write(zoneID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; + } + + // UpdateTeamsOnWorld, with the team's fields passed in + inline void UpdateTeamsOnWorld(LWOOBJID teamID, uint8_t lootFlag, const std::vector& memberIDs, bool deleteTeam) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::TEAM_GET_STATUS); + + bitStream.Write(teamID); + bitStream.Write(deleteTeam); + + if (!deleteTeam) { + bitStream.Write(lootFlag); + bitStream.Write(memberIDs.size()); + for (const auto memberID : memberIDs) { + bitStream.Write(memberID); + } + } + + Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); + } + + // dChatServer/PlayerContainer.cpp + inline void BroadcastMuteUpdate(LWOOBJID player, time_t time) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE); + + bitStream.Write(player); + bitStream.Write(time); + + Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); + } + + // World -> chat senders (they wrote to Game::chatServer; here into bitStream) + + // dWorldServer/WorldServer.cpp, LoadPlayer + inline void WriteLoginSessionNotify(RakNet::BitStream& bitStream, LWOOBJID objectID, const std::string& playerName, const LWOZONEID& zone, time_t muteExpire, eGameMasterLevel gmLevel) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::LOGIN_SESSION_NOTIFY); + bitStream.Write(objectID); + bitStream.Write(playerName.size()); + for (size_t i = 0; i < playerName.size(); i++) { + bitStream.Write(playerName[i]); + } + + bitStream.Write(zone.GetMapID()); + bitStream.Write(zone.GetInstanceID()); + bitStream.Write(zone.GetCloneID()); + bitStream.Write(muteExpire); + bitStream.Write(gmLevel); + } + + // dWorldServer/WorldServer.cpp and dGame/UserManager.cpp + inline void WriteUnexpectedDisconnect(RakNet::BitStream& bitStream, LWOOBJID objectID) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); + bitStream.Write(objectID); + } + + // dGame/Entity.cpp, SetGMLevel + inline void WriteGMLevelUpdate(RakNet::BitStream& bitStream, LWOOBJID m_ObjectID, eGameMasterLevel m_GMLevel) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GMLEVEL_UPDATE); + bitStream.Write(m_ObjectID); + bitStream.Write(m_GMLevel); + } + + // dGame/User.cpp and the /mute command + inline void WriteGMMute(RakNet::BitStream& bitStream, LWOOBJID characterId, time_t expire) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE); + + bitStream.Write(characterId); + bitStream.Write(expire); + } + + // dGame/dComponents/ActivityComponent.cpp, ActivityInstance::StartZone + inline void WriteCreateTeam(RakNet::BitStream& bitStream, LWOOBJID leaderID, const std::vector& m_Participants, LWOZONEID zoneId) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::CREATE_TEAM); + + bitStream.Write(leaderID); + bitStream.Write(m_Participants.size()); + + for (const auto& participant : m_Participants) { + bitStream.Write(participant); + } + + bitStream.Write(zoneId); + } + + // dGame/dUtilities/SlashCommandHandler.cpp, SendAnnouncement + inline void WriteGMAnnounce(RakNet::BitStream& bitStream, const std::string& title, const std::string& message) { + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_ANNOUNCE); + + bitStream.Write(title.size()); + for (auto character : title) { + bitStream.Write(character); + } + + bitStream.Write(message.size()); + for (auto character : message) { + bitStream.Write(character); + } + } + + // Readers. What the old handlers read, in order; the stream is past the packet header. + + struct PlayerAndName { + LWOOBJID playerID{}; + LUWString name; + char extra{}; + }; + + // HandleFriendRequest + inline PlayerAndName ReadFriendRequest(RakNet::BitStream& inStream) { + LWOOBJID requestorPlayerID; + LUWString LUplayerName; + char isBestFriendRequest{}; + + inStream.Read(requestorPlayerID); + inStream.IgnoreBytes(4); + inStream.Read(LUplayerName); + inStream.Read(isBestFriendRequest); + return { requestorPlayerID, LUplayerName, isBestFriendRequest }; + } + + // HandleFriendResponse + inline PlayerAndName ReadFriendResponse(RakNet::BitStream& inStream) { + LWOOBJID playerID; + eAddFriendResponseCode clientResponseCode; + LUWString friendName; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(clientResponseCode); + inStream.Read(friendName); + return { playerID, friendName, static_cast(clientResponseCode) }; + } + + // HandleRemoveFriend, AddIgnore, RemoveIgnore, HandleTeamInvite, HandleTeamKick, HandleTeamPromote + inline PlayerAndName ReadPlayerAndName(RakNet::BitStream& inStream) { + LWOOBJID playerID; + LUWString LUFriendName; + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(LUFriendName); + return { playerID, LUFriendName, 0 }; + } + + struct ChatRead { + LWOOBJID playerID{}; + eChatChannel channel{}; + uint32_t size{}; + LUWString receiverName; + LUWString message; + }; + + // HandleChatMessage (without the sender check) + inline ChatRead ReadChatMessage(RakNet::BitStream& inStream) { + LWOOBJID playerID; + inStream.Read(playerID); + + eChatChannel channel; + uint32_t size; + + inStream.IgnoreBytes(4); + inStream.Read(channel); + inStream.Read(size); + + inStream.IgnoreBytes(77); + + LUWString message(size); + inStream.Read(message); + return { playerID, channel, size, LUWString(), message }; + } + + // HandlePrivateChatMessage (without the sender check) + inline ChatRead ReadPrivateChatMessage(RakNet::BitStream& inStream) { + LWOOBJID playerID; + inStream.Read(playerID); + + eChatChannel channel; + uint32_t size; + LUWString LUReceiverName; + + inStream.IgnoreBytes(4); + inStream.Read(channel); + + inStream.Read(size); + + inStream.IgnoreBytes(77); + + inStream.Read(LUReceiverName); + inStream.IgnoreBytes(2); + + LUWString message(size); + inStream.Read(message); + return { playerID, channel, size, LUReceiverName, message }; + } + + struct TeamInviteResponseRead { + LWOOBJID playerID{}; + char declined{}; + LWOOBJID leaderID{}; + }; + + // HandleTeamInviteResponse + inline TeamInviteResponseRead ReadTeamInviteResponse(RakNet::BitStream& inStream) { + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + char declined = 0; + inStream.Read(declined); + LWOOBJID leaderID = LWOOBJID_EMPTY; + inStream.Read(leaderID); + return { playerID, declined, leaderID }; + } + + // HandleTeamLootOption + inline std::pair ReadTeamLootOption(RakNet::BitStream& inStream) { + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + + char option; + inStream.Read(option); + return { playerID, option }; + } + + struct CreateTeamRead { + LWOOBJID playerID{}; + std::vector members; + LWOZONEID zoneId; + }; + + // TeamContainer::CreateTeamServer (without the size check) + inline CreateTeamRead ReadCreateTeam(RakNet::BitStream& inStream) { + LWOOBJID playerID; + inStream.Read(playerID); + size_t membersSize = 0; + inStream.Read(membersSize); + + std::vector members; + + members.reserve(membersSize); + + for (size_t i = 0; i < membersSize; i++) { + LWOOBJID member; + inStream.Read(member); + members.push_back(member); + } + + LWOZONEID zoneId; + + inStream.Read(zoneId); + return { playerID, members, zoneId }; + } + + // PlayerContainer::InsertPlayer (into a PlayerData) + inline bool ReadLoginSessionNotify(RakNet::BitStream& inStream, PlayerData& data) { + LWOOBJID playerId; + if (!inStream.Read(playerId)) { + return false; + } + data.playerID = playerId; + + uint32_t len; + if (!inStream.Read(len)) return false; + + if (len > 33) { + return false; + } + + data.playerName.resize(len); + inStream.ReadAlignedBytes(reinterpret_cast(data.playerName.data()), len); + + if (!inStream.Read(data.zoneID)) return false; + if (!inStream.Read(data.muteExpire)) return false; + if (!inStream.Read(data.gmLevel)) return false; + return true; + } + + // dWorldServer/WorldServer.cpp, HandlePacketChat GM_ANNOUNCE + inline std::pair ReadGMAnnounce(RakNet::BitStream& inStream) { + std::string title; + std::string msg; + + uint32_t len; + inStream.Read(len); + for (uint32_t i = 0; len > i; i++) { + char character; + inStream.Read(character); + title += character; + } + + len = 0; + inStream.Read(len); + for (uint32_t i = 0; len > i; i++) { + char character; + inStream.Read(character); + msg += character; + } + return { title, msg }; + } + + struct TeamStatusRead { + LWOOBJID teamID = 0; + bool deleteTeam{}; + char lootOption = 0; + std::vector members; + }; + + // dWorldServer/WorldServer.cpp, HandlePacketChat TEAM_GET_STATUS + inline TeamStatusRead ReadTeamStatus(RakNet::BitStream& inStream) { + LWOOBJID teamID = 0; + char lootOption = 0; + char memberCount = 0; + std::vector members; + + inStream.Read(teamID); + bool deleteTeam = inStream.ReadBit(); + + if (deleteTeam) { + return { teamID, deleteTeam, 0, {} }; + } + + inStream.Read(lootOption); + inStream.Read(memberCount); + for (char i = 0; i < memberCount; i++) { + LWOOBJID member = LWOOBJID_EMPTY; + inStream.Read(member); + members.push_back(member); + } + return { teamID, deleteTeam, lootOption, members }; + } + + // dWorldServer/WorldServer.cpp, HandlePacketChat WORLD_ROUTE_PACKET: the bytes sent on to the player + inline std::pair> ReadWorldRoute(RakNet::BitStream& inStream) { + LWOOBJID playerID; + inStream.Read(playerID); + + std::vector out; + unsigned char data; + while (inStream.Read(data)) { + out.push_back(data); + } + return { playerID, out }; + } +} + +#endif // CHATPACKETSLEGACY_H