mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-12 07:43:51 +00:00
PLAYER_VISIBLE_ITEM_1_ENTRYID for WotLK 3.3.5a is at UNIT_END(148) + 260 = field index 408 with stride 2. The previous default of 284 (UNIT_END+136) was in the quest log field range, causing item IDs like "Lesser Invisibility Potion" and "Deathstalker Report" to be read as equipment entries. This was the root cause of other players appearing naked — item queries returned valid responses but for the WRONG items (quest log entries instead of equipment), so displayInfoIds were consumable/quest item appearances. The heuristic auto-detection still overrides for Classic/TBC (different stride per expansion), so this only affects the WotLK default before detection runs. Also filter addon whispers (GearScore GS_*, DBM, oRA, BigWigs, tab-prefixed) from chat display — these are invisible in the real WoW client.
729 lines
29 KiB
C++
729 lines
29 KiB
C++
#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 <algorithm>
|
|
|
|
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<int>(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<Player>(entity);
|
|
if (player && !player->getName().empty()) {
|
|
data.senderName = player->getName();
|
|
}
|
|
} else if (entity->getType() == ObjectType::UNIT) {
|
|
auto unit = std::dynamic_pointer_cast<Unit>(entity);
|
|
if (unit && !unit->getName().empty()) {
|
|
data.senderName = unit->getName();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.senderName.empty()) {
|
|
owner_.queryPlayerName(data.senderGuid);
|
|
}
|
|
}
|
|
|
|
// 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<unsigned char>(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, "<AFK> " + reply, data.senderName);
|
|
} else if (owner_.dndStatus_ && !data.senderName.empty()) {
|
|
std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_;
|
|
sendChatMessage(ChatType::WHISPER, "<DND> " + 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<int>(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<Unit>(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<int>(joinedChannels_.size())) return "";
|
|
return joinedChannels_[index - 1];
|
|
}
|
|
|
|
int ChatHandler::getChannelIndex(const std::string& channelName) const {
|
|
for (int i = 0; i < static_cast<int>(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<int>(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<int>(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<Player>(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
|