#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 #include #include namespace wowee { namespace game { // Free function defined in game_handler.cpp std::string buildItemLink(uint32_t itemId, uint32_t quality, const std::string& name); static bool packetHasRemaining(const network::Packet& packet, size_t need) { const size_t size = packet.getSize(); const size_t pos = packet.getReadPos(); return pos <= size && need <= (size - pos); } 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) { if (packet.getSize() - packet.getReadPos() < 1) return; uint8_t ignCount = packet.readUInt8(); for (uint8_t i = 0; i < ignCount; ++i) { if (packet.getSize() - packet.getReadPos() < 8) break; uint64_t ignGuid = packet.readUInt64(); std::string ignName = packet.readString(); if (!ignName.empty() && ignGuid != 0) owner_.ignoreCache[ignName] = 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_.entityManager.getEntity(initiatorGuid); if (auto* unit = dynamic_cast(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_.playerNameCache.find(respGuid); std::string rname; if (nit != owner_.playerNameCache.end()) rname = nit->second; else { auto ent = owner_.entityManager.getEntity(respGuid); if (ent) rname = std::static_pointer_cast(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_.playerNameCache.find(g); if (nit != owner_.playerNameCache.end()) return nit->second; auto ent = owner_.entityManager.getEntity(g); if (ent && (ent->getType() == game::ObjectType::UNIT || ent->getType() == game::ObjectType::PLAYER)) return std::static_pointer_cast(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_.playerNameCache.find(guid); if (it != owner_.playerNameCache.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_.playerNameCache.find(guid); if (it != owner_.playerNameCache.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_.entityManager.getEntity(roleGuid)) if (auto u = std::dynamic_pointer_cast(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_.entityManager.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(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( 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_.entityManager.getEntity(guid); std::string name = "Target"; if (entity) { auto player = std::dynamic_pointer_cast(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_.entityManager.getEntity(guid); std::string playerName = "Target"; if (entity) { auto player = std::dynamic_pointer_cast(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 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_.entityManager.getEntity(duelChallengerGuid_); if (auto* unit = dynamic_cast(entity.get())) duelChallengerName_ = unit->getName(); if (duelChallengerName_.empty()) { auto nit = owner_.playerNameCache.find(duelChallengerGuid_); if (nit != owner_.playerNameCache.end()) duelChallengerName_ = nit->second; } if (duelChallengerName_.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", static_cast(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(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 uint32_t prevCount = partyData.memberCount; 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(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(packet.readUInt16()); member->posY = static_cast(packet.readUInt16()); } } if (updateFlags & 0x0200) { if (remaining() >= 8) { uint64_t auraMask = packet.readUInt64(); std::vector newAuras; for (int i = 0; i < 64; ++i) { if (auraMask & (uint64_t(1) << i)) { AuraSlot a; a.level = static_cast(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(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_.playerNameCache.find(guid); std::string name; if (nit != owner_.playerNameCache.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_.playerNameCache.find(guid); if (nit != owner_.playerNameCache.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_.playerNameCache.find(guid); if (nit != owner_.playerNameCache.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_.playerNameCache.find(data.guid); if (it != owner_.playerNameCache.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_.playerNameCache.find(data.rollerGuid); if (it != owner_.playerNameCache.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 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(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(packet.readUInt32()); int32_t waitTime = static_cast(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 (!packetHasRemaining(packet, 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 (packetHasRemaining(packet, 4)) { uint32_t rewardCount = packet.readUInt32(); for (uint32_t i = 0; i < rewardCount && packetHasRemaining(packet, 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 (!packetHasRemaining(packet, 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(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_.entityManager.getEntity(ps.guid); if (ent && (ent->getType() == game::ObjectType::PLAYER || ent->getType() == game::ObjectType::UNIT)) { auto u = std::static_pointer_cast(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(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(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(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(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(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(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