From a90c130d6e5e8c100b320be4f0ced90ac9f90c4e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Feb 2026 15:05:18 -0800 Subject: [PATCH] Fix guild roster, /who, /inspect, and character preview bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guild O tab: fallback to character guildId when guildName_ not yet queried, re-query guild info on roster open. /who: add missing stringCount field and fix maxLevel default (0→100). /inspect: add SMSG_INSPECT_TALENT opcode (0x3F4) and rewrite parser for WotLK PackedGUID+talent format. Character preview: reset all tracking variables in setAssetManager() to force model reload on login. --- Data/expansions/classic/opcodes.json | 3 +- Data/expansions/tbc/opcodes.json | 3 +- Data/expansions/turtle/opcodes.json | 3 +- Data/expansions/wotlk/opcodes.json | 3 +- include/game/game_handler.hpp | 6 +- include/game/opcode_table.hpp | 1 + include/game/world_packets.hpp | 2 +- include/ui/character_screen.hpp | 5 + src/game/game_handler.cpp | 133 +++++++++++++++------------ src/game/opcode_table.cpp | 2 + src/game/world_packets.cpp | 5 +- src/ui/game_screen.cpp | 7 ++ 12 files changed, 108 insertions(+), 65 deletions(-) diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index 538fe7aa..d2ab7d63 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -237,5 +237,6 @@ "CMSG_LEAVE_CHANNEL": "0x098", "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", - "SMSG_CHANNEL_LIST": "0x09B" + "SMSG_CHANNEL_LIST": "0x09B", + "SMSG_INSPECT_TALENT": "0x3F4" } diff --git a/Data/expansions/tbc/opcodes.json b/Data/expansions/tbc/opcodes.json index f9684c6e..44e50cc6 100644 --- a/Data/expansions/tbc/opcodes.json +++ b/Data/expansions/tbc/opcodes.json @@ -266,5 +266,6 @@ "CMSG_LEAVE_CHANNEL": "0x098", "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", - "SMSG_CHANNEL_LIST": "0x09B" + "SMSG_CHANNEL_LIST": "0x09B", + "SMSG_INSPECT_TALENT": "0x3F4" } diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index 538fe7aa..d2ab7d63 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -237,5 +237,6 @@ "CMSG_LEAVE_CHANNEL": "0x098", "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", - "SMSG_CHANNEL_LIST": "0x09B" + "SMSG_CHANNEL_LIST": "0x09B", + "SMSG_INSPECT_TALENT": "0x3F4" } diff --git a/Data/expansions/wotlk/opcodes.json b/Data/expansions/wotlk/opcodes.json index e86c339e..e0c3e3e3 100644 --- a/Data/expansions/wotlk/opcodes.json +++ b/Data/expansions/wotlk/opcodes.json @@ -266,5 +266,6 @@ "CMSG_LEAVE_CHANNEL": "0x098", "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", - "SMSG_CHANNEL_LIST": "0x09B" + "SMSG_CHANNEL_LIST": "0x09B", + "SMSG_INSPECT_TALENT": "0x3F4" } diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 4b063ff0..b6848044 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -330,7 +330,11 @@ public: void queryGuildInfo(uint32_t guildId); // Guild state accessors - bool isInGuild() const { return !guildName_.empty(); } + bool isInGuild() const { + if (!guildName_.empty()) return true; + const Character* ch = getActiveCharacter(); + return ch && ch->hasGuild(); + } const std::string& getGuildName() const { return guildName_; } const GuildRosterData& getGuildRoster() const { return guildRoster_; } bool hasGuildRoster() const { return hasGuildRoster_; } diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 7f793aab..afe0d40e 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -279,6 +279,7 @@ enum class LogicalOpcode : uint16_t { SMSG_INVENTORY_CHANGE_FAILURE, CMSG_INSPECT, SMSG_INSPECT_RESULTS, + SMSG_INSPECT_TALENT, // ---- Death/Respawn ---- CMSG_REPOP_REQUEST, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 248e7205..d01381c8 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -823,7 +823,7 @@ public: /** CMSG_WHO packet builder */ class WhoPacket { public: - static network::Packet build(uint32_t minLevel = 0, uint32_t maxLevel = 0, + static network::Packet build(uint32_t minLevel = 0, uint32_t maxLevel = 100, const std::string& playerName = "", const std::string& guildName = "", uint32_t raceMask = 0xFFFFFFFF, diff --git a/include/ui/character_screen.hpp b/include/ui/character_screen.hpp index f9487525..3899b16a 100644 --- a/include/ui/character_screen.hpp +++ b/include/ui/character_screen.hpp @@ -29,6 +29,11 @@ public: void setAssetManager(pipeline::AssetManager* am) { assetManager_ = am; previewInitialized_ = false; + previewGuid_ = 0; + previewAppearanceBytes_ = 0; + previewFacialFeatures_ = 0; + previewUseFemaleModel_ = false; + previewEquipHash_ = 0; } /** diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 84d1e534..a026dabd 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -801,6 +801,7 @@ void GameHandler::handlePacket(network::Packet& packet) { break; case Opcode::SMSG_INSPECT_RESULTS: + case Opcode::SMSG_INSPECT_TALENT: handleInspectResults(packet); break; @@ -5259,76 +5260,92 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) { } void GameHandler::handleInspectResults(network::Packet& packet) { - // Best-effort parsing across Classic/TBC/WotLK variants. - // We only care about item entry IDs per equip slot. - if (packet.getSize() - packet.getReadPos() < 8) return; + // SMSG_INSPECT_TALENT (WotLK 3.3.5a) format: + // PackedGUID, uint32 unspentTalents, uint8 talentGroupCount, uint8 activeTalentGroup + // Per talent group: uint8 talentCount, [talentId(u32) + rank(u8)]..., uint8 glyphCount, [glyphId(u16)]... + // Then enchantment bitmask + enchant IDs + if (packet.getSize() - packet.getReadPos() < 4) return; - uint64_t guid = packet.readUInt64(); + uint64_t guid = UpdateObjectParser::readPackedGuid(packet); if (guid == 0) return; - const size_t remaining = packet.getSize() - packet.getReadPos(); - - auto tryParseFixed = [&](size_t perSlotBytes, size_t itemIdOffset) -> std::optional> { - if (remaining < 19 * perSlotBytes) return std::nullopt; - auto saved = packet.getReadPos(); - std::array items{}; - bool plausible = false; - - for (int i = 0; i < 19; i++) { - if (perSlotBytes == 4) { - items[i] = packet.readUInt32(); - } else if (perSlotBytes == 8) { - uint32_t a = packet.readUInt32(); - uint32_t b = packet.readUInt32(); - items[i] = (itemIdOffset == 0) ? a : b; - } else { - packet.setReadPos(saved); - return std::nullopt; - } - if (items[i] > 0 && items[i] < 5000000u) plausible = true; + size_t bytesLeft = packet.getSize() - packet.getReadPos(); + if (bytesLeft < 6) { + LOG_WARNING("SMSG_INSPECT_TALENT: too short after guid, ", bytesLeft, " bytes"); + // Show basic inspect message even without talent data + auto entity = entityManager.getEntity(guid); + std::string name = "Target"; + if (entity) { + auto player = std::dynamic_pointer_cast(entity); + if (player && !player->getName().empty()) name = player->getName(); } - - // Rewind to allow other attempts if implausible. - if (!plausible) { - packet.setReadPos(saved); - return std::nullopt; - } - return items; - }; - - std::optional> parsed; - // Common shapes: [guid][19*uint32 itemId] or [guid][19*(uint32 itemId, uint32 enchant)]. - parsed = tryParseFixed(4, 0); - if (!parsed) parsed = tryParseFixed(8, 0); - if (!parsed) parsed = tryParseFixed(8, 4); // sometimes itemId is second dword - - if (!parsed) { - LOG_WARNING("SMSG_INSPECT_RESULTS: unrecognized payload size=", remaining, " for guid=0x", std::hex, guid, std::dec); + addSystemChatMessage("Inspecting " + name + " (no talent data available)."); return; } - inspectedPlayerItemEntries_[guid] = *parsed; + uint32_t unspentTalents = packet.readUInt32(); + uint8_t talentGroupCount = packet.readUInt8(); + uint8_t activeTalentGroup = packet.readUInt8(); - // Query item templates so we can resolve displayInfoId/inventoryType. - for (uint32_t entry : *parsed) { - if (entry == 0) continue; - queryItemInfo(entry, 0); + // Resolve player name + auto entity = entityManager.getEntity(guid); + std::string playerName = "Target"; + if (entity) { + auto player = std::dynamic_pointer_cast(entity); + if (player && !player->getName().empty()) playerName = player->getName(); } - // If templates already exist, emit immediately. - if (playerEquipmentCallback_) { - std::array displayIds{}; - std::array invTypes{}; - for (int s = 0; s < 19; s++) { - uint32_t entry = (*parsed)[s]; - if (entry == 0) continue; - auto infoIt = itemInfoCache_.find(entry); - if (infoIt == itemInfoCache_.end()) continue; - displayIds[s] = infoIt->second.displayInfoId; - invTypes[s] = static_cast(infoIt->second.inventoryType); + // Parse talent groups + 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(); // talentId + packet.readUInt8(); // rank + 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(); // glyphId } - playerEquipmentCallback_(guid, displayIds, invTypes); } + + // Parse enchantment slot mask + enchant IDs + 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; + packet.readUInt16(); // enchantId + } + } + } + + // Display inspect results + std::string msg = "Inspect: " + playerName; + msg += " - " + std::to_string(totalTalents) + " talent points spent"; + if (unspentTalents > 0) { + msg += ", " + std::to_string(unspentTalents) + " unspent"; + } + if (talentGroupCount > 1) { + msg += " (dual spec, active: " + std::to_string(activeTalentGroup + 1) + ")"; + } + addSystemChatMessage(msg); + + LOG_INFO("Inspect results for ", playerName, ": ", totalTalents, " talents, ", + unspentTalents, " unspent, ", (int)talentGroupCount, " specs"); } uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const { diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 0cedea36..c0705e00 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -290,6 +290,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"SMSG_CHANNEL_NOTIFY", LogicalOpcode::SMSG_CHANNEL_NOTIFY}, {"CMSG_CHANNEL_LIST", LogicalOpcode::CMSG_CHANNEL_LIST}, {"SMSG_CHANNEL_LIST", LogicalOpcode::SMSG_CHANNEL_LIST}, + {"SMSG_INSPECT_TALENT", LogicalOpcode::SMSG_INSPECT_TALENT}, }; // clang-format on @@ -581,6 +582,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_CHANNEL_NOTIFY, 0x099}, {LogicalOpcode::CMSG_CHANNEL_LIST, 0x09A}, {LogicalOpcode::SMSG_CHANNEL_LIST, 0x09B}, + {LogicalOpcode::SMSG_INSPECT_TALENT, 0x3F4}, }; logicalToWire_.clear(); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 982b3d7f..6dbd2cff 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1431,7 +1431,10 @@ network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel, packet.writeString(guildName); packet.writeUInt32(raceMask); packet.writeUInt32(classMask); - packet.writeUInt32(zones); // Number of zones + packet.writeUInt32(zones); // Number of zone IDs (0 = no zone filter) + // Zone ID array would go here if zones > 0 + packet.writeUInt32(0); // stringCount (number of search strings) + // String array would go here if stringCount > 0 LOG_DEBUG("Built CMSG_WHO: player=", playerName); return packet; } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index fcee6d61..45cb400c 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -3369,6 +3369,13 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { showGuildRoster_ = false; return; } + // Re-query guild name if we have guildId but no name yet + if (gameHandler.getGuildName().empty()) { + const auto* ch = gameHandler.getActiveCharacter(); + if (ch && ch->hasGuild()) { + gameHandler.queryGuildInfo(ch->guildId); + } + } gameHandler.requestGuildRoster(); } }