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