Kelsidavis-WoWee/src/game/social_handler.cpp
Kelsi 35b952bc6f fix: SMSG_IGNORE_LIST read phantom string field after each GUID
The packet only contains uint8 count + count×uint64 GUIDs, but the
handler called readString() after each GUID. This consumed raw bytes of
subsequent GUIDs as a string, corrupting all entries after the first.
Now stores GUIDs in ignoreListGuids_ and resolves names asynchronously
via SMSG_NAME_QUERY_RESPONSE, matching the friends list pattern.

Also fixes unsafe static_pointer_cast in ready check (no type guard)
and removes redundant packetHasRemaining wrapper (duplicates Packet API).
2026-03-29 18:11:29 -07:00

2716 lines
125 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "game/social_handler.hpp"
#include "game/game_handler.hpp"
#include "game/game_utils.hpp"
#include "game/entity.hpp"
#include "game/packet_parsers.hpp"
#include "game/update_field_table.hpp"
#include "game/opcode_table.hpp"
#include "audio/ui_sound_manager.hpp"
#include "network/world_socket.hpp"
#include "rendering/renderer.hpp"
#include "core/logger.hpp"
#include "core/application.hpp"
#include <algorithm>
#include <cstdio>
#include <cmath>
namespace wowee {
namespace game {
static const char* lfgJoinResultString(uint8_t result) {
switch (result) {
case 0: return nullptr;
case 1: return "Role check failed.";
case 2: return "No LFG slots available for your group.";
case 3: return "No LFG object found.";
case 4: return "No slots available (player).";
case 5: return "No slots available (party).";
case 6: return "Dungeon requirements not met by all members.";
case 7: return "Party members are from different realms.";
case 8: return "Not all members are present.";
case 9: return "Get info timeout.";
case 10: return "Invalid dungeon slot.";
case 11: return "You are marked as a deserter.";
case 12: return "A party member is marked as a deserter.";
case 13: return "You are on a random dungeon cooldown.";
case 14: return "A party member is on a random dungeon cooldown.";
case 16: return "No spec/role available.";
default: return "Cannot join dungeon finder.";
}
}
static const char* lfgTeleportDeniedString(uint8_t reason) {
switch (reason) {
case 0: return "You are not in a LFG group.";
case 1: return "You are not in the dungeon.";
case 2: return "You have a summon pending.";
case 3: return "You are dead.";
case 4: return "You have Deserter.";
case 5: return "You do not meet the requirements.";
default: return "Teleport to dungeon denied.";
}
}
static const std::string kEmptyString;
SocialHandler::SocialHandler(GameHandler& owner)
: owner_(owner) {}
// ============================================================
// registerOpcodes
// ============================================================
void SocialHandler::registerOpcodes(DispatchTable& table) {
// ---- Player info queries / social ----
table[Opcode::SMSG_QUERY_TIME_RESPONSE] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleQueryTimeResponse(packet);
};
table[Opcode::SMSG_PLAYED_TIME] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handlePlayedTime(packet);
};
table[Opcode::SMSG_WHO] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleWho(packet);
};
table[Opcode::SMSG_WHOIS] = [this](network::Packet& packet) {
if (packet.getReadPos() < packet.getSize()) {
std::string whoisText = packet.readString();
if (!whoisText.empty()) {
std::string line;
for (char c : whoisText) {
if (c == '\n') { if (!line.empty()) owner_.addSystemChatMessage("[Whois] " + line); line.clear(); }
else line += c;
}
if (!line.empty()) owner_.addSystemChatMessage("[Whois] " + line);
LOG_INFO("SMSG_WHOIS: ", whoisText);
}
}
};
table[Opcode::SMSG_FRIEND_STATUS] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleFriendStatus(packet);
};
table[Opcode::SMSG_CONTACT_LIST] = [this](network::Packet& packet) { handleContactList(packet); };
table[Opcode::SMSG_FRIEND_LIST] = [this](network::Packet& packet) { handleFriendList(packet); };
table[Opcode::SMSG_IGNORE_LIST] = [this](network::Packet& packet) {
// Format: uint8 count + count × uint64 guid (no name strings in packet).
// Names are resolved via SMSG_NAME_QUERY_RESPONSE after the list arrives.
if (!packet.hasRemaining(1)) return;
uint8_t ignCount = packet.readUInt8();
owner_.ignoreListGuids_.clear();
for (uint8_t i = 0; i < ignCount; ++i) {
if (!packet.hasRemaining(8)) break;
uint64_t ignGuid = packet.readUInt64();
if (ignGuid != 0) {
owner_.ignoreListGuids_.insert(ignGuid);
// Query name so UI can display it later
owner_.queryPlayerName(ignGuid);
}
}
LOG_DEBUG("SMSG_IGNORE_LIST: loaded ", (int)ignCount, " ignored players");
};
table[Opcode::MSG_RANDOM_ROLL] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleRandomRoll(packet);
};
// ---- Logout ----
table[Opcode::SMSG_LOGOUT_RESPONSE] = [this](network::Packet& packet) { handleLogoutResponse(packet); };
table[Opcode::SMSG_LOGOUT_COMPLETE] = [this](network::Packet& packet) { handleLogoutComplete(packet); };
// ---- Inspect ----
table[Opcode::SMSG_INSPECT_TALENT] = [this](network::Packet& packet) { handleInspectResults(packet); };
table[Opcode::SMSG_INSPECT_RESULTS_UPDATE] = [this](network::Packet& packet) { handleInspectResults(packet); };
// ---- Group ----
table[Opcode::SMSG_GROUP_INVITE] = [this](network::Packet& packet) { handleGroupInvite(packet); };
table[Opcode::SMSG_GROUP_DECLINE] = [this](network::Packet& packet) { handleGroupDecline(packet); };
table[Opcode::SMSG_GROUP_LIST] = [this](network::Packet& packet) { handleGroupList(packet); };
table[Opcode::SMSG_GROUP_DESTROYED] = [this](network::Packet& /*packet*/) {
partyData.members.clear();
partyData.memberCount = 0;
partyData.leaderGuid = 0;
owner_.addUIError("Your party has been disbanded.");
owner_.addSystemChatMessage("Your party has been disbanded.");
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("GROUP_ROSTER_UPDATE", {});
owner_.addonEventCallback_("PARTY_MEMBERS_CHANGED", {});
}
};
table[Opcode::SMSG_GROUP_CANCEL] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("Group invite cancelled.");
};
table[Opcode::SMSG_GROUP_UNINVITE] = [this](network::Packet& packet) { handleGroupUninvite(packet); };
table[Opcode::SMSG_PARTY_COMMAND_RESULT] = [this](network::Packet& packet) { handlePartyCommandResult(packet); };
table[Opcode::SMSG_PARTY_MEMBER_STATS] = [this](network::Packet& packet) { handlePartyMemberStats(packet, false); };
table[Opcode::SMSG_PARTY_MEMBER_STATS_FULL] = [this](network::Packet& packet) { handlePartyMemberStats(packet, true); };
// ---- Ready check ----
table[Opcode::MSG_RAID_READY_CHECK] = [this](network::Packet& packet) {
pendingReadyCheck_ = true;
readyCheckReadyCount_ = 0;
readyCheckNotReadyCount_ = 0;
readyCheckInitiator_.clear();
readyCheckResults_.clear();
if (packet.getSize() - packet.getReadPos() >= 8) {
uint64_t initiatorGuid = packet.readUInt64();
auto entity = owner_.getEntityManager().getEntity(initiatorGuid);
if (auto* unit = dynamic_cast<Unit*>(entity.get()))
readyCheckInitiator_ = unit->getName();
}
if (readyCheckInitiator_.empty() && partyData.leaderGuid != 0) {
for (const auto& member : partyData.members) {
if (member.guid == partyData.leaderGuid) { readyCheckInitiator_ = member.name; break; }
}
}
owner_.addSystemChatMessage(readyCheckInitiator_.empty()
? "Ready check initiated!"
: readyCheckInitiator_ + " initiated a ready check!");
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("READY_CHECK", {readyCheckInitiator_});
};
table[Opcode::MSG_RAID_READY_CHECK_CONFIRM] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 9) { packet.setReadPos(packet.getSize()); return; }
uint64_t respGuid = packet.readUInt64();
uint8_t isReady = packet.readUInt8();
if (isReady) ++readyCheckReadyCount_; else ++readyCheckNotReadyCount_;
auto nit = owner_.getPlayerNameCache().find(respGuid);
std::string rname;
if (nit != owner_.getPlayerNameCache().end()) rname = nit->second;
else {
// Only cast to Unit if the entity actually is one — a raw
// static_pointer_cast on a GameObject would be undefined behavior.
auto ent = owner_.getEntityManager().getEntity(respGuid);
if (ent && (ent->getType() == ObjectType::UNIT || ent->getType() == ObjectType::PLAYER))
rname = std::static_pointer_cast<game::Unit>(ent)->getName();
}
if (!rname.empty()) {
bool found = false;
for (auto& r : readyCheckResults_) {
if (r.name == rname) { r.ready = (isReady != 0); found = true; break; }
}
if (!found) readyCheckResults_.push_back({ rname, isReady != 0 });
char rbuf[128];
std::snprintf(rbuf, sizeof(rbuf), "%s is %s.", rname.c_str(), isReady ? "Ready" : "Not Ready");
owner_.addSystemChatMessage(rbuf);
}
if (owner_.addonEventCallback_) {
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)respGuid);
owner_.addonEventCallback_("READY_CHECK_CONFIRM", {guidBuf, isReady ? "1" : "0"});
}
};
table[Opcode::MSG_RAID_READY_CHECK_FINISHED] = [this](network::Packet& /*packet*/) {
char fbuf[128];
std::snprintf(fbuf, sizeof(fbuf), "Ready check complete: %u ready, %u not ready.",
readyCheckReadyCount_, readyCheckNotReadyCount_);
owner_.addSystemChatMessage(fbuf);
pendingReadyCheck_ = false;
readyCheckReadyCount_ = 0;
readyCheckNotReadyCount_ = 0;
readyCheckResults_.clear();
if (owner_.addonEventCallback_) owner_.addonEventCallback_("READY_CHECK_FINISHED", {});
};
table[Opcode::SMSG_RAID_INSTANCE_INFO] = [this](network::Packet& packet) { handleRaidInstanceInfo(packet); };
// ---- Duels ----
table[Opcode::SMSG_DUEL_REQUESTED] = [this](network::Packet& packet) { handleDuelRequested(packet); };
table[Opcode::SMSG_DUEL_COMPLETE] = [this](network::Packet& packet) { handleDuelComplete(packet); };
table[Opcode::SMSG_DUEL_WINNER] = [this](network::Packet& packet) { handleDuelWinner(packet); };
table[Opcode::SMSG_DUEL_OUTOFBOUNDS] = [this](network::Packet& /*packet*/) {
owner_.addUIError("You are out of the duel area!");
owner_.addSystemChatMessage("You are out of the duel area!");
};
table[Opcode::SMSG_DUEL_INBOUNDS] = [this](network::Packet& /*packet*/) {};
table[Opcode::SMSG_DUEL_COUNTDOWN] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t ms = packet.readUInt32();
duelCountdownMs_ = (ms > 0 && ms <= 30000) ? ms : 3000;
duelCountdownStartedAt_ = std::chrono::steady_clock::now();
}
};
table[Opcode::SMSG_PARTYKILLLOG] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 16) return;
uint64_t killerGuid = packet.readUInt64();
uint64_t victimGuid = packet.readUInt64();
auto nameFor = [this](uint64_t g) -> std::string {
auto nit = owner_.getPlayerNameCache().find(g);
if (nit != owner_.getPlayerNameCache().end()) return nit->second;
auto ent = owner_.getEntityManager().getEntity(g);
if (ent && (ent->getType() == game::ObjectType::UNIT ||
ent->getType() == game::ObjectType::PLAYER))
return std::static_pointer_cast<game::Unit>(ent)->getName();
return {};
};
std::string killerName = nameFor(killerGuid);
std::string victimName = nameFor(victimGuid);
if (!killerName.empty() && !victimName.empty()) {
char buf[256];
std::snprintf(buf, sizeof(buf), "%s killed %s.", killerName.c_str(), victimName.c_str());
owner_.addSystemChatMessage(buf);
}
};
// ---- Guild ----
table[Opcode::SMSG_GUILD_INFO] = [this](network::Packet& packet) { handleGuildInfo(packet); };
table[Opcode::SMSG_GUILD_ROSTER] = [this](network::Packet& packet) { handleGuildRoster(packet); };
table[Opcode::SMSG_GUILD_QUERY_RESPONSE] = [this](network::Packet& packet) { handleGuildQueryResponse(packet); };
table[Opcode::SMSG_GUILD_EVENT] = [this](network::Packet& packet) { handleGuildEvent(packet); };
table[Opcode::SMSG_GUILD_INVITE] = [this](network::Packet& packet) { handleGuildInvite(packet); };
table[Opcode::SMSG_GUILD_COMMAND_RESULT] = [this](network::Packet& packet) { handleGuildCommandResult(packet); };
table[Opcode::SMSG_PETITION_SHOWLIST] = [this](network::Packet& packet) { handlePetitionShowlist(packet); };
table[Opcode::SMSG_TURN_IN_PETITION_RESULTS] = [this](network::Packet& packet) { handleTurnInPetitionResults(packet); };
table[Opcode::SMSG_OFFER_PETITION_ERROR] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t err = packet.readUInt32();
if (err == 1) owner_.addSystemChatMessage("Player is already in a guild.");
else if (err == 2) owner_.addSystemChatMessage("Player already has a petition.");
else owner_.addSystemChatMessage("Cannot offer petition to that player.");
}
};
table[Opcode::SMSG_PETITION_QUERY_RESPONSE] = [this](network::Packet& packet) { handlePetitionQueryResponse(packet); };
table[Opcode::SMSG_PETITION_SHOW_SIGNATURES] = [this](network::Packet& packet) { handlePetitionShowSignatures(packet); };
table[Opcode::SMSG_PETITION_SIGN_RESULTS] = [this](network::Packet& packet) { handlePetitionSignResults(packet); };
// ---- Battlefield / BG ----
table[Opcode::SMSG_BATTLEFIELD_STATUS] = [this](network::Packet& packet) { handleBattlefieldStatus(packet); };
table[Opcode::SMSG_BATTLEFIELD_LIST] = [this](network::Packet& packet) { handleBattlefieldList(packet); };
table[Opcode::SMSG_BATTLEFIELD_PORT_DENIED] = [this](network::Packet& /*packet*/) {
owner_.addUIError("Battlefield port denied.");
owner_.addSystemChatMessage("Battlefield port denied.");
};
table[Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS] = [this](network::Packet& packet) {
bgPlayerPositions_.clear();
for (int grp = 0; grp < 2; ++grp) {
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t count = packet.readUInt32();
for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 16; ++i) {
BgPlayerPosition pos;
pos.guid = packet.readUInt64();
pos.wowX = packet.readFloat();
pos.wowY = packet.readFloat();
pos.group = grp;
bgPlayerPositions_.push_back(pos);
}
}
};
table[Opcode::SMSG_REMOVED_FROM_PVP_QUEUE] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("You have been removed from the PvP queue.");
};
table[Opcode::SMSG_GROUP_JOINED_BATTLEGROUND] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("Your group has joined the battleground.");
};
table[Opcode::SMSG_JOINED_BATTLEGROUND_QUEUE] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("You have joined the battleground queue.");
};
table[Opcode::SMSG_BATTLEGROUND_PLAYER_JOINED] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 8) {
uint64_t guid = packet.readUInt64();
auto it = owner_.getPlayerNameCache().find(guid);
if (it != owner_.getPlayerNameCache().end() && !it->second.empty())
owner_.addSystemChatMessage(it->second + " has entered the battleground.");
}
};
table[Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 8) {
uint64_t guid = packet.readUInt64();
auto it = owner_.getPlayerNameCache().find(guid);
if (it != owner_.getPlayerNameCache().end() && !it->second.empty())
owner_.addSystemChatMessage(it->second + " has left the battleground.");
}
};
// ---- Instance ----
for (auto op : { Opcode::SMSG_INSTANCE_DIFFICULTY, Opcode::MSG_SET_DUNGEON_DIFFICULTY }) {
table[op] = [this](network::Packet& packet) { handleInstanceDifficulty(packet); };
// ---- Guild / RAF / PvP AFK (moved from GameHandler) ----
table[Opcode::SMSG_GUILD_DECLINE] = [this](network::Packet& packet) {
if (packet.hasData()) {
std::string name = packet.readString();
owner_.addSystemChatMessage(name + " declined your guild invitation.");
}
};
table[Opcode::SMSG_REFER_A_FRIEND_EXPIRED] = [this](network::Packet& packet) {
owner_.addSystemChatMessage("Your Recruit-A-Friend link has expired.");
packet.skipAll();
};
table[Opcode::SMSG_REFER_A_FRIEND_FAILURE] = [this](network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t reason = packet.readUInt32();
static const char* kRafErrors[] = {
"Not eligible", // 0
"Target not eligible", // 1
"Too many referrals", // 2
"Wrong faction", // 3
"Not a recruit", // 4
"Recruit requirements not met", // 5
"Level above requirement", // 6
"Friend needs account upgrade", // 7
};
const char* msg = (reason < 8) ? kRafErrors[reason]
: "Recruit-A-Friend failed.";
owner_.addSystemChatMessage(std::string("Recruit-A-Friend: ") + msg);
}
packet.skipAll();
};
table[Opcode::SMSG_REPORT_PVP_AFK_RESULT] = [this](network::Packet& packet) {
if (packet.hasRemaining(1)) {
uint8_t result = packet.readUInt8();
if (result == 0)
owner_.addSystemChatMessage("AFK report submitted.");
else
owner_.addSystemChatMessage("Cannot report that player as AFK right now.");
}
packet.skipAll();
};
}
table[Opcode::SMSG_INSTANCE_SAVE_CREATED] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("You are now saved to this instance.");
};
table[Opcode::SMSG_RAID_INSTANCE_MESSAGE] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 12) return;
uint32_t msgType = packet.readUInt32();
uint32_t mapId = packet.readUInt32();
packet.readUInt32(); // diff
std::string mapLabel = owner_.getMapName(mapId);
if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId);
if (msgType == 1 && packet.getSize() - packet.getReadPos() >= 4) {
uint32_t timeLeft = packet.readUInt32();
owner_.addSystemChatMessage(mapLabel + " will reset in " + std::to_string(timeLeft / 60) + " minute(s).");
} else if (msgType == 2) {
owner_.addSystemChatMessage("You have been saved to " + mapLabel + ".");
} else if (msgType == 3) {
owner_.addSystemChatMessage("Welcome to " + mapLabel + ".");
}
};
table[Opcode::SMSG_INSTANCE_RESET] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t mapId = packet.readUInt32();
auto it = std::remove_if(instanceLockouts_.begin(), instanceLockouts_.end(),
[mapId](const InstanceLockout& lo){ return lo.mapId == mapId; });
instanceLockouts_.erase(it, instanceLockouts_.end());
std::string mapLabel = owner_.getMapName(mapId);
if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId);
owner_.addSystemChatMessage(mapLabel + " has been reset.");
};
table[Opcode::SMSG_INSTANCE_RESET_FAILED] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 8) return;
uint32_t mapId = packet.readUInt32();
uint32_t reason = packet.readUInt32();
static const char* resetFailReasons[] = {
"Not max level.", "Offline party members.", "Party members inside.",
"Party members changing zone.", "Heroic difficulty only."
};
const char* reasonMsg = (reason < 5) ? resetFailReasons[reason] : "Unknown reason.";
std::string mapLabel = owner_.getMapName(mapId);
if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId);
owner_.addUIError("Cannot reset " + mapLabel + ": " + reasonMsg);
owner_.addSystemChatMessage("Cannot reset " + mapLabel + ": " + reasonMsg);
};
table[Opcode::SMSG_INSTANCE_LOCK_WARNING_QUERY] = [this](network::Packet& packet) {
if (!owner_.socket || packet.getSize() - packet.getReadPos() < 17) return;
uint32_t ilMapId = packet.readUInt32();
uint32_t ilDiff = packet.readUInt32();
uint32_t ilTimeLeft = packet.readUInt32();
packet.readUInt32(); // unk
uint8_t ilLocked = packet.readUInt8();
std::string ilName = owner_.getMapName(ilMapId);
if (ilName.empty()) ilName = "instance #" + std::to_string(ilMapId);
static const char* kDiff[] = {"Normal","Heroic","25-Man","25-Man Heroic"};
std::string ilMsg = "Entering " + ilName;
if (ilDiff < 4) ilMsg += std::string(" (") + kDiff[ilDiff] + ")";
if (ilLocked && ilTimeLeft > 0)
ilMsg += "" + std::to_string(ilTimeLeft / 60) + " min remaining.";
else
ilMsg += ".";
owner_.addSystemChatMessage(ilMsg);
network::Packet resp(wireOpcode(Opcode::CMSG_INSTANCE_LOCK_RESPONSE));
resp.writeUInt8(1);
owner_.socket->send(resp);
};
// ---- LFG ----
table[Opcode::SMSG_LFG_JOIN_RESULT] = [this](network::Packet& packet) { handleLfgJoinResult(packet); };
table[Opcode::SMSG_LFG_QUEUE_STATUS] = [this](network::Packet& packet) { handleLfgQueueStatus(packet); };
table[Opcode::SMSG_LFG_PROPOSAL_UPDATE] = [this](network::Packet& packet) { handleLfgProposalUpdate(packet); };
table[Opcode::SMSG_LFG_ROLE_CHECK_UPDATE] = [this](network::Packet& packet) { handleLfgRoleCheckUpdate(packet); };
for (auto op : { Opcode::SMSG_LFG_UPDATE_PLAYER, Opcode::SMSG_LFG_UPDATE_PARTY }) {
table[op] = [this](network::Packet& packet) { handleLfgUpdatePlayer(packet); };
}
table[Opcode::SMSG_LFG_PLAYER_REWARD] = [this](network::Packet& packet) { handleLfgPlayerReward(packet); };
table[Opcode::SMSG_LFG_BOOT_PROPOSAL_UPDATE] = [this](network::Packet& packet) { handleLfgBootProposalUpdate(packet); };
table[Opcode::SMSG_LFG_TELEPORT_DENIED] = [this](network::Packet& packet) { handleLfgTeleportDenied(packet); };
table[Opcode::SMSG_LFG_DISABLED] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("The Dungeon Finder is currently disabled.");
};
table[Opcode::SMSG_LFG_OFFER_CONTINUE] = [this](network::Packet& /*packet*/) {
owner_.addSystemChatMessage("Dungeon Finder: You may continue your dungeon.");
};
table[Opcode::SMSG_LFG_ROLE_CHOSEN] = [this](network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 13) { packet.setReadPos(packet.getSize()); return; }
uint64_t roleGuid = packet.readUInt64();
uint8_t ready = packet.readUInt8();
uint32_t roles = packet.readUInt32();
std::string roleName;
if (roles & 0x02) roleName += "Tank ";
if (roles & 0x04) roleName += "Healer ";
if (roles & 0x08) roleName += "DPS ";
if (roleName.empty()) roleName = "None";
std::string pName = "A player";
if (auto e = owner_.getEntityManager().getEntity(roleGuid))
if (auto u = std::dynamic_pointer_cast<Unit>(e))
pName = u->getName();
if (ready) owner_.addSystemChatMessage(pName + " has chosen: " + roleName);
packet.setReadPos(packet.getSize());
};
for (auto op : { Opcode::SMSG_LFG_UPDATE_SEARCH, Opcode::SMSG_UPDATE_LFG_LIST,
Opcode::SMSG_LFG_PLAYER_INFO, Opcode::SMSG_LFG_PARTY_INFO }) {
table[op] = [](network::Packet& packet) { packet.setReadPos(packet.getSize()); };
}
table[Opcode::SMSG_OPEN_LFG_DUNGEON_FINDER] = [this](network::Packet& packet) {
packet.setReadPos(packet.getSize());
if (owner_.openLfgCallback_) owner_.openLfgCallback_();
};
// ---- Arena ----
table[Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT] = [this](network::Packet& packet) { handleArenaTeamCommandResult(packet); };
table[Opcode::SMSG_ARENA_TEAM_QUERY_RESPONSE] = [this](network::Packet& packet) { handleArenaTeamQueryResponse(packet); };
table[Opcode::SMSG_ARENA_TEAM_ROSTER] = [this](network::Packet& packet) { handleArenaTeamRoster(packet); };
table[Opcode::SMSG_ARENA_TEAM_INVITE] = [this](network::Packet& packet) { handleArenaTeamInvite(packet); };
table[Opcode::SMSG_ARENA_TEAM_EVENT] = [this](network::Packet& packet) { handleArenaTeamEvent(packet); };
table[Opcode::SMSG_ARENA_TEAM_STATS] = [this](network::Packet& packet) { handleArenaTeamStats(packet); };
table[Opcode::SMSG_ARENA_ERROR] = [this](network::Packet& packet) { handleArenaError(packet); };
table[Opcode::MSG_PVP_LOG_DATA] = [this](network::Packet& packet) { handlePvpLogData(packet); };
// ---- Factions / group leader ----
table[Opcode::SMSG_INITIALIZE_FACTIONS] = [this](network::Packet& p) { handleInitializeFactions(p); };
table[Opcode::SMSG_SET_FACTION_STANDING] = [this](network::Packet& p) { handleSetFactionStanding(p); };
table[Opcode::SMSG_SET_FACTION_ATWAR] = [this](network::Packet& p) { handleSetFactionAtWar(p); };
table[Opcode::SMSG_SET_FACTION_VISIBLE] = [this](network::Packet& p) { handleSetFactionVisible(p); };
table[Opcode::SMSG_GROUP_SET_LEADER] = [this](network::Packet& p) { handleGroupSetLeader(p); };
}
// ============================================================
// Non-inline accessors requiring GameHandler
// ============================================================
std::string SocialHandler::getWhoAreaName(uint32_t zoneId) const {
return owner_.getAreaName(zoneId);
}
std::string SocialHandler::getCurrentLfgDungeonName() const {
return owner_.getLfgDungeonName(lfgDungeonId_);
}
bool SocialHandler::isInGuild() const {
if (!guildName_.empty()) return true;
const Character* ch = owner_.getActiveCharacter();
return ch && ch->hasGuild();
}
uint32_t SocialHandler::getEntityGuildId(uint64_t guid) const {
auto entity = owner_.getEntityManager().getEntity(guid);
if (!entity || entity->getType() != ObjectType::PLAYER) return 0;
const uint16_t ufUnitEnd = fieldIndex(UF::UNIT_END);
if (ufUnitEnd == 0xFFFF) return 0;
return entity->getField(ufUnitEnd + 3);
}
const std::string& SocialHandler::lookupGuildName(uint32_t guildId) {
if (guildId == 0) return kEmptyString;
auto it = guildNameCache_.find(guildId);
if (it != guildNameCache_.end()) return it->second;
if (pendingGuildNameQueries_.insert(guildId).second) {
queryGuildInfo(guildId);
}
return kEmptyString;
}
// ============================================================
// Inspection
// ============================================================
void SocialHandler::inspectTarget() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) {
LOG_WARNING("Cannot inspect: not in world or not connected");
return;
}
if (owner_.targetGuid == 0) {
owner_.addSystemChatMessage("You must target a player to inspect.");
return;
}
auto target = owner_.getTarget();
if (!target || target->getType() != ObjectType::PLAYER) {
owner_.addSystemChatMessage("You can only inspect players.");
return;
}
auto packet = InspectPacket::build(owner_.targetGuid);
owner_.socket->send(packet);
if (isActiveExpansion("wotlk")) {
auto achPkt = QueryInspectAchievementsPacket::build(owner_.targetGuid);
owner_.socket->send(achPkt);
}
auto player = std::static_pointer_cast<Player>(target);
std::string name = player->getName().empty() ? "Target" : player->getName();
owner_.addSystemChatMessage("Inspecting " + name + "...");
LOG_INFO("Sent inspect request for player: ", name, " (GUID: 0x", std::hex, owner_.targetGuid, std::dec, ")");
}
void SocialHandler::handleInspectResults(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
uint8_t talentType = packet.readUInt8();
if (talentType == 0) {
// Own talent info
if (packet.getSize() - packet.getReadPos() < 6) {
LOG_DEBUG("SMSG_TALENTS_INFO type=0: too short");
return;
}
uint32_t unspentTalents = packet.readUInt32();
uint8_t talentGroupCount = packet.readUInt8();
uint8_t activeTalentGroup = packet.readUInt8();
if (activeTalentGroup > 1) activeTalentGroup = 0;
owner_.activeTalentSpec_ = activeTalentGroup;
for (uint8_t g = 0; g < talentGroupCount && g < 2; ++g) {
if (packet.getSize() - packet.getReadPos() < 1) break;
uint8_t talentCount = packet.readUInt8();
owner_.learnedTalents_[g].clear();
for (uint8_t t = 0; t < talentCount; ++t) {
if (packet.getSize() - packet.getReadPos() < 5) break;
uint32_t talentId = packet.readUInt32();
uint8_t rank = packet.readUInt8();
owner_.learnedTalents_[g][talentId] = rank + 1u;
}
if (packet.getSize() - packet.getReadPos() < 1) break;
owner_.learnedGlyphs_[g].fill(0);
uint8_t glyphCount = packet.readUInt8();
for (uint8_t gl = 0; gl < glyphCount; ++gl) {
if (packet.getSize() - packet.getReadPos() < 2) break;
uint16_t glyphId = packet.readUInt16();
if (gl < GameHandler::MAX_GLYPH_SLOTS) owner_.learnedGlyphs_[g][gl] = glyphId;
}
}
owner_.unspentTalentPoints_[activeTalentGroup] = static_cast<uint8_t>(
unspentTalents > 255 ? 255 : unspentTalents);
if (!owner_.talentsInitialized_) {
owner_.talentsInitialized_ = true;
if (unspentTalents > 0) {
owner_.addSystemChatMessage("You have " + std::to_string(unspentTalents)
+ " unspent talent point" + (unspentTalents != 1 ? "s" : "") + ".");
}
}
LOG_INFO("SMSG_TALENTS_INFO type=0: unspent=", unspentTalents,
" groups=", (int)talentGroupCount, " active=", (int)activeTalentGroup,
" learned=", owner_.learnedTalents_[activeTalentGroup].size());
return;
}
// talentType == 1: inspect result
const bool talentTbc = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (talentTbc ? 8u : 2u)) return;
uint64_t guid = talentTbc
? packet.readUInt64() : packet.readPackedGuid();
if (guid == 0) return;
size_t bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft < 6) {
LOG_WARNING("SMSG_TALENTS_INFO: too short after guid, ", bytesLeft, " bytes");
auto entity = owner_.getEntityManager().getEntity(guid);
std::string name = "Target";
if (entity) {
auto player = std::dynamic_pointer_cast<Player>(entity);
if (player && !player->getName().empty()) name = player->getName();
}
owner_.addSystemChatMessage("Inspecting " + name + " (no talent data available).");
return;
}
uint32_t unspentTalents = packet.readUInt32();
uint8_t talentGroupCount = packet.readUInt8();
uint8_t activeTalentGroup = packet.readUInt8();
auto entity = owner_.getEntityManager().getEntity(guid);
std::string playerName = "Target";
if (entity) {
auto player = std::dynamic_pointer_cast<Player>(entity);
if (player && !player->getName().empty()) playerName = player->getName();
}
uint32_t totalTalents = 0;
for (uint8_t g = 0; g < talentGroupCount && g < 2; ++g) {
bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft < 1) break;
uint8_t talentCount = packet.readUInt8();
for (uint8_t t = 0; t < talentCount; ++t) {
bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft < 5) break;
packet.readUInt32();
packet.readUInt8();
totalTalents++;
}
bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft < 1) break;
uint8_t glyphCount = packet.readUInt8();
for (uint8_t gl = 0; gl < glyphCount; ++gl) {
bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft < 2) break;
packet.readUInt16();
}
}
std::array<uint16_t, 19> enchantIds{};
bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft >= 4) {
uint32_t slotMask = packet.readUInt32();
for (int slot = 0; slot < 19; ++slot) {
if (slotMask & (1u << slot)) {
bytesLeft = packet.getSize() - packet.getReadPos();
if (bytesLeft < 2) break;
enchantIds[slot] = packet.readUInt16();
}
}
}
inspectResult_.guid = guid;
inspectResult_.playerName = playerName;
inspectResult_.totalTalents = totalTalents;
inspectResult_.unspentTalents = unspentTalents;
inspectResult_.talentGroups = talentGroupCount;
inspectResult_.activeTalentGroup = activeTalentGroup;
inspectResult_.enchantIds = enchantIds;
auto gearIt = owner_.inspectedPlayerItemEntries_.find(guid);
if (gearIt != owner_.inspectedPlayerItemEntries_.end()) {
inspectResult_.itemEntries = gearIt->second;
} else {
inspectResult_.itemEntries = {};
}
LOG_INFO("Inspect results for ", playerName, ": ", totalTalents, " talents, ",
unspentTalents, " unspent, ", (int)talentGroupCount, " specs");
if (owner_.addonEventCallback_) {
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)guid);
owner_.addonEventCallback_("INSPECT_READY", {guidBuf});
}
}
// ============================================================
// Server Info / Who / Social
// ============================================================
void SocialHandler::queryServerTime() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = QueryTimePacket::build();
owner_.socket->send(packet);
LOG_INFO("Requested server time");
}
void SocialHandler::requestPlayedTime() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = RequestPlayedTimePacket::build(true);
owner_.socket->send(packet);
LOG_INFO("Requested played time");
}
void SocialHandler::queryWho(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = WhoPacket::build(0, 0, playerName);
owner_.socket->send(packet);
LOG_INFO("Sent WHO query", playerName.empty() ? "" : " for: " + playerName);
}
void SocialHandler::addFriend(const std::string& playerName, const std::string& note) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto packet = AddFriendPacket::build(playerName, note);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Sending friend request to " + playerName + "...");
LOG_INFO("Sent friend request to: ", playerName);
}
void SocialHandler::removeFriend(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto it = owner_.friendsCache.find(playerName);
if (it == owner_.friendsCache.end()) {
owner_.addSystemChatMessage(playerName + " is not in your friends list.");
return;
}
auto packet = DelFriendPacket::build(it->second);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Removing " + playerName + " from friends list...");
LOG_INFO("Sent remove friend request for: ", playerName);
}
void SocialHandler::setFriendNote(const std::string& playerName, const std::string& note) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto it = owner_.friendsCache.find(playerName);
if (it == owner_.friendsCache.end()) {
owner_.addSystemChatMessage(playerName + " is not in your friends list.");
return;
}
auto packet = SetContactNotesPacket::build(it->second, note);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Updated note for " + playerName);
LOG_INFO("Set friend note for: ", playerName);
}
void SocialHandler::addIgnore(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto packet = AddIgnorePacket::build(playerName);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Adding " + playerName + " to ignore list...");
LOG_INFO("Sent ignore request for: ", playerName);
}
void SocialHandler::removeIgnore(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto it = owner_.ignoreCache.find(playerName);
if (it == owner_.ignoreCache.end()) {
owner_.addSystemChatMessage(playerName + " is not in your ignore list.");
return;
}
auto packet = DelIgnorePacket::build(it->second);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Removing " + playerName + " from ignore list...");
owner_.ignoreCache.erase(it);
LOG_INFO("Sent remove ignore request for: ", playerName);
}
void SocialHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (minRoll > maxRoll) std::swap(minRoll, maxRoll);
if (maxRoll > 10000) maxRoll = 10000;
auto packet = RandomRollPacket::build(minRoll, maxRoll);
owner_.socket->send(packet);
LOG_INFO("Rolled ", minRoll, "-", maxRoll);
}
// ============================================================
// Logout
// ============================================================
void SocialHandler::requestLogout() {
if (!owner_.socket) return;
if (loggingOut_) { owner_.addSystemChatMessage("Already logging out."); return; }
auto packet = LogoutRequestPacket::build();
owner_.socket->send(packet);
loggingOut_ = true;
LOG_INFO("Sent logout request");
}
void SocialHandler::cancelLogout() {
if (!owner_.socket) return;
if (!loggingOut_) { owner_.addSystemChatMessage("Not currently logging out."); return; }
auto packet = LogoutCancelPacket::build();
owner_.socket->send(packet);
loggingOut_ = false;
logoutCountdown_ = 0.0f;
owner_.addSystemChatMessage("Logout cancelled.");
LOG_INFO("Cancelled logout");
}
// ============================================================
// Guild
// ============================================================
void SocialHandler::requestGuildInfo() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildInfoPacket::build();
owner_.socket->send(packet);
}
void SocialHandler::requestGuildRoster() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildRosterPacket::build();
owner_.socket->send(packet);
owner_.addSystemChatMessage("Requesting guild roster...");
}
void SocialHandler::setGuildMotd(const std::string& motd) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildMotdPacket::build(motd);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Guild MOTD updated.");
}
void SocialHandler::promoteGuildMember(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto packet = GuildPromotePacket::build(playerName);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Promoting " + playerName + "...");
}
void SocialHandler::demoteGuildMember(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto packet = GuildDemotePacket::build(playerName);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Demoting " + playerName + "...");
}
void SocialHandler::leaveGuild() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildLeavePacket::build();
owner_.socket->send(packet);
owner_.addSystemChatMessage("Leaving guild...");
}
void SocialHandler::inviteToGuild(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name."); return; }
auto packet = GuildInvitePacket::build(playerName);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Inviting " + playerName + " to guild...");
}
void SocialHandler::kickGuildMember(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildRemovePacket::build(playerName);
owner_.socket->send(packet);
}
void SocialHandler::disbandGuild() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildDisbandPacket::build();
owner_.socket->send(packet);
}
void SocialHandler::setGuildLeader(const std::string& name) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildLeaderPacket::build(name);
owner_.socket->send(packet);
}
void SocialHandler::setGuildPublicNote(const std::string& name, const std::string& note) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildSetPublicNotePacket::build(name, note);
owner_.socket->send(packet);
}
void SocialHandler::setGuildOfficerNote(const std::string& name, const std::string& note) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildSetOfficerNotePacket::build(name, note);
owner_.socket->send(packet);
}
void SocialHandler::acceptGuildInvite() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
pendingGuildInvite_ = false;
auto packet = GuildAcceptPacket::build();
owner_.socket->send(packet);
}
void SocialHandler::declineGuildInvite() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
pendingGuildInvite_ = false;
auto packet = GuildDeclineInvitationPacket::build();
owner_.socket->send(packet);
}
void SocialHandler::queryGuildInfo(uint32_t guildId) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildQueryPacket::build(guildId);
owner_.socket->send(packet);
}
void SocialHandler::createGuild(const std::string& guildName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildCreatePacket::build(guildName);
owner_.socket->send(packet);
}
void SocialHandler::addGuildRank(const std::string& rankName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildAddRankPacket::build(rankName);
owner_.socket->send(packet);
requestGuildRoster();
}
void SocialHandler::deleteGuildRank() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GuildDelRankPacket::build();
owner_.socket->send(packet);
requestGuildRoster();
}
void SocialHandler::requestPetitionShowlist(uint64_t npcGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = PetitionShowlistPacket::build(npcGuid);
owner_.socket->send(packet);
}
void SocialHandler::buyPetition(uint64_t npcGuid, const std::string& guildName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = PetitionBuyPacket::build(npcGuid, guildName);
owner_.socket->send(packet);
}
void SocialHandler::signPetition(uint64_t petitionGuid) {
if (!owner_.socket || owner_.getState() != WorldState::IN_WORLD) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_PETITION_SIGN));
pkt.writeUInt64(petitionGuid);
pkt.writeUInt8(0);
owner_.socket->send(pkt);
}
void SocialHandler::turnInPetition(uint64_t petitionGuid) {
if (!owner_.socket || owner_.getState() != WorldState::IN_WORLD) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_TURN_IN_PETITION));
pkt.writeUInt64(petitionGuid);
owner_.socket->send(pkt);
}
// ============================================================
// Ready Check
// ============================================================
void SocialHandler::initiateReadyCheck() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (!isInGroup()) { owner_.addSystemChatMessage("You must be in a group to initiate a ready check."); return; }
auto packet = ReadyCheckPacket::build();
owner_.socket->send(packet);
owner_.addSystemChatMessage("Ready check initiated.");
}
void SocialHandler::respondToReadyCheck(bool ready) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = ReadyCheckConfirmPacket::build(ready);
owner_.socket->send(packet);
owner_.addSystemChatMessage(ready ? "You are ready." : "You are not ready.");
}
// ============================================================
// Duel
// ============================================================
void SocialHandler::acceptDuel() {
if (!pendingDuelRequest_ || owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
pendingDuelRequest_ = false;
auto pkt = DuelAcceptPacket::build();
owner_.socket->send(pkt);
owner_.addSystemChatMessage("You accept the duel.");
}
void SocialHandler::forfeitDuel() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
pendingDuelRequest_ = false;
auto packet = DuelCancelPacket::build();
owner_.socket->send(packet);
owner_.addSystemChatMessage("You have forfeited the duel.");
}
void SocialHandler::proposeDuel(uint64_t targetGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (targetGuid == 0) { owner_.addSystemChatMessage("You must target a player to challenge to a duel."); return; }
auto packet = DuelProposedPacket::build(targetGuid);
owner_.socket->send(packet);
owner_.addSystemChatMessage("You have challenged your target to a duel.");
}
void SocialHandler::reportPlayer(uint64_t targetGuid, const std::string& reason) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (targetGuid == 0) {
owner_.addSystemChatMessage("You must target a player to report.");
return;
}
auto packet = ComplainPacket::build(targetGuid, reason);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Player report submitted.");
LOG_INFO("Reported player: 0x", std::hex, targetGuid, std::dec, " reason=", reason);
}
void SocialHandler::handleDuelRequested(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 16) { packet.setReadPos(packet.getSize()); return; }
duelChallengerGuid_ = packet.readUInt64();
duelFlagGuid_ = packet.readUInt64();
duelChallengerName_.clear();
auto entity = owner_.getEntityManager().getEntity(duelChallengerGuid_);
if (auto* unit = dynamic_cast<Unit*>(entity.get()))
duelChallengerName_ = unit->getName();
if (duelChallengerName_.empty()) {
auto nit = owner_.getPlayerNameCache().find(duelChallengerGuid_);
if (nit != owner_.getPlayerNameCache().end()) duelChallengerName_ = nit->second;
}
if (duelChallengerName_.empty()) {
char tmp[32];
std::snprintf(tmp, sizeof(tmp), "0x%llX", static_cast<unsigned long long>(duelChallengerGuid_));
duelChallengerName_ = tmp;
}
pendingDuelRequest_ = true;
owner_.addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!");
if (auto* renderer = core::Application::getInstance().getRenderer())
if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect();
if (owner_.addonEventCallback_) owner_.addonEventCallback_("DUEL_REQUESTED", {duelChallengerName_});
}
void SocialHandler::handleDuelComplete(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
uint8_t started = packet.readUInt8();
pendingDuelRequest_ = false;
duelCountdownMs_ = 0;
if (!started) owner_.addSystemChatMessage("The duel was cancelled.");
if (owner_.addonEventCallback_) owner_.addonEventCallback_("DUEL_FINISHED", {});
}
void SocialHandler::handleDuelWinner(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 3) return;
uint8_t duelType = packet.readUInt8();
std::string winner = packet.readString();
std::string loser = packet.readString();
std::string msg = (duelType == 1)
? loser + " has fled from the duel. " + winner + " wins!"
: winner + " has defeated " + loser + " in a duel!";
owner_.addSystemChatMessage(msg);
}
// ============================================================
// Party / Raid
// ============================================================
void SocialHandler::inviteToGroup(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GroupInvitePacket::build(playerName);
owner_.socket->send(packet);
}
void SocialHandler::acceptGroupInvite() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
pendingGroupInvite = false;
auto packet = GroupAcceptPacket::build();
owner_.socket->send(packet);
}
void SocialHandler::declineGroupInvite() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
pendingGroupInvite = false;
auto packet = GroupDeclinePacket::build();
owner_.socket->send(packet);
}
void SocialHandler::leaveGroup() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GroupDisbandPacket::build();
owner_.socket->send(packet);
partyData = GroupListData{};
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("GROUP_ROSTER_UPDATE", {});
owner_.addonEventCallback_("PARTY_MEMBERS_CHANGED", {});
}
}
void SocialHandler::convertToRaid() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (!isInGroup()) {
owner_.addSystemChatMessage("You are not in a group.");
return;
}
if (partyData.leaderGuid != owner_.getPlayerGuid()) {
owner_.addSystemChatMessage("You must be the party leader to convert to raid.");
return;
}
if (partyData.groupType == 1) {
owner_.addSystemChatMessage("You are already in a raid group.");
return;
}
auto packet = GroupRaidConvertPacket::build();
owner_.socket->send(packet);
LOG_INFO("Sent CMSG_GROUP_RAID_CONVERT");
}
void SocialHandler::sendSetLootMethod(uint32_t method, uint32_t threshold, uint64_t masterLooterGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = SetLootMethodPacket::build(method, threshold, masterLooterGuid);
owner_.socket->send(packet);
LOG_INFO("sendSetLootMethod: method=", method, " threshold=", threshold);
}
void SocialHandler::uninvitePlayer(const std::string& playerName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (playerName.empty()) { owner_.addSystemChatMessage("You must specify a player name to uninvite."); return; }
auto packet = GroupUninvitePacket::build(playerName);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Removed " + playerName + " from the group.");
}
void SocialHandler::leaveParty() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = GroupDisbandPacket::build();
owner_.socket->send(packet);
owner_.addSystemChatMessage("You have left the group.");
}
void SocialHandler::setMainTank(uint64_t targetGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (targetGuid == 0) { owner_.addSystemChatMessage("You must have a target selected."); return; }
auto packet = RaidTargetUpdatePacket::build(0, targetGuid);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Main tank set.");
}
void SocialHandler::setMainAssist(uint64_t targetGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (targetGuid == 0) { owner_.addSystemChatMessage("You must have a target selected."); return; }
auto packet = RaidTargetUpdatePacket::build(1, targetGuid);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Main assist set.");
}
void SocialHandler::clearMainTank() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = RaidTargetUpdatePacket::build(0, 0);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Main tank cleared.");
}
void SocialHandler::clearMainAssist() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = RaidTargetUpdatePacket::build(1, 0);
owner_.socket->send(packet);
owner_.addSystemChatMessage("Main assist cleared.");
}
void SocialHandler::setRaidMark(uint64_t guid, uint8_t icon) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
if (icon == 0xFF) {
for (int i = 0; i < 8; ++i) {
if (raidTargetGuids_[i] == guid) {
auto packet = RaidTargetUpdatePacket::build(static_cast<uint8_t>(i), 0);
owner_.socket->send(packet);
break;
}
}
} else if (icon < 8) {
auto packet = RaidTargetUpdatePacket::build(icon, guid);
owner_.socket->send(packet);
}
}
void SocialHandler::requestRaidInfo() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = RequestRaidInfoPacket::build();
owner_.socket->send(packet);
owner_.addSystemChatMessage("Requesting raid lockout information...");
}
// ============================================================
// Group Handlers
// ============================================================
void SocialHandler::handleGroupInvite(network::Packet& packet) {
GroupInviteResponseData data;
if (!GroupInviteResponseParser::parse(packet, data)) return;
pendingGroupInvite = true;
pendingInviterName = data.inviterName;
if (!data.inviterName.empty())
owner_.addSystemChatMessage(data.inviterName + " has invited you to a group.");
if (auto* renderer = core::Application::getInstance().getRenderer())
if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect();
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PARTY_INVITE_REQUEST", {data.inviterName});
}
void SocialHandler::handleGroupDecline(network::Packet& packet) {
GroupDeclineData data;
if (!GroupDeclineResponseParser::parse(packet, data)) return;
owner_.addSystemChatMessage(data.playerName + " has declined your group invitation.");
}
void SocialHandler::handleGroupList(network::Packet& packet) {
const bool hasRoles = isActiveExpansion("wotlk");
const uint8_t prevLootMethod = partyData.lootMethod;
const bool wasInGroup = !partyData.isEmpty();
partyData = GroupListData{};
if (!GroupListParser::parse(packet, partyData, hasRoles)) return;
const bool nowInGroup = !partyData.isEmpty();
if (!nowInGroup && wasInGroup) {
owner_.addSystemChatMessage("You are no longer in a group.");
} else if (nowInGroup && !wasInGroup) {
owner_.addSystemChatMessage("You are now in a group.");
}
// Loot method change notification
if (wasInGroup && nowInGroup && partyData.lootMethod != prevLootMethod) {
static const char* kLootMethods[] = {
"Free for All", "Round Robin", "Master Looter", "Group Loot", "Need Before Greed"
};
const char* methodName = (partyData.lootMethod < 5) ? kLootMethods[partyData.lootMethod] : "Unknown";
owner_.addSystemChatMessage(std::string("Loot method changed to ") + methodName + ".");
}
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("GROUP_ROSTER_UPDATE", {});
owner_.addonEventCallback_("PARTY_MEMBERS_CHANGED", {});
if (partyData.groupType == 1)
owner_.addonEventCallback_("RAID_ROSTER_UPDATE", {});
}
}
void SocialHandler::handleGroupUninvite(network::Packet& packet) {
(void)packet;
partyData = GroupListData{};
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("GROUP_ROSTER_UPDATE", {});
owner_.addonEventCallback_("PARTY_MEMBERS_CHANGED", {});
owner_.addonEventCallback_("RAID_ROSTER_UPDATE", {});
}
owner_.addUIError("You have been removed from the group.");
owner_.addSystemChatMessage("You have been removed from the group.");
}
void SocialHandler::handlePartyCommandResult(network::Packet& packet) {
PartyCommandResultData data;
if (!PartyCommandResultParser::parse(packet, data)) return;
if (data.result != PartyResult::OK) {
const char* errText = nullptr;
switch (data.result) {
case PartyResult::BAD_PLAYER_NAME: errText = "No player named \"%s\" is currently online."; break;
case PartyResult::TARGET_NOT_IN_GROUP: errText = "%s is not in your group."; break;
case PartyResult::TARGET_NOT_IN_INSTANCE:errText = "%s is not in your instance."; break;
case PartyResult::GROUP_FULL: errText = "Your party is full."; break;
case PartyResult::ALREADY_IN_GROUP: errText = "%s is already in a group."; break;
case PartyResult::NOT_IN_GROUP: errText = "You are not in a group."; break;
case PartyResult::NOT_LEADER: errText = "You are not the group leader."; break;
case PartyResult::PLAYER_WRONG_FACTION: errText = "%s is the wrong faction for this group."; break;
case PartyResult::IGNORING_YOU: errText = "%s is ignoring you."; break;
case PartyResult::LFG_PENDING: errText = "You cannot do that while in a LFG queue."; break;
case PartyResult::INVITE_RESTRICTED: errText = "Target is not accepting group invites."; break;
default: errText = "Party command failed."; break;
}
char buf[256];
if (!data.name.empty() && errText && std::strstr(errText, "%s"))
std::snprintf(buf, sizeof(buf), errText, data.name.c_str());
else if (errText)
std::snprintf(buf, sizeof(buf), "%s", errText);
else
std::snprintf(buf, sizeof(buf), "Party command failed (error %u).", static_cast<uint32_t>(data.result));
owner_.addUIError(buf);
owner_.addSystemChatMessage(buf);
}
}
void SocialHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) {
auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); };
const bool isWotLK = isActiveExpansion("wotlk");
if (isFull) { if (remaining() < 1) return; packet.readUInt8(); }
const bool pmsTbc = isActiveExpansion("tbc");
if (remaining() < (pmsTbc ? 8u : 1u)) return;
uint64_t memberGuid = pmsTbc
? packet.readUInt64() : packet.readPackedGuid();
if (remaining() < 4) return;
uint32_t updateFlags = packet.readUInt32();
game::GroupMember* member = nullptr;
for (auto& m : partyData.members) {
if (m.guid == memberGuid) { member = &m; break; }
}
if (!member) { packet.setReadPos(packet.getSize()); return; }
if (updateFlags & 0x0001) { if (remaining() >= 2) member->onlineStatus = packet.readUInt16(); }
if (updateFlags & 0x0002) {
if (isWotLK) { if (remaining() >= 4) member->curHealth = packet.readUInt32(); }
else { if (remaining() >= 2) member->curHealth = packet.readUInt16(); }
}
if (updateFlags & 0x0004) {
if (isWotLK) { if (remaining() >= 4) member->maxHealth = packet.readUInt32(); }
else { if (remaining() >= 2) member->maxHealth = packet.readUInt16(); }
}
if (updateFlags & 0x0008) { if (remaining() >= 1) member->powerType = packet.readUInt8(); }
if (updateFlags & 0x0010) { if (remaining() >= 2) member->curPower = packet.readUInt16(); }
if (updateFlags & 0x0020) { if (remaining() >= 2) member->maxPower = packet.readUInt16(); }
if (updateFlags & 0x0040) { if (remaining() >= 2) member->level = packet.readUInt16(); }
if (updateFlags & 0x0080) { if (remaining() >= 2) member->zoneId = packet.readUInt16(); }
if (updateFlags & 0x0100) {
if (remaining() >= 4) {
member->posX = static_cast<int16_t>(packet.readUInt16());
member->posY = static_cast<int16_t>(packet.readUInt16());
}
}
if (updateFlags & 0x0200) {
if (remaining() >= 8) {
uint64_t auraMask = packet.readUInt64();
std::vector<AuraSlot> newAuras;
for (int i = 0; i < 64; ++i) {
if (auraMask & (uint64_t(1) << i)) {
AuraSlot a;
a.level = static_cast<uint8_t>(i);
if (isWotLK) {
if (remaining() < 5) break;
a.spellId = packet.readUInt32();
a.flags = packet.readUInt8();
} else {
if (remaining() < 2) break;
a.spellId = packet.readUInt16();
uint8_t dt = owner_.getSpellDispelType(a.spellId);
if (dt > 0) a.flags = 0x80;
}
if (a.spellId != 0) newAuras.push_back(a);
}
}
if (memberGuid != 0 && memberGuid != owner_.playerGuid && memberGuid != owner_.targetGuid) {
owner_.unitAurasCache_[memberGuid] = std::move(newAuras);
}
}
}
// Skip pet fields and vehicle seat
if (updateFlags & 0x0400) { if (remaining() >= 8) packet.readUInt64(); }
if (updateFlags & 0x0800) { if (remaining() > 0) packet.readString(); }
if (updateFlags & 0x1000) { if (remaining() >= 2) packet.readUInt16(); }
if (updateFlags & 0x2000) { if (isWotLK) { if (remaining() >= 4) packet.readUInt32(); } else { if (remaining() >= 2) packet.readUInt16(); } }
if (updateFlags & 0x4000) { if (isWotLK) { if (remaining() >= 4) packet.readUInt32(); } else { if (remaining() >= 2) packet.readUInt16(); } }
if (updateFlags & 0x8000) { if (remaining() >= 1) packet.readUInt8(); }
if (updateFlags & 0x10000) { if (remaining() >= 2) packet.readUInt16(); }
if (updateFlags & 0x20000) { if (remaining() >= 2) packet.readUInt16(); }
if (updateFlags & 0x40000) {
if (remaining() >= 8) {
uint64_t petAuraMask = packet.readUInt64();
for (int i = 0; i < 64; ++i) {
if (petAuraMask & (uint64_t(1) << i)) {
if (isWotLK) { if (remaining() < 5) break; packet.readUInt32(); packet.readUInt8(); }
else { if (remaining() < 2) break; packet.readUInt16(); }
}
}
}
}
if (isWotLK && (updateFlags & 0x80000)) { if (remaining() >= 4) packet.readUInt32(); }
member->hasPartyStats = true;
if (owner_.addonEventCallback_) {
std::string unitId;
if (partyData.groupType == 1) {
for (size_t i = 0; i < partyData.members.size(); ++i) {
if (partyData.members[i].guid == memberGuid) { unitId = "raid" + std::to_string(i + 1); break; }
}
} else {
int found = 0;
for (const auto& m : partyData.members) {
if (m.guid == owner_.playerGuid) continue;
++found;
if (m.guid == memberGuid) { unitId = "party" + std::to_string(found); break; }
}
}
if (!unitId.empty()) {
if (updateFlags & (0x0002 | 0x0004)) owner_.addonEventCallback_("UNIT_HEALTH", {unitId});
if (updateFlags & (0x0010 | 0x0020)) owner_.addonEventCallback_("UNIT_POWER", {unitId});
if (updateFlags & 0x0200) owner_.addonEventCallback_("UNIT_AURA", {unitId});
}
}
}
// ============================================================
// Guild Handlers
// ============================================================
void SocialHandler::handleGuildInfo(network::Packet& packet) {
GuildInfoData data;
if (!GuildInfoParser::parse(packet, data)) return;
guildInfoData_ = data;
owner_.addSystemChatMessage("Guild: " + data.guildName + " (" +
std::to_string(data.numMembers) + " members, " +
std::to_string(data.numAccounts) + " accounts)");
}
void SocialHandler::handleGuildRoster(network::Packet& packet) {
GuildRosterData data;
if (!owner_.packetParsers_->parseGuildRoster(packet, data)) return;
guildRoster_ = std::move(data);
hasGuildRoster_ = true;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("GUILD_ROSTER_UPDATE", {});
}
void SocialHandler::handleGuildQueryResponse(network::Packet& packet) {
GuildQueryResponseData data;
if (!owner_.packetParsers_->parseGuildQueryResponse(packet, data)) return;
if (data.guildId != 0 && !data.guildName.empty()) {
guildNameCache_[data.guildId] = data.guildName;
pendingGuildNameQueries_.erase(data.guildId);
}
const Character* ch = owner_.getActiveCharacter();
bool isLocalGuild = (ch && ch->hasGuild() && ch->guildId == data.guildId);
if (isLocalGuild) {
const bool wasUnknown = guildName_.empty();
guildName_ = data.guildName;
guildQueryData_ = data;
guildRankNames_.clear();
for (uint32_t i = 0; i < 10; ++i) guildRankNames_.push_back(data.rankNames[i]);
if (wasUnknown && !guildName_.empty()) {
owner_.addSystemChatMessage("Guild: <" + guildName_ + ">");
if (owner_.addonEventCallback_) owner_.addonEventCallback_("PLAYER_GUILD_UPDATE", {});
}
}
}
void SocialHandler::handleGuildEvent(network::Packet& packet) {
GuildEventData data;
if (!GuildEventParser::parse(packet, data)) return;
std::string msg;
switch (data.eventType) {
case GuildEvent::PROMOTION:
if (data.numStrings >= 3)
msg = data.strings[0] + " has promoted " + data.strings[1] + " to " + data.strings[2] + ".";
break;
case GuildEvent::DEMOTION:
if (data.numStrings >= 3)
msg = data.strings[0] + " has demoted " + data.strings[1] + " to " + data.strings[2] + ".";
break;
case GuildEvent::MOTD:
if (data.numStrings >= 1) msg = "Guild MOTD: " + data.strings[0];
break;
case GuildEvent::JOINED:
if (data.numStrings >= 1) msg = data.strings[0] + " has joined the guild.";
break;
case GuildEvent::LEFT:
if (data.numStrings >= 1) msg = data.strings[0] + " has left the guild.";
break;
case GuildEvent::REMOVED:
if (data.numStrings >= 2) msg = data.strings[1] + " has been kicked from the guild by " + data.strings[0] + ".";
break;
case GuildEvent::LEADER_IS:
if (data.numStrings >= 1) msg = data.strings[0] + " is the guild leader.";
break;
case GuildEvent::LEADER_CHANGED:
if (data.numStrings >= 2) msg = data.strings[0] + " has made " + data.strings[1] + " the new guild leader.";
break;
case GuildEvent::DISBANDED:
msg = "Guild has been disbanded.";
guildName_.clear();
guildRankNames_.clear();
guildRoster_ = GuildRosterData{};
hasGuildRoster_ = false;
if (owner_.addonEventCallback_) owner_.addonEventCallback_("PLAYER_GUILD_UPDATE", {});
break;
case GuildEvent::SIGNED_ON:
if (data.numStrings >= 1) msg = "[Guild] " + data.strings[0] + " has come online.";
break;
case GuildEvent::SIGNED_OFF:
if (data.numStrings >= 1) msg = "[Guild] " + data.strings[0] + " has gone offline.";
break;
default:
msg = "Guild event " + std::to_string(data.eventType);
if (!data.numStrings && data.numStrings >= 1) msg += ": " + data.strings[0];
break;
}
if (!msg.empty()) {
MessageChatData chatMsg;
chatMsg.type = ChatType::GUILD;
chatMsg.language = ChatLanguage::UNIVERSAL;
chatMsg.message = msg;
owner_.addLocalChatMessage(chatMsg);
}
if (owner_.addonEventCallback_) {
switch (data.eventType) {
case GuildEvent::MOTD:
owner_.addonEventCallback_("GUILD_MOTD", {data.numStrings >= 1 ? data.strings[0] : ""});
break;
case GuildEvent::SIGNED_ON: case GuildEvent::SIGNED_OFF:
case GuildEvent::PROMOTION: case GuildEvent::DEMOTION:
case GuildEvent::JOINED: case GuildEvent::LEFT:
case GuildEvent::REMOVED: case GuildEvent::LEADER_CHANGED:
case GuildEvent::DISBANDED:
owner_.addonEventCallback_("GUILD_ROSTER_UPDATE", {});
break;
default: break;
}
}
switch (data.eventType) {
case GuildEvent::PROMOTION: case GuildEvent::DEMOTION:
case GuildEvent::JOINED: case GuildEvent::LEFT:
case GuildEvent::REMOVED: case GuildEvent::LEADER_CHANGED:
if (hasGuildRoster_) requestGuildRoster();
break;
default: break;
}
}
void SocialHandler::handleGuildInvite(network::Packet& packet) {
GuildInviteResponseData data;
if (!GuildInviteResponseParser::parse(packet, data)) return;
pendingGuildInvite_ = true;
pendingGuildInviterName_ = data.inviterName;
pendingGuildInviteGuildName_ = data.guildName;
owner_.addSystemChatMessage(data.inviterName + " has invited you to join " + data.guildName + ".");
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("GUILD_INVITE_REQUEST", {data.inviterName, data.guildName});
}
void SocialHandler::handleGuildCommandResult(network::Packet& packet) {
GuildCommandResultData data;
if (!GuildCommandResultParser::parse(packet, data)) return;
if (data.errorCode == 0) {
switch (data.command) {
case 0: owner_.addSystemChatMessage("Guild created."); break;
case 1:
if (!data.name.empty()) owner_.addSystemChatMessage("You have invited " + data.name + " to the guild.");
break;
case 2:
owner_.addSystemChatMessage("You have left the guild.");
guildName_.clear(); guildRankNames_.clear();
guildRoster_ = GuildRosterData{}; hasGuildRoster_ = false;
break;
default: break;
}
return;
}
const char* errStr = nullptr;
switch (data.errorCode) {
case 2: errStr = "You are not in a guild."; break;
case 4: errStr = "No player named \"%s\" is online."; break;
case 11: errStr = "\"%s\" is already in a guild."; break;
case 13: errStr = "You are already in a guild."; break;
case 14: errStr = "\"%s\" has already been invited to a guild."; break;
case 16: case 17: errStr = "You are not the guild leader."; break;
case 22: errStr = "That player is ignoring you."; break;
default: break;
}
std::string errorMsg;
if (errStr) {
std::string fmt = errStr;
auto pos = fmt.find("%s");
if (pos != std::string::npos && !data.name.empty()) fmt.replace(pos, 2, data.name);
else if (pos != std::string::npos) fmt.replace(pos, 2, "that player");
errorMsg = fmt;
} else {
errorMsg = "Guild command failed";
if (!data.name.empty()) errorMsg += " for " + data.name;
errorMsg += " (error " + std::to_string(data.errorCode) + ")";
}
owner_.addUIError(errorMsg);
owner_.addSystemChatMessage(errorMsg);
}
void SocialHandler::handlePetitionShowlist(network::Packet& packet) {
PetitionShowlistData data;
if (!PetitionShowlistParser::parse(packet, data)) return;
petitionNpcGuid_ = data.npcGuid;
petitionCost_ = data.cost;
showPetitionDialog_ = true;
}
void SocialHandler::handlePetitionQueryResponse(network::Packet& packet) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 12) return;
/*uint32_t entry =*/ packet.readUInt32();
uint64_t petGuid = packet.readUInt64();
std::string guildName = packet.readString();
/*std::string body =*/ packet.readString();
if (petitionInfo_.petitionGuid == petGuid) petitionInfo_.guildName = guildName;
packet.setReadPos(packet.getSize());
}
void SocialHandler::handlePetitionShowSignatures(network::Packet& packet) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 21) return;
petitionInfo_ = PetitionInfo{};
petitionInfo_.petitionGuid = packet.readUInt64();
petitionInfo_.ownerGuid = packet.readUInt64();
/*uint32_t petEntry =*/ packet.readUInt32();
uint8_t sigCount = packet.readUInt8();
petitionInfo_.signatureCount = sigCount;
petitionInfo_.signatures.reserve(sigCount);
for (uint8_t i = 0; i < sigCount; ++i) {
if (rem() < 12) break;
PetitionSignature sig;
sig.playerGuid = packet.readUInt64();
/*uint32_t unk =*/ packet.readUInt32();
petitionInfo_.signatures.push_back(sig);
}
petitionInfo_.showUI = true;
}
void SocialHandler::handlePetitionSignResults(network::Packet& packet) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 20) return;
uint64_t petGuid = packet.readUInt64();
uint64_t playerGuid = packet.readUInt64();
uint32_t result = packet.readUInt32();
switch (result) {
case 0:
owner_.addSystemChatMessage("Petition signed successfully.");
if (petitionInfo_.petitionGuid == petGuid) {
petitionInfo_.signatureCount++;
PetitionSignature sig; sig.playerGuid = playerGuid;
petitionInfo_.signatures.push_back(sig);
}
break;
case 1: owner_.addSystemChatMessage("You have already signed that petition."); break;
case 2: owner_.addSystemChatMessage("You are already in a guild."); break;
case 3: owner_.addSystemChatMessage("You cannot sign your own petition."); break;
default: owner_.addSystemChatMessage("Cannot sign petition (error " + std::to_string(result) + ")."); break;
}
}
void SocialHandler::handleTurnInPetitionResults(network::Packet& packet) {
uint32_t result = 0;
if (!TurnInPetitionResultsParser::parse(packet, result)) return;
switch (result) {
case 0: owner_.addSystemChatMessage("Guild created successfully!"); break;
case 1: owner_.addSystemChatMessage("Guild creation failed: already in a guild."); break;
case 2: owner_.addSystemChatMessage("Guild creation failed: not enough signatures."); break;
case 3: owner_.addSystemChatMessage("Guild creation failed: name already taken."); break;
default: owner_.addSystemChatMessage("Guild creation failed (error " + std::to_string(result) + ")."); break;
}
}
// ============================================================
// Server Info Handlers
// ============================================================
void SocialHandler::handleQueryTimeResponse(network::Packet& packet) {
QueryTimeResponseData data;
if (!QueryTimeResponseParser::parse(packet, data)) return;
time_t serverTime = static_cast<time_t>(data.serverTime);
struct tm* timeInfo = localtime(&serverTime);
char timeStr[64];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", timeInfo);
owner_.addSystemChatMessage("Server time: " + std::string(timeStr));
}
void SocialHandler::handlePlayedTime(network::Packet& packet) {
PlayedTimeData data;
if (!PlayedTimeParser::parse(packet, data)) return;
totalTimePlayed_ = data.totalTimePlayed;
levelTimePlayed_ = data.levelTimePlayed;
if (data.triggerMessage) {
uint32_t totalDays = data.totalTimePlayed / 86400;
uint32_t totalHours = (data.totalTimePlayed % 86400) / 3600;
uint32_t totalMinutes = (data.totalTimePlayed % 3600) / 60;
uint32_t levelDays = data.levelTimePlayed / 86400;
uint32_t levelHours = (data.levelTimePlayed % 86400) / 3600;
uint32_t levelMinutes = (data.levelTimePlayed % 3600) / 60;
std::string totalMsg = "Total time played: ";
if (totalDays > 0) totalMsg += std::to_string(totalDays) + " days, ";
if (totalHours > 0 || totalDays > 0) totalMsg += std::to_string(totalHours) + " hours, ";
totalMsg += std::to_string(totalMinutes) + " minutes";
std::string levelMsg = "Time played this level: ";
if (levelDays > 0) levelMsg += std::to_string(levelDays) + " days, ";
if (levelHours > 0 || levelDays > 0) levelMsg += std::to_string(levelHours) + " hours, ";
levelMsg += std::to_string(levelMinutes) + " minutes";
owner_.addSystemChatMessage(totalMsg);
owner_.addSystemChatMessage(levelMsg);
}
}
void SocialHandler::handleWho(network::Packet& packet) {
const bool hasGender = isActiveExpansion("wotlk");
uint32_t displayCount = packet.readUInt32();
uint32_t onlineCount = packet.readUInt32();
whoResults_.clear();
whoOnlineCount_ = onlineCount;
if (displayCount == 0) { owner_.addSystemChatMessage("No players found."); return; }
for (uint32_t i = 0; i < displayCount; ++i) {
if (packet.getReadPos() >= packet.getSize()) break;
std::string playerName = packet.readString();
std::string guildName = packet.readString();
if (packet.getSize() - packet.getReadPos() < 12) break;
uint32_t level = packet.readUInt32();
uint32_t classId = packet.readUInt32();
uint32_t raceId = packet.readUInt32();
if (hasGender && packet.getSize() - packet.getReadPos() >= 1) packet.readUInt8();
uint32_t zoneId = 0;
if (packet.getSize() - packet.getReadPos() >= 4) zoneId = packet.readUInt32();
WhoEntry entry;
entry.name = playerName; entry.guildName = guildName;
entry.level = level; entry.classId = classId;
entry.raceId = raceId; entry.zoneId = zoneId;
whoResults_.push_back(std::move(entry));
}
}
// ============================================================
// Social (Friend/Ignore/Random Roll) Handlers
// ============================================================
void SocialHandler::handleFriendList(network::Packet& packet) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 1) return;
uint8_t count = packet.readUInt8();
owner_.contacts_.erase(std::remove_if(owner_.contacts_.begin(), owner_.contacts_.end(),
[](const ContactEntry& e){ return e.isFriend(); }), owner_.contacts_.end());
for (uint8_t i = 0; i < count && rem() >= 9; ++i) {
uint64_t guid = packet.readUInt64();
uint8_t status = packet.readUInt8();
uint32_t area = 0, level = 0, classId = 0;
if (status != 0 && rem() >= 12) {
area = packet.readUInt32();
level = packet.readUInt32();
classId = packet.readUInt32();
}
owner_.friendGuids_.insert(guid);
auto nit = owner_.getPlayerNameCache().find(guid);
std::string name;
if (nit != owner_.getPlayerNameCache().end()) {
name = nit->second;
owner_.friendsCache[name] = guid;
} else {
owner_.queryPlayerName(guid);
}
ContactEntry entry;
entry.guid = guid; entry.name = name; entry.flags = 0x1;
entry.status = status; entry.areaId = area; entry.level = level; entry.classId = classId;
owner_.contacts_.push_back(std::move(entry));
}
if (owner_.addonEventCallback_) owner_.addonEventCallback_("FRIENDLIST_UPDATE", {});
}
void SocialHandler::handleContactList(network::Packet& packet) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 8) { packet.setReadPos(packet.getSize()); return; }
owner_.lastContactListMask_ = packet.readUInt32();
owner_.lastContactListCount_ = packet.readUInt32();
owner_.contacts_.clear();
for (uint32_t i = 0; i < owner_.lastContactListCount_ && rem() >= 8; ++i) {
uint64_t guid = packet.readUInt64();
if (rem() < 4) break;
uint32_t flags = packet.readUInt32();
std::string note = packet.readString();
uint8_t status = 0; uint32_t areaId = 0, level = 0, classId = 0;
if (flags & 0x1) {
if (rem() < 1) break;
status = packet.readUInt8();
if (status != 0 && rem() >= 12) {
areaId = packet.readUInt32(); level = packet.readUInt32(); classId = packet.readUInt32();
}
owner_.friendGuids_.insert(guid);
auto nit = owner_.getPlayerNameCache().find(guid);
if (nit != owner_.getPlayerNameCache().end()) owner_.friendsCache[nit->second] = guid;
else owner_.queryPlayerName(guid);
}
ContactEntry entry;
entry.guid = guid; entry.flags = flags; entry.note = std::move(note);
entry.status = status; entry.areaId = areaId; entry.level = level; entry.classId = classId;
auto nit = owner_.getPlayerNameCache().find(guid);
if (nit != owner_.getPlayerNameCache().end()) entry.name = nit->second;
owner_.contacts_.push_back(std::move(entry));
}
if (owner_.addonEventCallback_) {
owner_.addonEventCallback_("FRIENDLIST_UPDATE", {});
if (owner_.lastContactListMask_ & 0x2) owner_.addonEventCallback_("IGNORELIST_UPDATE", {});
}
}
void SocialHandler::handleFriendStatus(network::Packet& packet) {
FriendStatusData data;
if (!FriendStatusParser::parse(packet, data)) return;
// Single lookup — reuse iterator for name resolution and update/erase below
auto cit = std::find_if(owner_.contacts_.begin(), owner_.contacts_.end(),
[&](const ContactEntry& e){ return e.guid == data.guid; });
// Look up player name: contacts_ (populated by SMSG_FRIEND_LIST) > playerNameCache
std::string playerName;
if (cit != owner_.contacts_.end() && !cit->name.empty()) {
playerName = cit->name;
} else {
auto it = owner_.getPlayerNameCache().find(data.guid);
if (it != owner_.getPlayerNameCache().end()) playerName = it->second;
}
if (data.status == 1 || data.status == 2) owner_.friendsCache[playerName] = data.guid;
else if (data.status == 0) owner_.friendsCache.erase(playerName);
if (data.status == 0) {
if (cit != owner_.contacts_.end())
owner_.contacts_.erase(cit);
} else {
if (cit != owner_.contacts_.end()) {
if (!playerName.empty() && playerName != "Unknown") cit->name = playerName;
if (data.status == 2) cit->status = 1; else if (data.status == 3) cit->status = 0;
} else {
ContactEntry entry;
entry.guid = data.guid; entry.name = playerName; entry.flags = 0x1;
entry.status = (data.status == 2) ? 1 : 0;
owner_.contacts_.push_back(std::move(entry));
}
}
switch (data.status) {
case 0: owner_.addSystemChatMessage(playerName + " has been removed from your friends list."); break;
case 1: owner_.addSystemChatMessage(playerName + " has been added to your friends list."); break;
case 2: owner_.addSystemChatMessage(playerName + " is now online."); break;
case 3: owner_.addSystemChatMessage(playerName + " is now offline."); break;
case 4: owner_.addSystemChatMessage("Player not found."); break;
case 5: owner_.addSystemChatMessage(playerName + " is already in your friends list."); break;
case 6: owner_.addSystemChatMessage("Your friends list is full."); break;
case 7: owner_.addSystemChatMessage(playerName + " is ignoring you."); break;
default: break;
}
if (owner_.addonEventCallback_) owner_.addonEventCallback_("FRIENDLIST_UPDATE", {});
}
void SocialHandler::handleRandomRoll(network::Packet& packet) {
RandomRollData data;
if (!RandomRollParser::parse(packet, data)) return;
std::string rollerName = (data.rollerGuid == owner_.playerGuid) ? "You" : "Someone";
if (data.rollerGuid != owner_.playerGuid) {
auto it = owner_.getPlayerNameCache().find(data.rollerGuid);
if (it != owner_.getPlayerNameCache().end()) rollerName = it->second;
}
std::string msg = rollerName + ((data.rollerGuid == owner_.playerGuid) ? " roll " : " rolls ");
msg += std::to_string(data.result) + " (" + std::to_string(data.minRoll) + "-" + std::to_string(data.maxRoll) + ")";
owner_.addSystemChatMessage(msg);
}
// ============================================================
// Logout Handlers
// ============================================================
void SocialHandler::handleLogoutResponse(network::Packet& packet) {
LogoutResponseData data;
if (!LogoutResponseParser::parse(packet, data)) return;
if (data.result == 0) {
if (data.instant) { owner_.addSystemChatMessage("Logging out..."); logoutCountdown_ = 0.0f; }
else { owner_.addSystemChatMessage("Logging out in 20 seconds..."); logoutCountdown_ = 20.0f; }
if (owner_.addonEventCallback_) owner_.addonEventCallback_("PLAYER_LOGOUT", {});
} else {
owner_.addSystemChatMessage("Cannot logout right now.");
loggingOut_ = false; logoutCountdown_ = 0.0f;
}
}
void SocialHandler::handleLogoutComplete(network::Packet& /*packet*/) {
owner_.addSystemChatMessage("Logout complete.");
loggingOut_ = false; logoutCountdown_ = 0.0f;
}
// ============================================================
// Battleground Handlers
// ============================================================
void SocialHandler::handleBattlefieldStatus(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t queueSlot = packet.readUInt32();
const bool classicFormat = isClassicLikeExpansion();
uint8_t arenaType = 0;
if (!classicFormat) {
if (packet.getSize() - packet.getReadPos() < 1) return;
arenaType = packet.readUInt8();
if (packet.getSize() - packet.getReadPos() < 1) return;
packet.readUInt8();
} else {
if (packet.getSize() - packet.getReadPos() < 4) return;
}
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t bgTypeId = packet.readUInt32();
if (packet.getSize() - packet.getReadPos() < 2) return;
packet.readUInt16();
if (packet.getSize() - packet.getReadPos() < 4) return;
packet.readUInt32(); // instanceId
if (packet.getSize() - packet.getReadPos() < 1) return;
packet.readUInt8(); // isRated
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t statusId = packet.readUInt32();
static const std::pair<uint32_t, const char*> kBgNames[] = {
{1,"Alterac Valley"},{2,"Warsong Gulch"},{3,"Arathi Basin"},
{4,"Nagrand Arena"},{5,"Blade's Edge Arena"},{6,"All Arenas"},
{7,"Eye of the Storm"},{8,"Ruins of Lordaeron"},{9,"Strand of the Ancients"},
{10,"Dalaran Sewers"},{11,"Ring of Valor"},{30,"Isle of Conquest"},{32,"Random Battleground"},
};
std::string bgName = "Battleground";
for (const auto& kv : kBgNames) { if (kv.first == bgTypeId) { bgName = kv.second; break; } }
if (bgName == "Battleground") bgName = "Battleground #" + std::to_string(bgTypeId);
if (arenaType > 0) {
bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena";
for (const auto& kv : kBgNames) { if (kv.first == bgTypeId) { bgName += " (" + std::string(kv.second) + ")"; break; } }
}
uint32_t inviteTimeout = 80, avgWaitSec = 0, timeInQueueSec = 0;
if (statusId == 1 && packet.getSize() - packet.getReadPos() >= 8) {
avgWaitSec = packet.readUInt32() / 1000; timeInQueueSec = packet.readUInt32() / 1000;
} else if (statusId == 2) {
if (packet.getSize() - packet.getReadPos() >= 4) inviteTimeout = packet.readUInt32();
if (packet.getSize() - packet.getReadPos() >= 4) packet.readUInt32();
} else if (statusId == 3 && packet.getSize() - packet.getReadPos() >= 8) {
packet.readUInt32(); packet.readUInt32();
}
if (queueSlot < bgQueues_.size()) {
bool wasInvite = (bgQueues_[queueSlot].statusId == 2);
bgQueues_[queueSlot].queueSlot = queueSlot;
bgQueues_[queueSlot].bgTypeId = bgTypeId;
bgQueues_[queueSlot].arenaType = arenaType;
bgQueues_[queueSlot].statusId = statusId;
bgQueues_[queueSlot].bgName = bgName;
if (statusId == 1) { bgQueues_[queueSlot].avgWaitTimeSec = avgWaitSec; bgQueues_[queueSlot].timeInQueueSec = timeInQueueSec; }
if (statusId == 2 && !wasInvite) { bgQueues_[queueSlot].inviteTimeout = inviteTimeout; bgQueues_[queueSlot].inviteReceivedTime = std::chrono::steady_clock::now(); }
}
switch (statusId) {
case 1: owner_.addSystemChatMessage("Queued for " + bgName + "."); break;
case 2: owner_.addSystemChatMessage(bgName + " is ready!"); break;
case 3: owner_.addSystemChatMessage("Entered " + bgName + "."); break;
default: break;
}
if (owner_.addonEventCallback_) owner_.addonEventCallback_("UPDATE_BATTLEFIELD_STATUS", {std::to_string(statusId)});
}
void SocialHandler::handleBattlefieldList(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 5) return;
AvailableBgInfo info;
info.bgTypeId = packet.readUInt32();
info.isRegistered = packet.readUInt8() != 0;
const bool isWotlk = isActiveExpansion("wotlk");
const bool isTbc = isActiveExpansion("tbc");
if (isTbc || isWotlk) { if (packet.getSize() - packet.getReadPos() < 1) return; info.isHoliday = packet.readUInt8() != 0; }
if (isWotlk) { if (packet.getSize() - packet.getReadPos() < 8) return; info.minLevel = packet.readUInt32(); info.maxLevel = packet.readUInt32(); }
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t count = std::min(packet.readUInt32(), 256u);
info.instanceIds.reserve(count);
for (uint32_t i = 0; i < count; ++i) {
if (packet.getSize() - packet.getReadPos() < 4) break;
info.instanceIds.push_back(packet.readUInt32());
}
bool updated = false;
for (auto& existing : availableBgs_) { if (existing.bgTypeId == info.bgTypeId) { existing = std::move(info); updated = true; break; } }
if (!updated) availableBgs_.push_back(std::move(info));
}
bool SocialHandler::hasPendingBgInvite() const {
for (const auto& slot : bgQueues_) { if (slot.statusId == 2) return true; }
return false;
}
void SocialHandler::acceptBattlefield(uint32_t queueSlot) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
const BgQueueSlot* slot = nullptr;
if (queueSlot == 0xFFFFFFFF) { for (const auto& s : bgQueues_) { if (s.statusId == 2) { slot = &s; break; } } }
else if (queueSlot < bgQueues_.size() && bgQueues_[queueSlot].statusId == 2) slot = &bgQueues_[queueSlot];
if (!slot) { owner_.addSystemChatMessage("No battleground invitation pending."); return; }
network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_PORT));
pkt.writeUInt8(slot->arenaType); pkt.writeUInt8(0x00); pkt.writeUInt32(slot->bgTypeId);
pkt.writeUInt16(0x0000); pkt.writeUInt8(1);
owner_.socket->send(pkt);
uint32_t clearSlot = slot->queueSlot;
if (clearSlot < bgQueues_.size()) bgQueues_[clearSlot].statusId = 3;
owner_.addSystemChatMessage("Accepting battleground invitation...");
}
void SocialHandler::declineBattlefield(uint32_t queueSlot) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
const BgQueueSlot* slot = nullptr;
if (queueSlot == 0xFFFFFFFF) { for (const auto& s : bgQueues_) { if (s.statusId == 2) { slot = &s; break; } } }
else if (queueSlot < bgQueues_.size() && bgQueues_[queueSlot].statusId == 2) slot = &bgQueues_[queueSlot];
if (!slot) { owner_.addSystemChatMessage("No battleground invitation pending."); return; }
network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_PORT));
pkt.writeUInt8(slot->arenaType); pkt.writeUInt8(0x00); pkt.writeUInt32(slot->bgTypeId);
pkt.writeUInt16(0x0000); pkt.writeUInt8(0);
owner_.socket->send(pkt);
uint32_t clearSlot = slot->queueSlot;
if (clearSlot < bgQueues_.size()) bgQueues_[clearSlot] = BgQueueSlot{};
owner_.addSystemChatMessage("Battleground invitation declined.");
}
void SocialHandler::requestPvpLog() {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::MSG_PVP_LOG_DATA));
owner_.socket->send(pkt);
}
// ============================================================
// Instance Handlers
// ============================================================
void SocialHandler::handleRaidInstanceInfo(network::Packet& packet) {
const bool isTbc = isActiveExpansion("tbc");
const bool isClassic = isClassicLikeExpansion();
const bool useTbcFormat = isTbc || isClassic;
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t count = packet.readUInt32();
instanceLockouts_.clear();
instanceLockouts_.reserve(count);
const size_t kEntrySize = useTbcFormat ? 13 : 18;
for (uint32_t i = 0; i < count; ++i) {
if (packet.getSize() - packet.getReadPos() < kEntrySize) break;
InstanceLockout lo;
lo.mapId = packet.readUInt32(); lo.difficulty = packet.readUInt32();
if (useTbcFormat) { lo.resetTime = packet.readUInt32(); lo.locked = packet.readUInt8() != 0; lo.extended = false; }
else { lo.resetTime = packet.readUInt64(); lo.locked = packet.readUInt8() != 0; lo.extended = packet.readUInt8() != 0; }
instanceLockouts_.push_back(lo);
}
}
void SocialHandler::handleInstanceDifficulty(network::Packet& packet) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 4) return;
uint32_t prevDifficulty = instanceDifficulty_;
instanceDifficulty_ = packet.readUInt32();
if (rem() >= 4) {
uint32_t secondField = packet.readUInt32();
if (rem() >= 4) instanceIsHeroic_ = (instanceDifficulty_ == 1);
else instanceIsHeroic_ = (secondField != 0);
} else {
instanceIsHeroic_ = (instanceDifficulty_ == 1);
}
inInstance_ = true;
if (instanceDifficulty_ != prevDifficulty) {
static const char* kDiffLabels[] = {"Normal", "Heroic", "25-Man Normal", "25-Man Heroic"};
const char* diffLabel = (instanceDifficulty_ < 4) ? kDiffLabels[instanceDifficulty_] : nullptr;
if (diffLabel) owner_.addSystemChatMessage(std::string("Dungeon difficulty set to ") + diffLabel + ".");
}
}
// ============================================================
// LFG Handlers
// ============================================================
void SocialHandler::handleLfgJoinResult(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 2) return;
uint8_t result = packet.readUInt8();
uint8_t state = packet.readUInt8();
if (result == 0) {
lfgState_ = static_cast<LfgState>(state);
std::string dName = owner_.getLfgDungeonName(lfgDungeonId_);
if (!dName.empty()) owner_.addSystemChatMessage("Dungeon Finder: Joined the queue for " + dName + ".");
else owner_.addSystemChatMessage("Dungeon Finder: Joined the queue.");
} else {
const char* msg = lfgJoinResultString(result);
std::string errMsg = std::string("Dungeon Finder: ") + (msg ? msg : "Join failed.");
owner_.addUIError(errMsg);
owner_.addSystemChatMessage(errMsg);
}
}
void SocialHandler::handleLfgQueueStatus(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 33) return;
lfgDungeonId_ = packet.readUInt32();
int32_t avgWait = static_cast<int32_t>(packet.readUInt32());
int32_t waitTime = static_cast<int32_t>(packet.readUInt32());
packet.readUInt32(); packet.readUInt32(); packet.readUInt32();
packet.readUInt8();
lfgTimeInQueueMs_ = packet.readUInt32();
lfgAvgWaitSec_ = (waitTime >= 0) ? (waitTime / 1000) : (avgWait / 1000);
lfgState_ = LfgState::Queued;
}
void SocialHandler::handleLfgProposalUpdate(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 17) return;
uint32_t dungeonId = packet.readUInt32();
uint32_t proposalId = packet.readUInt32();
uint32_t proposalState = packet.readUInt32();
packet.readUInt32(); packet.readUInt8();
lfgDungeonId_ = dungeonId; lfgProposalId_ = proposalId;
switch (proposalState) {
case 0: lfgState_ = LfgState::Queued; lfgProposalId_ = 0;
owner_.addUIError("Dungeon Finder: Group proposal failed."); owner_.addSystemChatMessage("Dungeon Finder: Group proposal failed."); break;
case 1: { lfgState_ = LfgState::InDungeon; lfgProposalId_ = 0;
std::string dName = owner_.getLfgDungeonName(dungeonId);
owner_.addSystemChatMessage(dName.empty() ? "Dungeon Finder: Group found! Entering dungeon..." : "Dungeon Finder: Group found for " + dName + "! Entering dungeon..."); break; }
case 2: { lfgState_ = LfgState::Proposal;
std::string dName = owner_.getLfgDungeonName(dungeonId);
owner_.addSystemChatMessage(dName.empty() ? "Dungeon Finder: A group has been found. Accept or decline." : "Dungeon Finder: A group has been found for " + dName + ". Accept or decline."); break; }
default: break;
}
}
void SocialHandler::handleLfgRoleCheckUpdate(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 6) return;
packet.readUInt32();
uint8_t roleCheckState = packet.readUInt8();
packet.readUInt8();
if (roleCheckState == 1) lfgState_ = LfgState::Queued;
else if (roleCheckState == 3) { lfgState_ = LfgState::None; owner_.addUIError("Dungeon Finder: Role check failed — missing required role."); owner_.addSystemChatMessage("Dungeon Finder: Role check failed — missing required role."); }
else if (roleCheckState == 2) { lfgState_ = LfgState::RoleCheck; owner_.addSystemChatMessage("Dungeon Finder: Performing role check..."); }
}
void SocialHandler::handleLfgUpdatePlayer(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
uint8_t updateType = packet.readUInt8();
bool hasExtra = (updateType != 0 && updateType != 1 && updateType != 15 && updateType != 17 && updateType != 18);
if (!hasExtra || packet.getSize() - packet.getReadPos() < 3) {
switch (updateType) {
case 8: lfgState_ = LfgState::None; owner_.addSystemChatMessage("Dungeon Finder: Removed from queue."); break;
case 9: lfgState_ = LfgState::Queued; owner_.addSystemChatMessage("Dungeon Finder: Proposal failed — re-queuing."); break;
case 10: lfgState_ = LfgState::Queued; owner_.addSystemChatMessage("Dungeon Finder: A member declined the proposal."); break;
case 15: lfgState_ = LfgState::None; owner_.addSystemChatMessage("Dungeon Finder: Left the queue."); break;
case 18: lfgState_ = LfgState::None; owner_.addSystemChatMessage("Dungeon Finder: Your group disbanded."); break;
default: break;
}
return;
}
packet.readUInt8(); packet.readUInt8(); packet.readUInt8();
if (packet.getSize() - packet.getReadPos() >= 1) {
uint8_t count = packet.readUInt8();
for (uint8_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 4; ++i) {
uint32_t dungeonEntry = packet.readUInt32();
if (i == 0) lfgDungeonId_ = dungeonEntry;
}
}
switch (updateType) {
case 6: lfgState_ = LfgState::Queued; owner_.addSystemChatMessage("Dungeon Finder: You have joined the queue."); break;
case 11: lfgState_ = LfgState::Proposal; owner_.addSystemChatMessage("Dungeon Finder: A group has been found!"); break;
case 12: lfgState_ = LfgState::Queued; owner_.addSystemChatMessage("Dungeon Finder: Added to queue."); break;
case 14: lfgState_ = LfgState::InDungeon; break;
default: break;
}
}
void SocialHandler::handleLfgPlayerReward(network::Packet& packet) {
if (!packet.hasRemaining( 13)) return;
packet.readUInt32(); packet.readUInt32(); packet.readUInt8();
uint32_t money = packet.readUInt32();
uint32_t xp = packet.readUInt32();
uint32_t gold = money / 10000, silver = (money % 10000) / 100, copper = money % 100;
char moneyBuf[64];
if (gold > 0) snprintf(moneyBuf, sizeof(moneyBuf), "%ug %us %uc", gold, silver, copper);
else if (silver > 0) snprintf(moneyBuf, sizeof(moneyBuf), "%us %uc", silver, copper);
else snprintf(moneyBuf, sizeof(moneyBuf), "%uc", copper);
std::string rewardMsg = std::string("Dungeon Finder reward: ") + moneyBuf + ", " + std::to_string(xp) + " XP";
if (packet.hasRemaining( 4)) {
uint32_t rewardCount = packet.readUInt32();
for (uint32_t i = 0; i < rewardCount && packet.hasRemaining( 9); ++i) {
uint32_t itemId = packet.readUInt32();
uint32_t itemCount = packet.readUInt32();
packet.readUInt8();
if (i == 0) {
std::string itemLabel = "item #" + std::to_string(itemId);
uint32_t lfgItemQuality = 1;
if (const ItemQueryResponseData* info = owner_.getItemInfo(itemId)) {
if (!info->name.empty()) itemLabel = info->name;
lfgItemQuality = info->quality;
}
rewardMsg += ", " + buildItemLink(itemId, lfgItemQuality, itemLabel);
if (itemCount > 1) rewardMsg += " x" + std::to_string(itemCount);
}
}
}
owner_.addSystemChatMessage(rewardMsg);
lfgState_ = LfgState::FinishedDungeon;
}
void SocialHandler::handleLfgBootProposalUpdate(network::Packet& packet) {
if (!packet.hasRemaining( 23)) return;
bool inProgress = packet.readUInt8() != 0;
packet.readUInt8(); packet.readUInt8();
uint32_t totalVotes = packet.readUInt32();
uint32_t bootVotes = packet.readUInt32();
uint32_t timeLeft = packet.readUInt32();
uint32_t votesNeeded = packet.readUInt32();
lfgBootVotes_ = bootVotes; lfgBootTotal_ = totalVotes;
lfgBootTimeLeft_ = timeLeft; lfgBootNeeded_ = votesNeeded;
if (packet.getReadPos() < packet.getSize()) lfgBootReason_ = packet.readString();
if (packet.getReadPos() < packet.getSize()) lfgBootTargetName_ = packet.readString();
if (inProgress) { lfgState_ = LfgState::Boot; }
else {
const bool bootPassed = (bootVotes >= votesNeeded);
lfgBootVotes_ = lfgBootTotal_ = lfgBootTimeLeft_ = lfgBootNeeded_ = 0;
lfgBootTargetName_.clear(); lfgBootReason_.clear();
lfgState_ = LfgState::InDungeon;
owner_.addSystemChatMessage(bootPassed ? "Dungeon Finder: Vote kick passed — member removed." : "Dungeon Finder: Vote kick failed.");
}
}
void SocialHandler::handleLfgTeleportDenied(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
uint8_t reason = packet.readUInt8();
owner_.addSystemChatMessage(std::string("Dungeon Finder: ") + lfgTeleportDeniedString(reason));
}
// ============================================================
// LFG Outgoing Packets
// ============================================================
void SocialHandler::lfgJoin(uint32_t dungeonId, uint8_t roles) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_JOIN));
pkt.writeUInt8(roles); pkt.writeUInt8(0); pkt.writeUInt8(0);
pkt.writeUInt8(1); pkt.writeUInt32(dungeonId); pkt.writeString("");
owner_.socket->send(pkt);
}
void SocialHandler::lfgLeave() {
if (!owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_LEAVE));
pkt.writeUInt32(0); pkt.writeUInt32(0); pkt.writeUInt32(0);
owner_.socket->send(pkt);
lfgState_ = LfgState::None;
}
void SocialHandler::lfgSetRoles(uint8_t roles) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
const uint32_t wire = wireOpcode(Opcode::CMSG_LFG_SET_ROLES);
if (wire == 0xFFFF) return;
network::Packet pkt(static_cast<uint16_t>(wire));
pkt.writeUInt8(roles);
owner_.socket->send(pkt);
}
void SocialHandler::lfgAcceptProposal(uint32_t proposalId, bool accept) {
if (!owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_PROPOSAL_RESULT));
pkt.writeUInt32(proposalId); pkt.writeUInt8(accept ? 1 : 0);
owner_.socket->send(pkt);
}
void SocialHandler::lfgTeleport(bool toLfgDungeon) {
if (!owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_TELEPORT));
pkt.writeUInt8(toLfgDungeon ? 0 : 1);
owner_.socket->send(pkt);
}
void SocialHandler::lfgSetBootVote(bool vote) {
if (!owner_.socket) return;
uint16_t wireOp = wireOpcode(Opcode::CMSG_LFG_SET_BOOT_VOTE);
if (wireOp == 0xFFFF) return;
network::Packet pkt(wireOp);
pkt.writeUInt8(vote ? 1 : 0);
owner_.socket->send(pkt);
}
// ============================================================
// Arena Handlers
// ============================================================
void SocialHandler::handleArenaTeamCommandResult(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 8) return;
uint32_t command = packet.readUInt32();
std::string name = packet.readString();
uint32_t error = packet.readUInt32();
static const char* commands[] = {"create","invite","leave","remove","disband","leader"};
std::string cmdName = (command < 6) ? commands[command] : "unknown";
if (error == 0) owner_.addSystemChatMessage("Arena team " + cmdName + " successful" + (name.empty() ? "." : ": " + name));
else owner_.addSystemChatMessage("Arena team " + cmdName + " failed" + (name.empty() ? "." : " for " + name + "."));
}
void SocialHandler::handleArenaTeamQueryResponse(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t teamId = packet.readUInt32();
std::string teamName = packet.readString();
uint32_t teamType = 0;
if (packet.getSize() - packet.getReadPos() >= 4) teamType = packet.readUInt32();
for (auto& s : arenaTeamStats_) { if (s.teamId == teamId) { s.teamName = teamName; s.teamType = teamType; return; } }
ArenaTeamStats stub; stub.teamId = teamId; stub.teamName = teamName; stub.teamType = teamType;
arenaTeamStats_.push_back(std::move(stub));
}
void SocialHandler::handleArenaTeamRoster(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 9) return;
uint32_t teamId = packet.readUInt32();
packet.readUInt8();
uint32_t memberCount = std::min(packet.readUInt32(), 100u);
ArenaTeamRoster roster; roster.teamId = teamId; roster.members.reserve(memberCount);
for (uint32_t i = 0; i < memberCount; ++i) {
if (packet.getSize() - packet.getReadPos() < 12) break;
ArenaTeamMember m;
m.guid = packet.readUInt64(); m.online = (packet.readUInt8() != 0); m.name = packet.readString();
if (packet.getSize() - packet.getReadPos() < 20) break;
m.weekGames = packet.readUInt32(); m.weekWins = packet.readUInt32();
m.seasonGames = packet.readUInt32(); m.seasonWins = packet.readUInt32(); m.personalRating = packet.readUInt32();
if (packet.getSize() - packet.getReadPos() >= 8) { packet.readFloat(); packet.readFloat(); }
roster.members.push_back(std::move(m));
}
for (auto& r : arenaTeamRosters_) { if (r.teamId == teamId) { r = std::move(roster); return; } }
arenaTeamRosters_.push_back(std::move(roster));
}
void SocialHandler::handleArenaTeamInvite(network::Packet& packet) {
std::string playerName = packet.readString();
std::string teamName = packet.readString();
owner_.addSystemChatMessage(playerName + " has invited you to join " + teamName + ".");
}
void SocialHandler::handleArenaTeamEvent(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
uint8_t event = packet.readUInt8();
uint8_t strCount = 0;
if (packet.getSize() - packet.getReadPos() >= 1) strCount = packet.readUInt8();
std::string param1, param2;
if (strCount >= 1 && packet.getSize() > packet.getReadPos()) param1 = packet.readString();
if (strCount >= 2 && packet.getSize() > packet.getReadPos()) param2 = packet.readString();
std::string msg;
switch (event) {
case 0: msg = param1.empty() ? "A player has joined your arena team." : param1 + " has joined your arena team."; break;
case 1: msg = param1.empty() ? "A player has left the arena team." : param1 + " has left the arena team."; break;
case 2: msg = (!param1.empty() && !param2.empty()) ? param1 + " has been removed from the arena team by " + param2 + "." : "A player has been removed from the arena team."; break;
case 3: msg = param1.empty() ? "The arena team captain has changed." : param1 + " is now the arena team captain."; break;
case 4: msg = "Your arena team has been disbanded."; break;
case 5: msg = param1.empty() ? "Your arena team has been created." : "Arena team \"" + param1 + "\" has been created."; break;
default: msg = "Arena team event " + std::to_string(event); if (!param1.empty()) msg += ": " + param1; break;
}
owner_.addSystemChatMessage(msg);
}
void SocialHandler::handleArenaTeamStats(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 28) return;
ArenaTeamStats stats;
stats.teamId = packet.readUInt32(); stats.rating = packet.readUInt32();
stats.weekGames = packet.readUInt32(); stats.weekWins = packet.readUInt32();
stats.seasonGames = packet.readUInt32(); stats.seasonWins = packet.readUInt32();
stats.rank = packet.readUInt32();
for (auto& s : arenaTeamStats_) {
if (s.teamId == stats.teamId) { stats.teamName = std::move(s.teamName); stats.teamType = s.teamType; s = std::move(stats); return; }
}
arenaTeamStats_.push_back(std::move(stats));
}
void SocialHandler::requestArenaTeamRoster(uint32_t teamId) {
if (!owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_ARENA_TEAM_ROSTER));
pkt.writeUInt32(teamId);
owner_.socket->send(pkt);
}
void SocialHandler::handleArenaError(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 4) return;
uint32_t error = packet.readUInt32();
std::string msg;
switch (error) {
case 1: msg = "The other team is not big enough."; break;
case 2: msg = "That team is full."; break;
case 3: msg = "Not enough members to start."; break;
case 4: msg = "Too many members."; break;
default: msg = "Arena error (code " + std::to_string(error) + ")"; break;
}
owner_.addSystemChatMessage(msg);
}
void SocialHandler::handlePvpLogData(network::Packet& packet) {
auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); };
if (remaining() < 1) return;
bgScoreboard_ = BgScoreboardData{};
bgScoreboard_.isArena = (packet.readUInt8() != 0);
if (bgScoreboard_.isArena) {
for (int t = 0; t < 2; ++t) {
if (remaining() < 20) { packet.setReadPos(packet.getSize()); return; }
bgScoreboard_.arenaTeams[t].ratingChange = packet.readUInt32();
bgScoreboard_.arenaTeams[t].newRating = packet.readUInt32();
packet.readUInt32(); packet.readUInt32(); packet.readUInt32();
bgScoreboard_.arenaTeams[t].teamName = remaining() > 0 ? packet.readString() : "";
}
}
if (remaining() < 4) return;
uint32_t playerCount = packet.readUInt32();
bgScoreboard_.players.reserve(playerCount);
for (uint32_t i = 0; i < playerCount && remaining() >= 13; ++i) {
BgPlayerScore ps;
ps.guid = packet.readUInt64(); ps.team = packet.readUInt8();
ps.killingBlows = packet.readUInt32(); ps.honorableKills = packet.readUInt32();
ps.deaths = packet.readUInt32(); ps.bonusHonor = packet.readUInt32();
{ auto ent = owner_.getEntityManager().getEntity(ps.guid);
if (ent && (ent->getType() == game::ObjectType::PLAYER || ent->getType() == game::ObjectType::UNIT))
{ auto u = std::static_pointer_cast<game::Unit>(ent); if (!u->getName().empty()) ps.name = u->getName(); } }
if (remaining() < 4) { bgScoreboard_.players.push_back(std::move(ps)); break; }
uint32_t statCount = packet.readUInt32();
for (uint32_t s = 0; s < statCount && remaining() >= 5; ++s) {
std::string fieldName;
while (remaining() > 0) { char c = static_cast<char>(packet.readUInt8()); if (c == '\0') break; fieldName += c; }
uint32_t val = (remaining() >= 4) ? packet.readUInt32() : 0;
ps.bgStats.emplace_back(std::move(fieldName), val);
}
bgScoreboard_.players.push_back(std::move(ps));
}
if (remaining() >= 1) {
bgScoreboard_.hasWinner = (packet.readUInt8() != 0);
if (bgScoreboard_.hasWinner && remaining() >= 1) bgScoreboard_.winner = packet.readUInt8();
}
}
void SocialHandler::updateLogoutCountdown(float deltaTime) {
if (loggingOut_ && logoutCountdown_ > 0.0f) {
logoutCountdown_ -= deltaTime;
if (logoutCountdown_ < 0.0f) logoutCountdown_ = 0.0f;
}
}
void SocialHandler::resetTransferState() {
encounterUnitGuids_.fill(0);
raidTargetGuids_.fill(0);
}
// ============================================================
// Moved opcode handlers (from GameHandler::registerOpcodeHandlers)
// ============================================================
void SocialHandler::handleInitializeFactions(network::Packet& packet) {
if (!packet.hasRemaining(4)) return;
uint32_t count = packet.readUInt32();
size_t needed = static_cast<size_t>(count) * 5;
if (!packet.hasRemaining(needed)) { packet.skipAll(); return; }
owner_.initialFactions_.clear();
owner_.initialFactions_.reserve(count);
for (uint32_t i = 0; i < count; ++i) {
GameHandler::FactionStandingInit fs{};
fs.flags = packet.readUInt8();
fs.standing = static_cast<int32_t>(packet.readUInt32());
owner_.initialFactions_.push_back(fs);
}
}
void SocialHandler::handleSetFactionStanding(network::Packet& packet) {
if (!packet.hasRemaining(5)) return;
/*uint8_t showVisual =*/ packet.readUInt8();
uint32_t count = packet.readUInt32();
count = std::min(count, 128u);
owner_.loadFactionNameCache();
for (uint32_t i = 0; i < count && packet.hasRemaining(8); ++i) {
uint32_t factionId = packet.readUInt32();
int32_t standing = static_cast<int32_t>(packet.readUInt32());
int32_t oldStanding = 0;
auto it = owner_.factionStandings_.find(factionId);
if (it != owner_.factionStandings_.end()) oldStanding = it->second;
owner_.factionStandings_[factionId] = standing;
int32_t delta = standing - oldStanding;
if (delta != 0) {
std::string name = owner_.getFactionName(factionId);
char buf[256];
std::snprintf(buf, sizeof(buf), "Reputation with %s %s by %d.",
name.c_str(), delta > 0 ? "increased" : "decreased", std::abs(delta));
owner_.addSystemChatMessage(buf);
owner_.watchedFactionId_ = factionId;
if (owner_.repChangeCallback_) owner_.repChangeCallback_(name, delta, standing);
owner_.fireAddonEvent("UPDATE_FACTION", {});
owner_.fireAddonEvent("CHAT_MSG_COMBAT_FACTION_CHANGE", {std::string(buf)});
}
}
}
void SocialHandler::handleSetFactionAtWar(network::Packet& packet) {
if (!packet.hasRemaining(5)) { packet.skipAll(); return; }
uint32_t repListId = packet.readUInt32();
uint8_t setAtWar = packet.readUInt8();
if (repListId < owner_.initialFactions_.size()) {
if (setAtWar)
owner_.initialFactions_[repListId].flags |= GameHandler::FACTION_FLAG_AT_WAR;
else
owner_.initialFactions_[repListId].flags &= ~GameHandler::FACTION_FLAG_AT_WAR;
}
}
void SocialHandler::handleSetFactionVisible(network::Packet& packet) {
if (!packet.hasRemaining(5)) { packet.skipAll(); return; }
uint32_t repListId = packet.readUInt32();
uint8_t visible = packet.readUInt8();
if (repListId < owner_.initialFactions_.size()) {
if (visible)
owner_.initialFactions_[repListId].flags |= GameHandler::FACTION_FLAG_VISIBLE;
else
owner_.initialFactions_[repListId].flags &= ~GameHandler::FACTION_FLAG_VISIBLE;
}
}
void SocialHandler::handleGroupSetLeader(network::Packet& packet) {
if (!packet.hasData()) return;
std::string leaderName = packet.readString();
auto& pd = mutablePartyData();
for (const auto& m : pd.members) {
if (m.name == leaderName) { pd.leaderGuid = m.guid; break; }
}
if (!leaderName.empty())
owner_.addSystemChatMessage(leaderName + " is now the group leader.");
owner_.fireAddonEvent("PARTY_LEADER_CHANGED", {});
owner_.fireAddonEvent("GROUP_ROSTER_UPDATE", {});
}
// ============================================================
// Minimap Ping
// ============================================================
void SocialHandler::sendMinimapPing(float wowX, float wowY) {
if (owner_.state != WorldState::IN_WORLD) return;
// MSG_MINIMAP_PING (CMSG direction): float posX + float posY
// Server convention: posX = east/west axis = canonical Y (west)
// posY = north/south axis = canonical X (north)
const float serverX = wowY; // canonical Y (west) → server posX
const float serverY = wowX; // canonical X (north) → server posY
network::Packet pkt(wireOpcode(Opcode::MSG_MINIMAP_PING));
pkt.writeFloat(serverX);
pkt.writeFloat(serverY);
owner_.socket->send(pkt);
// Add ping locally so the sender sees their own ping immediately
GameHandler::MinimapPing localPing;
localPing.senderGuid = owner_.activeCharacterGuid_;
localPing.wowX = wowX;
localPing.wowY = wowY;
localPing.age = 0.0f;
owner_.minimapPings_.push_back(localPing);
}
// ============================================================
// Summon Request
// ============================================================
void SocialHandler::handleSummonRequest(network::Packet& packet) {
if (!packet.hasRemaining(16)) return;
owner_.summonerGuid_ = packet.readUInt64();
uint32_t zoneId = packet.readUInt32();
uint32_t timeoutMs = packet.readUInt32();
owner_.summonTimeoutSec_ = timeoutMs / 1000.0f;
owner_.pendingSummonRequest_= true;
owner_.summonerName_.clear();
if (auto* unit = owner_.getUnitByGuid(owner_.summonerGuid_)) {
owner_.summonerName_ = unit->getName();
}
if (owner_.summonerName_.empty()) {
owner_.summonerName_ = owner_.lookupName(owner_.summonerGuid_);
}
if (owner_.summonerName_.empty()) {
char tmp[32];
std::snprintf(tmp, sizeof(tmp), "0x%llX",
static_cast<unsigned long long>(owner_.summonerGuid_));
owner_.summonerName_ = tmp;
}
std::string msg = owner_.summonerName_ + " is summoning you";
std::string zoneName = owner_.getAreaName(zoneId);
if (!zoneName.empty())
msg += " to " + zoneName;
msg += '.';
owner_.addSystemChatMessage(msg);
LOG_INFO("SMSG_SUMMON_REQUEST: summoner=", owner_.summonerName_,
" zoneId=", zoneId, " timeout=", owner_.summonTimeoutSec_, "s");
owner_.fireAddonEvent("CONFIRM_SUMMON", {});
}
void SocialHandler::acceptSummon() {
if (!owner_.pendingSummonRequest_ || !owner_.socket) return;
owner_.pendingSummonRequest_ = false;
network::Packet pkt(wireOpcode(Opcode::CMSG_SUMMON_RESPONSE));
pkt.writeUInt8(1); // 1 = accept
owner_.socket->send(pkt);
owner_.addSystemChatMessage("Accepting summon...");
LOG_INFO("Accepted summon from ", owner_.summonerName_);
}
void SocialHandler::declineSummon() {
if (!owner_.socket) return;
owner_.pendingSummonRequest_ = false;
network::Packet pkt(wireOpcode(Opcode::CMSG_SUMMON_RESPONSE));
pkt.writeUInt8(0); // 0 = decline
owner_.socket->send(pkt);
owner_.addSystemChatMessage("Summon declined.");
}
// ============================================================
// Battlefield Manager
// ============================================================
void SocialHandler::acceptBfMgrInvite() {
if (!owner_.bfMgrInvitePending_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
// CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE: uint8 accepted = 1
network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE));
pkt.writeUInt8(1); // accepted
owner_.socket->send(pkt);
owner_.bfMgrInvitePending_ = false;
LOG_INFO("acceptBfMgrInvite: sent CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE accepted=1");
}
void SocialHandler::declineBfMgrInvite() {
if (!owner_.bfMgrInvitePending_ || owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
// CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE: uint8 accepted = 0
network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE));
pkt.writeUInt8(0); // declined
owner_.socket->send(pkt);
owner_.bfMgrInvitePending_ = false;
LOG_INFO("declineBfMgrInvite: sent CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE accepted=0");
}
// ============================================================
// Calendar
// ============================================================
void SocialHandler::requestCalendar() {
if (!owner_.isInWorld()) return;
// CMSG_CALENDAR_GET_CALENDAR has no payload
network::Packet pkt(wireOpcode(Opcode::CMSG_CALENDAR_GET_CALENDAR));
owner_.socket->send(pkt);
LOG_INFO("requestCalendar: sent CMSG_CALENDAR_GET_CALENDAR");
// Also request pending invite count
network::Packet numPkt(wireOpcode(Opcode::CMSG_CALENDAR_GET_NUM_PENDING));
owner_.socket->send(numPkt);
}
// ============================================================
// Methods moved from GameHandler
// ============================================================
void SocialHandler::sendSetDifficulty(uint32_t difficulty) {
if (!owner_.isInWorld()) {
LOG_WARNING("Cannot change difficulty: not in world");
return;
}
network::Packet packet(wireOpcode(Opcode::CMSG_CHANGEPLAYER_DIFFICULTY));
packet.writeUInt32(difficulty);
owner_.socket->send(packet);
LOG_INFO("CMSG_CHANGEPLAYER_DIFFICULTY sent: difficulty=", difficulty);
}
void SocialHandler::toggleHelm() {
if (!owner_.isInWorld()) {
LOG_WARNING("Cannot toggle helm: not in world or not connected");
return;
}
owner_.helmVisible_ = !owner_.helmVisible_;
auto packet = ShowingHelmPacket::build(owner_.helmVisible_);
owner_.socket->send(packet);
owner_.addSystemChatMessage(owner_.helmVisible_ ? "Helm is now visible." : "Helm is now hidden.");
LOG_INFO("Helm visibility toggled: ", owner_.helmVisible_);
}
void SocialHandler::toggleCloak() {
if (!owner_.isInWorld()) {
LOG_WARNING("Cannot toggle cloak: not in world or not connected");
return;
}
owner_.cloakVisible_ = !owner_.cloakVisible_;
auto packet = ShowingCloakPacket::build(owner_.cloakVisible_);
owner_.socket->send(packet);
owner_.addSystemChatMessage(owner_.cloakVisible_ ? "Cloak is now visible." : "Cloak is now hidden.");
LOG_INFO("Cloak visibility toggled: ", owner_.cloakVisible_);
}
void SocialHandler::setStandState(uint8_t standState) {
if (!owner_.isInWorld()) {
LOG_WARNING("Cannot change stand state: not in world or not connected");
return;
}
auto packet = StandStateChangePacket::build(standState);
owner_.socket->send(packet);
LOG_INFO("Changed stand state to: ", static_cast<int>(standState));
}
void SocialHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) {
if (!owner_.isInWorld()) return;
auto pkt = AlterAppearancePacket::build(hairStyle, hairColor, facialHair);
owner_.socket->send(pkt);
LOG_INFO("sendAlterAppearance: hair=", hairStyle, " color=", hairColor, " facial=", facialHair);
}
void SocialHandler::deleteGmTicket() {
if (!owner_.isInWorld()) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_DELETETICKET));
owner_.socket->send(pkt);
owner_.gmTicketActive_ = false;
owner_.gmTicketText_.clear();
LOG_INFO("Deleting GM ticket");
}
void SocialHandler::requestGmTicket() {
if (!owner_.isInWorld()) return;
// CMSG_GMTICKET_GETTICKET has no payload — server responds with SMSG_GMTICKET_GETTICKET
network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_GETTICKET));
owner_.socket->send(pkt);
LOG_DEBUG("Sent CMSG_GMTICKET_GETTICKET — querying open ticket status");
}
} // namespace game
} // namespace wowee