mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-17 18:22:50 +00:00
refactor: chat packets as structs
Every packet of the chat service is now an LUBitStream struct in dNet/ChatPackets.h (docs/PacketArchitecture.md, PR 13), and nothing in the chat server or the chat side of the world server reads or writes a packet by hand any more. - World <-> chat: LoginSessionNotify, UnexpectedDisconnect, GMLevelUpdate, GMMute, Announcement (GM announce), CreateTeam, TeamUpdate (TEAM_GET_STATUS to worlds), AchievementNotify, ShowAllRequest, FindPlayerRequest. - Client -> world -> chat (friends, ignore list, teams, general and private chat): GetFriendsList, AddFriendRequest, AddFriendResponse, RemoveFriend, GetIgnoreList, AddIgnore, RemoveIgnore, GeneralChatMessage, PrivateChatMessage, TeamInvite, TeamInviteResponse, TeamLeave, TeamKick, TeamSetLeader, TeamSetLoot, TeamGetStatus. The 77 bytes the handlers skipped are named fields now (the sender block the client fills in); the 4 unused bytes after the player ID are kept as `unknown`. - What the client receives. Chat service (ChatPackets::Client): GeneralChatMessage (replaces SendChatMessage; SendSystemMessage stays as a helper built on it), PrivateChatMessage. Client service (ClientPackets, as structs go in the file of the ServiceType in their header): SendCannedText (replaces SendMessageFail), GetFriendsListResponse, AddFriendRequest, AddFriendResponse, RemoveFriendResponse, UpdateFriendNotify, WhoResponse, ShowAllResponse, Get/Add/RemoveIgnoreResponse, TeamInvite, TeamInviteInitialResponse, and the team game messages chat writes (TeamInviteConfirm, TeamGetStatusResponse, TeamSetLeader, TeamAddPlayer, TeamRemovePlayer, TeamSetOffWorldFlag) as TeamGameMsg structs, since chat doesn't link dGame's NetGameMsg. - WORLD_ROUTE_PACKET is ChatPackets::WorldRoutePacket (its own file, dNet/WorldRoutePacket.h, since it carries packets of other services): the target and the inner packet. ChatPacketHandler::SendRouted replaces the per-function route headers and SendRoutedMsg. Dispatch: dNet/PacketDispatcher.h is a dispatch map from packet ID to (struct, handler function); packets that fail to Deserialize are logged and dropped. The chat server's switch and the world's HandlePacketChat switch are now maps. The handlers take the structs; their logic is unchanged. World code sends to chat with ChatServerLink::Send (dGame) instead of writing to Game::chatServer by hand. eChatChannel, eChatMessageResponseCode and eAddIgnoreResponse moved to dCommon/dEnums so the structs can use them. Verified: tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h is a verbatim copy of the old senders and readers; ChatPacketsTests (25 tests) sends a grid of inputs through both and requires identical bits, bytes and destination, checks the old readers read what the structs write, round trips every struct, checks truncated packets are refused, and has hand written golden packets for general chat, canned text, GM mute and a routed team game message. Breaking one field width (UpdateFriendNotify) and the TeamAddPlayer zone flag made the tests fail. No wire bytes changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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<uint64_t>(0);
|
||||
bitStream.Write(chatChannel);
|
||||
|
||||
bitStream.Write<uint32_t>(message.size());
|
||||
bitStream.Write(LUWString(senderName));
|
||||
|
||||
bitStream.Write(playerObjectID);
|
||||
bitStream.Write<uint16_t>(0);
|
||||
bitStream.Write<char>(0);
|
||||
|
||||
for (uint32_t i = 0; i < message.size(); ++i) {
|
||||
bitStream.Write<uint16_t>(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<uint16_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(bitStream, title);
|
||||
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, message);
|
||||
}
|
||||
|
||||
bool Announcement::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, title));
|
||||
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, message));
|
||||
return true;
|
||||
}
|
||||
|
||||
void CreateTeam::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(leaderID);
|
||||
bitStream.Write<uint64_t>(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<uint8_t>(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<uint64_t>(0); // Packing
|
||||
bitStream.Write<uint32_t>(0); // Packing
|
||||
bitStream.Write<uint8_t>(0); // Packing
|
||||
bitStream.Write(earnerName);
|
||||
bitStream.Write<uint64_t>(0); // Packing / No way to know meaning because of not enough data.
|
||||
bitStream.Write<uint32_t>(0); // Packing / No way to know meaning because of not enough data.
|
||||
bitStream.Write<uint16_t>(0); // Packing / No way to know meaning because of not enough data.
|
||||
bitStream.Write<uint8_t>(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<uint32_t>(message.size());
|
||||
bitStream.Write(senderName);
|
||||
bitStream.Write(senderID);
|
||||
bitStream.Write(sourceID);
|
||||
bitStream.Write(senderGMLevel);
|
||||
bitStream.Write(message);
|
||||
bitStream.Write<uint16_t>(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<unsigned char*>(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<uint64_t>(0);
|
||||
bitStream.Write<char>(4);
|
||||
|
||||
bitStream.Write<uint32_t>(message.size());
|
||||
bitStream.Write(LUWString("", 33));
|
||||
|
||||
bitStream.Write<uint64_t>(0);
|
||||
bitStream.Write<uint16_t>(0);
|
||||
bitStream.Write<char>(0);
|
||||
|
||||
for (uint32_t i = 0; i < message.size(); ++i) {
|
||||
bitStream.Write<uint16_t>(message[i]);
|
||||
}
|
||||
|
||||
bitStream.Write<uint16_t>(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<uint8_t>(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<uint32_t>(title.size());
|
||||
bitStream.Write(title);
|
||||
bitStream.Write<uint32_t>(message.size());
|
||||
bitStream.Write(message);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPackets::AchievementNotify::Serialize(RakNet::BitStream& bitstream) const {
|
||||
bitstream.Write<uint64_t>(0); // Packing
|
||||
bitstream.Write<uint32_t>(0); // Packing
|
||||
bitstream.Write<uint8_t>(0); // Packing
|
||||
bitstream.Write(earnerName);
|
||||
bitstream.Write<uint64_t>(0); // Packing / No way to know meaning because of not enough data.
|
||||
bitstream.Write<uint32_t>(0); // Packing / No way to know meaning because of not enough data.
|
||||
bitstream.Write<uint16_t>(0); // Packing / No way to know meaning because of not enough data.
|
||||
bitstream.Write<uint8_t>(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<uint8_t>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,50 +12,139 @@ struct SystemAddress;
|
||||
#include <vector>
|
||||
#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<LWOOBJID> 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<LWOOBJID> 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
|
||||
|
||||
@@ -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<uint16_t>(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<uint8_t>(!displayZoneData && !displayIndividualPlayers);
|
||||
bitStream.Write(playerCount);
|
||||
bitStream.Write(simCount);
|
||||
bitStream.Write<uint8_t>(displayIndividualPlayers);
|
||||
bitStream.Write<uint8_t>(displayZoneData);
|
||||
if (!displayZoneData && !displayIndividualPlayers) return;
|
||||
for (const auto& player : players) {
|
||||
bitStream.Write<uint8_t>(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<uint16_t>(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<uint8_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FriendData> 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<Player> 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> 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
|
||||
|
||||
77
dNet/PacketDispatcher.h
Normal file
77
dNet/PacketDispatcher.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef PACKETDISPATCHER_H
|
||||
#define PACKETDISPATCHER_H
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
#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<MessageType::Master> handlers;
|
||||
* handlers.On<MasterPackets::PlayerAdded>(MessageType::Master::PLAYER_ADDED, &OnPlayerAdded);
|
||||
* ...
|
||||
* handlers.Dispatch(packet); // reads the header and routes by packet ID
|
||||
*/
|
||||
template<typename IdT>
|
||||
class PacketDispatcher {
|
||||
public:
|
||||
template<typename Msg>
|
||||
using Handler = std::function<void(Msg& msg, const SystemAddress& sysAddr)>;
|
||||
|
||||
// Registers the struct packets with this ID are read into, and what to do with them
|
||||
template<typename Msg>
|
||||
void On(const IdT id, Handler<Msg> handler) {
|
||||
static_assert(std::is_base_of_v<LUBitStream, Msg>, "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<uint32_t>(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<IdT>(header.internalPacketID), bitStream, packet->systemAddress);
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<IdT, std::function<bool(RakNet::BitStream&, const SystemAddress&)>> m_Handlers;
|
||||
};
|
||||
|
||||
#endif // PACKETDISPATCHER_H
|
||||
17
dNet/WorldRoutePacket.cpp
Normal file
17
dNet/WorldRoutePacket.cpp
Normal file
@@ -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<uint32_t>(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<uint32_t>(routedData.size()));
|
||||
}
|
||||
}
|
||||
31
dNet/WorldRoutePacket.h
Normal file
31
dNet/WorldRoutePacket.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef WORLDROUTEPACKET_H
|
||||
#define WORLDROUTEPACKET_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<uint8_t> 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
|
||||
Reference in New Issue
Block a user