Files
DarkflameServer/dNet/ChatPackets.h
Aaron Kimbrell 796fd1b941 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>
2026-09-28 22:30:50 -05:00

390 lines
14 KiB
C++

/*
* Darkflame Universe
* Copyright 2018
*/
#ifndef CHATPACKETS_H
#define CHATPACKETS_H
struct SystemAddress;
#include <string>
#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"
/**
* 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;
LWOOBJID playerID{};
std::string playerName; // u32 length, then 1 byte per character
LWOZONEID zoneID{};
int64_t muteExpire{}; // time_t
eGameMasterLevel gmLevel{};
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;
};
// World -> chat: /showall
struct ShowAllRequest : public LUBitStream {
LWOOBJID requestor = LWOOBJID_EMPTY;
bool displayZoneData = true; // one bit
bool displayIndividualPlayers = true; // one bit
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,
// the object ID of the player who sent it (the chat server has no other way to know), then the routed bytes.
struct RoutedFromClient : public LUBitStream {
LWOOBJID senderID{};
std::vector<uint8_t> data{};
RoutedFromClient(uint8_t routedMessageID = 0) : LUBitStream(ServiceType::CHAT, routedMessageID) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// 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);
};
#endif // CHATPACKETS_H