From 3c13cf4b1297cf3570e13606e0d9fe935741afce Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Feb 2026 13:16:38 -0800 Subject: [PATCH] Fix talent system packet parsing and rank logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse SMSG_TALENTS_INFO with correct byte-for-byte structure: * Header: uint8 spec, uint8 unspent, be32 talentCount, be16 entryCount * Entries: entryCount × (le32 id + uint8 rank) * Glyphs: uint8 glyphSlots + glyphSlots × le16 glyphId - Fix rank storage: store all talents from packet (rank 0 = first point) - Fix UI rank logic: send rank 0 for new, rank+1 for upgrades - Fix rank display: show (rank+1) for learned talents - Add sanity checks: entryCount max 64, glyphSlots max 12 - Add network boundary logging for packet debugging --- include/game/world_packets.hpp | 1 + src/game/game_handler.cpp | 19 +++++- src/game/world_packets.cpp | 102 +++++++++++++++++++++++++-------- src/network/world_socket.cpp | 25 ++++++++ src/ui/talent_screen.cpp | 25 ++++++-- 5 files changed, 141 insertions(+), 31 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index b32c9a99..045de7d3 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1768,6 +1768,7 @@ struct TalentsInfoData { uint8_t talentSpec = 0; // Active spec (0 or 1 for dual-spec) uint8_t unspentPoints = 0; // Talent points available std::vector talents; // Learned talents + std::vector glyphs; // Glyph spell IDs bool isValid() const { return true; } }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8e243f8e..d19c3792 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4413,6 +4413,20 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) { uint32_t spellId = packet.readUInt32(); knownSpells.push_back(spellId); LOG_INFO("Learned spell: ", spellId); + + // Check if this spell corresponds to a talent rank + for (const auto& [talentId, talent] : talentCache_) { + for (int rank = 0; rank < 5; ++rank) { + if (talent.rankSpells[rank] == spellId) { + // Found the talent! Update the rank for the active spec + uint8_t newRank = rank + 1; // rank is 0-indexed in array, but stored as 1-indexed + learnedTalents_[activeTalentSpec_][talentId] = newRank; + LOG_INFO("Talent learned: id=", talentId, " rank=", (int)newRank, + " (spell ", spellId, ") in spec ", (int)activeTalentSpec_); + return; + } + } + } } void GameHandler::handleRemovedSpell(network::Packet& packet) { @@ -4483,11 +4497,10 @@ void GameHandler::handleTalentsInfo(network::Packet& packet) { unspentTalentPoints_[data.talentSpec] = data.unspentPoints; // Clear and rebuild learned talents map for this spec + // Note: If a talent appears in the packet, it's learned (ranks are 0-indexed) learnedTalents_[data.talentSpec].clear(); for (const auto& talent : data.talents) { - if (talent.currentRank > 0) { - learnedTalents_[data.talentSpec][talent.talentId] = talent.currentRank; - } + learnedTalents_[data.talentSpec][talent.talentId] = talent.currentRank; } LOG_INFO("Talents loaded: spec=", (int)data.talentSpec, diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 3b1859c5..84355f1b 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include namespace wowee { @@ -2733,44 +2735,98 @@ network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spel // ============================================================ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { - // WotLK 3.3.5a SMSG_TALENTS_INFO format: - // uint8 talentSpec (0 or 1 for dual-spec) - // uint8 unspentPoints - // uint8 talentCount - // for each talent: - // uint32 talentId - // uint8 currentRank (0-5) + // SMSG_TALENTS_INFO format (AzerothCore variant): + // uint8 activeSpec + // uint8 unspentPoints + // be32 talentCount (metadata, may not match entry count) + // be16 entryCount (actual number of id+rank entries) + // Entry[entryCount]: { le32 id, uint8 rank } + // le32 glyphSlots + // le16 glyphIds[glyphSlots] - data = TalentsInfoData{}; + const size_t startPos = packet.getReadPos(); + const size_t remaining = packet.getSize() - startPos; - if (packet.getSize() - packet.getReadPos() < 3) { - LOG_ERROR("TalentsInfoParser: packet too short"); + if (remaining < 2 + 4 + 2) { + LOG_ERROR("SMSG_TALENTS_INFO: packet too short (remaining=", remaining, ")"); return false; } + data = TalentsInfoData{}; + + // Read header data.talentSpec = packet.readUInt8(); data.unspentPoints = packet.readUInt8(); - uint8_t talentCount = packet.readUInt8(); + + // These two counts are big-endian (network byte order) + uint32_t talentCountBE = packet.readUInt32(); + uint32_t talentCount = __builtin_bswap32(talentCountBE); + + uint16_t entryCountBE = packet.readUInt16(); + uint16_t entryCount = __builtin_bswap16(entryCountBE); + + // Sanity check: prevent corrupt packets from allocating excessive memory + if (entryCount > 64) { + LOG_ERROR("SMSG_TALENTS_INFO: entryCount too large (", entryCount, "), rejecting packet"); + return false; + } LOG_INFO("SMSG_TALENTS_INFO: spec=", (int)data.talentSpec, - " unspentPoints=", (int)data.unspentPoints, - " talentCount=", (int)talentCount); + " unspent=", (int)data.unspentPoints, + " talentCount=", talentCount, + " entryCount=", entryCount); - data.talents.reserve(talentCount); - for (uint8_t i = 0; i < talentCount; ++i) { + // Parse learned entries (id + rank pairs) + // These may be talents, glyphs, or other learned abilities + data.talents.clear(); + data.talents.reserve(entryCount); + + for (uint16_t i = 0; i < entryCount; ++i) { if (packet.getSize() - packet.getReadPos() < 5) { - LOG_WARNING("TalentsInfoParser: truncated talent data at index ", (int)i); - break; + LOG_ERROR("SMSG_TALENTS_INFO: truncated entry list at i=", i); + return false; } + uint32_t id = packet.readUInt32(); // LE + uint8_t rank = packet.readUInt8(); + data.talents.push_back({id, rank}); - TalentInfo talent; - talent.talentId = packet.readUInt32(); - talent.currentRank = packet.readUInt8(); - data.talents.push_back(talent); - - LOG_INFO(" Talent: id=", talent.talentId, " rank=", (int)talent.currentRank); + LOG_INFO(" Entry: id=", id, " rank=", (int)rank); } + // Parse glyph tail: glyphSlots + glyphIds[] + if (packet.getSize() - packet.getReadPos() < 1) { + LOG_WARNING("SMSG_TALENTS_INFO: no glyph tail data"); + return true; // Not fatal, older formats may not have glyphs + } + + uint8_t glyphSlots = packet.readUInt8(); + + // Sanity check: Wrath has 6 glyph slots, cap at 12 for safety + if (glyphSlots > 12) { + LOG_WARNING("SMSG_TALENTS_INFO: glyphSlots too large (", (int)glyphSlots, "), clamping to 12"); + glyphSlots = 12; + } + + LOG_INFO(" GlyphSlots: ", (int)glyphSlots); + + data.glyphs.clear(); + data.glyphs.reserve(glyphSlots); + + for (uint8_t i = 0; i < glyphSlots; ++i) { + if (packet.getSize() - packet.getReadPos() < 2) { + LOG_ERROR("SMSG_TALENTS_INFO: truncated glyph list at i=", i); + return false; + } + uint16_t glyphId = packet.readUInt16(); // LE + data.glyphs.push_back(glyphId); + if (glyphId != 0) { + LOG_INFO(" Glyph slot ", i, ": ", glyphId); + } + } + + LOG_INFO("SMSG_TALENTS_INFO: bytesConsumed=", (packet.getReadPos() - startPos), + " bytesRemaining=", (packet.getSize() - packet.getReadPos())); + return true; } diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 0062a8c3..d281b135 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -3,6 +3,8 @@ #include "network/net_platform.hpp" #include "auth/crypto.hpp" #include "core/logger.hpp" +#include +#include namespace wowee { namespace network { @@ -259,6 +261,29 @@ void WorldSocket::tryParsePackets() { // Create packet with opcode and payload Packet packet(opcode, packetData); + // Log raw SMSG_TALENTS_INFO packets at network boundary + if (opcode == 0x4C0) { // SMSG_TALENTS_INFO + std::stringstream headerHex, payloadHex; + headerHex << std::hex << std::setfill('0'); + payloadHex << std::hex << std::setfill('0'); + + // Header (4 bytes from receiveBuffer before packetData extraction) + // Note: receiveBuffer still has the full packet at this point + for (size_t i = 0; i < 4 && i < receiveBuffer.size(); ++i) { + headerHex << std::setw(2) << (int)(uint8_t)receiveBuffer[i] << " "; + } + + // Payload (ALL bytes) + for (size_t i = 0; i < packetData.size(); ++i) { + payloadHex << std::setw(2) << (int)(uint8_t)packetData[i] << " "; + } + + LOG_INFO("=== SMSG_TALENTS_INFO RAW PACKET ==="); + LOG_INFO("Header: ", headerHex.str()); + LOG_INFO("Payload: ", payloadHex.str()); + LOG_INFO("Total payload size: ", packetData.size(), " bytes"); + } + // Remove parsed data from buffer and reset header decryption counter receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize); headerBytesDecrypted = 0; diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index ae0fd540..741243f7 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -300,8 +300,12 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, ImVec2 pMax = ImGui::GetItemRectMax(); auto* drawList = ImGui::GetWindowDrawList(); + // Display rank: if learned, show (rank+1) since ranks are 0-indexed + const auto& learned = gameHandler.getLearnedTalents(); + uint8_t displayRank = (learned.find(talent.talentId) != learned.end()) ? currentRank + 1 : 0; + char rankText[16]; - snprintf(rankText, sizeof(rankText), "%u/%u", currentRank, talent.maxRank); + snprintf(rankText, sizeof(rankText), "%u/%u", displayRank, talent.maxRank); ImVec2 textSize = ImGui::CalcTextSize(rankText); ImVec2 textPos(pMax.x - textSize.x - 2, pMax.y - textSize.y - 2); @@ -309,8 +313,8 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, // Shadow drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText); // Text - ImU32 rankCol = currentRank == talent.maxRank ? IM_COL32(0, 255, 0, 255) : - currentRank > 0 ? IM_COL32(255, 255, 0, 255) : + ImU32 rankCol = displayRank == talent.maxRank ? IM_COL32(0, 255, 0, 255) : + displayRank > 0 ? IM_COL32(255, 255, 0, 255) : IM_COL32(255, 255, 255, 255); drawList->AddText(textPos, rankCol, rankText); } @@ -409,8 +413,19 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, " unspent=", static_cast(gameHandler.getUnspentTalentPoints())); if (canLearn && prereqsMet) { - LOG_INFO("Sending CMSG_LEARN_TALENT for talent ", talent.talentId, " rank ", static_cast(nextRank)); - gameHandler.learnTalent(talent.talentId, nextRank); + // Rank is 0-indexed: first point = rank 0, second = rank 1, etc. + // Check if talent is already learned + const auto& learned = gameHandler.getLearnedTalents(); + uint8_t desiredRank; + if (learned.find(talent.talentId) == learned.end()) { + // Not learned yet, learn first rank (0) + desiredRank = 0; + } else { + // Already learned, upgrade to next rank + desiredRank = currentRank + 1; + } + LOG_INFO("Sending CMSG_LEARN_TALENT for talent ", talent.talentId, " rank ", static_cast(desiredRank), " (0-indexed)"); + gameHandler.learnTalent(talent.talentId, desiredRank); } else { if (!canLearn) LOG_WARNING("Cannot learn: canLearn=false"); if (!prereqsMet) LOG_WARNING("Cannot learn: prereqsMet=false");