#include "game/chat_handler.hpp" #include "game/game_handler.hpp" #include "game/game_utils.hpp" #include "game/packet_parsers.hpp" #include "game/entity.hpp" #include "game/opcode_table.hpp" #include "network/world_socket.hpp" #include "rendering/renderer.hpp" #include "core/logger.hpp" #include namespace wowee { namespace game { ChatHandler::ChatHandler(GameHandler& owner) : owner_(owner) {} void ChatHandler::registerOpcodes(DispatchTable& table) { table[Opcode::SMSG_MESSAGECHAT] = [this](network::Packet& packet) { if (owner_.getState() == WorldState::IN_WORLD) handleMessageChat(packet); }; table[Opcode::SMSG_GM_MESSAGECHAT] = [this](network::Packet& packet) { if (owner_.getState() == WorldState::IN_WORLD) handleMessageChat(packet); }; table[Opcode::SMSG_TEXT_EMOTE] = [this](network::Packet& packet) { if (owner_.getState() == WorldState::IN_WORLD) handleTextEmote(packet); }; table[Opcode::SMSG_EMOTE] = [this](network::Packet& packet) { if (owner_.getState() != WorldState::IN_WORLD) return; if (packet.getSize() - packet.getReadPos() < 12) return; uint32_t emoteAnim = packet.readUInt32(); uint64_t sourceGuid = packet.readUInt64(); if (owner_.emoteAnimCallback_ && sourceGuid != 0) owner_.emoteAnimCallback_(sourceGuid, emoteAnim); }; table[Opcode::SMSG_CHANNEL_NOTIFY] = [this](network::Packet& packet) { if (owner_.getState() == WorldState::IN_WORLD || owner_.getState() == WorldState::ENTERING_WORLD) handleChannelNotify(packet); }; table[Opcode::SMSG_CHAT_PLAYER_NOT_FOUND] = [this](network::Packet& packet) { std::string name = packet.readString(); if (!name.empty()) addSystemChatMessage("No player named '" + name + "' is currently playing."); }; table[Opcode::SMSG_CHAT_PLAYER_AMBIGUOUS] = [this](network::Packet& packet) { std::string name = packet.readString(); if (!name.empty()) addSystemChatMessage("Player name '" + name + "' is ambiguous."); }; table[Opcode::SMSG_CHAT_WRONG_FACTION] = [this](network::Packet& /*packet*/) { owner_.addUIError("You cannot send messages to members of that faction."); addSystemChatMessage("You cannot send messages to members of that faction."); }; table[Opcode::SMSG_CHAT_NOT_IN_PARTY] = [this](network::Packet& /*packet*/) { owner_.addUIError("You are not in a party."); addSystemChatMessage("You are not in a party."); }; table[Opcode::SMSG_CHAT_RESTRICTED] = [this](network::Packet& /*packet*/) { owner_.addUIError("You cannot send chat messages in this area."); addSystemChatMessage("You cannot send chat messages in this area."); }; // ---- Channel list ---- // ---- Server / defense / area-trigger messages (moved from GameHandler) ---- table[Opcode::SMSG_DEFENSE_MESSAGE] = [this](network::Packet& packet) { if (packet.hasRemaining(5)) { /*uint32_t zoneId =*/ packet.readUInt32(); std::string defMsg = packet.readString(); if (!defMsg.empty()) addSystemChatMessage("[Defense] " + defMsg); } }; // Server messages table[Opcode::SMSG_SERVER_MESSAGE] = [this](network::Packet& packet) { if (packet.hasRemaining(4)) { uint32_t msgType = packet.readUInt32(); std::string msg = packet.readString(); if (!msg.empty()) { std::string prefix; switch (msgType) { case 1: prefix = "[Shutdown] "; owner_.addUIError("Server shutdown: " + msg); break; case 2: prefix = "[Restart] "; owner_.addUIError("Server restart: " + msg); break; case 4: prefix = "[Shutdown cancelled] "; break; case 5: prefix = "[Restart cancelled] "; break; default: prefix = "[Server] "; break; } addSystemChatMessage(prefix + msg); } } }; table[Opcode::SMSG_CHAT_SERVER_MESSAGE] = [this](network::Packet& packet) { if (packet.hasRemaining(4)) { /*uint32_t msgType =*/ packet.readUInt32(); std::string msg = packet.readString(); if (!msg.empty()) addSystemChatMessage("[Announcement] " + msg); } }; table[Opcode::SMSG_AREA_TRIGGER_MESSAGE] = [this](network::Packet& packet) { if (packet.hasRemaining(4)) { /*uint32_t len =*/ packet.readUInt32(); std::string msg = packet.readString(); if (!msg.empty()) { owner_.addUIError(msg); addSystemChatMessage(msg); owner_.areaTriggerMsgs_.push_back(msg); } } }; table[Opcode::SMSG_CHANNEL_LIST] = [this](network::Packet& p) { handleChannelList(p); }; } void ChatHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) { if (owner_.getState() != WorldState::IN_WORLD) { LOG_WARNING("Cannot send chat in state: ", static_cast(owner_.getState())); return; } if (message.empty()) { LOG_WARNING("Cannot send empty chat message"); return; } LOG_INFO("Sending chat message: [", getChatTypeString(type), "] ", message); ChatLanguage language = ChatLanguage::COMMON; auto packet = MessageChatPacket::build(type, language, message, target); owner_.socket->send(packet); // Add local echo so the player sees their own message immediately MessageChatData echo; echo.senderGuid = owner_.playerGuid; echo.language = language; echo.message = message; auto nameIt = owner_.playerNameCache.find(owner_.playerGuid); if (nameIt != owner_.playerNameCache.end()) { echo.senderName = nameIt->second; } if (type == ChatType::WHISPER) { echo.type = ChatType::WHISPER_INFORM; echo.senderName = target; } else { echo.type = type; } if (type == ChatType::CHANNEL) { echo.channelName = target; } addLocalChatMessage(echo); } void ChatHandler::handleMessageChat(network::Packet& packet) { LOG_DEBUG("Handling SMSG_MESSAGECHAT"); MessageChatData data; if (!owner_.packetParsers_->parseMessageChat(packet, data)) { LOG_WARNING("Failed to parse SMSG_MESSAGECHAT"); return; } // Skip server echo of our own messages (we already added a local echo) if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) { if (data.type == ChatType::WHISPER && !data.senderName.empty()) { owner_.lastWhisperSender_ = data.senderName; } return; } // Resolve sender name from entity/cache if not already set by parser if (data.senderName.empty() && data.senderGuid != 0) { auto nameIt = owner_.playerNameCache.find(data.senderGuid); if (nameIt != owner_.playerNameCache.end()) { data.senderName = nameIt->second; } else { auto entity = owner_.entityManager.getEntity(data.senderGuid); if (entity) { if (entity->getType() == ObjectType::PLAYER) { auto player = std::dynamic_pointer_cast(entity); if (player && !player->getName().empty()) { data.senderName = player->getName(); } } else if (entity->getType() == ObjectType::UNIT) { auto unit = std::dynamic_pointer_cast(entity); if (unit && !unit->getName().empty()) { data.senderName = unit->getName(); } } } } if (data.senderName.empty()) { owner_.queryPlayerName(data.senderGuid); } } // Filter BG queue announcer spam (server-side module on ChromieCraft/AzerothCore). // Only filter SYSTEM messages to avoid suppressing player chat. if (data.type == ChatType::SYSTEM) { const auto& msg = data.message; if (msg.find("Queue status") != std::string::npos || msg.find("BG Queue") != std::string::npos || msg.find("BGAnnouncer") != std::string::npos) { return; } } // Filter officer chat if player doesn't have officer chat permission. // Some servers send officer chat to all guild members regardless of rank. // WoW guild right bit 0x40 = GR_RIGHT_OFFCHATSPEAK, 0x80 = GR_RIGHT_OFFCHATLISTEN if (data.type == ChatType::OFFICER) { const auto& roster = owner_.getGuildRoster(); uint64_t myGuid = owner_.getPlayerGuid(); uint32_t myRankIdx = 0; for (const auto& m : roster.members) { if (m.guid == myGuid) { myRankIdx = m.rankIndex; break; } } if (myRankIdx < roster.ranks.size()) { uint32_t rights = roster.ranks[myRankIdx].rights; if (!(rights & 0x80)) { // GR_RIGHT_OFFCHATLISTEN = 0x80 return; // Don't show officer chat to non-officers } } } // Filter addon-to-addon whispers (GearScore, DBM, oRA, etc.) from player chat. // These are invisible in the real WoW client. if (data.type == ChatType::WHISPER || data.type == ChatType::WHISPER_INFORM) { const auto& msg = data.message; if (msg.size() >= 3 && ( msg.rfind("GS_", 0) == 0 || // GearScore msg.rfind("DVNE", 0) == 0 || // DBM (DeadlyBossMods) msg.rfind("oRA", 0) == 0 || // oRA raid addon msg.rfind("BWVQ", 0) == 0 || // BigWigs msg.rfind("AVR", 0) == 0 || // AVR (Augmented Virtual Reality) msg.rfind("\t", 0) == 0 || // Tab-prefixed addon messages (msg.size() > 4 && static_cast(msg[0]) > 127))) { // Binary data return; // Silently discard addon whisper } } // Add to chat history chatHistory_.push_back(data); if (chatHistory_.size() > maxChatHistory_) { chatHistory_.erase(chatHistory_.begin()); } // Track whisper sender for /r command if (data.type == ChatType::WHISPER && !data.senderName.empty()) { owner_.lastWhisperSender_ = data.senderName; if (owner_.afkStatus_ && !data.senderName.empty()) { std::string reply = owner_.afkMessage_.empty() ? "Away from Keyboard" : owner_.afkMessage_; sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); } else if (owner_.dndStatus_ && !data.senderName.empty()) { std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_; sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); } } // Trigger chat bubble for SAY/YELL messages from others if (owner_.chatBubbleCallback_ && data.senderGuid != 0) { if (data.type == ChatType::SAY || data.type == ChatType::YELL || data.type == ChatType::MONSTER_SAY || data.type == ChatType::MONSTER_YELL || data.type == ChatType::MONSTER_PARTY) { bool isYell = (data.type == ChatType::YELL || data.type == ChatType::MONSTER_YELL); owner_.chatBubbleCallback_(data.senderGuid, data.message, isYell); } } // Log the message std::string senderInfo; if (!data.senderName.empty()) { senderInfo = data.senderName; } else if (data.senderGuid != 0) { senderInfo = "Unknown-" + std::to_string(data.senderGuid); } else { senderInfo = "System"; } std::string channelInfo; if (!data.channelName.empty()) { channelInfo = "[" + data.channelName + "] "; } LOG_DEBUG("[", getChatTypeString(data.type), "] ", channelInfo, senderInfo, ": ", data.message); // Detect addon messages if (owner_.addonEventCallback_ && data.type != ChatType::SAY && data.type != ChatType::YELL && data.type != ChatType::EMOTE && data.type != ChatType::TEXT_EMOTE && data.type != ChatType::MONSTER_SAY && data.type != ChatType::MONSTER_YELL) { auto tabPos = data.message.find('\t'); if (tabPos != std::string::npos && tabPos > 0 && tabPos <= 16 && tabPos < data.message.size() - 1) { std::string prefix = data.message.substr(0, tabPos); if (prefix.find(' ') == std::string::npos) { std::string body = data.message.substr(tabPos + 1); std::string channel = getChatTypeString(data.type); owner_.addonEventCallback_("CHAT_MSG_ADDON", {prefix, body, channel, data.senderName}); return; } } } // Fire CHAT_MSG_* addon events if (owner_.addonChatCallback_) owner_.addonChatCallback_(data); if (owner_.addonEventCallback_) { std::string eventName = "CHAT_MSG_"; eventName += getChatTypeString(data.type); std::string lang = std::to_string(static_cast(data.language)); char guidBuf[32]; snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)data.senderGuid); owner_.addonEventCallback_(eventName, { data.message, data.senderName, lang, data.channelName, senderInfo, "", "0", "0", "", "0", "0", guidBuf }); } } void ChatHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) { if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return; auto packet = TextEmotePacket::build(textEmoteId, targetGuid); owner_.socket->send(packet); } void ChatHandler::handleTextEmote(network::Packet& packet) { const bool legacyFormat = isClassicLikeExpansion() || isActiveExpansion("tbc"); TextEmoteData data; if (!TextEmoteParser::parse(packet, data, legacyFormat)) { LOG_WARNING("Failed to parse SMSG_TEXT_EMOTE"); return; } if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) { return; } std::string senderName; auto nameIt = owner_.playerNameCache.find(data.senderGuid); if (nameIt != owner_.playerNameCache.end()) { senderName = nameIt->second; } else { auto entity = owner_.entityManager.getEntity(data.senderGuid); if (entity) { auto unit = std::dynamic_pointer_cast(entity); if (unit) senderName = unit->getName(); } } if (senderName.empty()) { senderName = "Unknown"; owner_.queryPlayerName(data.senderGuid); } const std::string* targetPtr = data.targetName.empty() ? nullptr : &data.targetName; std::string emoteText = rendering::Renderer::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr); if (emoteText.empty()) { emoteText = data.targetName.empty() ? senderName + " performs an emote." : senderName + " performs an emote at " + data.targetName + "."; } MessageChatData chatMsg; chatMsg.type = ChatType::TEXT_EMOTE; chatMsg.language = ChatLanguage::COMMON; chatMsg.senderGuid = data.senderGuid; chatMsg.senderName = senderName; chatMsg.message = emoteText; addLocalChatMessage(chatMsg); uint32_t animId = rendering::Renderer::getEmoteAnimByDbcId(data.textEmoteId); if (animId != 0 && owner_.emoteAnimCallback_) { owner_.emoteAnimCallback_(data.senderGuid, animId); } LOG_INFO("TEXT_EMOTE from ", senderName, " (emoteId=", data.textEmoteId, ", anim=", animId, ")"); } void ChatHandler::joinChannel(const std::string& channelName, const std::string& password) { if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return; auto packet = owner_.packetParsers_ ? owner_.packetParsers_->buildJoinChannel(channelName, password) : JoinChannelPacket::build(channelName, password); owner_.socket->send(packet); LOG_INFO("Requesting to join channel: ", channelName); } void ChatHandler::leaveChannel(const std::string& channelName) { if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return; auto packet = owner_.packetParsers_ ? owner_.packetParsers_->buildLeaveChannel(channelName) : LeaveChannelPacket::build(channelName); owner_.socket->send(packet); LOG_INFO("Requesting to leave channel: ", channelName); } std::string ChatHandler::getChannelByIndex(int index) const { if (index < 1 || index > static_cast(joinedChannels_.size())) return ""; return joinedChannels_[index - 1]; } int ChatHandler::getChannelIndex(const std::string& channelName) const { for (int i = 0; i < static_cast(joinedChannels_.size()); ++i) { if (joinedChannels_[i] == channelName) return i + 1; } return 0; } void ChatHandler::handleChannelNotify(network::Packet& packet) { ChannelNotifyData data; if (!ChannelNotifyParser::parse(packet, data)) { LOG_WARNING("Failed to parse SMSG_CHANNEL_NOTIFY"); return; } switch (data.notifyType) { case ChannelNotifyType::YOU_JOINED: { bool found = false; for (const auto& ch : joinedChannels_) { if (ch == data.channelName) { found = true; break; } } if (!found) { joinedChannels_.push_back(data.channelName); } MessageChatData msg; msg.type = ChatType::SYSTEM; msg.message = "Joined channel: " + data.channelName; addLocalChatMessage(msg); LOG_INFO("Joined channel: ", data.channelName); break; } case ChannelNotifyType::YOU_LEFT: { joinedChannels_.erase( std::remove(joinedChannels_.begin(), joinedChannels_.end(), data.channelName), joinedChannels_.end()); MessageChatData msg; msg.type = ChatType::SYSTEM; msg.message = "Left channel: " + data.channelName; addLocalChatMessage(msg); LOG_INFO("Left channel: ", data.channelName); break; } case ChannelNotifyType::PLAYER_ALREADY_MEMBER: { bool found = false; for (const auto& ch : joinedChannels_) { if (ch == data.channelName) { found = true; break; } } if (!found) { joinedChannels_.push_back(data.channelName); LOG_INFO("Already in channel: ", data.channelName); } break; } case ChannelNotifyType::NOT_IN_AREA: addSystemChatMessage("You must be in the area to join '" + data.channelName + "'."); LOG_DEBUG("Cannot join channel ", data.channelName, " (not in area)"); break; case ChannelNotifyType::WRONG_PASSWORD: addSystemChatMessage("Wrong password for channel '" + data.channelName + "'."); break; case ChannelNotifyType::NOT_MEMBER: addSystemChatMessage("You are not in channel '" + data.channelName + "'."); break; case ChannelNotifyType::NOT_MODERATOR: addSystemChatMessage("You are not a moderator of '" + data.channelName + "'."); break; case ChannelNotifyType::MUTED: addSystemChatMessage("You are muted in channel '" + data.channelName + "'."); break; case ChannelNotifyType::BANNED: addSystemChatMessage("You are banned from channel '" + data.channelName + "'."); break; case ChannelNotifyType::THROTTLED: addSystemChatMessage("Channel '" + data.channelName + "' is throttled. Please wait."); break; case ChannelNotifyType::NOT_IN_LFG: addSystemChatMessage("You must be in a LFG queue to join '" + data.channelName + "'."); break; case ChannelNotifyType::PLAYER_KICKED: addSystemChatMessage("A player was kicked from '" + data.channelName + "'."); break; case ChannelNotifyType::PASSWORD_CHANGED: addSystemChatMessage("Password for '" + data.channelName + "' changed."); break; case ChannelNotifyType::OWNER_CHANGED: addSystemChatMessage("Owner of '" + data.channelName + "' changed."); break; case ChannelNotifyType::NOT_OWNER: addSystemChatMessage("You are not the owner of '" + data.channelName + "'."); break; case ChannelNotifyType::INVALID_NAME: addSystemChatMessage("Invalid channel name '" + data.channelName + "'."); break; case ChannelNotifyType::PLAYER_NOT_FOUND: addSystemChatMessage("Player not found."); break; case ChannelNotifyType::ANNOUNCEMENTS_ON: addSystemChatMessage("Channel '" + data.channelName + "': announcements enabled."); break; case ChannelNotifyType::ANNOUNCEMENTS_OFF: addSystemChatMessage("Channel '" + data.channelName + "': announcements disabled."); break; case ChannelNotifyType::MODERATION_ON: addSystemChatMessage("Channel '" + data.channelName + "' is now moderated."); break; case ChannelNotifyType::MODERATION_OFF: addSystemChatMessage("Channel '" + data.channelName + "' is no longer moderated."); break; case ChannelNotifyType::PLAYER_BANNED: addSystemChatMessage("A player was banned from '" + data.channelName + "'."); break; case ChannelNotifyType::PLAYER_UNBANNED: addSystemChatMessage("A player was unbanned from '" + data.channelName + "'."); break; case ChannelNotifyType::PLAYER_NOT_BANNED: addSystemChatMessage("That player is not banned from '" + data.channelName + "'."); break; case ChannelNotifyType::INVITE: addSystemChatMessage("You have been invited to join channel '" + data.channelName + "'."); break; case ChannelNotifyType::INVITE_WRONG_FACTION: case ChannelNotifyType::WRONG_FACTION: addSystemChatMessage("Wrong faction for channel '" + data.channelName + "'."); break; case ChannelNotifyType::NOT_MODERATED: addSystemChatMessage("Channel '" + data.channelName + "' is not moderated."); break; case ChannelNotifyType::PLAYER_INVITED: addSystemChatMessage("Player invited to channel '" + data.channelName + "'."); break; case ChannelNotifyType::PLAYER_INVITE_BANNED: addSystemChatMessage("That player is banned from '" + data.channelName + "'."); break; default: LOG_DEBUG("Channel notify type ", static_cast(data.notifyType), " for channel ", data.channelName); break; } } void ChatHandler::autoJoinDefaultChannels() { LOG_INFO("autoJoinDefaultChannels: general=", chatAutoJoin.general, " trade=", chatAutoJoin.trade, " localDefense=", chatAutoJoin.localDefense, " lfg=", chatAutoJoin.lfg, " local=", chatAutoJoin.local); if (chatAutoJoin.general) joinChannel("General"); if (chatAutoJoin.trade) joinChannel("Trade"); if (chatAutoJoin.localDefense) joinChannel("LocalDefense"); if (chatAutoJoin.lfg) joinChannel("LookingForGroup"); if (chatAutoJoin.local) joinChannel("Local"); } void ChatHandler::addLocalChatMessage(const MessageChatData& msg) { chatHistory_.push_back(msg); if (chatHistory_.size() > maxChatHistory_) { chatHistory_.pop_front(); } if (owner_.addonChatCallback_) owner_.addonChatCallback_(msg); if (owner_.addonEventCallback_) { std::string eventName = "CHAT_MSG_"; eventName += getChatTypeString(msg.type); const Character* ac = owner_.getActiveCharacter(); std::string senderName = msg.senderName.empty() ? (ac ? ac->name : std::string{}) : msg.senderName; char guidBuf[32]; snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)(msg.senderGuid != 0 ? msg.senderGuid : owner_.playerGuid)); owner_.addonEventCallback_(eventName, { msg.message, senderName, std::to_string(static_cast(msg.language)), msg.channelName, senderName, "", "0", "0", "", "0", "0", guidBuf }); } } void ChatHandler::addSystemChatMessage(const std::string& message) { if (message.empty()) return; MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; msg.message = message; addLocalChatMessage(msg); } void ChatHandler::toggleAfk(const std::string& message) { owner_.afkStatus_ = !owner_.afkStatus_; owner_.afkMessage_ = message; if (owner_.afkStatus_) { if (message.empty()) { addSystemChatMessage("You are now AFK."); } else { addSystemChatMessage("You are now AFK: " + message); } // If DND was active, turn it off if (owner_.dndStatus_) { owner_.dndStatus_ = false; owner_.dndMessage_.clear(); } } else { addSystemChatMessage("You are no longer AFK."); owner_.afkMessage_.clear(); } LOG_INFO("AFK status: ", owner_.afkStatus_, ", message: ", message); } void ChatHandler::toggleDnd(const std::string& message) { owner_.dndStatus_ = !owner_.dndStatus_; owner_.dndMessage_ = message; if (owner_.dndStatus_) { if (message.empty()) { addSystemChatMessage("You are now DND (Do Not Disturb)."); } else { addSystemChatMessage("You are now DND: " + message); } // If AFK was active, turn it off if (owner_.afkStatus_) { owner_.afkStatus_ = false; owner_.afkMessage_.clear(); } } else { addSystemChatMessage("You are no longer DND."); owner_.dndMessage_.clear(); } LOG_INFO("DND status: ", owner_.dndStatus_, ", message: ", message); } void ChatHandler::replyToLastWhisper(const std::string& message) { if (!owner_.isInWorld()) { LOG_WARNING("Cannot send whisper: not in world or not connected"); return; } if (owner_.lastWhisperSender_.empty()) { addSystemChatMessage("No one has whispered you yet."); return; } if (message.empty()) { addSystemChatMessage("You must specify a message to send."); return; } // Send whisper using the standard message chat function sendChatMessage(ChatType::WHISPER, message, owner_.lastWhisperSender_); LOG_INFO("Replied to ", owner_.lastWhisperSender_, ": ", message); } // ============================================================ // Moved opcode handlers (from GameHandler::registerOpcodeHandlers) // ============================================================ void ChatHandler::handleChannelList(network::Packet& packet) { std::string chanName = packet.readString(); if (!packet.hasRemaining(5)) return; /*uint8_t chanFlags =*/ packet.readUInt8(); uint32_t memberCount = packet.readUInt32(); memberCount = std::min(memberCount, 200u); addSystemChatMessage(chanName + " has " + std::to_string(memberCount) + " member(s):"); for (uint32_t i = 0; i < memberCount; ++i) { if (!packet.hasRemaining(9)) break; uint64_t memberGuid = packet.readUInt64(); uint8_t memberFlags = packet.readUInt8(); std::string name; auto entity = owner_.entityManager.getEntity(memberGuid); if (entity) { auto player = std::dynamic_pointer_cast(entity); if (player && !player->getName().empty()) name = player->getName(); } if (name.empty()) name = owner_.lookupName(memberGuid); if (name.empty()) name = "(unknown)"; std::string entry = " " + name; if (memberFlags & 0x01) entry += " [Moderator]"; if (memberFlags & 0x02) entry += " [Muted]"; addSystemChatMessage(entry); } } // ============================================================ // Methods moved from GameHandler // ============================================================ void ChatHandler::submitGmTicket(const std::string& text) { if (!owner_.isInWorld()) return; // CMSG_GMTICKET_CREATE (WotLK 3.3.5a): // string ticket_text // float[3] position (server coords) // float facing // uint32 mapId // uint8 need_response (1 = yes) network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_CREATE)); pkt.writeString(text); pkt.writeFloat(owner_.movementInfo.x); pkt.writeFloat(owner_.movementInfo.y); pkt.writeFloat(owner_.movementInfo.z); pkt.writeFloat(owner_.movementInfo.orientation); pkt.writeUInt32(owner_.currentMapId_); pkt.writeUInt8(1); // need_response = yes owner_.socket->send(pkt); LOG_INFO("Submitted GM ticket: '", text, "'"); } void ChatHandler::handleMotd(network::Packet& packet) { LOG_INFO("Handling SMSG_MOTD"); MotdData data; if (!MotdParser::parse(packet, data)) { LOG_WARNING("Failed to parse SMSG_MOTD"); return; } if (!data.isEmpty()) { LOG_INFO("========================================"); LOG_INFO(" MESSAGE OF THE DAY"); LOG_INFO("========================================"); for (const auto& line : data.lines) { LOG_INFO(line); addSystemChatMessage(std::string("MOTD: ") + line); } // Add a visual separator after MOTD block so subsequent messages don't // appear glued to the last MOTD line. MessageChatData spacer; spacer.type = ChatType::SYSTEM; spacer.language = ChatLanguage::UNIVERSAL; spacer.message = ""; addLocalChatMessage(spacer); LOG_INFO("========================================"); } } void ChatHandler::handleNotification(network::Packet& packet) { // SMSG_NOTIFICATION: single null-terminated string std::string message = packet.readString(); if (!message.empty()) { LOG_INFO("Server notification: ", message); addSystemChatMessage(message); } } } // namespace game } // namespace wowee