Add chat tabs, networked text emotes, channel system, and chat bubbles

Chat tabs filter messages into General/Combat/Whispers/Trade tabs. Text
emotes now send CMSG_TEXT_EMOTE to server and display incoming emotes
from other players. Channel system auto-joins General/Trade on login with
/join, /leave, and /1-/9 shortcuts. Chat bubbles render as ImGui overlays
above entities for SAY/YELL messages with fade-out animation.
This commit is contained in:
Kelsi 2026-02-14 14:30:09 -08:00
parent ca3150e43d
commit 9bcead6a0f
14 changed files with 670 additions and 23 deletions

View file

@ -229,6 +229,15 @@ public:
* @param target Target name (for whispers, empty otherwise)
*/
void sendChatMessage(ChatType type, const std::string& message, const std::string& target = "");
void sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid = 0);
void joinChannel(const std::string& channelName, const std::string& password = "");
void leaveChannel(const std::string& channelName);
const std::vector<std::string>& getJoinedChannels() const { return joinedChannels_; }
std::string getChannelByIndex(int index) const;
// Chat bubble callback: (senderGuid, message, isYell)
using ChatBubbleCallback = std::function<void(uint64_t, const std::string&, bool)>;
void setChatBubbleCallback(ChatBubbleCallback cb) { chatBubbleCallback_ = std::move(cb); }
/**
* Get chat history (recent messages)
@ -845,6 +854,9 @@ private:
* Handle SMSG_MESSAGECHAT from server
*/
void handleMessageChat(network::Packet& packet);
void handleTextEmote(network::Packet& packet);
void handleChannelNotify(network::Packet& packet);
void autoJoinDefaultChannels();
// ---- Phase 1 handlers ----
void handleNameQueryResponse(network::Packet& packet);
@ -1031,6 +1043,8 @@ private:
// Chat
std::deque<MessageChatData> chatHistory; // Recent chat messages
size_t maxChatHistory = 100; // Maximum chat messages to keep
std::vector<std::string> joinedChannels_; // Active channel memberships
ChatBubbleCallback chatBubbleCallback_;
// Targeting
uint64_t targetGuid = 0;

View file

@ -351,6 +351,19 @@ enum class LogicalOpcode : uint16_t {
SMSG_ARENA_ERROR,
MSG_INSPECT_ARENA_TEAMS,
// ---- Emotes ----
CMSG_EMOTE,
SMSG_EMOTE,
CMSG_TEXT_EMOTE,
SMSG_TEXT_EMOTE,
// ---- Channels ----
CMSG_JOIN_CHANNEL,
CMSG_LEAVE_CHANNEL,
SMSG_CHANNEL_NOTIFY,
CMSG_CHANNEL_LIST,
SMSG_CHANNEL_LIST,
// Sentinel
COUNT
};

View file

@ -670,6 +670,115 @@ public:
*/
const char* getChatTypeString(ChatType type);
// ============================================================
// Text Emotes
// ============================================================
/**
* CMSG_TEXT_EMOTE packet builder
*/
class TextEmotePacket {
public:
static network::Packet build(uint32_t textEmoteId, uint64_t targetGuid = 0);
};
/**
* SMSG_TEXT_EMOTE data
*/
struct TextEmoteData {
uint64_t senderGuid = 0;
uint32_t textEmoteId = 0;
uint32_t emoteNum = 0;
std::string targetName;
bool isValid() const { return senderGuid != 0; }
};
/**
* SMSG_TEXT_EMOTE parser
*/
class TextEmoteParser {
public:
static bool parse(network::Packet& packet, TextEmoteData& data);
};
// ============================================================
// Channel System
// ============================================================
/**
* CMSG_JOIN_CHANNEL packet builder
*/
class JoinChannelPacket {
public:
static network::Packet build(const std::string& channelName, const std::string& password = "");
};
/**
* CMSG_LEAVE_CHANNEL packet builder
*/
class LeaveChannelPacket {
public:
static network::Packet build(const std::string& channelName);
};
/**
* Channel notification types
*/
enum class ChannelNotifyType : uint8_t {
YOU_JOINED = 0x00,
YOU_LEFT = 0x01,
WRONG_PASSWORD = 0x02,
NOT_MEMBER = 0x03,
NOT_MODERATOR = 0x04,
PASSWORD_CHANGED = 0x05,
OWNER_CHANGED = 0x06,
PLAYER_NOT_FOUND = 0x07,
NOT_OWNER = 0x08,
CHANNEL_OWNER = 0x09,
MODE_CHANGE = 0x0A,
ANNOUNCEMENTS_ON = 0x0B,
ANNOUNCEMENTS_OFF = 0x0C,
MODERATION_ON = 0x0D,
MODERATION_OFF = 0x0E,
MUTED = 0x0F,
PLAYER_KICKED = 0x10,
BANNED = 0x11,
PLAYER_BANNED = 0x12,
PLAYER_UNBANNED = 0x13,
PLAYER_NOT_BANNED = 0x14,
PLAYER_ALREADY_MEMBER = 0x15,
INVITE = 0x16,
INVITE_WRONG_FACTION = 0x17,
WRONG_FACTION = 0x18,
INVALID_NAME = 0x19,
NOT_MODERATED = 0x1A,
PLAYER_INVITED = 0x1B,
PLAYER_INVITE_BANNED = 0x1C,
THROTTLED = 0x1D,
NOT_IN_AREA = 0x1E,
NOT_IN_LFG = 0x1F,
};
/**
* SMSG_CHANNEL_NOTIFY data
*/
struct ChannelNotifyData {
ChannelNotifyType notifyType = ChannelNotifyType::YOU_JOINED;
std::string channelName;
uint64_t senderGuid = 0;
bool isValid() const { return !channelName.empty(); }
};
/**
* SMSG_CHANNEL_NOTIFY parser
*/
class ChannelNotifyParser {
public:
static bool parse(network::Packet& packet, ChannelNotifyData& data);
};
// ============================================================
// Server Info Commands
// ============================================================

View file

@ -125,6 +125,7 @@ public:
void cancelEmote();
bool isEmoteActive() const { return emoteActive; }
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
static uint32_t getEmoteDbcId(const std::string& emoteName);
// Targeting support
void setTargetPosition(const glm::vec3* pos);

View file

@ -49,6 +49,16 @@ private:
int lastChatType = 0; // Track chat type changes
bool chatInputMoveCursorToEnd = false;
// Chat tabs
int activeChatTab_ = 0;
struct ChatTab {
std::string name;
uint32_t typeMask; // bitmask of ChatType values to show
};
std::vector<ChatTab> chatTabs_;
void initChatTabs();
bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const;
// UI state
bool showEntityWindow = false;
bool showChatWindow = true;
@ -170,6 +180,7 @@ private:
void renderMinimapMarkers(game::GameHandler& gameHandler);
void renderGuildRoster(game::GameHandler& gameHandler);
void renderGuildInvitePopup(game::GameHandler& gameHandler);
void renderChatBubbles(game::GameHandler& gameHandler);
/**
* Inventory screen
@ -209,6 +220,17 @@ private:
// Gender placeholder replacement
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler);
// Chat bubbles
struct ChatBubble {
uint64_t senderGuid = 0;
std::string message;
float timeRemaining = 0.0f;
float totalDuration = 0.0f;
bool isYell = false;
};
std::vector<ChatBubble> chatBubbles_;
bool chatBubbleCallbackSet_ = false;
// Left-click targeting: distinguish click from camera drag
glm::vec2 leftClickPressPos_ = glm::vec2(0.0f);
bool leftClickWasPress_ = false;