mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +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
ba79374689
commit
a193da4ed9
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 talentSpec = 0; // Active spec (0 or 1 for dual-spec)
|
||||||
uint8_t unspentPoints = 0; // Talent points available
|
uint8_t unspentPoints = 0; // Talent points available
|
||||||
std::vector<TalentInfo> talents; // Learned talents
|
std::vector<TalentInfo> talents; // Learned talents
|
||||||
|
std::vector<uint32_t> glyphs; // Glyph spell IDs
|
||||||
|
|
||||||
bool isValid() const { return true; }
|
bool isValid() const { return true; }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4413,6 +4413,20 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||||
uint32_t spellId = packet.readUInt32();
|
uint32_t spellId = packet.readUInt32();
|
||||||
knownSpells.push_back(spellId);
|
knownSpells.push_back(spellId);
|
||||||
LOG_INFO("Learned spell: ", 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) {
|
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
||||||
|
|
@ -4483,11 +4497,10 @@ void GameHandler::handleTalentsInfo(network::Packet& packet) {
|
||||||
unspentTalentPoints_[data.talentSpec] = data.unspentPoints;
|
unspentTalentPoints_[data.talentSpec] = data.unspentPoints;
|
||||||
|
|
||||||
// Clear and rebuild learned talents map for this spec
|
// 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();
|
learnedTalents_[data.talentSpec].clear();
|
||||||
for (const auto& talent : data.talents) {
|
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,
|
LOG_INFO("Talents loaded: spec=", (int)data.talentSpec,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -2733,44 +2735,98 @@ network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spel
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
|
bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
|
||||||
// WotLK 3.3.5a SMSG_TALENTS_INFO format:
|
// SMSG_TALENTS_INFO format (AzerothCore variant):
|
||||||
// uint8 talentSpec (0 or 1 for dual-spec)
|
// uint8 activeSpec
|
||||||
// uint8 unspentPoints
|
// uint8 unspentPoints
|
||||||
// uint8 talentCount
|
// be32 talentCount (metadata, may not match entry count)
|
||||||
// for each talent:
|
// be16 entryCount (actual number of id+rank entries)
|
||||||
// uint32 talentId
|
// Entry[entryCount]: { le32 id, uint8 rank }
|
||||||
// uint8 currentRank (0-5)
|
// 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) {
|
if (remaining < 2 + 4 + 2) {
|
||||||
LOG_ERROR("TalentsInfoParser: packet too short");
|
LOG_ERROR("SMSG_TALENTS_INFO: packet too short (remaining=", remaining, ")");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data = TalentsInfoData{};
|
||||||
|
|
||||||
|
// Read header
|
||||||
data.talentSpec = packet.readUInt8();
|
data.talentSpec = packet.readUInt8();
|
||||||
data.unspentPoints = 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,
|
LOG_INFO("SMSG_TALENTS_INFO: spec=", (int)data.talentSpec,
|
||||||
" unspentPoints=", (int)data.unspentPoints,
|
" unspent=", (int)data.unspentPoints,
|
||||||
" talentCount=", (int)talentCount);
|
" talentCount=", talentCount,
|
||||||
|
" entryCount=", entryCount);
|
||||||
|
|
||||||
data.talents.reserve(talentCount);
|
// Parse learned entries (id + rank pairs)
|
||||||
for (uint8_t i = 0; i < talentCount; ++i) {
|
// 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) {
|
if (packet.getSize() - packet.getReadPos() < 5) {
|
||||||
LOG_WARNING("TalentsInfoParser: truncated talent data at index ", (int)i);
|
LOG_ERROR("SMSG_TALENTS_INFO: truncated entry list at i=", i);
|
||||||
break;
|
return false;
|
||||||
}
|
}
|
||||||
|
uint32_t id = packet.readUInt32(); // LE
|
||||||
|
uint8_t rank = packet.readUInt8();
|
||||||
|
data.talents.push_back({id, rank});
|
||||||
|
|
||||||
TalentInfo talent;
|
LOG_INFO(" Entry: id=", id, " rank=", (int)rank);
|
||||||
talent.talentId = packet.readUInt32();
|
|
||||||
talent.currentRank = packet.readUInt8();
|
|
||||||
data.talents.push_back(talent);
|
|
||||||
|
|
||||||
LOG_INFO(" Talent: id=", talent.talentId, " rank=", (int)talent.currentRank);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#include "network/net_platform.hpp"
|
#include "network/net_platform.hpp"
|
||||||
#include "auth/crypto.hpp"
|
#include "auth/crypto.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace network {
|
namespace network {
|
||||||
|
|
@ -259,6 +261,29 @@ void WorldSocket::tryParsePackets() {
|
||||||
// Create packet with opcode and payload
|
// Create packet with opcode and payload
|
||||||
Packet packet(opcode, packetData);
|
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
|
// Remove parsed data from buffer and reset header decryption counter
|
||||||
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize);
|
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize);
|
||||||
headerBytesDecrypted = 0;
|
headerBytesDecrypted = 0;
|
||||||
|
|
|
||||||
|
|
@ -300,8 +300,12 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
||||||
ImVec2 pMax = ImGui::GetItemRectMax();
|
ImVec2 pMax = ImGui::GetItemRectMax();
|
||||||
auto* drawList = ImGui::GetWindowDrawList();
|
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];
|
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 textSize = ImGui::CalcTextSize(rankText);
|
||||||
ImVec2 textPos(pMax.x - textSize.x - 2, pMax.y - textSize.y - 2);
|
ImVec2 textPos(pMax.x - textSize.x - 2, pMax.y - textSize.y - 2);
|
||||||
|
|
@ -309,8 +313,8 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
||||||
// Shadow
|
// Shadow
|
||||||
drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText);
|
drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText);
|
||||||
// Text
|
// Text
|
||||||
ImU32 rankCol = currentRank == talent.maxRank ? IM_COL32(0, 255, 0, 255) :
|
ImU32 rankCol = displayRank == talent.maxRank ? IM_COL32(0, 255, 0, 255) :
|
||||||
currentRank > 0 ? IM_COL32(255, 255, 0, 255) :
|
displayRank > 0 ? IM_COL32(255, 255, 0, 255) :
|
||||||
IM_COL32(255, 255, 255, 255);
|
IM_COL32(255, 255, 255, 255);
|
||||||
drawList->AddText(textPos, rankCol, rankText);
|
drawList->AddText(textPos, rankCol, rankText);
|
||||||
}
|
}
|
||||||
|
|
@ -409,8 +413,19 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
|
||||||
" unspent=", static_cast<int>(gameHandler.getUnspentTalentPoints()));
|
" unspent=", static_cast<int>(gameHandler.getUnspentTalentPoints()));
|
||||||
|
|
||||||
if (canLearn && prereqsMet) {
|
if (canLearn && prereqsMet) {
|
||||||
LOG_INFO("Sending CMSG_LEARN_TALENT for talent ", talent.talentId, " rank ", static_cast<int>(nextRank));
|
// Rank is 0-indexed: first point = rank 0, second = rank 1, etc.
|
||||||
gameHandler.learnTalent(talent.talentId, nextRank);
|
// 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 {
|
} else {
|
||||||
if (!canLearn) LOG_WARNING("Cannot learn: canLearn=false");
|
if (!canLearn) LOG_WARNING("Cannot learn: canLearn=false");
|
||||||
if (!prereqsMet) LOG_WARNING("Cannot learn: prereqsMet=false");
|
if (!prereqsMet) LOG_WARNING("Cannot learn: prereqsMet=false");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue