mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09:33:51 +00:00
Fix talent system packet parsing and rank logic
- 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
This commit is contained in:
parent
e7556605d7
commit
3c13cf4b12
5 changed files with 141 additions and 31 deletions
|
|
@ -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<TalentInfo> talents; // Learned talents
|
||||
std::vector<uint32_t> glyphs; // Glyph spell IDs
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <zlib.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include "network/net_platform.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<int>(gameHandler.getUnspentTalentPoints()));
|
||||
|
||||
if (canLearn && prereqsMet) {
|
||||
LOG_INFO("Sending CMSG_LEARN_TALENT for talent ", talent.talentId, " rank ", static_cast<int>(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<int>(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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue