chore(refactor): god-object decomposition and mega-file splits

Split all mega-files by single-responsibility concern and
partially extracting AudioCoordinator and
OverlaySystem from the Renderer facade. No behavioral changes.

Splits:
- game_handler.cpp (5,247 LOC) → core + callbacks + packets (3 files)
- world_packets.cpp (4,453 LOC) → economy/entity/social/world (4 files)
- game_screen.cpp  (5,786 LOC) → core + frames + hud + minimap (4 files)
- m2_renderer.cpp  (3,343 LOC) → core + instance + particles + render (4 files)
- chat_panel.cpp   (3,140 LOC) → core + commands + utils (3 files)
- entity_spawner.cpp (2,750 LOC) → core + player + processing (3 files)

Extractions:
- AudioCoordinator: include/audio/ + src/audio/ (owned by Renderer)
- OverlaySystem: include/rendering/ + src/rendering/overlay_system.*

CMakeLists.txt: registered all 17 new translation units.
Related handler/callback files: minor include fixups post-split.
This commit is contained in:
Paul 2026-04-05 19:30:44 +03:00
parent 6dcc06697b
commit 34c0e3ca28
49 changed files with 29113 additions and 28109 deletions

View file

@ -6,6 +6,7 @@
#include "game/opcode_table.hpp"
#include "network/world_socket.hpp"
#include "rendering/renderer.hpp"
#include "rendering/animation_controller.hpp"
#include "core/logger.hpp"
#include <algorithm>
@ -54,8 +55,8 @@ void ChatHandler::registerOpcodes(DispatchTable& table) {
if (!packet.hasRemaining(12)) return;
uint32_t emoteAnim = packet.readUInt32();
uint64_t sourceGuid = packet.readUInt64();
if (owner_.emoteAnimCallback_ && sourceGuid != 0)
owner_.emoteAnimCallback_(sourceGuid, emoteAnim);
if (owner_.emoteAnimCallbackRef() && sourceGuid != 0)
owner_.emoteAnimCallbackRef()(sourceGuid, emoteAnim);
};
table[Opcode::SMSG_CHANNEL_NOTIFY] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD ||
@ -125,7 +126,7 @@ void ChatHandler::registerOpcodes(DispatchTable& table) {
if (!msg.empty()) {
owner_.addUIError(msg);
addSystemChatMessage(msg);
owner_.areaTriggerMsgs_.push_back(msg);
owner_.areaTriggerMsgsRef().push_back(msg);
}
}
};
@ -155,15 +156,15 @@ void ChatHandler::sendChatMessage(ChatType type, const std::string& message, con
ChatLanguage language = isHorde ? ChatLanguage::ORCISH : ChatLanguage::COMMON;
auto packet = MessageChatPacket::build(type, language, message, target);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
// Add local echo so the player sees their own message immediately
MessageChatData echo;
echo.senderGuid = owner_.playerGuid;
echo.senderGuid = owner_.getPlayerGuid();
echo.language = language;
echo.message = message;
auto nameIt = owner_.getPlayerNameCache().find(owner_.playerGuid);
auto nameIt = owner_.getPlayerNameCache().find(owner_.getPlayerGuid());
if (nameIt != owner_.getPlayerNameCache().end()) {
echo.senderName = nameIt->second;
}
@ -186,7 +187,7 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_MESSAGECHAT");
MessageChatData data;
if (!owner_.packetParsers_->parseMessageChat(packet, data)) {
if (!owner_.getPacketParsers()->parseMessageChat(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT, size=", packet.getSize());
return;
}
@ -195,9 +196,9 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
" '", 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.senderGuid == owner_.getPlayerGuid() && data.senderGuid != 0) {
if (data.type == ChatType::WHISPER && !data.senderName.empty()) {
owner_.lastWhisperSender_ = data.senderName;
owner_.lastWhisperSenderRef() = data.senderName;
}
return;
}
@ -284,29 +285,29 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
// Always store GUID so getLastWhisperSender() can resolve the name
// from the player name cache even if name wasn't available yet
if (data.senderGuid != 0)
owner_.lastWhisperSenderGuid_ = data.senderGuid;
owner_.lastWhisperSenderGuidRef() = data.senderGuid;
if (!data.senderName.empty())
owner_.lastWhisperSender_ = data.senderName;
owner_.lastWhisperSenderRef() = data.senderName;
if (!data.senderName.empty()) {
// Only auto-reply once per sender per AFK/DND session to prevent loops
if (owner_.afkStatus_ && afkAutoRepliedSenders_.insert(data.senderName).second) {
std::string reply = owner_.afkMessage_.empty() ? "Away from Keyboard" : owner_.afkMessage_;
if (owner_.afkStatusRef() && afkAutoRepliedSenders_.insert(data.senderName).second) {
std::string reply = owner_.afkMessageRef().empty() ? "Away from Keyboard" : owner_.afkMessageRef();
sendChatMessage(ChatType::WHISPER, "<AFK> " + reply, data.senderName);
} else if (owner_.dndStatus_ && afkAutoRepliedSenders_.insert(data.senderName).second) {
std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_;
} else if (owner_.dndStatusRef() && afkAutoRepliedSenders_.insert(data.senderName).second) {
std::string reply = owner_.dndMessageRef().empty() ? "Do Not Disturb" : owner_.dndMessageRef();
sendChatMessage(ChatType::WHISPER, "<DND> " + reply, data.senderName);
}
}
}
// Trigger chat bubble for SAY/YELL messages from others
if (owner_.chatBubbleCallback_ && data.senderGuid != 0) {
if (owner_.chatBubbleCallbackRef() && 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);
owner_.chatBubbleCallbackRef()(data.senderGuid, data.message, isYell);
}
}
@ -328,7 +329,7 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
LOG_DEBUG("[", getChatTypeString(data.type), "] ", channelInfo, senderInfo, ": ", data.message);
// Detect addon messages
if (owner_.addonEventCallback_ &&
if (owner_.addonEventCallbackRef() &&
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) {
@ -339,21 +340,21 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
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});
owner_.addonEventCallbackRef()("CHAT_MSG_ADDON", {prefix, body, channel, data.senderName});
return;
}
}
}
// Fire CHAT_MSG_* addon events
if (owner_.addonChatCallback_) owner_.addonChatCallback_(data);
if (owner_.addonEventCallback_) {
if (owner_.addonChatCallbackRef()) owner_.addonChatCallbackRef()(data);
if (owner_.addonEventCallbackRef()) {
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, {
owner_.addonEventCallbackRef()(eventName, {
data.message,
data.senderName,
lang,
@ -371,9 +372,9 @@ void ChatHandler::handleMessageChat(network::Packet& packet) {
}
void ChatHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
auto packet = TextEmotePacket::build(textEmoteId, targetGuid);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
}
void ChatHandler::handleTextEmote(network::Packet& packet) {
@ -384,7 +385,7 @@ void ChatHandler::handleTextEmote(network::Packet& packet) {
return;
}
if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) {
if (data.senderGuid == owner_.getPlayerGuid() && data.senderGuid != 0) {
return;
}
@ -405,7 +406,7 @@ void ChatHandler::handleTextEmote(network::Packet& packet) {
}
const std::string* targetPtr = data.targetName.empty() ? nullptr : &data.targetName;
std::string emoteText = rendering::Renderer::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr);
std::string emoteText = rendering::AnimationController::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr);
if (emoteText.empty()) {
emoteText = data.targetName.empty()
? senderName + " performs an emote."
@ -421,29 +422,29 @@ void ChatHandler::handleTextEmote(network::Packet& packet) {
addLocalChatMessage(chatMsg);
uint32_t animId = rendering::Renderer::getEmoteAnimByDbcId(data.textEmoteId);
if (animId != 0 && owner_.emoteAnimCallback_) {
owner_.emoteAnimCallback_(data.senderGuid, animId);
uint32_t animId = rendering::AnimationController::getEmoteAnimByDbcId(data.textEmoteId);
if (animId != 0 && owner_.emoteAnimCallbackRef()) {
owner_.emoteAnimCallbackRef()(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)
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
auto packet = owner_.getPacketParsers()
? owner_.getPacketParsers()->buildJoinChannel(channelName, password)
: JoinChannelPacket::build(channelName, password);
owner_.socket->send(packet);
owner_.getSocket()->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)
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
auto packet = owner_.getPacketParsers()
? owner_.getPacketParsers()->buildLeaveChannel(channelName)
: LeaveChannelPacket::build(channelName);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
LOG_INFO("Requesting to leave channel: ", channelName);
}
@ -601,9 +602,9 @@ void ChatHandler::addLocalChatMessage(const MessageChatData& msg) {
if (chatHistory_.size() > maxChatHistory_) {
chatHistory_.pop_front();
}
if (owner_.addonChatCallback_) owner_.addonChatCallback_(msg);
if (owner_.addonChatCallbackRef()) owner_.addonChatCallbackRef()(msg);
if (owner_.addonEventCallback_) {
if (owner_.addonEventCallbackRef()) {
std::string eventName = "CHAT_MSG_";
eventName += getChatTypeString(msg.type);
const Character* ac = owner_.getActiveCharacter();
@ -611,8 +612,8 @@ void ChatHandler::addLocalChatMessage(const MessageChatData& msg) {
? (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, {
(unsigned long long)(msg.senderGuid != 0 ? msg.senderGuid : owner_.getPlayerGuid()));
owner_.addonEventCallbackRef()(eventName, {
msg.message, senderName,
std::to_string(static_cast<int>(msg.language)),
msg.channelName, senderName, "", "0", "0", "", "0", "0", guidBuf
@ -630,51 +631,51 @@ void ChatHandler::addSystemChatMessage(const std::string& message) {
}
void ChatHandler::toggleAfk(const std::string& message) {
owner_.afkStatus_ = !owner_.afkStatus_;
owner_.afkMessage_ = message;
owner_.afkStatusRef() = !owner_.afkStatusRef();
owner_.afkMessageRef() = message;
if (owner_.afkStatus_) {
if (owner_.afkStatusRef()) {
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();
if (owner_.dndStatusRef()) {
owner_.dndStatusRef() = false;
owner_.dndMessageRef().clear();
}
} else {
addSystemChatMessage("You are no longer AFK.");
owner_.afkMessage_.clear();
owner_.afkMessageRef().clear();
afkAutoRepliedSenders_.clear();
}
LOG_INFO("AFK status: ", owner_.afkStatus_, ", message: ", message);
LOG_INFO("AFK status: ", owner_.afkStatusRef(), ", message: ", message);
}
void ChatHandler::toggleDnd(const std::string& message) {
owner_.dndStatus_ = !owner_.dndStatus_;
owner_.dndMessage_ = message;
owner_.dndStatusRef() = !owner_.dndStatusRef();
owner_.dndMessageRef() = message;
if (owner_.dndStatus_) {
if (owner_.dndStatusRef()) {
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();
if (owner_.afkStatusRef()) {
owner_.afkStatusRef() = false;
owner_.afkMessageRef().clear();
}
} else {
addSystemChatMessage("You are no longer DND.");
owner_.dndMessage_.clear();
owner_.dndMessageRef().clear();
afkAutoRepliedSenders_.clear();
}
LOG_INFO("DND status: ", owner_.dndStatus_, ", message: ", message);
LOG_INFO("DND status: ", owner_.dndStatusRef(), ", message: ", message);
}
void ChatHandler::replyToLastWhisper(const std::string& message) {
@ -683,7 +684,7 @@ void ChatHandler::replyToLastWhisper(const std::string& message) {
return;
}
if (owner_.lastWhisperSender_.empty()) {
if (owner_.lastWhisperSenderRef().empty()) {
addSystemChatMessage("No one has whispered you yet.");
return;
}
@ -694,8 +695,8 @@ void ChatHandler::replyToLastWhisper(const std::string& message) {
}
// Send whisper using the standard message chat function
sendChatMessage(ChatType::WHISPER, message, owner_.lastWhisperSender_);
LOG_INFO("Replied to ", owner_.lastWhisperSender_, ": ", message);
sendChatMessage(ChatType::WHISPER, message, owner_.lastWhisperSenderRef());
LOG_INFO("Replied to ", owner_.lastWhisperSenderRef(), ": ", message);
}
// ============================================================
@ -743,13 +744,13 @@ void ChatHandler::submitGmTicket(const std::string& text) {
// 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.writeFloat(owner_.movementInfoRef().x);
pkt.writeFloat(owner_.movementInfoRef().y);
pkt.writeFloat(owner_.movementInfoRef().z);
pkt.writeFloat(owner_.movementInfoRef().orientation);
pkt.writeUInt32(owner_.currentMapIdRef());
pkt.writeUInt8(1); // need_response = yes
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
LOG_INFO("Submitted GM ticket: '", text, "'");
}

View file

@ -31,7 +31,7 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
};
table[Opcode::SMSG_THREAT_CLEAR] = [this](network::Packet& /*packet*/) {
threatLists_.clear();
if (owner_.addonEventCallback_) owner_.addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("UNIT_THREAT_LIST_UPDATE", {});
};
table[Opcode::SMSG_THREAT_REMOVE] = [this](network::Packet& packet) {
if (!packet.hasRemaining(1)) return;
@ -67,10 +67,10 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
if (autoAttackRequested_ && autoAttackTarget_ != 0) {
auto targetEntity = owner_.getEntityManager().getEntity(autoAttackTarget_);
if (targetEntity) {
float toTargetX = targetEntity->getX() - owner_.movementInfo.x;
float toTargetY = targetEntity->getY() - owner_.movementInfo.y;
float toTargetX = targetEntity->getX() - owner_.movementInfoRef().x;
float toTargetY = targetEntity->getY() - owner_.movementInfoRef().y;
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
owner_.movementInfo.orientation = std::atan2(-toTargetY, toTargetX);
owner_.movementInfoRef().orientation = std::atan2(-toTargetY, toTargetX);
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
}
}
@ -96,10 +96,10 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
if (!packet.hasRemaining(12)) return;
uint64_t guid = packet.readUInt64();
uint32_t reaction = packet.readUInt32();
if (reaction == 2 && owner_.npcAggroCallback_) {
if (reaction == 2 && owner_.npcAggroCallbackRef()) {
auto entity = owner_.getEntityManager().getEntity(guid);
if (entity)
owner_.npcAggroCallback_(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
owner_.npcAggroCallbackRef()(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
}
};
table[Opcode::SMSG_SPELLNONMELEEDAMAGELOG] = [this](network::Packet& packet) { handleSpellDamageLog(packet); };
@ -115,7 +115,7 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
uint32_t dmg = packet.readUInt32();
uint32_t envAbs = packet.readUInt32();
uint32_t envRes = packet.readUInt32();
if (victimGuid == owner_.playerGuid) {
if (victimGuid == owner_.getPlayerGuid()) {
// Environmental damage: pass envType via powerType field for display differentiation
if (dmg > 0)
addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast<int32_t>(dmg), 0, false, envType, 0, victimGuid);
@ -124,8 +124,8 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
if (envRes > 0)
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(envRes), 0, false, 0, 0, victimGuid);
// Drowning damage → play DROWN one-shot on player
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallback_)
owner_.emoteAnimCallback_(victimGuid, 131); // anim::DROWN
if (envType == 1 && dmg > 0 && owner_.emoteAnimCallbackRef())
owner_.emoteAnimCallbackRef()(victimGuid, 131); // anim::DROWN
}
packet.skipAll();
};
@ -158,8 +158,8 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
std::sort(list.begin(), list.end(),
[](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; });
threatLists_[unitGuid] = std::move(list);
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {});
if (owner_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("UNIT_THREAT_LIST_UPDATE", {});
};
}
@ -198,7 +198,7 @@ void CombatHandler::registerOpcodes(DispatchTable& table) {
void CombatHandler::startAutoAttack(uint64_t targetGuid) {
// Can't attack yourself
if (targetGuid == owner_.playerGuid) return;
if (targetGuid == owner_.getPlayerGuid()) return;
if (targetGuid == 0) return;
// Dismount when entering combat
@ -209,9 +209,9 @@ void CombatHandler::startAutoAttack(uint64_t targetGuid) {
// Client-side melee range gate to avoid starting "swing forever" loops when
// target is already clearly out of range.
if (auto target = owner_.getEntityManager().getEntity(targetGuid)) {
float dx = owner_.movementInfo.x - target->getLatestX();
float dy = owner_.movementInfo.y - target->getLatestY();
float dz = owner_.movementInfo.z - target->getLatestZ();
float dx = owner_.movementInfoRef().x - target->getLatestX();
float dy = owner_.movementInfoRef().y - target->getLatestY();
float dz = owner_.movementInfoRef().z - target->getLatestZ();
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
if (dist3d > 8.0f) {
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
@ -232,9 +232,9 @@ void CombatHandler::startAutoAttack(uint64_t targetGuid) {
autoAttackOutOfRangeTime_ = 0.0f;
autoAttackResendTimer_ = 0.0f;
autoAttackFacingSyncTimer_ = 0.0f;
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
if (owner_.getState() == WorldState::IN_WORLD && owner_.getSocket()) {
auto packet = AttackSwingPacket::build(targetGuid);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
}
LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec);
}
@ -249,13 +249,13 @@ void CombatHandler::stopAutoAttack() {
autoAttackOutOfRangeTime_ = 0.0f;
autoAttackResendTimer_ = 0.0f;
autoAttackFacingSyncTimer_ = 0.0f;
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
if (owner_.getState() == WorldState::IN_WORLD && owner_.getSocket()) {
auto packet = AttackStopPacket::build();
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
}
LOG_INFO("Stopping auto-attack");
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PLAYER_LEAVE_COMBAT", {});
if (owner_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("PLAYER_LEAVE_COMBAT", {});
}
// ============================================================
@ -292,9 +292,9 @@ void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, ui
// preserve "unknown/no source" (e.g. environmental damage) instead of
// backfilling from current target.
uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid
: ((dstGuid != 0) ? 0 : (isPlayerSource ? owner_.playerGuid : owner_.targetGuid));
: ((dstGuid != 0) ? 0 : (isPlayerSource ? owner_.getPlayerGuid() : owner_.getTargetGuid()));
uint64_t effectiveDst = (dstGuid != 0) ? dstGuid
: (isPlayerSource ? owner_.targetGuid : owner_.playerGuid);
: (isPlayerSource ? owner_.getTargetGuid() : owner_.getPlayerGuid());
log.sourceName = owner_.lookupName(effectiveSrc);
log.targetName = (effectiveDst != 0) ? owner_.lookupName(effectiveDst) : std::string{};
if (combatLog_.size() >= MAX_COMBAT_LOG)
@ -303,7 +303,7 @@ void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, ui
// Fire COMBAT_LOG_EVENT_UNFILTERED for Lua addons
// Args: subevent, sourceGUID, sourceName, 0 (sourceFlags), destGUID, destName, 0 (destFlags), spellId, spellName, amount
if (owner_.addonEventCallback_) {
if (owner_.addonEventCallbackRef()) {
static const char* kSubevents[] = {
"SWING_DAMAGE", "SPELL_DAMAGE", "SPELL_HEAL", "SWING_MISSED", "SWING_MISSED",
"SWING_MISSED", "SWING_MISSED", "SWING_MISSED", "SPELL_DAMAGE", "SPELL_HEAL",
@ -320,7 +320,7 @@ void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, ui
snprintf(dstBuf, sizeof(dstBuf), "0x%016llX", (unsigned long long)effectiveDst);
std::string spellName = (spellId != 0) ? owner_.getSpellName(spellId) : std::string{};
std::string timestamp = std::to_string(static_cast<double>(std::time(nullptr)));
owner_.addonEventCallback_("COMBAT_LOG_EVENT_UNFILTERED", {
owner_.addonEventCallbackRef()("COMBAT_LOG_EVENT_UNFILTERED", {
timestamp, subevent,
srcBuf, log.sourceName, "0",
dstBuf, log.targetName, "0",
@ -370,8 +370,8 @@ void CombatHandler::updateCombatText(float deltaTime) {
// ============================================================
void CombatHandler::autoTargetAttacker(uint64_t attackerGuid) {
if (attackerGuid == 0 || attackerGuid == owner_.playerGuid) return;
if (owner_.targetGuid != 0) return;
if (attackerGuid == 0 || attackerGuid == owner_.getPlayerGuid()) return;
if (owner_.getTargetGuid() != 0) return;
if (!owner_.getEntityManager().hasEntity(attackerGuid)) return;
owner_.setTarget(attackerGuid);
}
@ -380,23 +380,23 @@ void CombatHandler::handleAttackStart(network::Packet& packet) {
AttackStartData data;
if (!AttackStartParser::parse(packet, data)) return;
if (data.attackerGuid == owner_.playerGuid) {
if (data.attackerGuid == owner_.getPlayerGuid()) {
autoAttackRequested_ = true;
autoAttacking_ = true;
autoAttackRetryPending_ = false;
autoAttackTarget_ = data.victimGuid;
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PLAYER_ENTER_COMBAT", {});
} else if (data.victimGuid == owner_.playerGuid && data.attackerGuid != 0) {
if (owner_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("PLAYER_ENTER_COMBAT", {});
} else if (data.victimGuid == owner_.getPlayerGuid() && data.attackerGuid != 0) {
hostileAttackers_.insert(data.attackerGuid);
autoTargetAttacker(data.attackerGuid);
// Play aggro sound when NPC attacks player
if (owner_.npcAggroCallback_) {
if (owner_.npcAggroCallbackRef()) {
auto entity = owner_.getEntityManager().getEntity(data.attackerGuid);
if (entity && entity->getType() == ObjectType::UNIT) {
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
owner_.npcAggroCallback_(data.attackerGuid, pos);
owner_.npcAggroCallbackRef()(data.attackerGuid, pos);
}
}
}
@ -421,32 +421,32 @@ void CombatHandler::handleAttackStop(network::Packet& packet) {
if (!AttackStopParser::parse(packet, data)) return;
// Keep intent, but clear server-confirmed active state until ATTACKSTART resumes.
if (data.attackerGuid == owner_.playerGuid) {
if (data.attackerGuid == owner_.getPlayerGuid()) {
autoAttacking_ = false;
autoAttackRetryPending_ = autoAttackRequested_;
autoAttackResendTimer_ = 0.0f;
LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)");
} else if (data.victimGuid == owner_.playerGuid) {
} else if (data.victimGuid == owner_.getPlayerGuid()) {
hostileAttackers_.erase(data.attackerGuid);
}
}
void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
AttackerStateUpdateData data;
if (!owner_.packetParsers_->parseAttackerStateUpdate(packet, data)) return;
if (!owner_.getPacketParsers()->parseAttackerStateUpdate(packet, data)) return;
bool isPlayerAttacker = (data.attackerGuid == owner_.playerGuid);
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
bool isPlayerAttacker = (data.attackerGuid == owner_.getPlayerGuid());
bool isPlayerTarget = (data.targetGuid == owner_.getPlayerGuid());
if (!isPlayerAttacker && !isPlayerTarget) return; // Not our combat
if (isPlayerAttacker) {
lastMeleeSwingMs_ = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(0);
if (owner_.meleeSwingCallbackRef()) owner_.meleeSwingCallbackRef()(0);
}
if (!isPlayerAttacker && owner_.npcSwingCallback_) {
owner_.npcSwingCallback_(data.attackerGuid);
if (!isPlayerAttacker && owner_.npcSwingCallbackRef()) {
owner_.npcSwingCallbackRef()(data.attackerGuid);
}
if (isPlayerTarget && data.attackerGuid != 0) {
@ -524,24 +524,24 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
}
// Fire hit reaction animation on the victim
if (owner_.hitReactionCallback_ && !data.isMiss()) {
if (owner_.hitReactionCallbackRef() && !data.isMiss()) {
using HR = GameHandler::HitReaction;
HR reaction = HR::WOUND;
if (data.victimState == 1) reaction = HR::DODGE;
else if (data.victimState == 2) reaction = HR::PARRY;
else if (data.victimState == 4) reaction = HR::BLOCK;
else if (data.isCrit()) reaction = HR::CRIT_WOUND;
owner_.hitReactionCallback_(data.targetGuid, reaction);
owner_.hitReactionCallbackRef()(data.targetGuid, reaction);
}
}
void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
SpellDamageLogData data;
if (!owner_.packetParsers_->parseSpellDamageLog(packet, data)) return;
if (!owner_.getPacketParsers()->parseSpellDamageLog(packet, data)) return;
bool isPlayerSource = (data.attackerGuid == owner_.playerGuid);
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
bool isPlayerSource = (data.attackerGuid == owner_.getPlayerGuid());
bool isPlayerTarget = (data.targetGuid == owner_.getPlayerGuid());
if (!isPlayerSource && !isPlayerTarget) return; // Not our combat
if (isPlayerTarget && data.attackerGuid != 0) {
@ -560,10 +560,10 @@ void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
void CombatHandler::handleSpellHealLog(network::Packet& packet) {
SpellHealLogData data;
if (!owner_.packetParsers_->parseSpellHealLog(packet, data)) return;
if (!owner_.getPacketParsers()->parseSpellHealLog(packet, data)) return;
bool isPlayerSource = (data.casterGuid == owner_.playerGuid);
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
bool isPlayerSource = (data.casterGuid == owner_.getPlayerGuid());
bool isPlayerTarget = (data.targetGuid == owner_.getPlayerGuid());
if (!isPlayerSource && !isPlayerTarget) return; // Not our combat
auto type = data.isCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::HEAL;
@ -608,9 +608,9 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
const float targetX = targetEntity->getLatestX();
const float targetY = targetEntity->getLatestY();
const float targetZ = targetEntity->getLatestZ();
float dx = owner_.movementInfo.x - targetX;
float dy = owner_.movementInfo.y - targetY;
float dz = owner_.movementInfo.z - targetZ;
float dx = owner_.movementInfoRef().x - targetX;
float dy = owner_.movementInfoRef().y - targetY;
float dz = owner_.movementInfoRef().z - targetZ;
float dist = std::sqrt(dx * dx + dy * dy);
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
const bool classicLike = isPreWotlk();
@ -652,7 +652,7 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
autoAttackResendTimer_ = 0.0f;
autoAttackRetryPending_ = false;
auto pkt = AttackSwingPacket::build(autoAttackTarget_);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
}
// Keep server-facing aligned while trying to acquire melee.
@ -661,16 +661,16 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
if (allowPeriodicFacingSync &&
autoAttackFacingSyncTimer_ >= facingSyncInterval) {
autoAttackFacingSyncTimer_ = 0.0f;
float toTargetX = targetX - owner_.movementInfo.x;
float toTargetY = targetY - owner_.movementInfo.y;
float toTargetX = targetX - owner_.movementInfoRef().x;
float toTargetY = targetY - owner_.movementInfoRef().y;
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
float desired = std::atan2(-toTargetY, toTargetX);
float diff = desired - owner_.movementInfo.orientation;
float diff = desired - owner_.movementInfoRef().orientation;
while (diff > static_cast<float>(M_PI)) diff -= 2.0f * static_cast<float>(M_PI);
while (diff < -static_cast<float>(M_PI)) diff += 2.0f * static_cast<float>(M_PI);
const float facingThreshold = classicLike ? 0.035f : 0.12f;
if (std::abs(diff) > facingThreshold) {
owner_.movementInfo.orientation = desired;
owner_.movementInfoRef().orientation = desired;
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
}
}
@ -685,8 +685,8 @@ void CombatHandler::updateAutoAttack(float deltaTime) {
for (uint64_t attackerGuid : hostileAttackers_) {
auto attacker = owner_.getEntityManager().getEntity(attackerGuid);
if (!attacker) continue;
float dx = owner_.movementInfo.x - attacker->getX();
float dy = owner_.movementInfo.y - attacker->getY();
float dx = owner_.movementInfoRef().x - attacker->getX();
float dy = owner_.movementInfoRef().y - attacker->getY();
if (std::abs(dx) < 0.01f && std::abs(dy) < 0.01f) continue;
attacker->setOrientation(std::atan2(-dy, dx));
}
@ -758,7 +758,7 @@ void CombatHandler::handlePowerUpdate(network::Packet& packet) {
auto unitId = owner_.guidToUnitId(guid);
if (!unitId.empty()) {
owner_.fireAddonEvent("UNIT_POWER", {unitId});
if (guid == owner_.playerGuid) {
if (guid == owner_.getPlayerGuid()) {
owner_.fireAddonEvent("ACTIONBAR_UPDATE_USABLE", {});
owner_.fireAddonEvent("SPELL_UPDATE_USABLE", {});
}
@ -771,10 +771,10 @@ void CombatHandler::handleUpdateComboPoints(network::Packet& packet) {
if (!packet.hasRemaining(cpTbc ? 8u : 2u) ) return;
uint64_t target = cpTbc ? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(1)) return;
owner_.comboPoints_ = packet.readUInt8();
owner_.comboTarget_ = target;
owner_.comboPointsRef() = packet.readUInt8();
owner_.comboTargetRef() = target;
LOG_DEBUG("SMSG_UPDATE_COMBO_POINTS: target=0x", std::hex, target,
std::dec, " points=", static_cast<int>(owner_.comboPoints_));
std::dec, " points=", static_cast<int>(owner_.comboPointsRef()));
owner_.fireAddonEvent("PLAYER_COMBO_POINTS", {});
}
@ -787,7 +787,7 @@ void CombatHandler::handlePvpCredit(network::Packet& packet) {
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
owner_.addSystemChatMessage(msg);
if (honor > 0) addCombatText(CombatTextEntry::HONOR_GAIN, static_cast<int32_t>(honor), 0, true);
if (owner_.pvpHonorCallback_) owner_.pvpHonorCallback_(honor, victimGuid, rank);
if (owner_.pvpHonorCallbackRef()) owner_.pvpHonorCallbackRef()(honor, victimGuid, rank);
owner_.fireAddonEvent("CHAT_MSG_COMBAT_HONOR_GAIN", {msg});
}
}
@ -805,8 +805,8 @@ void CombatHandler::handleProcResist(network::Packet& packet) {
uint64_t victim = readPrGuid();
if (!packet.hasRemaining(4)) return;
uint32_t spellId = packet.readUInt32();
if (victim == owner_.playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim);
else if (caster == owner_.playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim);
if (victim == owner_.getPlayerGuid()) addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim);
else if (caster == owner_.getPlayerGuid()) addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim);
packet.skipAll();
}
@ -846,10 +846,10 @@ void CombatHandler::handleSpellDamageShield(network::Packet& packet) {
/*uint32_t absorbed =*/ packet.readUInt32();
/*uint32_t school =*/ packet.readUInt32();
// Show combat text: damage shield reflect
if (casterGuid == owner_.playerGuid) {
if (casterGuid == owner_.getPlayerGuid()) {
// We have a damage shield that reflected damage
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(damage), shieldSpellId, true, 0, casterGuid, victimGuid);
} else if (victimGuid == owner_.playerGuid) {
} else if (victimGuid == owner_.getPlayerGuid()) {
// A damage shield hit us (e.g. target's Thorns)
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(damage), shieldSpellId, false, 0, casterGuid, victimGuid);
}
@ -878,9 +878,9 @@ void CombatHandler::handleSpellOrDamageImmune(network::Packet& packet) {
/*uint8_t saveType =*/ packet.readUInt8();
// Show IMMUNE text when the player is the caster (we hit an immune target)
// or the victim (we are immune)
if (casterGuid == owner_.playerGuid || victimGuid == owner_.playerGuid) {
if (casterGuid == owner_.getPlayerGuid() || victimGuid == owner_.getPlayerGuid()) {
addCombatText(CombatTextEntry::IMMUNE, 0, immuneSpellId,
casterGuid == owner_.playerGuid, 0, casterGuid, victimGuid);
casterGuid == owner_.getPlayerGuid(), 0, casterGuid, victimGuid);
}
}
@ -916,9 +916,9 @@ void CombatHandler::handleResistLog(network::Packet& packet) {
/*uint32_t targetRes =*/ packet.readUInt32();
int32_t resistedAmount = static_cast<int32_t>(packet.readUInt32());
// Show RESIST when the player is involved on either side.
if (resistedAmount > 0 && victimGuid == owner_.playerGuid) {
if (resistedAmount > 0 && victimGuid == owner_.getPlayerGuid()) {
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid);
} else if (resistedAmount > 0 && attackerGuid == owner_.playerGuid) {
} else if (resistedAmount > 0 && attackerGuid == owner_.getPlayerGuid()) {
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid);
}
packet.skipAll();
@ -985,10 +985,10 @@ void CombatHandler::handlePetCastFailed(network::Packet& packet) {
void CombatHandler::handlePetBroken(network::Packet& packet) {
// Pet bond broken (died or forcibly dismissed) — clear pet state
owner_.petGuid_ = 0;
owner_.petSpellList_.clear();
owner_.petAutocastSpells_.clear();
memset(owner_.petActionSlots_, 0, sizeof(owner_.petActionSlots_));
owner_.petGuidRef() = 0;
owner_.petSpellListRef().clear();
owner_.petAutocastSpellsRef().clear();
memset(owner_.petActionSlotsRef(), 0, sizeof(owner_.petActionSlotsRef()));
owner_.addSystemChatMessage("Your pet has died.");
LOG_INFO("SMSG_PET_BROKEN: pet bond broken");
packet.skipAll();
@ -997,7 +997,7 @@ void CombatHandler::handlePetBroken(network::Packet& packet) {
void CombatHandler::handlePetLearnedSpell(network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t spellId = packet.readUInt32();
owner_.petSpellList_.push_back(spellId);
owner_.petSpellListRef().push_back(spellId);
const std::string& sname = owner_.getSpellName(spellId);
owner_.addSystemChatMessage("Your pet has learned " + (sname.empty() ? "a new ability." : sname + "."));
LOG_DEBUG("SMSG_PET_LEARNED_SPELL: spellId=", spellId);
@ -1009,10 +1009,10 @@ void CombatHandler::handlePetLearnedSpell(network::Packet& packet) {
void CombatHandler::handlePetUnlearnedSpell(network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t spellId = packet.readUInt32();
owner_.petSpellList_.erase(
std::remove(owner_.petSpellList_.begin(), owner_.petSpellList_.end(), spellId),
owner_.petSpellList_.end());
owner_.petAutocastSpells_.erase(spellId);
owner_.petSpellListRef().erase(
std::remove(owner_.petSpellListRef().begin(), owner_.petSpellListRef().end(), spellId),
owner_.petSpellListRef().end());
owner_.petAutocastSpellsRef().erase(spellId);
LOG_DEBUG("SMSG_PET_UNLEARNED_SPELL: spellId=", spellId);
}
packet.skipAll();
@ -1024,11 +1024,11 @@ void CombatHandler::handlePetMode(network::Packet& packet) {
if (packet.hasRemaining(12)) {
uint64_t modeGuid = packet.readUInt64();
uint32_t mode = packet.readUInt32();
if (modeGuid == owner_.petGuid_) {
owner_.petCommand_ = static_cast<uint8_t>(mode & 0xFF);
owner_.petReact_ = static_cast<uint8_t>((mode >> 8) & 0xFF);
LOG_DEBUG("SMSG_PET_MODE: command=", static_cast<int>(owner_.petCommand_),
" react=", static_cast<int>(owner_.petReact_));
if (modeGuid == owner_.petGuidRef()) {
owner_.petCommandRef() = static_cast<uint8_t>(mode & 0xFF);
owner_.petReactRef() = static_cast<uint8_t>((mode >> 8) & 0xFF);
LOG_DEBUG("SMSG_PET_MODE: command=", static_cast<int>(owner_.petCommandRef()),
" react=", static_cast<int>(owner_.petReactRef()));
}
}
packet.skipAll();
@ -1054,18 +1054,18 @@ void CombatHandler::handleResurrectFailed(network::Packet& packet) {
// ============================================================
void CombatHandler::setTarget(uint64_t guid) {
if (guid == owner_.targetGuid) return;
if (guid == owner_.getTargetGuid()) return;
// Save previous target
if (owner_.targetGuid != 0) {
owner_.lastTargetGuid = owner_.targetGuid;
if (owner_.getTargetGuid() != 0) {
owner_.lastTargetGuidRef() = owner_.getTargetGuid();
}
owner_.targetGuid = guid;
owner_.setTargetGuidRaw(guid);
// Clear stale aura data from the previous target so the buff bar shows
// an empty state until the server sends SMSG_AURA_UPDATE_ALL for the new target.
if (owner_.spellHandler_) for (auto& slot : owner_.spellHandler_->targetAuras_) slot = AuraSlot{};
if (owner_.getSpellHandler()) for (auto& slot : owner_.getSpellHandler()->targetAuras_) slot = AuraSlot{};
// Clear previous target's cast bar on target change
// (the new target's cast state is naturally fetched from spellHandler_->unitCastStates_ by GUID)
@ -1073,7 +1073,7 @@ void CombatHandler::setTarget(uint64_t guid) {
// Inform server of target selection
if (owner_.isInWorld()) {
auto packet = SetSelectionPacket::build(guid);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
}
if (guid != 0) {
@ -1083,27 +1083,27 @@ void CombatHandler::setTarget(uint64_t guid) {
}
void CombatHandler::clearTarget() {
if (owner_.targetGuid != 0) {
if (owner_.getTargetGuid() != 0) {
LOG_INFO("Target cleared");
// Zero the GUID before firing the event so callbacks/addons that query
// the current target see null (consistent with setTarget which updates
// targetGuid before the event).
owner_.targetGuid = 0;
owner_.setTargetGuidRaw(0);
owner_.fireAddonEvent("PLAYER_TARGET_CHANGED", {});
} else {
owner_.targetGuid = 0;
owner_.setTargetGuidRaw(0);
}
owner_.tabCycleIndex = -1;
owner_.tabCycleStale = true;
owner_.tabCycleIndexRef() = -1;
owner_.tabCycleStaleRef() = true;
}
std::shared_ptr<Entity> CombatHandler::getTarget() const {
if (owner_.targetGuid == 0) return nullptr;
return owner_.getEntityManager().getEntity(owner_.targetGuid);
if (owner_.getTargetGuid() == 0) return nullptr;
return owner_.getEntityManager().getEntity(owner_.getTargetGuid());
}
void CombatHandler::setFocus(uint64_t guid) {
owner_.focusGuid = guid;
owner_.focusGuidRef() = guid;
owner_.fireAddonEvent("PLAYER_FOCUS_CHANGED", {});
if (guid != 0) {
auto entity = owner_.getEntityManager().getEntity(guid);
@ -1122,36 +1122,36 @@ void CombatHandler::setFocus(uint64_t guid) {
}
void CombatHandler::clearFocus() {
if (owner_.focusGuid != 0) {
if (owner_.focusGuidRef() != 0) {
owner_.addSystemChatMessage("Focus cleared.");
LOG_INFO("Focus cleared");
}
owner_.focusGuid = 0;
owner_.focusGuidRef() = 0;
owner_.fireAddonEvent("PLAYER_FOCUS_CHANGED", {});
}
std::shared_ptr<Entity> CombatHandler::getFocus() const {
if (owner_.focusGuid == 0) return nullptr;
return owner_.getEntityManager().getEntity(owner_.focusGuid);
if (owner_.focusGuidRef() == 0) return nullptr;
return owner_.getEntityManager().getEntity(owner_.focusGuidRef());
}
void CombatHandler::setMouseoverGuid(uint64_t guid) {
if (owner_.mouseoverGuid_ != guid) {
owner_.mouseoverGuid_ = guid;
if (owner_.mouseoverGuidRef() != guid) {
owner_.mouseoverGuidRef() = guid;
owner_.fireAddonEvent("UPDATE_MOUSEOVER_UNIT", {});
}
}
void CombatHandler::targetLastTarget() {
if (owner_.lastTargetGuid == 0) {
if (owner_.lastTargetGuidRef() == 0) {
owner_.addSystemChatMessage("No previous target.");
return;
}
// Swap current and last target
uint64_t temp = owner_.targetGuid;
setTarget(owner_.lastTargetGuid);
owner_.lastTargetGuid = temp;
uint64_t temp = owner_.getTargetGuid();
setTarget(owner_.lastTargetGuidRef());
owner_.lastTargetGuidRef() = temp;
}
void CombatHandler::targetEnemy(bool reverse) {
@ -1162,7 +1162,7 @@ void CombatHandler::targetEnemy(bool reverse) {
for (const auto& [guid, entity] : entities) {
if (entity->getType() == ObjectType::UNIT) {
auto unit = std::dynamic_pointer_cast<Unit>(entity);
if (unit && guid != owner_.playerGuid && unit->isHostile()) {
if (unit && guid != owner_.getPlayerGuid() && unit->isHostile()) {
hostiles.push_back(guid);
}
}
@ -1174,7 +1174,7 @@ void CombatHandler::targetEnemy(bool reverse) {
}
// Find current target in list
auto it = std::find(hostiles.begin(), hostiles.end(), owner_.targetGuid);
auto it = std::find(hostiles.begin(), hostiles.end(), owner_.getTargetGuid());
if (it == hostiles.end()) {
// Not currently targeting a hostile, target first one
@ -1204,7 +1204,7 @@ void CombatHandler::targetFriend(bool reverse) {
auto& entities = owner_.getEntityManager().getEntities();
for (const auto& [guid, entity] : entities) {
if (entity->getType() == ObjectType::PLAYER && guid != owner_.playerGuid) {
if (entity->getType() == ObjectType::PLAYER && guid != owner_.getPlayerGuid()) {
friendlies.push_back(guid);
}
}
@ -1215,7 +1215,7 @@ void CombatHandler::targetFriend(bool reverse) {
}
// Find current target in list
auto it = std::find(friendlies.begin(), friendlies.end(), owner_.targetGuid);
auto it = std::find(friendlies.begin(), friendlies.end(), owner_.getTargetGuid());
if (it == friendlies.end()) {
// Not currently targeting a friend, target first one
@ -1247,8 +1247,8 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
auto* unit = dynamic_cast<Unit*>(e.get());
if (!unit) return false;
if (unit->getHealth() == 0) {
auto lootIt = owner_.localLootState_.find(guid);
if (lootIt == owner_.localLootState_.end() || lootIt->second.data.items.empty()) {
auto lootIt = owner_.localLootStateRef().find(guid);
if (lootIt == owner_.localLootStateRef().end() || lootIt->second.data.items.empty()) {
return false;
}
return true;
@ -1260,9 +1260,9 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
};
// Rebuild cycle list if stale (entity added/removed since last tab press).
if (owner_.tabCycleStale) {
owner_.tabCycleList.clear();
owner_.tabCycleIndex = -1;
if (owner_.tabCycleStaleRef()) {
owner_.tabCycleListRef().clear();
owner_.tabCycleIndexRef() = -1;
struct EntityDist { uint64_t guid; float distance; };
std::vector<EntityDist> sortable;
@ -1270,7 +1270,7 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
auto t = entity->getType();
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
if (guid == owner_.playerGuid) continue;
if (guid == owner_.getPlayerGuid()) continue;
if (!isValidTabTarget(entity)) continue;
float dx = entity->getX() - playerX;
float dy = entity->getY() - playerY;
@ -1282,22 +1282,22 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
[](const EntityDist& a, const EntityDist& b) { return a.distance < b.distance; });
for (const auto& ed : sortable) {
owner_.tabCycleList.push_back(ed.guid);
owner_.tabCycleListRef().push_back(ed.guid);
}
owner_.tabCycleStale = false;
owner_.tabCycleStaleRef() = false;
}
if (owner_.tabCycleList.empty()) {
if (owner_.tabCycleListRef().empty()) {
clearTarget();
return;
}
// Advance through the cycle, skipping any entry that has since died or
// turned friendly (e.g. NPC killed between two tab presses).
int tries = static_cast<int>(owner_.tabCycleList.size());
int tries = static_cast<int>(owner_.tabCycleListRef().size());
while (tries-- > 0) {
owner_.tabCycleIndex = (owner_.tabCycleIndex + 1) % static_cast<int>(owner_.tabCycleList.size());
uint64_t guid = owner_.tabCycleList[owner_.tabCycleIndex];
owner_.tabCycleIndexRef() = (owner_.tabCycleIndexRef() + 1) % static_cast<int>(owner_.tabCycleListRef().size());
uint64_t guid = owner_.tabCycleListRef()[owner_.tabCycleIndexRef()];
auto entity = owner_.getEntityManager().getEntity(guid);
if (isValidTabTarget(entity)) {
setTarget(guid);
@ -1306,17 +1306,17 @@ void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
}
// All cached entries are stale — clear target and force a fresh rebuild next time.
owner_.tabCycleStale = true;
owner_.tabCycleStaleRef() = true;
clearTarget();
}
void CombatHandler::assistTarget() {
if (owner_.state != WorldState::IN_WORLD) {
if (owner_.getState() != WorldState::IN_WORLD) {
LOG_WARNING("Cannot assist: not in world");
return;
}
if (owner_.targetGuid == 0) {
if (owner_.getTargetGuid() == 0) {
owner_.addSystemChatMessage("You must target someone to assist.");
return;
}
@ -1373,8 +1373,8 @@ void CombatHandler::togglePvp() {
}
auto packet = TogglePvpPacket::build();
owner_.socket->send(packet);
auto entity = owner_.getEntityManager().getEntity(owner_.playerGuid);
owner_.getSocket()->send(packet);
auto entity = owner_.getEntityManager().getEntity(owner_.getPlayerGuid());
bool currentlyPvp = false;
if (entity) {
// UNIT_FIELD_FLAGS (index 59), bit 0x1000 = UNIT_FLAG_PVP
@ -1393,93 +1393,93 @@ void CombatHandler::togglePvp() {
// ============================================================
void CombatHandler::releaseSpirit() {
if (owner_.socket && owner_.state == WorldState::IN_WORLD) {
if (owner_.getSocket() && owner_.getState() == WorldState::IN_WORLD) {
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
if (owner_.repopPending_ && now - static_cast<int64_t>(owner_.lastRepopRequestMs_) < 1000) {
if (owner_.repopPendingRef() && now - static_cast<int64_t>(owner_.lastRepopRequestMsRef()) < 1000) {
return;
}
auto packet = RepopRequestPacket::build();
owner_.socket->send(packet);
owner_.selfResAvailable_ = false;
owner_.repopPending_ = true;
owner_.lastRepopRequestMs_ = static_cast<uint64_t>(now);
owner_.getSocket()->send(packet);
owner_.selfResAvailableRef() = false;
owner_.repopPendingRef() = true;
owner_.lastRepopRequestMsRef() = static_cast<uint64_t>(now);
LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)");
network::Packet cq(wireOpcode(Opcode::MSG_CORPSE_QUERY));
owner_.socket->send(cq);
owner_.getSocket()->send(cq);
}
}
bool CombatHandler::canReclaimCorpse() const {
if (!owner_.releasedSpirit_ || owner_.corpseGuid_ == 0 || owner_.corpseMapId_ == 0) return false;
if (owner_.currentMapId_ != owner_.corpseMapId_) return false;
float dx = owner_.movementInfo.x - owner_.corpseY_;
float dy = owner_.movementInfo.y - owner_.corpseX_;
float dz = owner_.movementInfo.z - owner_.corpseZ_;
if (!owner_.releasedSpiritRef() || owner_.corpseGuidRef() == 0 || owner_.corpseMapIdRef() == 0) return false;
if (owner_.currentMapIdRef() != owner_.corpseMapIdRef()) return false;
float dx = owner_.movementInfoRef().x - owner_.corpseYRef();
float dy = owner_.movementInfoRef().y - owner_.corpseXRef();
float dz = owner_.movementInfoRef().z - owner_.corpseZRef();
return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f);
}
float CombatHandler::getCorpseReclaimDelaySec() const {
if (owner_.corpseReclaimAvailableMs_ == 0) return 0.0f;
if (owner_.corpseReclaimAvailableMsRef() == 0) return 0.0f;
auto nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
if (nowMs >= owner_.corpseReclaimAvailableMs_) return 0.0f;
return static_cast<float>(owner_.corpseReclaimAvailableMs_ - nowMs) / 1000.0f;
if (nowMs >= owner_.corpseReclaimAvailableMsRef()) return 0.0f;
return static_cast<float>(owner_.corpseReclaimAvailableMsRef() - nowMs) / 1000.0f;
}
void CombatHandler::reclaimCorpse() {
if (!canReclaimCorpse() || !owner_.socket) return;
if (owner_.corpseGuid_ == 0) {
if (!canReclaimCorpse() || !owner_.getSocket()) return;
if (owner_.corpseGuidRef() == 0) {
LOG_WARNING("reclaimCorpse: corpse GUID not yet known (corpse object not received); cannot reclaim");
return;
}
auto packet = ReclaimCorpsePacket::build(owner_.corpseGuid_);
owner_.socket->send(packet);
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, owner_.corpseGuid_, std::dec);
auto packet = ReclaimCorpsePacket::build(owner_.corpseGuidRef());
owner_.getSocket()->send(packet);
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, owner_.corpseGuidRef(), std::dec);
}
void CombatHandler::useSelfRes() {
if (!owner_.selfResAvailable_ || !owner_.socket) return;
if (!owner_.selfResAvailableRef() || !owner_.getSocket()) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_SELF_RES));
owner_.socket->send(pkt);
owner_.selfResAvailable_ = false;
owner_.getSocket()->send(pkt);
owner_.selfResAvailableRef() = false;
LOG_INFO("Sent CMSG_SELF_RES (Reincarnation / Twisting Nether)");
}
void CombatHandler::activateSpiritHealer(uint64_t npcGuid) {
if (!owner_.isInWorld()) return;
owner_.pendingSpiritHealerGuid_ = npcGuid;
owner_.pendingSpiritHealerGuidRef() = npcGuid;
auto packet = SpiritHealerActivatePacket::build(npcGuid);
owner_.socket->send(packet);
owner_.resurrectPending_ = true;
owner_.getSocket()->send(packet);
owner_.resurrectPendingRef() = true;
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x", std::hex, npcGuid, std::dec);
}
void CombatHandler::acceptResurrect() {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !owner_.resurrectRequestPending_) return;
if (owner_.resurrectIsSpiritHealer_) {
auto activate = SpiritHealerActivatePacket::build(owner_.resurrectCasterGuid_);
owner_.socket->send(activate);
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !owner_.resurrectRequestPendingRef()) return;
if (owner_.resurrectIsSpiritHealerRef()) {
auto activate = SpiritHealerActivatePacket::build(owner_.resurrectCasterGuidRef());
owner_.getSocket()->send(activate);
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x",
std::hex, owner_.resurrectCasterGuid_, std::dec);
std::hex, owner_.resurrectCasterGuidRef(), std::dec);
} else {
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuid_, true);
owner_.socket->send(resp);
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuidRef(), true);
owner_.getSocket()->send(resp);
LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (accept) for 0x",
std::hex, owner_.resurrectCasterGuid_, std::dec);
std::hex, owner_.resurrectCasterGuidRef(), std::dec);
}
owner_.resurrectRequestPending_ = false;
owner_.resurrectPending_ = true;
owner_.resurrectRequestPendingRef() = false;
owner_.resurrectPendingRef() = true;
}
void CombatHandler::declineResurrect() {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !owner_.resurrectRequestPending_) return;
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuid_, false);
owner_.socket->send(resp);
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !owner_.resurrectRequestPendingRef()) return;
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuidRef(), false);
owner_.getSocket()->send(resp);
LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (decline) for 0x",
std::hex, owner_.resurrectCasterGuid_, std::dec);
owner_.resurrectRequestPending_ = false;
std::hex, owner_.resurrectCasterGuidRef(), std::dec);
owner_.resurrectRequestPendingRef() = false;
}
// ============================================================

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -338,7 +338,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
table[Opcode::SMSG_QUESTGIVER_STATUS] = [this](network::Packet& packet) {
if (packet.hasRemaining(9)) {
uint64_t npcGuid = packet.readUInt64();
uint8_t status = owner_.packetParsers_->readQuestGiverStatus(packet);
uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet);
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
}
};
@ -350,7 +350,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
for (uint32_t i = 0; i < count; ++i) {
if (!packet.hasRemaining(9)) break;
uint64_t npcGuid = packet.readUInt64();
uint8_t status = owner_.packetParsers_->readQuestGiverStatus(packet);
uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet);
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
}
};
@ -466,8 +466,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
for (auto it = questLog_.begin(); it != questLog_.end(); ++it) {
if (it->questId == questId) {
// Fire toast callback before erasing
if (owner_.questCompleteCallback_) {
owner_.questCompleteCallback_(questId, it->title);
if (owner_.questCompleteCallbackRef()) {
owner_.questCompleteCallbackRef()(questId, it->title);
}
// Play quest-complete sound
if (auto* ac = owner_.services().audioCoordinator) {
@ -476,25 +476,25 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
}
questLog_.erase(it);
LOG_INFO(" Removed quest ", questId, " from quest log");
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("QUEST_TURNED_IN", {std::to_string(questId)});
if (owner_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("QUEST_TURNED_IN", {std::to_string(questId)});
break;
}
}
}
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
if (owner_.addonEventCallbackRef()) {
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
}
// Re-query all nearby quest giver NPCs so markers refresh
if (owner_.socket) {
if (owner_.getSocket()) {
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
if (entity->getType() != ObjectType::UNIT) continue;
auto unit = std::static_pointer_cast<Unit>(entity);
if (unit->getNpcFlags() & 0x02) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(guid);
owner_.socket->send(qsPkt);
owner_.getSocket()->send(qsPkt);
}
}
}
@ -548,13 +548,13 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
progressMsg += std::to_string(count) + "/" + std::to_string(reqCount);
owner_.addSystemChatMessage(progressMsg);
if (owner_.questProgressCallback_) {
owner_.questProgressCallback_(quest.title, creatureName, count, reqCount);
if (owner_.questProgressCallbackRef()) {
owner_.questProgressCallbackRef()(quest.title, creatureName, count, reqCount);
}
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("QUEST_WATCH_UPDATE", {std::to_string(questId)});
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
if (owner_.addonEventCallbackRef()) {
owner_.addonEventCallbackRef()("QUEST_WATCH_UPDATE", {std::to_string(questId)});
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
}
LOG_INFO("Updated kill count for quest ", questId, ": ",
@ -614,7 +614,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
}
owner_.addSystemChatMessage("Quest item: " + buildItemLink(itemId, questItemQuality, itemLabel) + " (" + std::to_string(count) + ")");
if (owner_.questProgressCallback_ && updatedAny) {
if (owner_.questProgressCallbackRef() && updatedAny) {
for (const auto& quest : questLog_) {
if (quest.complete) continue;
if (quest.itemCounts.count(itemId) == 0) continue;
@ -627,15 +627,15 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
}
}
if (required == 0) required = count;
owner_.questProgressCallback_(quest.title, itemLabel, count, required);
owner_.questProgressCallbackRef()(quest.title, itemLabel, count, required);
break;
}
}
if (owner_.addonEventCallback_ && updatedAny) {
owner_.addonEventCallback_("QUEST_WATCH_UPDATE", {});
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
if (owner_.addonEventCallbackRef() && updatedAny) {
owner_.addonEventCallbackRef()("QUEST_WATCH_UPDATE", {});
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
}
LOG_INFO("Quest item update: itemId=", itemId, " count=", count,
" trackedQuestsUpdated=", updatedAny);
@ -690,12 +690,12 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
// WotLK uses this opcode as SMSG_SET_REST_START
if (!isClassicLikeExpansion() && !isActiveExpansion("tbc")) {
bool nowResting = (value != 0);
if (nowResting != owner_.isResting_) {
owner_.isResting_ = nowResting;
owner_.addSystemChatMessage(owner_.isResting_ ? "You are now resting."
if (nowResting != owner_.isRestingRef()) {
owner_.isRestingRef() = nowResting;
owner_.addSystemChatMessage(owner_.isRestingRef() ? "You are now resting."
: "You are no longer resting.");
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PLAYER_UPDATE_RESTING", {});
if (owner_.addonEventCallbackRef())
owner_.addonEventCallbackRef()("PLAYER_UPDATE_RESTING", {});
}
return;
}
@ -740,10 +740,10 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
} else {
owner_.addSystemChatMessage("Quest removed (ID " + std::to_string(questId) + ").");
}
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
owner_.addonEventCallback_("QUEST_REMOVED", {std::to_string(questId)});
if (owner_.addonEventCallbackRef()) {
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
owner_.addonEventCallbackRef()("QUEST_REMOVED", {std::to_string(questId)});
}
}
};
@ -758,7 +758,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
uint32_t questId = packet.readUInt32();
packet.readUInt32(); // questMethod
const bool isClassicLayout = owner_.packetParsers_ && owner_.packetParsers_->questLogStride() <= 4;
const bool isClassicLayout = owner_.getPacketParsers() && owner_.getPacketParsers()->questLogStride() <= 4;
const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout);
const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout);
const QuestQueryRewards rwds = tryParseQuestRewards(packet.getData(), isClassicLayout);
@ -880,7 +880,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
for (uint32_t i = 0; i < count; ++i) {
if (!packet.hasRemaining(4)) break;
uint32_t questId = packet.readUInt32();
owner_.completedQuests_.insert(questId);
owner_.completedQuestsRef().insert(questId);
}
LOG_DEBUG("SMSG_QUERY_QUESTS_COMPLETED_RESPONSE: ", count, " completed quests");
}
@ -894,13 +894,13 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
// ---------------------------------------------------------------------------
void QuestHandler::selectGossipOption(uint32_t optionId) {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !gossipWindowOpen_) return;
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !gossipWindowOpen_) return;
LOG_INFO("selectGossipOption: optionId=", optionId,
" npcGuid=0x", std::hex, currentGossip_.npcGuid, std::dec,
" menuId=", currentGossip_.menuId,
" numOptions=", currentGossip_.options.size());
auto packet = GossipSelectOptionPacket::build(currentGossip_.npcGuid, currentGossip_.menuId, optionId);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
for (const auto& opt : currentGossip_.options) {
if (opt.id != optionId) continue;
@ -919,21 +919,21 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
if (opt.icon == 6) {
auto pkt = BankerActivatePacket::build(currentGossip_.npcGuid);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
sentBanker = true;
LOG_INFO("Sent CMSG_BANKER_ACTIVATE (icon) for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
}
if (!sentAuction && (text == "GOSSIP_OPTION_AUCTIONEER" || textLower.find("auction") != std::string::npos)) {
auto pkt = AuctionHelloPacket::build(currentGossip_.npcGuid);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
sentAuction = true;
LOG_INFO("Sent MSG_AUCTION_HELLO for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
}
if (!sentBanker && (text == "GOSSIP_OPTION_BANKER" || textLower.find("deposit box") != std::string::npos)) {
auto pkt = BankerActivatePacket::build(currentGossip_.npcGuid);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
sentBanker = true;
LOG_INFO("Sent CMSG_BANKER_ACTIVATE (text) for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
}
@ -947,14 +947,14 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
owner_.setVendorCanRepair(true);
}
auto pkt = ListInventoryPacket::build(currentGossip_.npcGuid);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
LOG_DEBUG("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
}
if (textLower.find("make this inn your home") != std::string::npos ||
textLower.find("set your home") != std::string::npos) {
auto bindPkt = BinderActivatePacket::build(currentGossip_.npcGuid);
owner_.socket->send(bindPkt);
owner_.getSocket()->send(bindPkt);
LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
}
@ -962,10 +962,10 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
if (text == "GOSSIP_OPTION_STABLE" ||
textLower.find("stable") != std::string::npos ||
textLower.find("my pet") != std::string::npos) {
owner_.stableMasterGuid_ = currentGossip_.npcGuid;
owner_.stableWindowOpen_ = false;
owner_.stableMasterGuidRef() = currentGossip_.npcGuid;
owner_.stableWindowOpenRef() = false;
auto listPkt = ListStabledPetsPacket::build(currentGossip_.npcGuid);
owner_.socket->send(listPkt);
owner_.getSocket()->send(listPkt);
LOG_INFO("Sent MSG_LIST_STABLED_PETS (gossip) to npc=0x",
std::hex, currentGossip_.npcGuid, std::dec);
}
@ -974,7 +974,7 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
}
void QuestHandler::selectGossipQuest(uint32_t questId) {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !gossipWindowOpen_) return;
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket() || !gossipWindowOpen_) return;
const QuestLogEntry* activeQuest = nullptr;
for (const auto& q : questLog_) {
@ -986,11 +986,11 @@ void QuestHandler::selectGossipQuest(uint32_t questId) {
// Validate against server-auth quest slot fields
auto questInServerLogSlots = [&](uint32_t qid) -> bool {
if (qid == 0 || owner_.lastPlayerFields_.empty()) return false;
if (qid == 0 || owner_.lastPlayerFieldsRef().empty()) return false;
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
const uint16_t ufQuestEnd = ufQuestStart + 25 * qStride;
for (const auto& [key, val] : owner_.lastPlayerFields_) {
for (const auto& [key, val] : owner_.lastPlayerFieldsRef()) {
if (key < ufQuestStart || key >= ufQuestEnd) continue;
if ((key - ufQuestStart) % qStride != 0) continue;
if (val == qid) return true;
@ -1015,37 +1015,37 @@ void QuestHandler::selectGossipQuest(uint32_t questId) {
pendingTurnInNpcGuid_ = currentGossip_.npcGuid;
pendingTurnInRewardRequest_ = activeQuest ? activeQuest->complete : false;
auto packet = QuestgiverCompleteQuestPacket::build(currentGossip_.npcGuid, questId);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
} else {
pendingTurnInQuestId_ = 0;
pendingTurnInNpcGuid_ = 0;
pendingTurnInRewardRequest_ = false;
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildQueryQuestPacket(currentGossip_.npcGuid, questId)
auto packet = owner_.getPacketParsers()
? owner_.getPacketParsers()->buildQueryQuestPacket(currentGossip_.npcGuid, questId)
: QuestgiverQueryQuestPacket::build(currentGossip_.npcGuid, questId);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
}
gossipWindowOpen_ = false;
}
bool QuestHandler::requestQuestQuery(uint32_t questId, bool force) {
if (questId == 0 || owner_.state != WorldState::IN_WORLD || !owner_.socket) return false;
if (questId == 0 || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return false;
if (!force && pendingQuestQueryIds_.count(questId)) return false;
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
pkt.writeUInt32(questId);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
pendingQuestQueryIds_.insert(questId);
// WotLK supports CMSG_QUEST_POI_QUERY to get objective map locations.
if (owner_.packetParsers_ && owner_.packetParsers_->questLogStride() == 5) {
if (owner_.getPacketParsers() && owner_.getPacketParsers()->questLogStride() == 5) {
const uint32_t wirePoiQuery = wireOpcode(Opcode::CMSG_QUEST_POI_QUERY);
if (wirePoiQuery != 0xFFFF) {
network::Packet poiPkt(static_cast<uint16_t>(wirePoiQuery));
poiPkt.writeUInt32(1); // count = 1
poiPkt.writeUInt32(questId);
owner_.socket->send(poiPkt);
owner_.getSocket()->send(poiPkt);
}
}
return true;
@ -1060,7 +1060,7 @@ void QuestHandler::setQuestTracked(uint32_t questId, bool tracked) {
}
void QuestHandler::acceptQuest() {
if (!questDetailsOpen_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
if (!questDetailsOpen_ || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
const uint32_t questId = currentQuestDetails_.questId;
if (questId == 0) return;
uint64_t npcGuid = currentQuestDetails_.npcGuid;
@ -1087,10 +1087,10 @@ void QuestHandler::acceptQuest() {
std::erase_if(questLog_, [&](const QuestLogEntry& q) { return q.questId == questId; });
}
network::Packet packet = owner_.packetParsers_
? owner_.packetParsers_->buildAcceptQuestPacket(npcGuid, questId)
network::Packet packet = owner_.getPacketParsers()
? owner_.getPacketParsers()->buildAcceptQuestPacket(npcGuid, questId)
: QuestgiverAcceptQuestPacket::build(npcGuid, questId);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
pendingQuestAcceptTimeouts_[questId] = 5.0f;
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
@ -1108,7 +1108,7 @@ void QuestHandler::acceptQuest() {
if (npcGuid) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(npcGuid);
owner_.socket->send(qsPkt);
owner_.getSocket()->send(qsPkt);
}
}
@ -1120,12 +1120,12 @@ void QuestHandler::declineQuest() {
void QuestHandler::closeGossip() {
gossipWindowOpen_ = false;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_CLOSED", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_CLOSED", {});
currentGossip_ = GossipMessageData{};
}
void QuestHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
if (itemGuid == 0 || questId == 0) {
owner_.addSystemChatMessage("Cannot start quest right now.");
return;
@ -1133,23 +1133,23 @@ void QuestHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) {
// Send CMSG_QUESTGIVER_QUERY_QUEST with the item GUID as the "questgiver."
// The server responds with SMSG_QUESTGIVER_QUEST_DETAILS which handleQuestDetails()
// picks up and opens the Accept/Decline dialog.
auto queryPkt = owner_.packetParsers_
? owner_.packetParsers_->buildQueryQuestPacket(itemGuid, questId)
auto queryPkt = owner_.getPacketParsers()
? owner_.getPacketParsers()->buildQueryQuestPacket(itemGuid, questId)
: QuestgiverQueryQuestPacket::build(itemGuid, questId);
owner_.socket->send(queryPkt);
owner_.getSocket()->send(queryPkt);
LOG_INFO("offerQuestFromItem: itemGuid=0x", std::hex, itemGuid, std::dec,
" questId=", questId);
}
void QuestHandler::completeQuest() {
if (!questRequestItemsOpen_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
if (!questRequestItemsOpen_ || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
pendingTurnInQuestId_ = currentQuestRequestItems_.questId;
pendingTurnInNpcGuid_ = currentQuestRequestItems_.npcGuid;
pendingTurnInRewardRequest_ = currentQuestRequestItems_.isCompletable();
auto packet = QuestgiverCompleteQuestPacket::build(
currentQuestRequestItems_.npcGuid, currentQuestRequestItems_.questId);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
questRequestItemsOpen_ = false;
currentQuestRequestItems_ = QuestRequestItemsData{};
}
@ -1161,13 +1161,13 @@ void QuestHandler::closeQuestRequestItems() {
}
void QuestHandler::chooseQuestReward(uint32_t rewardIndex) {
if (!questOfferRewardOpen_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
if (!questOfferRewardOpen_ || owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) return;
uint64_t npcGuid = currentQuestOfferReward_.npcGuid;
LOG_INFO("Completing quest: questId=", currentQuestOfferReward_.questId,
" npcGuid=", npcGuid, " rewardIndex=", rewardIndex);
auto packet = QuestgiverChooseRewardPacket::build(
npcGuid, currentQuestOfferReward_.questId, rewardIndex);
owner_.socket->send(packet);
owner_.getSocket()->send(packet);
pendingTurnInQuestId_ = 0;
pendingTurnInNpcGuid_ = 0;
pendingTurnInRewardRequest_ = false;
@ -1178,7 +1178,7 @@ void QuestHandler::chooseQuestReward(uint32_t rewardIndex) {
if (npcGuid) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(npcGuid);
owner_.socket->send(qsPkt);
owner_.getSocket()->send(qsPkt);
}
}
@ -1205,10 +1205,10 @@ void QuestHandler::abandonQuest(uint32_t questId) {
}
if (slotIndex >= 0 && slotIndex < 25) {
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
if (owner_.getState() == WorldState::IN_WORLD && owner_.getSocket()) {
network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
pkt.writeUInt8(static_cast<uint8_t>(slotIndex));
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
}
} else {
LOG_WARNING("Abandon quest failed: no quest-log slot found for questId=", questId);
@ -1216,10 +1216,10 @@ void QuestHandler::abandonQuest(uint32_t questId) {
if (localIndex >= 0) {
questLog_.erase(questLog_.begin() + static_cast<ptrdiff_t>(localIndex));
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
owner_.addonEventCallback_("QUEST_REMOVED", {std::to_string(questId)});
if (owner_.addonEventCallbackRef()) {
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
owner_.addonEventCallbackRef()("QUEST_REMOVED", {std::to_string(questId)});
}
}
@ -1231,7 +1231,7 @@ void QuestHandler::abandonQuest(uint32_t questId) {
}
void QuestHandler::shareQuestWithParty(uint32_t questId) {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.getSocket()) {
owner_.addSystemChatMessage("Cannot share quest: not in world.");
return;
}
@ -1241,7 +1241,7 @@ void QuestHandler::shareQuestWithParty(uint32_t questId) {
}
network::Packet pkt(wireOpcode(Opcode::CMSG_PUSHQUESTTOPARTY));
pkt.writeUInt32(questId);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
// Local feedback: find quest title
for (const auto& q : questLog_) {
if (q.questId == questId && !q.title.empty()) {
@ -1253,11 +1253,11 @@ void QuestHandler::shareQuestWithParty(uint32_t questId) {
}
void QuestHandler::acceptSharedQuest() {
if (!pendingSharedQuest_ || !owner_.socket) return;
if (!pendingSharedQuest_ || !owner_.getSocket()) return;
pendingSharedQuest_ = false;
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_CONFIRM_ACCEPT));
pkt.writeUInt32(sharedQuestId_);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
owner_.addSystemChatMessage("Accepted: " + sharedQuestTitle_);
}
@ -1278,13 +1278,13 @@ bool QuestHandler::hasQuestInLog(uint32_t questId) const {
}
int QuestHandler::findQuestLogSlotIndexFromServer(uint32_t questId) const {
if (questId == 0 || owner_.lastPlayerFields_.empty()) return -1;
if (questId == 0 || owner_.lastPlayerFieldsRef().empty()) return -1;
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
for (uint16_t slot = 0; slot < 25; ++slot) {
const uint16_t idField = ufQuestStart + slot * qStride;
auto it = owner_.lastPlayerFields_.find(idField);
if (it != owner_.lastPlayerFields_.end() && it->second == questId) {
auto it = owner_.lastPlayerFieldsRef().find(idField);
if (it != owner_.lastPlayerFieldsRef().end() && it->second == questId) {
return static_cast<int>(slot);
}
}
@ -1298,18 +1298,18 @@ void QuestHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::stri
entry.title = title.empty() ? ("Quest #" + std::to_string(questId)) : title;
entry.objectives = objectives;
questLog_.push_back(std::move(entry));
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("QUEST_ACCEPTED", {std::to_string(questId)});
owner_.addonEventCallback_("QUEST_LOG_UPDATE", {});
owner_.addonEventCallback_("UNIT_QUEST_LOG_CHANGED", {"player"});
if (owner_.addonEventCallbackRef()) {
owner_.addonEventCallbackRef()("QUEST_ACCEPTED", {std::to_string(questId)});
owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {});
owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"});
}
}
bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
if (owner_.lastPlayerFields_.empty()) return false;
if (owner_.lastPlayerFieldsRef().empty()) return false;
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
static constexpr uint32_t kQuestStatusComplete = 1;
@ -1318,15 +1318,15 @@ bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
for (uint16_t slot = 0; slot < 25; ++slot) {
const uint16_t idField = ufQuestStart + slot * qStride;
const uint16_t stateField = ufQuestStart + slot * qStride + 1;
auto it = owner_.lastPlayerFields_.find(idField);
if (it == owner_.lastPlayerFields_.end()) continue;
auto it = owner_.lastPlayerFieldsRef().find(idField);
if (it == owner_.lastPlayerFieldsRef().end()) continue;
uint32_t questId = it->second;
if (questId == 0) continue;
bool complete = false;
if (qStride >= 2) {
auto stateIt = owner_.lastPlayerFields_.find(stateField);
if (stateIt != owner_.lastPlayerFields_.end()) {
auto stateIt = owner_.lastPlayerFieldsRef().find(stateField);
if (stateIt != owner_.lastPlayerFieldsRef().end()) {
uint32_t state = stateIt->second & 0xFF;
complete = (state == kQuestStatusComplete);
}
@ -1378,7 +1378,7 @@ void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
if (ufQuestStart == 0xFFFF || questLog_.empty()) return;
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
if (qStride < 2) return;
static constexpr uint32_t kQuestStatusComplete = 1;
@ -1407,12 +1407,12 @@ void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
}
void QuestHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) {
if (owner_.lastPlayerFields_.empty()) return;
if (owner_.lastPlayerFieldsRef().empty()) return;
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
if (ufQuestStart == 0xFFFF) return;
const uint8_t qStride = owner_.packetParsers_ ? owner_.packetParsers_->questLogStride() : 5;
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
if (qStride < 3) return;
int slot = findQuestLogSlotIndexFromServer(quest.questId);
@ -1423,14 +1423,14 @@ void QuestHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) {
? static_cast<uint16_t>(countField1 + 1)
: static_cast<uint16_t>(0xFFFF);
auto f1It = owner_.lastPlayerFields_.find(countField1);
if (f1It == owner_.lastPlayerFields_.end()) return;
auto f1It = owner_.lastPlayerFieldsRef().find(countField1);
if (f1It == owner_.lastPlayerFieldsRef().end()) return;
const uint32_t packed1 = f1It->second;
uint32_t packed2 = 0;
if (countField2 != 0xFFFF) {
auto f2It = owner_.lastPlayerFields_.find(countField2);
if (f2It != owner_.lastPlayerFields_.end()) packed2 = f2It->second;
auto f2It = owner_.lastPlayerFieldsRef().find(countField2);
if (f2It != owner_.lastPlayerFieldsRef().end()) packed2 = f2It->second;
}
auto unpack6 = [](uint32_t word, int idx) -> uint8_t {
@ -1474,7 +1474,7 @@ void QuestHandler::clearPendingQuestAccept(uint32_t questId) {
}
void QuestHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason) {
if (questId == 0 || !owner_.socket || owner_.state != WorldState::IN_WORLD) return;
if (questId == 0 || !owner_.getSocket() || owner_.getState() != WorldState::IN_WORLD) return;
LOG_INFO("Quest accept resync: questId=", questId, " reason=", reason ? reason : "unknown");
requestQuestQuery(questId, true);
@ -1482,12 +1482,12 @@ void QuestHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid,
if (npcGuid != 0) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(npcGuid);
owner_.socket->send(qsPkt);
owner_.getSocket()->send(qsPkt);
auto queryPkt = owner_.packetParsers_
? owner_.packetParsers_->buildQueryQuestPacket(npcGuid, questId)
auto queryPkt = owner_.getPacketParsers()
? owner_.getPacketParsers()->buildQueryQuestPacket(npcGuid, questId)
: QuestgiverQueryQuestPacket::build(npcGuid, questId);
owner_.socket->send(queryPkt);
owner_.getSocket()->send(queryPkt);
}
}
@ -1496,23 +1496,23 @@ void QuestHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid,
// ---------------------------------------------------------------------------
void QuestHandler::handleGossipMessage(network::Packet& packet) {
bool ok = owner_.packetParsers_ ? owner_.packetParsers_->parseGossipMessage(packet, currentGossip_)
bool ok = owner_.getPacketParsers() ? owner_.getPacketParsers()->parseGossipMessage(packet, currentGossip_)
: GossipMessageParser::parse(packet, currentGossip_);
if (!ok) return;
if (questDetailsOpen_) return; // Don't reopen gossip while viewing quest
gossipWindowOpen_ = true;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_SHOW", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_SHOW", {});
owner_.closeVendor(); // Close vendor if gossip opens
// Classify gossip quests and update quest log + overhead NPC markers.
classifyGossipQuests(true);
// Play NPC greeting voice
if (owner_.npcGreetingCallback_ && currentGossip_.npcGuid != 0) {
if (owner_.npcGreetingCallbackRef() && currentGossip_.npcGuid != 0) {
auto entity = owner_.getEntityManager().getEntity(currentGossip_.npcGuid);
if (entity) {
glm::vec3 npcPos(entity->getX(), entity->getY(), entity->getZ());
owner_.npcGreetingCallback_(currentGossip_.npcGuid, npcPos);
owner_.npcGreetingCallbackRef()(currentGossip_.npcGuid, npcPos);
}
}
}
@ -1563,7 +1563,7 @@ void QuestHandler::handleQuestgiverQuestList(network::Packet& packet) {
currentGossip_ = std::move(data);
gossipWindowOpen_ = true;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_SHOW", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_SHOW", {});
owner_.closeVendor();
classifyGossipQuests(false);
@ -1617,16 +1617,16 @@ void QuestHandler::handleGossipComplete(network::Packet& packet) {
(void)packet;
// Play farewell sound before closing
if (owner_.npcFarewellCallback_ && currentGossip_.npcGuid != 0) {
if (owner_.npcFarewellCallbackRef() && currentGossip_.npcGuid != 0) {
auto entity = owner_.getEntityManager().getEntity(currentGossip_.npcGuid);
if (entity && entity->getType() == ObjectType::UNIT) {
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
owner_.npcFarewellCallback_(currentGossip_.npcGuid, pos);
owner_.npcFarewellCallbackRef()(currentGossip_.npcGuid, pos);
}
}
gossipWindowOpen_ = false;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GOSSIP_CLOSED", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("GOSSIP_CLOSED", {});
currentGossip_ = GossipMessageData{};
}
@ -1687,7 +1687,7 @@ void QuestHandler::handleQuestPoiQueryResponse(network::Packet& packet) {
sumY += static_cast<float>(py);
}
// Skip POIs for maps other than the player's current map.
if (mapId != owner_.currentMapId_) continue;
if (mapId != owner_.currentMapIdRef()) continue;
GossipPoi poi;
poi.x = sumX / static_cast<float>(pointCount);
poi.y = sumY / static_cast<float>(pointCount);
@ -1704,7 +1704,7 @@ void QuestHandler::handleQuestPoiQueryResponse(network::Packet& packet) {
void QuestHandler::handleQuestDetails(network::Packet& packet) {
QuestDetailsData data;
bool ok = owner_.packetParsers_ ? owner_.packetParsers_->parseQuestDetails(packet, data)
bool ok = owner_.getPacketParsers() ? owner_.getPacketParsers()->parseQuestDetails(packet, data)
: QuestDetailsParser::parse(packet, data);
if (!ok) {
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_QUEST_DETAILS");
@ -1727,7 +1727,7 @@ void QuestHandler::handleQuestDetails(network::Packet& packet) {
// Delay opening the window slightly to allow item queries to complete
questDetailsOpenTime_ = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
gossipWindowOpen_ = false;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("QUEST_DETAIL", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("QUEST_DETAIL", {});
}
void QuestHandler::handleQuestRequestItems(network::Packet& packet) {
@ -1742,9 +1742,9 @@ void QuestHandler::handleQuestRequestItems(network::Packet& packet) {
data.questId == pendingTurnInQuestId_ &&
data.npcGuid == pendingTurnInNpcGuid_ &&
data.isCompletable() &&
owner_.socket) {
owner_.getSocket()) {
auto rewardReq = QuestgiverRequestRewardPacket::build(data.npcGuid, data.questId);
owner_.socket->send(rewardReq);
owner_.getSocket()->send(rewardReq);
pendingTurnInRewardRequest_ = false;
}
@ -1809,7 +1809,7 @@ void QuestHandler::handleQuestOfferReward(network::Packet& packet) {
gossipWindowOpen_ = false;
questDetailsOpen_ = false;
questDetailsOpenTime_ = std::chrono::steady_clock::time_point{};
if (owner_.addonEventCallback_) owner_.addonEventCallback_("QUEST_COMPLETE", {});
if (owner_.addonEventCallbackRef()) owner_.addonEventCallbackRef()("QUEST_COMPLETE", {});
// Query item names for reward items
for (const auto& item : data.choiceRewards)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -193,8 +193,8 @@ void WardenHandler::update(float deltaTime) {
for (uint8_t byte : encrypted) {
response.writeUInt8(byte);
}
if (owner_.socket && owner_.socket->isConnected()) {
owner_.socket->send(response);
if (owner_.getSocket() && owner_.getSocket()->isConnected()) {
owner_.getSocket()->send(response);
LOG_WARNING("Warden: Sent async CHEAT_CHECKS_RESULT (", plaintext.size(), " bytes plaintext)");
}
}
@ -202,11 +202,11 @@ void WardenHandler::update(float deltaTime) {
}
// Post-gate visibility
if (wardenGateSeen_ && owner_.socket && owner_.socket->isConnected()) {
if (wardenGateSeen_ && owner_.getSocket() && owner_.getSocket()->isConnected()) {
wardenGateElapsed_ += deltaTime;
if (wardenGateElapsed_ >= wardenGateNextStatusLog_) {
LOG_DEBUG("Warden gate status: elapsed=", wardenGateElapsed_,
"s connected=", owner_.socket->isConnected() ? "yes" : "no",
"s connected=", owner_.getSocket()->isConnected() ? "yes" : "no",
" packetsAfterGate=", wardenPacketsAfterGate_);
wardenGateNextStatusLog_ += 30.0f;
}
@ -302,12 +302,12 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
// Initialize Warden crypto from session key on first packet
if (!wardenCrypto_) {
wardenCrypto_ = std::make_unique<WardenCrypto>();
if (owner_.sessionKey.size() != 40) {
LOG_ERROR("Warden: No valid session key (size=", owner_.sessionKey.size(), "), cannot init crypto");
if (owner_.getSessionKey().size() != 40) {
LOG_ERROR("Warden: No valid session key (size=", owner_.getSessionKey().size(), "), cannot init crypto");
wardenCrypto_.reset();
return;
}
if (!wardenCrypto_->initFromSessionKey(owner_.sessionKey)) {
if (!wardenCrypto_->initFromSessionKey(owner_.getSessionKey())) {
LOG_ERROR("Warden: Failed to initialize crypto from session key");
wardenCrypto_.reset();
return;
@ -348,8 +348,8 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
for (uint8_t byte : encrypted) {
response.writeUInt8(byte);
}
if (owner_.socket && owner_.socket->isConnected()) {
owner_.socket->send(response);
if (owner_.getSocket() && owner_.getSocket()->isConnected()) {
owner_.getSocket()->send(response);
LOG_DEBUG("Warden: Sent response (", plaintext.size(), " bytes plaintext)");
}
};
@ -447,12 +447,12 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
wardenLoadedModule_->setCallbackDependencies(
wardenCrypto_.get(),
[this](const uint8_t* data, size_t len) {
if (!wardenCrypto_ || !owner_.socket) return;
if (!wardenCrypto_ || !owner_.getSocket()) return;
std::vector<uint8_t> plaintext(data, data + len);
auto encrypted = wardenCrypto_->encrypt(plaintext);
network::Packet pkt(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t b : encrypted) pkt.writeUInt8(b);
owner_.socket->send(pkt);
owner_.getSocket()->send(pkt);
LOG_DEBUG("Warden: Module sendPacket callback sent ", len, " bytes");
});
if (wardenLoadedModule_->load(wardenModuleData_, wardenModuleHash_, wardenModuleKey_)) { // codeql[cpp/weak-cryptographic-algorithm]
@ -533,7 +533,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; }
bool isTurtle = isActiveExpansion("turtle");
bool isClassic = (owner_.build <= 6005) && !isTurtle;
bool isClassic = (owner_.getBuild() <= 6005) && !isTurtle;
if (!isTurtle && !isClassic) {
// WotLK/TBC: don't respond to HASH_REQUEST without a valid CR match.
@ -619,7 +619,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
// Ensure wardenMemory_ is loaded on main thread before launching async task
if (!wardenMemory_) {
wardenMemory_ = std::make_unique<WardenMemory>();
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.build), isActiveExpansion("turtle"))) {
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.getBuild()), isActiveExpansion("turtle"))) {
LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK");
}
}
@ -1054,7 +1054,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
// Lazy-load WoW.exe PE image on first MEM_CHECK
if (!wardenMemory_) {
wardenMemory_ = std::make_unique<WardenMemory>();
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.build), isActiveExpansion("turtle"))) {
if (!wardenMemory_->load(static_cast<uint16_t>(owner_.getBuild()), isActiveExpansion("turtle"))) {
LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK");
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,666 @@
#include "game/world_packets.hpp"
#include "game/packet_parsers.hpp"
#include "game/opcodes.hpp"
#include "game/character.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <sstream>
#include <iomanip>
#include <zlib.h>
namespace wowee {
namespace game {
bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data) {
// Minimum: windowInfo(4) + npcGuid(8) + nearestNode(4) + at least 1 mask uint32(4)
size_t remaining = packet.getRemainingSize();
if (remaining < 4 + 8 + 4 + 4) {
LOG_ERROR("ShowTaxiNodesParser: packet too short (", remaining, " bytes)");
return false;
}
data.windowInfo = packet.readUInt32();
data.npcGuid = packet.readUInt64();
data.nearestNode = packet.readUInt32();
// Read as many mask uint32s as available (Classic/Vanilla=4, WotLK=12)
size_t maskBytes = packet.getRemainingSize();
uint32_t maskCount = static_cast<uint32_t>(maskBytes / 4);
if (maskCount > TLK_TAXI_MASK_SIZE) maskCount = TLK_TAXI_MASK_SIZE;
for (uint32_t i = 0; i < maskCount; ++i) {
data.nodeMask[i] = packet.readUInt32();
}
LOG_INFO("ShowTaxiNodes: window=", data.windowInfo, " npc=0x", std::hex, data.npcGuid, std::dec,
" nearest=", data.nearestNode, " maskSlots=", maskCount);
return true;
}
bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyData& data) {
size_t remaining = packet.getRemainingSize();
if (remaining >= 4) {
data.result = packet.readUInt32();
} else if (remaining >= 1) {
data.result = packet.readUInt8();
} else {
LOG_ERROR("ActivateTaxiReplyParser: packet too short");
return false;
}
LOG_INFO("ActivateTaxiReply: result=", data.result);
return true;
}
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes) {
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXIEXPRESS));
packet.writeUInt64(npcGuid);
packet.writeUInt32(totalCost);
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
for (uint32_t nodeId : pathNodes) {
packet.writeUInt32(nodeId);
}
LOG_INFO("ActivateTaxiExpress: npc=0x", std::hex, npcGuid, std::dec,
" cost=", totalCost, " nodes=", pathNodes.size());
return packet;
}
network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode) {
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXI));
packet.writeUInt64(npcGuid);
packet.writeUInt32(srcNode);
packet.writeUInt32(destNode);
return packet;
}
network::Packet GameObjectUsePacket::build(uint64_t guid) {
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJ_USE));
packet.writeUInt64(guid);
return packet;
}
// ============================================================
// Mail System
// ============================================================
network::Packet GetMailListPacket::build(uint64_t mailboxGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_GET_MAIL_LIST));
packet.writeUInt64(mailboxGuid);
return packet;
}
network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& recipient,
const std::string& subject, const std::string& body,
uint64_t money, uint64_t cod,
const std::vector<uint64_t>& itemGuids) {
// WotLK 3.3.5a format
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
packet.writeUInt64(mailboxGuid);
packet.writeString(recipient);
packet.writeString(subject);
packet.writeString(body);
packet.writeUInt32(0); // stationery
packet.writeUInt32(0); // unknown
uint8_t attachCount = static_cast<uint8_t>(itemGuids.size());
packet.writeUInt8(attachCount);
for (uint8_t i = 0; i < attachCount; ++i) {
packet.writeUInt8(i); // attachment slot index
packet.writeUInt64(itemGuids[i]);
}
packet.writeUInt64(money);
packet.writeUInt64(cod);
return packet;
}
network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_MONEY));
packet.writeUInt64(mailboxGuid);
packet.writeUInt32(mailId);
return packet;
}
network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) {
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
packet.writeUInt64(mailboxGuid);
packet.writeUInt32(mailId);
// WotLK expects attachment item GUID low, not attachment slot index.
packet.writeUInt32(itemGuidLow);
return packet;
}
network::Packet MailDeletePacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) {
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE));
packet.writeUInt64(mailboxGuid);
packet.writeUInt32(mailId);
packet.writeUInt32(mailTemplateId);
return packet;
}
network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_MARK_AS_READ));
packet.writeUInt64(mailboxGuid);
packet.writeUInt32(mailId);
return packet;
}
// ============================================================================
// PacketParsers::parseMailList — WotLK 3.3.5a format (base/default)
// ============================================================================
bool PacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) {
size_t remaining = packet.getRemainingSize();
if (remaining < 5) return false;
uint32_t totalCount = packet.readUInt32();
uint8_t shownCount = packet.readUInt8();
(void)totalCount;
LOG_INFO("SMSG_MAIL_LIST_RESULT (WotLK): total=", totalCount, " shown=", static_cast<int>(shownCount));
inbox.clear();
inbox.reserve(shownCount);
for (uint8_t i = 0; i < shownCount; ++i) {
remaining = packet.getRemainingSize();
if (remaining < 2) break;
uint16_t msgSize = packet.readUInt16();
size_t startPos = packet.getReadPos();
MailMessage msg;
if (remaining < static_cast<size_t>(msgSize) + 2) {
LOG_WARNING("Mail entry ", i, " truncated");
break;
}
msg.messageId = packet.readUInt32();
msg.messageType = packet.readUInt8();
switch (msg.messageType) {
case 0: msg.senderGuid = packet.readUInt64(); break;
case 2: case 3: case 4: case 5:
msg.senderEntry = packet.readUInt32(); break;
default: msg.senderEntry = packet.readUInt32(); break;
}
msg.cod = packet.readUInt64();
packet.readUInt32(); // item text id
packet.readUInt32(); // unknown
msg.stationeryId = packet.readUInt32();
msg.money = packet.readUInt64();
msg.flags = packet.readUInt32();
msg.expirationTime = packet.readFloat();
msg.mailTemplateId = packet.readUInt32();
msg.subject = packet.readString();
// WotLK 3.3.5a always includes body text in SMSG_MAIL_LIST_RESULT.
// mailTemplateId != 0 still carries a (possibly empty) body string.
msg.body = packet.readString();
uint8_t attachCount = packet.readUInt8();
msg.attachments.reserve(attachCount);
for (uint8_t j = 0; j < attachCount; ++j) {
MailAttachment att;
att.slot = packet.readUInt8();
att.itemGuidLow = packet.readUInt32();
att.itemId = packet.readUInt32();
for (int e = 0; e < 7; ++e) {
uint32_t enchId = packet.readUInt32();
packet.readUInt32(); // duration
packet.readUInt32(); // charges
if (e == 0) att.enchantId = enchId;
}
att.randomPropertyId = packet.readUInt32();
att.randomSuffix = packet.readUInt32();
att.stackCount = packet.readUInt32();
att.chargesOrDurability = packet.readUInt32();
att.maxDurability = packet.readUInt32();
packet.readUInt32(); // durability/current durability
packet.readUInt8(); // unknown WotLK trailing byte per attachment
msg.attachments.push_back(att);
}
msg.read = (msg.flags & 0x01) != 0;
inbox.push_back(std::move(msg));
// Skip unread bytes
size_t consumed = packet.getReadPos() - startPos;
if (consumed < msgSize) {
size_t skip = msgSize - consumed;
for (size_t s = 0; s < skip && packet.hasData(); ++s)
packet.readUInt8();
}
}
LOG_INFO("Parsed ", inbox.size(), " mail messages");
return true;
}
// ============================================================
// Bank System
// ============================================================
network::Packet BankerActivatePacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::CMSG_BANKER_ACTIVATE));
p.writeUInt64(guid);
return p;
}
network::Packet BuyBankSlotPacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::CMSG_BUY_BANK_SLOT));
p.writeUInt64(guid);
return p;
}
network::Packet AutoBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet p(wireOpcode(Opcode::CMSG_AUTOBANK_ITEM));
p.writeUInt8(srcBag);
p.writeUInt8(srcSlot);
return p;
}
network::Packet AutoStoreBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet p(wireOpcode(Opcode::CMSG_AUTOSTORE_BANK_ITEM));
p.writeUInt8(srcBag);
p.writeUInt8(srcSlot);
return p;
}
// ============================================================
// Guild Bank System
// ============================================================
network::Packet GuildBankerActivatePacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANKER_ACTIVATE));
p.writeUInt64(guid);
p.writeUInt8(0); // full slots update
return p;
}
network::Packet GuildBankQueryTabPacket::build(uint64_t guid, uint8_t tabId, bool fullUpdate) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_QUERY_TAB));
p.writeUInt64(guid);
p.writeUInt8(tabId);
p.writeUInt8(fullUpdate ? 1 : 0);
return p;
}
network::Packet GuildBankBuyTabPacket::build(uint64_t guid, uint8_t tabId) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_BUY_TAB));
p.writeUInt64(guid);
p.writeUInt8(tabId);
return p;
}
network::Packet GuildBankDepositMoneyPacket::build(uint64_t guid, uint32_t amount) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_DEPOSIT_MONEY));
p.writeUInt64(guid);
p.writeUInt32(amount);
return p;
}
network::Packet GuildBankWithdrawMoneyPacket::build(uint64_t guid, uint32_t amount) {
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_WITHDRAW_MONEY));
p.writeUInt64(guid);
p.writeUInt32(amount);
return p;
}
network::Packet GuildBankSwapItemsPacket::buildBankToInventory(
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
uint8_t destBag, uint8_t destSlot, uint32_t splitCount)
{
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
p.writeUInt64(guid);
p.writeUInt8(0); // bankToCharacter = false -> bank source
p.writeUInt8(tabId);
p.writeUInt8(bankSlot);
p.writeUInt32(0); // itemEntry (unused client side)
p.writeUInt8(0); // autoStore = false
if (splitCount > 0) {
p.writeUInt8(splitCount);
}
p.writeUInt8(destBag);
p.writeUInt8(destSlot);
return p;
}
network::Packet GuildBankSwapItemsPacket::buildInventoryToBank(
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
uint8_t srcBag, uint8_t srcSlot, uint32_t splitCount)
{
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
p.writeUInt64(guid);
p.writeUInt8(1); // bankToCharacter = true -> char to bank
p.writeUInt8(tabId);
p.writeUInt8(bankSlot);
p.writeUInt32(0); // itemEntry
p.writeUInt8(0); // autoStore
if (splitCount > 0) {
p.writeUInt8(splitCount);
}
p.writeUInt8(srcBag);
p.writeUInt8(srcSlot);
return p;
}
bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) {
if (!packet.hasRemaining(14)) return false;
data.money = packet.readUInt64();
data.tabId = packet.readUInt8();
data.withdrawAmount = static_cast<int32_t>(packet.readUInt32());
uint8_t fullUpdate = packet.readUInt8();
if (fullUpdate) {
if (!packet.hasRemaining(1)) {
LOG_WARNING("GuildBankListParser: truncated before tabCount");
data.tabs.clear();
} else {
uint8_t tabCount = packet.readUInt8();
// Cap at 8 (normal guild bank tab limit in WoW)
if (tabCount > 8) {
LOG_WARNING("GuildBankListParser: tabCount capped (requested=", static_cast<int>(tabCount), ")");
tabCount = 8;
}
data.tabs.resize(tabCount);
for (uint8_t i = 0; i < tabCount; ++i) {
// Validate before reading strings
if (!packet.hasData()) {
LOG_WARNING("GuildBankListParser: truncated tab at index ", static_cast<int>(i));
break;
}
data.tabs[i].tabName = packet.readString();
if (!packet.hasData()) {
data.tabs[i].tabIcon.clear();
} else {
data.tabs[i].tabIcon = packet.readString();
}
}
}
}
if (!packet.hasRemaining(1)) {
LOG_WARNING("GuildBankListParser: truncated before numSlots");
data.tabItems.clear();
return true;
}
uint8_t numSlots = packet.readUInt8();
data.tabItems.clear();
for (uint8_t i = 0; i < numSlots; ++i) {
// Validate minimum bytes before reading slot (slotId(1) + itemEntry(4) = 5)
if (!packet.hasRemaining(5)) {
LOG_WARNING("GuildBankListParser: truncated slot at index ", static_cast<int>(i));
break;
}
GuildBankItemSlot slot;
slot.slotId = packet.readUInt8();
slot.itemEntry = packet.readUInt32();
if (slot.itemEntry != 0) {
// Validate before reading enchant mask
if (!packet.hasRemaining(4)) break;
// Enchant info
uint32_t enchantMask = packet.readUInt32();
for (int bit = 0; bit < 10; ++bit) {
if (enchantMask & (1u << bit)) {
if (!packet.hasRemaining(12)) {
LOG_WARNING("GuildBankListParser: truncated enchant data");
break;
}
uint32_t enchId = packet.readUInt32();
uint32_t enchDur = packet.readUInt32();
uint32_t enchCharges = packet.readUInt32();
if (bit == 0) slot.enchantId = enchId;
(void)enchDur; (void)enchCharges;
}
}
// Validate before reading remaining item fields
if (!packet.hasRemaining(12)) {
LOG_WARNING("GuildBankListParser: truncated item fields");
break;
}
slot.stackCount = packet.readUInt32();
/*spare=*/ packet.readUInt32();
slot.randomPropertyId = packet.readUInt32();
if (slot.randomPropertyId) {
if (!packet.hasRemaining(4)) {
LOG_WARNING("GuildBankListParser: truncated suffix factor");
break;
}
/*suffixFactor=*/ packet.readUInt32();
}
}
data.tabItems.push_back(slot);
}
return true;
}
// ============================================================
// Auction House System
// ============================================================
network::Packet AuctionHelloPacket::build(uint64_t guid) {
network::Packet p(wireOpcode(Opcode::MSG_AUCTION_HELLO));
p.writeUInt64(guid);
return p;
}
bool AuctionHelloParser::parse(network::Packet& packet, AuctionHelloData& data) {
size_t remaining = packet.getRemainingSize();
if (remaining < 12) {
LOG_WARNING("AuctionHelloParser: too small, remaining=", remaining);
return false;
}
data.auctioneerGuid = packet.readUInt64();
data.auctionHouseId = packet.readUInt32();
// WotLK has an extra uint8 enabled field; Vanilla does not
if (packet.hasData()) {
data.enabled = packet.readUInt8();
} else {
data.enabled = 1;
}
return true;
}
network::Packet AuctionListItemsPacket::build(
uint64_t guid, uint32_t offset,
const std::string& searchName,
uint8_t levelMin, uint8_t levelMax,
uint32_t invTypeMask, uint32_t itemClass,
uint32_t itemSubClass, uint32_t quality,
uint8_t usableOnly, uint8_t exactMatch)
{
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_ITEMS));
p.writeUInt64(guid);
p.writeUInt32(offset);
p.writeString(searchName);
p.writeUInt8(levelMin);
p.writeUInt8(levelMax);
p.writeUInt32(invTypeMask);
p.writeUInt32(itemClass);
p.writeUInt32(itemSubClass);
p.writeUInt32(quality);
p.writeUInt8(usableOnly);
p.writeUInt8(0); // getAll (0 = normal search)
p.writeUInt8(exactMatch);
// Sort columns (0 = none)
p.writeUInt8(0);
return p;
}
network::Packet AuctionSellItemPacket::build(
uint64_t auctioneerGuid, uint64_t itemGuid,
uint32_t stackCount, uint32_t bid,
uint32_t buyout, uint32_t duration,
bool preWotlk)
{
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_SELL_ITEM));
p.writeUInt64(auctioneerGuid);
if (!preWotlk) {
// WotLK: itemCount(4) + per-item [guid(8) + stackCount(4)]
p.writeUInt32(1);
p.writeUInt64(itemGuid);
p.writeUInt32(stackCount);
} else {
// Classic/TBC: just itemGuid, no count fields
p.writeUInt64(itemGuid);
}
p.writeUInt32(bid);
p.writeUInt32(buyout);
p.writeUInt32(duration);
return p;
}
network::Packet AuctionPlaceBidPacket::build(uint64_t auctioneerGuid, uint32_t auctionId, uint32_t amount) {
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_PLACE_BID));
p.writeUInt64(auctioneerGuid);
p.writeUInt32(auctionId);
p.writeUInt32(amount);
return p;
}
network::Packet AuctionRemoveItemPacket::build(uint64_t auctioneerGuid, uint32_t auctionId) {
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_REMOVE_ITEM));
p.writeUInt64(auctioneerGuid);
p.writeUInt32(auctionId);
return p;
}
network::Packet AuctionListOwnerItemsPacket::build(uint64_t auctioneerGuid, uint32_t offset) {
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_OWNER_ITEMS));
p.writeUInt64(auctioneerGuid);
p.writeUInt32(offset);
return p;
}
network::Packet AuctionListBidderItemsPacket::build(
uint64_t auctioneerGuid, uint32_t offset,
const std::vector<uint32_t>& outbiddedIds)
{
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_BIDDER_ITEMS));
p.writeUInt64(auctioneerGuid);
p.writeUInt32(offset);
p.writeUInt32(static_cast<uint32_t>(outbiddedIds.size()));
for (uint32_t id : outbiddedIds)
p.writeUInt32(id);
return p;
}
bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data, int numEnchantSlots) {
// Per-entry fixed size: auctionId(4) + itemEntry(4) + enchantSlots×3×4 +
// randProp(4) + suffix(4) + stack(4) + charges(4) + flags(4) +
// ownerGuid(8) + startBid(4) + outbid(4) + buyout(4) + expire(4) +
// bidderGuid(8) + curBid(4)
// Classic: numEnchantSlots=1 → 80 bytes/entry
// TBC/WotLK: numEnchantSlots=3 → 104 bytes/entry
if (!packet.hasRemaining(4)) return false;
uint32_t count = packet.readUInt32();
// Cap auction count to prevent unbounded memory allocation
const uint32_t MAX_AUCTION_RESULTS = 256;
if (count > MAX_AUCTION_RESULTS) {
LOG_WARNING("AuctionListResultParser: count capped (requested=", count, ")");
count = MAX_AUCTION_RESULTS;
}
data.auctions.clear();
data.auctions.reserve(count);
const size_t minPerEntry = static_cast<size_t>(8 + numEnchantSlots * 12 + 28 + 8 + 8);
for (uint32_t i = 0; i < count; ++i) {
if (!packet.hasRemaining(minPerEntry)) break;
AuctionEntry e;
e.auctionId = packet.readUInt32();
e.itemEntry = packet.readUInt32();
// First enchant slot always present
e.enchantId = packet.readUInt32();
packet.readUInt32(); // enchant1 duration
packet.readUInt32(); // enchant1 charges
// Extra enchant slots for TBC/WotLK
for (int s = 1; s < numEnchantSlots; ++s) {
packet.readUInt32(); // enchant N id
packet.readUInt32(); // enchant N duration
packet.readUInt32(); // enchant N charges
}
e.randomPropertyId = packet.readUInt32();
e.suffixFactor = packet.readUInt32();
e.stackCount = packet.readUInt32();
packet.readUInt32(); // item charges
packet.readUInt32(); // item flags (unused)
e.ownerGuid = packet.readUInt64();
e.startBid = packet.readUInt32();
e.minBidIncrement = packet.readUInt32();
e.buyoutPrice = packet.readUInt32();
e.timeLeftMs = packet.readUInt32();
e.bidderGuid = packet.readUInt64();
e.currentBid = packet.readUInt32();
data.auctions.push_back(e);
}
if (packet.hasRemaining(8)) {
data.totalCount = packet.readUInt32();
data.searchDelay = packet.readUInt32();
}
return true;
}
bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandResult& data) {
if (!packet.hasRemaining(12)) return false;
data.auctionId = packet.readUInt32();
data.action = packet.readUInt32();
data.errorCode = packet.readUInt32();
if (data.errorCode != 0 && data.action == 2 && packet.hasRemaining(4)) {
data.bidError = packet.readUInt32();
}
return true;
}
// ============================================================
// Pet Stable System
// ============================================================
network::Packet ListStabledPetsPacket::build(uint64_t stableMasterGuid) {
network::Packet p(wireOpcode(Opcode::MSG_LIST_STABLED_PETS));
p.writeUInt64(stableMasterGuid);
return p;
}
network::Packet StablePetPacket::build(uint64_t stableMasterGuid, uint8_t slot) {
network::Packet p(wireOpcode(Opcode::CMSG_STABLE_PET));
p.writeUInt64(stableMasterGuid);
p.writeUInt8(slot);
return p;
}
network::Packet UnstablePetPacket::build(uint64_t stableMasterGuid, uint32_t petNumber) {
network::Packet p(wireOpcode(Opcode::CMSG_UNSTABLE_PET));
p.writeUInt64(stableMasterGuid);
p.writeUInt32(petNumber);
return p;
}
network::Packet PetRenamePacket::build(uint64_t petGuid, const std::string& name, uint8_t isDeclined) {
network::Packet p(wireOpcode(Opcode::CMSG_PET_RENAME));
p.writeUInt64(petGuid);
p.writeString(name); // null-terminated
p.writeUInt8(isDeclined);
return p;
}
network::Packet SetTitlePacket::build(int32_t titleBit) {
// CMSG_SET_TITLE: int32 titleBit (-1 = remove active title)
network::Packet p(wireOpcode(Opcode::CMSG_SET_TITLE));
p.writeUInt32(static_cast<uint32_t>(titleBit));
return p;
}
network::Packet AlterAppearancePacket::build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) {
// CMSG_ALTER_APPEARANCE: uint32 hairStyle + uint32 hairColor + uint32 facialHair
network::Packet p(wireOpcode(Opcode::CMSG_ALTER_APPEARANCE));
p.writeUInt32(hairStyle);
p.writeUInt32(hairColor);
p.writeUInt32(facialHair);
return p;
}
} // namespace game
} // namespace wowee

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff