diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index f25e4ae6d..c45043826 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -21,6 +21,7 @@ #include "eGameMasterLevel.h" #include "ChatPackets.h" #include "TeamContainer.h" +#include "MinimumChatMode.h" void ChatPacketHandler::SendRouted(const LWOOBJID target, const SystemAddress& world, const LUBitStream& msg, const bool broadcast) { ChatPackets::WorldRoutePacket route; @@ -350,6 +351,41 @@ void ChatPacketHandler::HandleGMLevelUpdate(const ChatPackets::GMLevelUpdate& up } +void ChatPacketHandler::HandleRequestMinimumChatMode(const ChatPackets::RequestMinimumChatMode& request, const SystemAddress& sysAddr) { + const auto& sender = Game::playerContainer.GetPlayerData(request.playerID); + if (!sender) return; + + // Everyone on the team who is online reads team chat; without a team only the sender does + std::vector readers{ sender.gmLevel }; + if (const auto* const team = TeamContainer::GetTeam(sender.playerID)) { + for (const auto memberID : team->memberIDs) { + const auto& member = Game::playerContainer.GetPlayerData(memberID); + if (member && member.playerID != sender.playerID) readers.push_back(member.gmLevel); + } + } + + ClientPackets::MinimumChatModeResponse response; + response.chatMode = MinimumChatMode::Of(readers); + response.chatChannel = request.chatChannel; + SendRouted(sender.playerID, sender.worldServerSysAddr, response); +} + +void ChatPacketHandler::HandleRequestMinimumChatModePrivate(const ChatPackets::RequestMinimumChatModePrivate& request, const SystemAddress& sysAddr) { + const auto& sender = Game::playerContainer.GetPlayerData(request.playerID); + if (!sender) return; + + const auto& recipient = Game::playerContainer.GetPlayerData(request.recipientName.GetAsString()); + std::vector readers{ sender.gmLevel }; + if (recipient) readers.push_back(recipient.gmLevel); + + ClientPackets::MinimumChatModeResponsePrivate response; + response.chatMode = MinimumChatMode::Of(readers); + response.chatChannel = request.chatChannel; + response.recipientName = request.recipientName; + response.recipientGMLevel = recipient ? static_cast(recipient.gmLevel) : 0; + SendRouted(sender.playerID, sender.worldServerSysAddr, response); +} + void ChatPacketHandler::HandleWho(const ChatPackets::FindPlayerRequest& request, const SystemAddress& sysAddr) { const auto& sender = Game::playerContainer.GetPlayerData(request.requestor); diff --git a/dChatServer/ChatPacketHandler.h b/dChatServer/ChatPacketHandler.h index 6187feca2..4b898a844 100644 --- a/dChatServer/ChatPacketHandler.h +++ b/dChatServer/ChatPacketHandler.h @@ -25,6 +25,8 @@ namespace ChatPacketHandler { 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 HandleRequestMinimumChatMode(const ChatPackets::RequestMinimumChatMode& request, const SystemAddress& sysAddr); + void HandleRequestMinimumChatModePrivate(const ChatPackets::RequestMinimumChatModePrivate& request, 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); diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index 904b48c9c..a37ac06c2 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -260,6 +260,8 @@ namespace { 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::REQUEST_MINIMUM_CHAT_MODE, ChatPacketHandler::HandleRequestMinimumChatMode); + handlers.On(Chat::REQUEST_MINIMUM_CHAT_MODE_PRIVATE, ChatPacketHandler::HandleRequestMinimumChatModePrivate); 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 ChatPackets::Announcement& announcement, const SystemAddress& sysAddr) { diff --git a/dChatServer/MinimumChatMode.h b/dChatServer/MinimumChatMode.h new file mode 100644 index 000000000..0cf4d9dbc --- /dev/null +++ b/dChatServer/MinimumChatMode.h @@ -0,0 +1,22 @@ +#ifndef MINIMUMCHATMODE_H +#define MINIMUMCHATMODE_H + +#include "eGameMasterLevel.h" + +#include +#include +#include + +// The answer to a client's RequestMinimumChatMode: the lowest chat mode of everyone who would read a message in that +// channel. The client asks only for private (7), team (8) and local team (10) chat and answers every other channel +// itself with its own chat mode (LWOChatComponent::RequestMinimumChatMode). A chat mode is what DLU sends as the +// player's chat mode everywhere else: their GM level. Live answered 0 in all 74 captured answers (team chat among +// players who were not GMs); taking the minimum for teams with GMs in them is inferred from the name. +namespace MinimumChatMode { + inline uint8_t Of(const std::vector& readers) { + if (readers.empty()) return 0; + return static_cast(*std::min_element(readers.begin(), readers.end())); + } +} + +#endif // MINIMUMCHATMODE_H diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index d7ffd0b78..0d3b6259c 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -424,6 +424,32 @@ namespace ChatPackets { VALIDATE_READ(bitStream.Read(playerID)); return true; } + + void RequestMinimumChatMode::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(chatChannel); + } + + bool RequestMinimumChatMode::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(chatChannel)); + return true; + } + + void RequestMinimumChatModePrivate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(unknown); + bitStream.Write(chatChannel); + bitStream.Write(recipientName); + } + + bool RequestMinimumChatModePrivate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(ReadPlayerHeader(bitStream, playerID, unknown)); + VALIDATE_READ(bitStream.Read(chatChannel)); + VALIDATE_READ(bitStream.Read(recipientName)); + return true; + } } namespace ChatPackets::Client { diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index 4b2016d7c..0406a389d 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -352,6 +352,29 @@ namespace ChatPackets { bool Deserialize(RakNet::BitStream& bitStream) override; }; + // The lowest chat mode among who would read the player's team chat (chatChannel 8 team, 10 local team) + struct RequestMinimumChatMode : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + uint8_t chatChannel{}; + + RequestMinimumChatMode() : LUBitStream(ServiceType::CHAT, MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // The same for a private message to recipientName (chatChannel 7) + struct RequestMinimumChatModePrivate : public LUBitStream { + LWOOBJID playerID{}; + uint32_t unknown{}; + uint8_t chatChannel{}; + LUWString recipientName; + + RequestMinimumChatModePrivate() : LUBitStream(ServiceType::CHAT, MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE_PRIVATE) {} + 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. */ diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index 5cd6de794..95ad72d46 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -533,6 +533,32 @@ namespace ClientPackets { return true; } + void MinimumChatModeResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(chatMode); + bitStream.Write(chatChannel); + } + + bool MinimumChatModeResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(chatMode)); + VALIDATE_READ(bitStream.Read(chatChannel)); + return true; + } + + void MinimumChatModeResponsePrivate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(chatMode); + bitStream.Write(chatChannel); + bitStream.Write(recipientName); + bitStream.Write(recipientGMLevel); + } + + bool MinimumChatModeResponsePrivate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(chatMode)); + VALIDATE_READ(bitStream.Read(chatChannel)); + VALIDATE_READ(bitStream.Read(recipientName)); + VALIDATE_READ(bitStream.Read(recipientGMLevel)); + return true; + } + void GetFriendsListResponse::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(responseCode); bitStream.Write(packetLength); diff --git a/dNet/ClientPackets.h b/dNet/ClientPackets.h index 5814378d7..1e876fa01 100644 --- a/dNet/ClientPackets.h +++ b/dNet/ClientPackets.h @@ -290,6 +290,29 @@ namespace ClientPackets { * list and team responses, and the team game messages. SendCannedText comes from the world. */ namespace ClientPackets { + // The answer to ChatPackets::RequestMinimumChatMode; the client hands both values to its chat UI + // (PacketHandler_MSG_CLIENT_MINIMUM_CHAT_MODE_RESPONSE) + struct MinimumChatModeResponse : public LUBitStream { + uint8_t chatMode{}; + uint8_t chatChannel{}; + + MinimumChatModeResponse() : LUBitStream(ServiceType::CLIENT, MessageType::Client::MINIMUM_CHAT_MODE_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // The answer to ChatPackets::RequestMinimumChatModePrivate + struct MinimumChatModeResponsePrivate : public LUBitStream { + uint8_t chatMode{}; + uint8_t chatChannel{}; + LUWString recipientName; + uint8_t recipientGMLevel{}; + + MinimumChatModeResponsePrivate() : LUBitStream(ServiceType::CLIENT, MessageType::Client::MINIMUM_CHAT_MODE_RESPONSE_PRIVATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + // 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 { diff --git a/tests/dGameTests/dNetTests/ChatPacketsTests.cpp b/tests/dGameTests/dNetTests/ChatPacketsTests.cpp index b1114309b..6ffef46f5 100644 --- a/tests/dGameTests/dNetTests/ChatPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/ChatPacketsTests.cpp @@ -1,6 +1,8 @@ #include "ChatPackets.h" #include "ClientPackets.h" #include "WorldRoutePacket.h" +#include "WorldPackets.h" +#include "MinimumChatMode.h" #include "GameDependencies.h" #include "PacketTestUtils.h" #include "dGameMessagesTests/GameMessageTestUtils.h" @@ -926,3 +928,80 @@ TEST_F(ChatPacketsTests, MailNotifyRoundTrips) { EXPECT_EQ(RoundTrip(notify).receiverID, notify.receiverID); ExpectTruncatedFails(notify); } + +// Minimum chat mode: the client's requests, as the chat server receives them from the world, and the answers. +namespace { + // What the chat server gets when the client routes routedData (the client's packet after its header) through + // its world with this message ID + template + T ReadRoutedFromClient(uint32_t routedMessageID, const std::vector& routedData, LWOOBJID sender) { + WorldPackets::RoutePacket route; + route.size = static_cast(routedData.size()); + route.routedService = ServiceType::CHAT; + route.routedMessageID = routedMessageID; + route.routedData = routedData; + RakNet::BitStream bitStream; + route.ToChat(sender).WritePacket(bitStream); + bitStream.IgnoreBytes(8); + T read; + EXPECT_TRUE(read.Deserialize(bitStream)); + return read; + } +} + +// The client's request: an empty object ID, then the channel (lu_packets' RequestMinimumChatMode sample, team chat) +TEST_F(ChatPacketsTests, RequestMinimumChatModeReadsTheChannel) { + const std::vector clientData = { 0, 0, 0, 0, 0, 0, 0, 0, 0x08 }; + const auto read = ReadRoutedFromClient(50, clientData, 0x1000000000000001LL); + EXPECT_EQ(read.playerID, 0x1000000000000001LL); + EXPECT_EQ(read.chatChannel, 8); + RoundTrip(read); + ExpectTruncatedFails(read); +} + +TEST_F(ChatPacketsTests, RequestMinimumChatModePrivateReadsTheRecipient) { + std::vector clientData = { 0, 0, 0, 0, 0, 0, 0, 0, 0x07 }; + const std::u16string name = u"Recipient"; + for (size_t i = 0; i < 33; i++) { + const char16_t c = i < name.size() ? name[i] : u'\0'; + clientData.push_back(static_cast(c & 0xff)); + clientData.push_back(static_cast(c >> 8)); + } + const auto read = ReadRoutedFromClient(51, clientData, 0x1000000000000001LL); + EXPECT_EQ(read.playerID, 0x1000000000000001LL); + EXPECT_EQ(read.chatChannel, 7); + EXPECT_EQ(read.recipientName.GetAsString(), "Recipient"); + RoundTrip(read); +} + +// The live answer to team chat (74 of 74 in the captures): chat mode 0, channel 8 +TEST_F(ChatPacketsTests, MinimumChatModeResponseMatchesLive) { + ClientPackets::MinimumChatModeResponse response; + response.chatMode = MinimumChatMode::Of({ eGameMasterLevel::CIVILIAN, eGameMasterLevel::CIVILIAN }); + response.chatChannel = 8; + EXPECT_PACKET_EQ(FromHex("53 05 00 39 00 00 00 00 00 08"), StructPacket(response)); + RoundTrip(response); + ExpectTruncatedFails(response); +} + +TEST_F(ChatPacketsTests, MinimumChatModePrivateResponseLayout) { + ClientPackets::MinimumChatModeResponsePrivate response; + response.chatMode = 0; + response.chatChannel = 7; + response.recipientName = LUWString(u"Bob"); + response.recipientGMLevel = 0; + const auto packet = StructPacket(response); + // header, mode, channel, 33 wide characters, GM level (lu_packets MinimumChatModeResponsePrivate) + ASSERT_EQ(packet.bytes.size(), 8u + 2u + 66u + 1u); + EXPECT_EQ(packet.bytes[3], 0x3a); + EXPECT_EQ(packet.bytes[9], 7); + EXPECT_EQ(packet.bytes[10], 'B'); + const auto copy = RoundTrip(response); + EXPECT_EQ(copy.recipientName.GetAsString(), "Bob"); +} + +TEST_F(ChatPacketsTests, MinimumChatModeIsTheLowestReader) { + EXPECT_EQ(MinimumChatMode::Of({}), 0); + EXPECT_EQ(MinimumChatMode::Of({ eGameMasterLevel::OPERATOR }), static_cast(eGameMasterLevel::OPERATOR)); + EXPECT_EQ(MinimumChatMode::Of({ eGameMasterLevel::OPERATOR, eGameMasterLevel::CIVILIAN }), 0); +}