Kelsidavis-WoWee/src/game/chat_handler.cpp

760 lines
30 KiB
C++
Raw Normal View History

#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.hasRemaining(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_DEBUG("OUTGOING CHAT: type=", static_cast<int>(type), " msg='", message.substr(0, 60), "'");
// Use the player's faction language. AzerothCore rejects wrong language.
// Alliance races: Human(1), Dwarf(3), NightElf(4), Gnome(7), Draenei(11) → COMMON (7)
// Horde races: Orc(2), Undead(5), Tauren(6), Troll(8), BloodElf(10) → ORCISH (1)
uint8_t race = owner_.getPlayerRace();
bool isHorde = (race == 2 || race == 5 || race == 6 || race == 8 || race == 10);
ChatLanguage language = isHorde ? ChatLanguage::ORCISH : 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;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto nameIt = owner_.getPlayerNameCache().find(owner_.playerGuid);
if (nameIt != owner_.getPlayerNameCache().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, size=", packet.getSize());
return;
}
LOG_DEBUG("SMSG_MESSAGECHAT: type=", static_cast<int>(data.type),
" sender='", data.senderName, "' msg='",
data.message.substr(0, 60), "'");
// 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) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto nameIt = owner_.getPlayerNameCache().find(data.senderGuid);
if (nameIt != owner_.getPlayerNameCache().end()) {
data.senderName = nameIt->second;
} else {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().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 BG queue announcer spam (server-side module on ChromieCraft/AzerothCore).
// Arrives as SAY (type=0) with color codes: |cffff0000[BG Queue Announcer]:|r ...
{
const auto& msg = data.message;
if (msg.find("BG Queue Announcer") != std::string::npos ||
msg.find("Queue status") != 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<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;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto nameIt = owner_.getPlayerNameCache().find(data.senderGuid);
if (nameIt != owner_.getPlayerNameCache().end()) {
senderName = nameIt->second;
} else {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().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: {
if (std::find(joinedChannels_.begin(), joinedChannels_.end(), data.channelName) == joinedChannels_.end()) {
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: {
// Server confirms we're in this channel but our local list doesn't have it yet —
// can happen after reconnect or if the join notification was missed.
if (std::find(joinedChannels_.begin(), joinedChannels_.end(), data.channelName) == joinedChannels_.end()) {
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;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().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