diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 6fbb64b2..930ae2e9 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1611,6 +1611,12 @@ public: auto it = achievementPointsCache_.find(id); return (it != achievementPointsCache_.end()) ? it->second : 0u; } + /// Returns the set of achievement IDs earned by an inspected player (via SMSG_RESPOND_INSPECT_ACHIEVEMENTS). + /// Returns nullptr if no inspect data is available for the given GUID. + const std::unordered_set* getInspectedPlayerAchievements(uint64_t guid) const { + auto it = inspectedPlayerAchievements_.find(guid); + return (it != inspectedPlayerAchievements_.end()) ? &it->second : nullptr; + } // Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received. // The soundId corresponds to a SoundEntries.dbc record. The receiver is @@ -2835,6 +2841,11 @@ private: std::unordered_map criteriaProgress_; void handleAllAchievementData(network::Packet& packet); + // Per-player achievement data from SMSG_RESPOND_INSPECT_ACHIEVEMENTS + // Key: inspected player's GUID; value: set of earned achievement IDs + std::unordered_map> inspectedPlayerAchievements_; + void handleRespondInspectAchievements(network::Packet& packet); + // Area name cache (lazy-loaded from WorldMapArea.dbc; maps AreaTable ID → display name) std::unordered_map areaNameCache_; bool areaNameCacheLoaded_ = false; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 5f16039c..e5c7e63c 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1448,6 +1448,12 @@ public: static network::Packet build(uint64_t targetGuid); }; +/** CMSG_QUERY_INSPECT_ACHIEVEMENTS packet builder (WotLK 3.3.5a) */ +class QueryInspectAchievementsPacket { +public: + static network::Packet build(uint64_t targetGuid); +}; + /** CMSG_NAME_QUERY packet builder */ class NameQueryPacket { public: diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9816ff03..a653c523 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6884,10 +6884,12 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_REDIRECT_CLIENT: case Opcode::SMSG_PVP_QUEUE_STATS: case Opcode::SMSG_NOTIFY_DEST_LOC_SPELL_CAST: - case Opcode::SMSG_RESPOND_INSPECT_ACHIEVEMENTS: case Opcode::SMSG_PLAYER_SKINNED: packet.setReadPos(packet.getSize()); break; + case Opcode::SMSG_RESPOND_INSPECT_ACHIEVEMENTS: + handleRespondInspectAchievements(packet); + break; case Opcode::SMSG_QUEST_POI_QUERY_RESPONSE: handleQuestPoiQueryResponse(packet); break; @@ -8039,6 +8041,9 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { encounterUnitGuids_.fill(0); raidTargetGuids_.fill(0); + // Clear inspect caches on world entry to avoid showing stale data + inspectedPlayerAchievements_.clear(); + // Reset talent initialization so the first SMSG_TALENTS_INFO after login // correctly sets the active spec (static locals don't reset across logins) talentsInitialized_ = false; @@ -11301,6 +11306,12 @@ void GameHandler::inspectTarget() { auto packet = InspectPacket::build(targetGuid); socket->send(packet); + // WotLK: also query the player's achievement data so the inspect UI can display it + if (isActiveExpansion("wotlk")) { + auto achPkt = QueryInspectAchievementsPacket::build(targetGuid); + socket->send(achPkt); + } + auto player = std::static_pointer_cast(target); std::string name = player->getName().empty() ? "Target" : player->getName(); addSystemChatMessage("Inspecting " + name + "..."); @@ -22077,6 +22088,55 @@ void GameHandler::handleAllAchievementData(network::Packet& packet) { " achievements, ", criteriaProgress_.size(), " criteria"); } +// --------------------------------------------------------------------------- +// SMSG_RESPOND_INSPECT_ACHIEVEMENTS (WotLK 3.3.5a) +// Wire format: packed_guid (inspected player) + same achievement/criteria +// blocks as SMSG_ALL_ACHIEVEMENT_DATA: +// Achievement records: repeated { uint32 id, uint32 packedDate } until 0xFFFFFFFF sentinel +// Criteria records: repeated { uint32 id, uint64 counter, uint32 date, uint32 unk } +// until 0xFFFFFFFF sentinel +// We store only the earned achievement IDs (not criteria) per inspected player. +// --------------------------------------------------------------------------- +void GameHandler::handleRespondInspectAchievements(network::Packet& packet) { + loadAchievementNameCache(); + + // Read the inspected player's packed guid + if (packet.getSize() - packet.getReadPos() < 1) return; + uint64_t inspectedGuid = UpdateObjectParser::readPackedGuid(packet); + if (inspectedGuid == 0) { + packet.setReadPos(packet.getSize()); + return; + } + + std::unordered_set achievements; + + // Achievement records: { uint32 id, uint32 packedDate } until sentinel 0xFFFFFFFF + while (packet.getSize() - packet.getReadPos() >= 4) { + uint32_t id = packet.readUInt32(); + if (id == 0xFFFFFFFF) break; + if (packet.getSize() - packet.getReadPos() < 4) break; + /*uint32_t date =*/ packet.readUInt32(); + achievements.insert(id); + } + + // Criteria records: { uint32 id, uint64 counter, uint32 date, uint32 unk } + // until sentinel 0xFFFFFFFF — consume but don't store for inspect use + while (packet.getSize() - packet.getReadPos() >= 4) { + uint32_t id = packet.readUInt32(); + if (id == 0xFFFFFFFF) break; + // counter(8) + date(4) + unk(4) = 16 bytes + if (packet.getSize() - packet.getReadPos() < 16) break; + packet.readUInt64(); // counter + packet.readUInt32(); // date + packet.readUInt32(); // unk + } + + inspectedPlayerAchievements_[inspectedGuid] = std::move(achievements); + + LOG_INFO("SMSG_RESPOND_INSPECT_ACHIEVEMENTS: guid=0x", std::hex, inspectedGuid, std::dec, + " achievements=", inspectedPlayerAchievements_[inspectedGuid].size()); +} + // --------------------------------------------------------------------------- // Faction name cache (lazily loaded from Faction.dbc) // --------------------------------------------------------------------------- diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 7e9be845..e2fb3772 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1722,6 +1722,15 @@ network::Packet InspectPacket::build(uint64_t targetGuid) { return packet; } +network::Packet QueryInspectAchievementsPacket::build(uint64_t targetGuid) { + // CMSG_QUERY_INSPECT_ACHIEVEMENTS: uint64 targetGuid + uint8 unk (always 0) + network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_INSPECT_ACHIEVEMENTS)); + packet.writeUInt64(targetGuid); + packet.writeUInt8(0); // unk / achievementSlot — always 0 for WotLK + LOG_DEBUG("Built CMSG_QUERY_INSPECT_ACHIEVEMENTS: target=0x", std::hex, targetGuid, std::dec); + return packet; +} + // ============================================================ // Server Info Commands // ============================================================