Kelsidavis-WoWee/src/game/world_packets_world.cpp
Pavel Okhlopkov de0383aa6b refactor: extract spline math, consolidate packet parsing, decompose TransportManager
Extract CatmullRomSpline (include/math/spline.hpp, src/math/spline.cpp) as a
standalone, immutable, thread-safe spline module with O(log n) binary segment
search and fused position+tangent evaluation — replacing the duplicated O(n)
evalTimedCatmullRom/orientationFromTangent pair in TransportManager.

Consolidate 7 copies of spline packet parsing into shared functions in
game/spline_packet.{hpp,cpp}: parseMonsterMoveSplineBody (WotLK/TBC),
parseMonsterMoveSplineBodyVanilla, parseClassicMoveUpdateSpline,
parseWotlkMoveUpdateSpline, and decodePackedDelta. Named SplineFlag constants
replace magic hex literals throughout.

Extract TransportPathRepository (game/transport_path_repository.{hpp,cpp}) from
TransportManager — owns path data, DBC loading, and path inference. Paths stored
as PathEntry wrapping CatmullRomSpline + metadata (zOnly, fromDBC, worldCoords).
TransportManager reduced from ~1200 to ~500 lines, focused on transport lifecycle
and server sync.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-11 08:30:28 +03:00

1420 lines
53 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "game/world_packets.hpp"
#include "game/packet_parsers.hpp"
#include "game/opcodes.hpp"
#include "game/character.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <sstream>
#include <iomanip>
#include <zlib.h>
namespace {
inline uint32_t bswap32(uint32_t v) {
return ((v & 0xFF000000u) >> 24) | ((v & 0x00FF0000u) >> 8)
| ((v & 0x0000FF00u) << 8) | ((v & 0x000000FFu) << 24);
}
inline uint16_t bswap16(uint16_t v) {
return static_cast<uint16_t>(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8));
}
} // anonymous namespace
namespace wowee {
namespace game {
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
// Always reset output to avoid stale targets when callers reuse buffers.
data = SpellGoData{};
// Packed GUIDs are variable-length, so only require the smallest possible
// shape up front: 2 GUID masks + fixed fields through hitCount.
if (!packet.hasRemaining(16)) return false;
size_t startPos = packet.getReadPos();
if (!packet.hasFullPackedGuid()) {
return false;
}
data.casterGuid = packet.readPackedGuid();
if (!packet.hasFullPackedGuid()) {
packet.setReadPos(startPos);
return false;
}
data.casterUnit = packet.readPackedGuid();
// Validate remaining fixed fields up to hitCount/missCount
if (!packet.hasRemaining(14)) { // castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1)
packet.setReadPos(startPos);
return false;
}
data.castCount = packet.readUInt8();
data.spellId = packet.readUInt32();
data.castFlags = packet.readUInt32();
// Timestamp in 3.3.5a
packet.readUInt32();
const uint8_t rawHitCount = packet.readUInt8();
if (rawHitCount > 128) {
LOG_WARNING("Spell go: hitCount capped (requested=", static_cast<int>(rawHitCount), ")");
}
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
bool truncatedTargets = false;
data.hitTargets.reserve(storedHitLimit);
for (uint16_t i = 0; i < rawHitCount; ++i) {
// WotLK 3.3.5a hit targets are full uint64 GUIDs (not PackedGuid).
if (!packet.hasRemaining(8)) {
LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", static_cast<int>(rawHitCount));
truncatedTargets = true;
break;
}
const uint64_t targetGuid = packet.readUInt64();
if (i < storedHitLimit) {
data.hitTargets.push_back(targetGuid);
}
}
if (truncatedTargets) {
packet.setReadPos(startPos);
return false;
}
data.hitCount = static_cast<uint8_t>(data.hitTargets.size());
// missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation.
if (!packet.hasRemaining(1)) {
LOG_WARNING("Spell go: missing missCount after hit target list");
packet.setReadPos(startPos);
return false;
}
const size_t missCountPos = packet.getReadPos();
const uint8_t rawMissCount = packet.readUInt8();
if (rawMissCount > 20) {
// Likely offset error — dump context bytes for diagnostics.
const auto& raw = packet.getData();
std::string hexCtx;
size_t dumpStart = (missCountPos >= 8) ? missCountPos - 8 : startPos;
size_t dumpEnd = std::min(missCountPos + 16, raw.size());
for (size_t i = dumpStart; i < dumpEnd; ++i) {
char buf[4];
std::snprintf(buf, sizeof(buf), "%02x ", raw[i]);
hexCtx += buf;
if (i == missCountPos - 1) hexCtx += "[";
if (i == missCountPos) hexCtx += "] ";
}
LOG_WARNING("Spell go: suspect missCount=", static_cast<int>(rawMissCount),
" spell=", data.spellId, " hits=", static_cast<int>(data.hitCount),
" castFlags=0x", std::hex, data.castFlags, std::dec,
" missCountPos=", missCountPos, " pktSize=", packet.getSize(),
" ctx=", hexCtx);
}
if (rawMissCount > 128) {
LOG_WARNING("Spell go: missCount capped (requested=", static_cast<int>(rawMissCount),
") spell=", data.spellId, " hits=", static_cast<int>(data.hitCount),
" remaining=", packet.getRemainingSize());
}
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
data.missTargets.reserve(storedMissLimit);
for (uint16_t i = 0; i < rawMissCount; ++i) {
// WotLK 3.3.5a miss targets are full uint64 GUIDs + uint8 missType.
// REFLECT additionally appends uint8 reflectResult.
if (!packet.hasRemaining(9)) { // 8 GUID + 1 missType
LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", static_cast<int>(rawMissCount),
" spell=", data.spellId, " hits=", static_cast<int>(data.hitCount));
truncatedTargets = true;
break;
}
SpellGoMissEntry m;
m.targetGuid = packet.readUInt64();
m.missType = packet.readUInt8();
if (m.missType == 11) { // SPELL_MISS_REFLECT
if (!packet.hasRemaining(1)) {
LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", static_cast<int>(rawMissCount));
truncatedTargets = true;
break;
}
(void)packet.readUInt8(); // reflectResult
}
if (i < storedMissLimit) {
data.missTargets.push_back(m);
}
}
data.missCount = static_cast<uint8_t>(data.missTargets.size());
// If miss targets were truncated, salvage the successfully-parsed hit data
// rather than discarding the entire spell. The server already applied effects;
// we just need the hit list for UI feedback (combat text, health bars).
if (truncatedTargets) {
LOG_DEBUG("Spell go: salvaging ", static_cast<int>(data.hitCount), " hits despite miss truncation");
packet.skipAll(); // consume remaining bytes
return true;
}
// WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that
// any trailing fields after the target section are not misaligned for
// ground-targeted or AoE spells. Same layout as SpellStartParser.
if (packet.hasData()) {
if (packet.hasRemaining(4)) {
uint32_t targetFlags = packet.readUInt32();
auto readPackedTarget = [&](uint64_t* out) -> bool {
if (!packet.hasFullPackedGuid()) return false;
uint64_t g = packet.readPackedGuid();
if (out) *out = g;
return true;
};
auto skipPackedAndFloats3 = [&]() -> bool {
if (!packet.hasFullPackedGuid()) return false;
packet.readPackedGuid(); // transport GUID
if (!packet.hasRemaining(12)) return false;
packet.readFloat(); packet.readFloat(); packet.readFloat();
return true;
};
// UNIT/UNIT_MINIPET/CORPSE_ALLY/GAMEOBJECT share one object target GUID
if (targetFlags & (0x0002u | 0x0004u | 0x0400u | 0x0800u)) {
readPackedTarget(&data.targetGuid);
}
// ITEM/TRADE_ITEM share one item target GUID
if (targetFlags & (0x0010u | 0x0100u)) {
readPackedTarget(nullptr);
}
// SOURCE_LOCATION: PackedGuid (transport) + float x,y,z
if (targetFlags & 0x0020u) {
skipPackedAndFloats3();
}
// DEST_LOCATION: PackedGuid (transport) + float x,y,z
if (targetFlags & 0x0040u) {
skipPackedAndFloats3();
}
// STRING: null-terminated
if (targetFlags & 0x0200u) {
while (packet.hasData() && packet.readUInt8() != 0) {}
}
}
}
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", static_cast<int>(data.hitCount),
" misses=", static_cast<int>(data.missCount));
return true;
}
bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool isAll) {
// Validation: packed GUID (1-8 bytes minimum for reading)
if (!packet.hasRemaining(1)) return false;
data.guid = packet.readPackedGuid();
// Cap number of aura entries to prevent unbounded loop DoS
uint32_t maxAuras = isAll ? 512 : 1;
uint32_t auraCount = 0;
while (packet.hasData() && auraCount < maxAuras) {
// Validate we can read slot (1) + spellId (4) = 5 bytes minimum
if (!packet.hasRemaining(5)) {
LOG_DEBUG("Aura update: truncated entry at position ", auraCount);
break;
}
uint8_t slot = packet.readUInt8();
uint32_t spellId = packet.readUInt32();
auraCount++;
AuraSlot aura;
if (spellId != 0) {
aura.spellId = spellId;
// Validate flags + level + charges (3 bytes)
if (!packet.hasRemaining(3)) {
LOG_WARNING("Aura update: truncated flags/level/charges at entry ", auraCount);
aura.flags = 0;
aura.level = 0;
aura.charges = 0;
} else {
aura.flags = packet.readUInt8();
aura.level = packet.readUInt8();
aura.charges = packet.readUInt8();
}
if (!(aura.flags & 0x08)) { // NOT_CASTER flag
// Validate space for packed GUID read (minimum 1 byte)
if (!packet.hasRemaining(1)) {
aura.casterGuid = 0;
} else {
aura.casterGuid = packet.readPackedGuid();
}
}
if (aura.flags & 0x20) { // DURATION - need 8 bytes (two uint32s)
if (!packet.hasRemaining(8)) {
LOG_WARNING("Aura update: truncated duration fields at entry ", auraCount);
aura.maxDurationMs = 0;
aura.durationMs = 0;
} else {
aura.maxDurationMs = static_cast<int32_t>(packet.readUInt32());
aura.durationMs = static_cast<int32_t>(packet.readUInt32());
}
}
if (aura.flags & 0x40) { // EFFECT_AMOUNTS
// Only read amounts for active effect indices (flags 0x01, 0x02, 0x04)
for (int i = 0; i < 3; ++i) {
if (aura.flags & (1 << i)) {
if (packet.hasRemaining(4)) {
packet.readUInt32();
} else {
LOG_WARNING("Aura update: truncated effect amount ", i, " at entry ", auraCount);
break;
}
}
}
}
}
data.updates.push_back({slot, aura});
// For single update, only one entry
if (!isAll) break;
}
if (auraCount >= maxAuras && packet.hasData()) {
LOG_WARNING("Aura update: capped at ", maxAuras, " entries, remaining data ignored");
}
LOG_DEBUG("Aura update for 0x", std::hex, data.guid, std::dec,
": ", data.updates.size(), " slots");
return true;
}
bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data) {
// Upfront validation: guid(8) + flags(1) = 9 bytes minimum
if (!packet.hasRemaining(9)) return false;
data.guid = packet.readUInt64();
data.flags = packet.readUInt8();
// Cap cooldown entries to prevent unbounded memory allocation (each entry is 8 bytes)
uint32_t maxCooldowns = 512;
uint32_t cooldownCount = 0;
while (packet.hasRemaining(8) && cooldownCount < maxCooldowns) {
uint32_t spellId = packet.readUInt32();
uint32_t cooldownMs = packet.readUInt32();
data.cooldowns.push_back({spellId, cooldownMs});
cooldownCount++;
}
if (cooldownCount >= maxCooldowns && packet.hasRemaining(8)) {
LOG_WARNING("Spell cooldowns: capped at ", maxCooldowns, " entries, remaining data ignored");
}
LOG_DEBUG("Spell cooldowns: ", data.cooldowns.size(), " entries");
return true;
}
// ============================================================
// Group/Party System
// ============================================================
network::Packet GroupInvitePacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_INVITE));
packet.writeString(playerName);
packet.writeUInt32(0); // unused
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
return packet;
}
bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteResponseData& data) {
// Validate minimum packet size: canAccept(1)
if (!packet.hasRemaining(1)) {
LOG_WARNING("SMSG_GROUP_INVITE: packet too small (", packet.getSize(), " bytes)");
return false;
}
data.canAccept = packet.readUInt8();
// Note: inviterName is a string, which is always safe to read even if empty
data.inviterName = packet.readString();
LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", static_cast<int>(data.canAccept), ")");
return true;
}
network::Packet GroupAcceptPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_ACCEPT));
packet.writeUInt32(0); // unused in 3.3.5a
return packet;
}
network::Packet GroupDeclinePacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DECLINE));
return packet;
}
bool GroupListParser::parse(network::Packet& packet, GroupListData& data, bool hasRoles) {
auto rem = [&]() { return packet.getRemainingSize(); };
if (rem() < 3) return false;
data.groupType = packet.readUInt8();
data.subGroup = packet.readUInt8();
data.flags = packet.readUInt8();
// WotLK 3.3.5a added a roles byte (tank/healer/dps) for the dungeon finder.
// Classic 1.12 and TBC 2.4.3 do not have this byte.
if (hasRoles) {
if (rem() < 1) return false;
data.roles = packet.readUInt8();
} else {
data.roles = 0;
}
// WotLK: LFG data gated by groupType bit 0x04 (LFD group type)
if (hasRoles && (data.groupType & 0x04)) {
if (rem() < 5) return false;
packet.readUInt8(); // lfg state
packet.readUInt32(); // lfg entry
// WotLK 3.3.5a may or may not send the lfg flags byte — read it only if present
if (rem() >= 13) { // enough for lfgFlags(1)+groupGuid(8)+counter(4)
packet.readUInt8(); // lfg flags
}
}
if (rem() < 12) return false;
packet.readUInt64(); // group GUID
packet.readUInt32(); // update counter
if (rem() < 4) return false;
data.memberCount = packet.readUInt32();
if (data.memberCount > 40) {
LOG_WARNING("GroupListParser: implausible memberCount=", data.memberCount, ", clamping");
data.memberCount = 40;
}
data.members.reserve(data.memberCount);
for (uint32_t i = 0; i < data.memberCount; ++i) {
if (rem() == 0) break;
GroupMember member;
member.name = packet.readString();
if (rem() < 8) break;
member.guid = packet.readUInt64();
if (rem() < 3) break;
member.isOnline = packet.readUInt8();
member.subGroup = packet.readUInt8();
member.flags = packet.readUInt8();
// WotLK added per-member roles byte; Classic/TBC do not have it.
if (hasRoles) {
if (rem() < 1) break;
member.roles = packet.readUInt8();
} else {
member.roles = 0;
}
data.members.push_back(member);
}
if (rem() < 8) {
LOG_INFO("Group list: ", data.memberCount, " members (no leader GUID in packet)");
return true;
}
data.leaderGuid = packet.readUInt64();
if (data.memberCount > 0 && rem() >= 10) {
data.lootMethod = packet.readUInt8();
data.looterGuid = packet.readUInt64();
data.lootThreshold = packet.readUInt8();
// Dungeon difficulty (heroic/normal) — Classic doesn't send this; TBC/WotLK do
if (rem() >= 1) data.difficultyId = packet.readUInt8();
// Raid difficulty — WotLK only
if (rem() >= 1) data.raidDifficultyId = packet.readUInt8();
// Extra byte in some 3.3.5a builds
if (hasRoles && rem() >= 1) packet.readUInt8();
}
LOG_INFO("Group list: ", data.memberCount, " members, leader=0x",
std::hex, data.leaderGuid, std::dec);
return true;
}
bool PartyCommandResultParser::parse(network::Packet& packet, PartyCommandResultData& data) {
// Upfront validation: command(4) + name(var) + result(4) = 8 bytes minimum (plus name string)
if (!packet.hasRemaining(8)) return false;
data.command = static_cast<PartyCommand>(packet.readUInt32());
data.name = packet.readString();
// Validate result field exists (4 bytes)
if (!packet.hasRemaining(4)) {
data.result = static_cast<PartyResult>(0);
return true; // Partial read is acceptable
}
data.result = static_cast<PartyResult>(packet.readUInt32());
LOG_DEBUG("Party command result: ", static_cast<int>(data.result));
return true;
}
bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData& data) {
// Upfront validation: playerName is a CString (minimum 1 null terminator)
if (!packet.hasRemaining(1)) return false;
data.playerName = packet.readString();
LOG_INFO("Group decline from: ", data.playerName);
return true;
}
// ============================================================
// Loot System
// ============================================================
network::Packet LootPacket::build(uint64_t targetGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
packet.writeUInt8(slotIndex);
return packet;
}
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId) {
network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM));
packet.writeUInt8(bagIndex);
packet.writeUInt8(slotIndex);
packet.writeUInt8(0); // cast count
packet.writeUInt32(spellId); // spell id from item data
packet.writeUInt64(itemGuid); // full 8-byte GUID
packet.writeUInt32(0); // glyph index
packet.writeUInt8(0); // cast flags
// SpellCastTargets: self
packet.writeUInt32(0x00);
return packet;
}
network::Packet OpenItemPacket::build(uint8_t bagIndex, uint8_t slotIndex) {
network::Packet packet(wireOpcode(Opcode::CMSG_OPEN_ITEM));
packet.writeUInt8(bagIndex);
packet.writeUInt8(slotIndex);
return packet;
}
network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOEQUIP_ITEM));
packet.writeUInt8(srcBag);
packet.writeUInt8(srcSlot);
return packet;
}
network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot) {
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_ITEM));
packet.writeUInt8(dstBag);
packet.writeUInt8(dstSlot);
packet.writeUInt8(srcBag);
packet.writeUInt8(srcSlot);
return packet;
}
network::Packet SplitItemPacket::build(uint8_t srcBag, uint8_t srcSlot,
uint8_t dstBag, uint8_t dstSlot, uint8_t count) {
network::Packet packet(wireOpcode(Opcode::CMSG_SPLIT_ITEM));
packet.writeUInt8(srcBag);
packet.writeUInt8(srcSlot);
packet.writeUInt8(dstBag);
packet.writeUInt8(dstSlot);
packet.writeUInt8(count);
return packet;
}
network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) {
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM));
packet.writeUInt8(srcSlot);
packet.writeUInt8(dstSlot);
return packet;
}
network::Packet LootMoneyPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_MONEY));
return packet;
}
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_RELEASE));
packet.writeUInt64(lootGuid);
return packet;
}
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat) {
data = LootResponseData{};
size_t avail = packet.getRemainingSize();
// Minimum is guid(8)+lootType(1) = 9 bytes. Servers send a short packet with
// lootType=0 (LOOT_NONE) when loot is unavailable (e.g. chest not yet opened,
// needs a key, or another player is looting). We treat this as an empty-loot
// signal and return false so the caller knows not to open the loot window.
if (avail < 9) {
LOG_WARNING("LootResponseParser: packet too short (", avail, " bytes)");
return false;
}
data.lootGuid = packet.readUInt64();
data.lootType = packet.readUInt8();
// Short failure packet — no gold/item data follows.
avail = packet.getRemainingSize();
if (avail < 5) {
LOG_DEBUG("LootResponseParser: lootType=", static_cast<int>(data.lootType), " (empty/failure response)");
return false;
}
data.gold = packet.readUInt32();
uint8_t itemCount = packet.readUInt8();
// Per-item wire size is 22 bytes across all expansions:
// slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22
constexpr size_t kItemSize = 22u;
auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool {
for (uint8_t i = 0; i < listCount; ++i) {
size_t remaining = packet.getRemainingSize();
if (remaining < kItemSize) {
return false;
}
LootItem item;
item.slotIndex = packet.readUInt8();
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
item.randomSuffix = packet.readUInt32();
item.randomPropertyId = packet.readUInt32();
item.lootSlotType = packet.readUInt8();
item.isQuestItem = markQuestItems;
data.items.push_back(item);
}
return true;
};
data.items.reserve(itemCount);
if (!parseLootItemList(itemCount, false)) {
LOG_WARNING("LootResponseParser: truncated regular item list");
return false;
}
// Quest item section only present in WotLK 3.3.5a
uint8_t questItemCount = 0;
if (isWotlkFormat && packet.hasRemaining(1)) {
questItemCount = packet.readUInt8();
data.items.reserve(data.items.size() + questItemCount);
if (!parseLootItemList(questItemCount, true)) {
LOG_WARNING("LootResponseParser: truncated quest item list");
return false;
}
}
LOG_DEBUG("Loot response: ", static_cast<int>(itemCount), " regular + ", static_cast<int>(questItemCount),
" quest items, ", data.gold, " copper");
return true;
}
// ============================================================
// NPC Gossip
// ============================================================
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_HELLO));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet QuestgiverHelloPacket::build(uint64_t npcGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_HELLO));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code) {
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_SELECT_OPTION));
packet.writeUInt64(npcGuid);
packet.writeUInt32(menuId);
packet.writeUInt32(optionId);
if (!code.empty()) {
packet.writeString(code);
}
return packet;
}
network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
packet.writeUInt8(1); // isDialogContinued = 1 (from gossip)
return packet;
}
network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
packet.writeUInt32(0); // AzerothCore/WotLK expects trailing unk1
return packet;
}
bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) {
if (packet.getSize() < 20) return false;
data.npcGuid = packet.readUInt64();
// WotLK has informUnit(u64) before questId; Vanilla/TBC do not.
// Detect: try WotLK first (read informUnit + questId), then check if title
// string looks valid. If not, rewind and try vanilla (questId directly).
size_t preInform = packet.getReadPos();
/*informUnit*/ packet.readUInt64();
data.questId = packet.readUInt32();
data.title = normalizeWowTextTokens(packet.readString());
if (data.title.empty() || data.questId > 100000) {
// Likely vanilla format — rewind past informUnit
packet.setReadPos(preInform);
data.questId = packet.readUInt32();
data.title = normalizeWowTextTokens(packet.readString());
}
data.details = normalizeWowTextTokens(packet.readString());
data.objectives = normalizeWowTextTokens(packet.readString());
if (!packet.hasRemaining(10)) {
LOG_DEBUG("Quest details (short): id=", data.questId, " title='", data.title, "'");
return true;
}
/*activateAccept*/ packet.readUInt8();
/*flags*/ packet.readUInt32();
data.suggestedPlayers = packet.readUInt32();
/*isFinished*/ packet.readUInt8();
// Reward choice items: server always writes 6 entries (QUEST_REWARD_CHOICES_COUNT)
if (packet.hasRemaining(4)) {
/*choiceCount*/ packet.readUInt32();
for (int i = 0; i < 6; i++) {
if (!packet.hasRemaining(12)) break;
uint32_t itemId = packet.readUInt32();
uint32_t count = packet.readUInt32();
uint32_t dispId = packet.readUInt32();
if (itemId != 0) {
QuestRewardItem ri;
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
ri.choiceSlot = static_cast<uint32_t>(i);
data.rewardChoiceItems.push_back(ri);
}
}
}
// Reward items: server always writes 4 entries (QUEST_REWARDS_COUNT)
if (packet.hasRemaining(4)) {
/*rewardCount*/ packet.readUInt32();
for (int i = 0; i < 4; i++) {
if (!packet.hasRemaining(12)) break;
uint32_t itemId = packet.readUInt32();
uint32_t count = packet.readUInt32();
uint32_t dispId = packet.readUInt32();
if (itemId != 0) {
QuestRewardItem ri;
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
data.rewardItems.push_back(ri);
}
}
}
// Money and XP rewards
if (packet.hasRemaining(4))
data.rewardMoney = packet.readUInt32();
if (packet.hasRemaining(4))
data.rewardXp = packet.readUInt32();
LOG_DEBUG("Quest details: id=", data.questId, " title='", data.title, "'");
return true;
}
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
// Upfront validation: npcGuid(8) + menuId(4) + titleTextId(4) + optionCount(4) = 20 bytes minimum
if (!packet.hasRemaining(20)) return false;
data.npcGuid = packet.readUInt64();
data.menuId = packet.readUInt32();
data.titleTextId = packet.readUInt32();
uint32_t optionCount = packet.readUInt32();
// Cap option count to prevent unbounded memory allocation
const uint32_t MAX_GOSSIP_OPTIONS = 64;
if (optionCount > MAX_GOSSIP_OPTIONS) {
LOG_WARNING("GossipMessageParser: optionCount capped (requested=", optionCount, ")");
optionCount = MAX_GOSSIP_OPTIONS;
}
data.options.clear();
data.options.reserve(optionCount);
for (uint32_t i = 0; i < optionCount; ++i) {
// Each option: id(4) + icon(1) + isCoded(1) + boxMoney(4) + text(var) + boxText(var)
// Minimum: 10 bytes + 2 empty strings (2 null terminators) = 12 bytes
if (!packet.hasRemaining(12)) {
LOG_WARNING("GossipMessageParser: truncated options at index ", i, "/", optionCount);
break;
}
GossipOption opt;
opt.id = packet.readUInt32();
opt.icon = packet.readUInt8();
opt.isCoded = (packet.readUInt8() != 0);
opt.boxMoney = packet.readUInt32();
opt.text = packet.readString();
opt.boxText = packet.readString();
data.options.push_back(opt);
}
// Validate questCount field exists (4 bytes)
if (!packet.hasRemaining(4)) {
LOG_DEBUG("Gossip: ", data.options.size(), " options (no quest data)");
return true;
}
uint32_t questCount = packet.readUInt32();
// Cap quest count to prevent unbounded memory allocation
const uint32_t MAX_GOSSIP_QUESTS = 64;
if (questCount > MAX_GOSSIP_QUESTS) {
LOG_WARNING("GossipMessageParser: questCount capped (requested=", questCount, ")");
questCount = MAX_GOSSIP_QUESTS;
}
data.quests.clear();
data.quests.reserve(questCount);
for (uint32_t i = 0; i < questCount; ++i) {
// Each quest: questId(4) + questIcon(4) + questLevel(4) + questFlags(4) + isRepeatable(1) + title(var)
// Minimum: 17 bytes + empty string (1 null terminator) = 18 bytes
if (!packet.hasRemaining(18)) {
LOG_WARNING("GossipMessageParser: truncated quests at index ", i, "/", questCount);
break;
}
GossipQuestItem quest;
quest.questId = packet.readUInt32();
quest.questIcon = packet.readUInt32();
quest.questLevel = static_cast<int32_t>(packet.readUInt32());
quest.questFlags = packet.readUInt32();
quest.isRepeatable = packet.readUInt8();
quest.title = normalizeWowTextTokens(packet.readString());
data.quests.push_back(quest);
}
LOG_DEBUG("Gossip: ", data.options.size(), " options, ", data.quests.size(), " quests");
return true;
}
// ============================================================
// Bind Point (Hearthstone)
// ============================================================
network::Packet BinderActivatePacket::build(uint64_t npcGuid) {
network::Packet pkt(wireOpcode(Opcode::CMSG_BINDER_ACTIVATE));
pkt.writeUInt64(npcGuid);
return pkt;
}
bool BindPointUpdateParser::parse(network::Packet& packet, BindPointUpdateData& data) {
if (packet.getSize() < 20) return false;
data.x = packet.readFloat();
data.y = packet.readFloat();
data.z = packet.readFloat();
data.mapId = packet.readUInt32();
data.zoneId = packet.readUInt32();
return true;
}
bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsData& data) {
if (!packet.hasRemaining(20)) return false;
data.npcGuid = packet.readUInt64();
data.questId = packet.readUInt32();
data.title = normalizeWowTextTokens(packet.readString());
data.completionText = normalizeWowTextTokens(packet.readString());
if (!packet.hasRemaining(9)) {
LOG_DEBUG("Quest request items (short): id=", data.questId, " title='", data.title, "'");
return true;
}
struct ParsedTail {
uint32_t requiredMoney = 0;
uint32_t completableFlags = 0;
std::vector<QuestRewardItem> requiredItems;
bool ok = false;
int score = -1;
};
auto parseTail = [&](size_t startPos, size_t prefixSkip) -> ParsedTail {
ParsedTail out;
packet.setReadPos(startPos);
if (!packet.hasRemaining(prefixSkip)) return out;
packet.setReadPos(packet.getReadPos() + prefixSkip);
if (!packet.hasRemaining(8)) return out;
out.requiredMoney = packet.readUInt32();
uint32_t requiredItemCount = packet.readUInt32();
if (requiredItemCount > 64) return out; // sanity guard against misalignment
out.requiredItems.reserve(requiredItemCount);
for (uint32_t i = 0; i < requiredItemCount; ++i) {
if (!packet.hasRemaining(12)) return out;
QuestRewardItem item;
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
if (item.itemId != 0) out.requiredItems.push_back(item);
}
if (!packet.hasRemaining(4)) return out;
out.completableFlags = packet.readUInt32();
out.ok = true;
// Prefer layouts that produce plausible quest-requirement shapes.
out.score = 0;
if (requiredItemCount <= 6) out.score += 4;
if (out.requiredItems.size() == requiredItemCount) out.score += 3;
if ((out.completableFlags & ~0x3u) == 0) out.score += 5;
if (out.requiredMoney == 0) out.score += 4;
else if (out.requiredMoney <= 100000) out.score += 2; // <=10g is common
else if (out.requiredMoney >= 1000000) out.score -= 3; // implausible for most quests
if (!out.requiredItems.empty()) out.score += 1;
size_t remaining = packet.getRemainingSize();
if (remaining <= 16) out.score += 3;
else if (remaining <= 32) out.score += 2;
else if (remaining <= 64) out.score += 1;
if (prefixSkip == 0) out.score += 1;
else if (prefixSkip <= 12) out.score += 1;
return out;
};
size_t tailStart = packet.getReadPos();
std::vector<ParsedTail> candidates;
candidates.reserve(25);
for (size_t skip = 0; skip <= 24; ++skip) {
candidates.push_back(parseTail(tailStart, skip));
}
const ParsedTail* chosen = nullptr;
for (const auto& cand : candidates) {
if (!cand.ok) continue;
if (!chosen || cand.score > chosen->score) chosen = &cand;
}
if (!chosen) {
return true;
}
data.requiredMoney = chosen->requiredMoney;
data.completableFlags = chosen->completableFlags;
data.requiredItems = chosen->requiredItems;
LOG_DEBUG("Quest request items: id=", data.questId, " title='", data.title,
"' items=", data.requiredItems.size(), " completable=", data.isCompletable());
return true;
}
bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData& data) {
if (!packet.hasRemaining(20)) return false;
data.npcGuid = packet.readUInt64();
data.questId = packet.readUInt32();
data.title = normalizeWowTextTokens(packet.readString());
data.rewardText = normalizeWowTextTokens(packet.readString());
if (!packet.hasRemaining(8)) {
LOG_DEBUG("Quest offer reward (short): id=", data.questId, " title='", data.title, "'");
return true;
}
// After the two strings the packet contains a variable prefix (autoFinish + optional fields)
// before the emoteCount. Different expansions and server emulator versions differ:
// Classic 1.12 : uint8 autoFinish + uint32 suggestedPlayers = 5 bytes
// TBC 2.4.3 : uint32 autoFinish + uint32 suggestedPlayers = 8 bytes (variable arrays)
// WotLK 3.3.5a : uint32 autoFinish + uint32 suggestedPlayers = 8 bytes (fixed 6/4 arrays)
// Some vanilla-family servers omit autoFinish entirely (0 bytes of prefix).
// We scan prefix sizes 0..16 bytes with both fixed and variable array layouts, scoring each.
struct ParsedTail {
uint32_t rewardMoney = 0;
uint32_t rewardXp = 0;
std::vector<QuestRewardItem> choiceRewards;
std::vector<QuestRewardItem> fixedRewards;
bool ok = false;
int score = -1000;
size_t prefixSkip = 0;
bool fixedArrays = false;
};
auto parseTail = [&](size_t startPos, size_t prefixSkip, bool fixedArrays) -> ParsedTail {
ParsedTail out;
out.prefixSkip = prefixSkip;
out.fixedArrays = fixedArrays;
packet.setReadPos(startPos);
// Skip the prefix bytes (autoFinish + optional suggestedPlayers before emoteCount)
if (!packet.hasRemaining(prefixSkip)) return out;
packet.setReadPos(packet.getReadPos() + prefixSkip);
if (!packet.hasRemaining(4)) return out;
uint32_t emoteCount = packet.readUInt32();
if (emoteCount > 32) return out; // guard against misalignment
for (uint32_t i = 0; i < emoteCount; ++i) {
if (!packet.hasRemaining(8)) return out;
packet.readUInt32(); // delay
packet.readUInt32(); // emote type
}
if (!packet.hasRemaining(4)) return out;
uint32_t choiceCount = packet.readUInt32();
if (choiceCount > 6) return out;
uint32_t choiceSlots = fixedArrays ? 6u : choiceCount;
out.choiceRewards.reserve(choiceCount);
uint32_t nonZeroChoice = 0;
for (uint32_t i = 0; i < choiceSlots; ++i) {
if (!packet.hasRemaining(12)) return out;
QuestRewardItem item;
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
item.choiceSlot = i;
if (item.itemId > 0) {
out.choiceRewards.push_back(item);
++nonZeroChoice;
}
}
if (!packet.hasRemaining(4)) return out;
uint32_t rewardCount = packet.readUInt32();
if (rewardCount > 4) return out;
uint32_t rewardSlots = fixedArrays ? 4u : rewardCount;
out.fixedRewards.reserve(rewardCount);
uint32_t nonZeroFixed = 0;
for (uint32_t i = 0; i < rewardSlots; ++i) {
if (!packet.hasRemaining(12)) return out;
QuestRewardItem item;
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
if (item.itemId > 0) {
out.fixedRewards.push_back(item);
++nonZeroFixed;
}
}
if (packet.hasRemaining(4))
out.rewardMoney = packet.readUInt32();
if (packet.hasRemaining(4))
out.rewardXp = packet.readUInt32();
out.ok = true;
out.score = 0;
// Prefer the standard WotLK/TBC 8-byte prefix (uint32 autoFinish + uint32 suggestedPlayers)
if (prefixSkip == 8) out.score += 3;
else if (prefixSkip == 5) out.score += 1; // Classic uint8 autoFinish + uint32 suggestedPlayers
// Prefer fixed arrays (WotLK/TBC servers always send 6+4 slots)
if (fixedArrays) out.score += 2;
// Valid counts
if (choiceCount <= 6) out.score += 3;
if (rewardCount <= 4) out.score += 3;
// All non-zero items are within declared counts
if (nonZeroChoice <= choiceCount) out.score += 2;
if (nonZeroFixed <= rewardCount) out.score += 2;
// No bytes left over (or only a few)
size_t remaining = packet.getRemainingSize();
if (remaining == 0) out.score += 5;
else if (remaining <= 4) out.score += 3;
else if (remaining <= 8) out.score += 2;
else if (remaining <= 16) out.score += 1;
else out.score -= static_cast<int>(remaining / 4);
// Plausible money/XP values
if (out.rewardMoney < 5000000u) out.score += 1; // < 500g
if (out.rewardXp < 200000u) out.score += 1; // < 200k XP
return out;
};
size_t tailStart = packet.getReadPos();
// Try prefix sizes 0..16 bytes with both fixed and variable array layouts
std::vector<ParsedTail> candidates;
candidates.reserve(34);
for (size_t skip = 0; skip <= 16; ++skip) {
candidates.push_back(parseTail(tailStart, skip, true)); // fixed arrays
candidates.push_back(parseTail(tailStart, skip, false)); // variable arrays
}
const ParsedTail* best = nullptr;
for (const auto& cand : candidates) {
if (!cand.ok) continue;
if (!best || cand.score > best->score) best = &cand;
}
if (best) {
data.choiceRewards = best->choiceRewards;
data.fixedRewards = best->fixedRewards;
data.rewardMoney = best->rewardMoney;
data.rewardXp = best->rewardXp;
}
LOG_DEBUG("Quest offer reward: id=", data.questId, " title='", data.title,
"' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size(),
" prefix=", (best ? best->prefixSkip : size_t(0)),
(best && best->fixedArrays ? " fixed" : " var"));
return true;
}
network::Packet QuestgiverCompleteQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_COMPLETE_QUEST));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
return packet;
}
network::Packet QuestgiverRequestRewardPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
return packet;
}
network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t questId, uint32_t rewardIndex) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
packet.writeUInt32(rewardIndex);
return packet;
}
// ============================================================
// Vendor
// ============================================================
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_LIST_INVENTORY));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) {
network::Packet packet(wireOpcode(Opcode::CMSG_BUY_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt32(itemId); // item entry
packet.writeUInt32(slot); // vendor slot index from SMSG_LIST_INVENTORY
packet.writeUInt32(count);
// Note: WotLK/AzerothCore expects a trailing byte; Classic/TBC do not.
// This static helper always adds it (appropriate for CMaNGOS/AzerothCore).
// For Classic/TBC, use the GameHandler::buyItem() path which checks expansion.
packet.writeUInt8(0);
return packet;
}
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
network::Packet packet(wireOpcode(Opcode::CMSG_SELL_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt64(itemGuid);
packet.writeUInt32(count);
return packet;
}
network::Packet BuybackItemPacket::build(uint64_t vendorGuid, uint32_t slot) {
network::Packet packet(wireOpcode(Opcode::CMSG_BUYBACK_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt32(slot);
return packet;
}
bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) {
// Preserve canRepair — it was set by the gossip handler before this packet
// arrived and is not part of the wire format.
const bool savedCanRepair = data.canRepair;
data = ListInventoryData{};
data.canRepair = savedCanRepair;
if (!packet.hasRemaining(9)) {
LOG_WARNING("ListInventoryParser: packet too short");
return false;
}
data.vendorGuid = packet.readUInt64();
uint8_t itemCount = packet.readUInt8();
if (itemCount == 0) {
LOG_INFO("Vendor has nothing for sale");
return true;
}
// Auto-detect whether server sends 7 fields (28 bytes/item) or 8 fields (32 bytes/item).
// Some servers omit the extendedCost field entirely; reading 8 fields on a 7-field packet
// misaligns every item after the first and produces garbage prices.
size_t remaining = packet.getRemainingSize();
const size_t bytesPerItemNoExt = 28;
const size_t bytesPerItemWithExt = 32;
bool hasExtendedCost = false;
if (remaining < static_cast<size_t>(itemCount) * bytesPerItemNoExt) {
LOG_WARNING("ListInventoryParser: truncated packet (items=", static_cast<int>(itemCount),
", remaining=", remaining, ")");
return false;
}
if (remaining >= static_cast<size_t>(itemCount) * bytesPerItemWithExt) {
hasExtendedCost = true;
}
data.items.reserve(itemCount);
for (uint8_t i = 0; i < itemCount; ++i) {
const size_t perItemBytes = hasExtendedCost ? bytesPerItemWithExt : bytesPerItemNoExt;
if (!packet.hasRemaining(perItemBytes)) {
LOG_WARNING("ListInventoryParser: item ", static_cast<int>(i), " truncated");
return false;
}
VendorItem item;
item.slot = packet.readUInt32();
item.itemId = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
item.maxCount = static_cast<int32_t>(packet.readUInt32());
item.buyPrice = packet.readUInt32();
item.durability = packet.readUInt32();
item.stackCount = packet.readUInt32();
item.extendedCost = hasExtendedCost ? packet.readUInt32() : 0;
data.items.push_back(item);
}
LOG_DEBUG("Vendor inventory: ", static_cast<int>(itemCount), " items (extendedCost: ", hasExtendedCost ? "yes" : "no", ")");
return true;
}
// ============================================================
// Trainer
// ============================================================
bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data, bool isClassic) {
// WotLK per-entry: spellId(4) + state(1) + cost(4) + profDialog(4) + profButton(4) +
// reqLevel(1) + reqSkill(4) + reqSkillValue(4) + chain×3(12) = 38 bytes
// Classic per-entry: spellId(4) + state(1) + cost(4) + reqLevel(1) +
// reqSkill(4) + reqSkillValue(4) + chain×3(12) + unk(4) = 34 bytes
data = TrainerListData{};
if (!packet.hasRemaining(16)) return false; // guid(8) + type(4) + count(4)
data.trainerGuid = packet.readUInt64();
data.trainerType = packet.readUInt32();
uint32_t spellCount = packet.readUInt32();
if (spellCount > 1000) {
LOG_ERROR("TrainerListParser: unreasonable spell count ", spellCount);
return false;
}
data.spells.reserve(spellCount);
for (uint32_t i = 0; i < spellCount; ++i) {
// Validate minimum entry size before reading
const size_t minEntrySize = isClassic ? 34 : 38;
if (!packet.hasRemaining(minEntrySize)) {
LOG_WARNING("TrainerListParser: truncated at spell ", i);
break;
}
TrainerSpell spell;
spell.spellId = packet.readUInt32();
spell.state = packet.readUInt8();
spell.spellCost = packet.readUInt32();
if (isClassic) {
// Classic 1.12: reqLevel immediately after cost; no profDialog/profButton
spell.profDialog = 0;
spell.profButton = 0;
spell.reqLevel = packet.readUInt8();
} else {
// TBC / WotLK: profDialog + profButton before reqLevel
spell.profDialog = packet.readUInt32();
spell.profButton = packet.readUInt32();
spell.reqLevel = packet.readUInt8();
}
spell.reqSkill = packet.readUInt32();
spell.reqSkillValue = packet.readUInt32();
spell.chainNode1 = packet.readUInt32();
spell.chainNode2 = packet.readUInt32();
spell.chainNode3 = packet.readUInt32();
if (isClassic) {
packet.readUInt32(); // trailing unk / sort index
}
data.spells.push_back(spell);
}
if (!packet.hasData()) {
LOG_WARNING("TrainerListParser: truncated before greeting");
data.greeting.clear();
} else {
data.greeting = packet.readString();
}
LOG_INFO("Trainer list (", isClassic ? "Classic" : "TBC/WotLK", "): ",
spellCount, " spells, type=", data.trainerType,
", greeting=\"", data.greeting, "\"");
return true;
}
network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spellId) {
network::Packet packet(wireOpcode(Opcode::CMSG_TRAINER_BUY_SPELL));
packet.writeUInt64(trainerGuid);
packet.writeUInt32(spellId);
return packet;
}
// ============================================================
// Talents
// ============================================================
bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
// 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]
const size_t startPos = packet.getReadPos();
const size_t remaining = packet.getSize() - startPos;
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();
// These two counts are big-endian (network byte order)
uint32_t talentCountBE = packet.readUInt32();
uint32_t talentCount = bswap32(talentCountBE);
uint16_t entryCountBE = packet.readUInt16();
uint16_t entryCount = 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=", static_cast<int>(data.talentSpec),
" unspent=", static_cast<int>(data.unspentPoints),
" talentCount=", talentCount,
" entryCount=", entryCount);
// 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.hasRemaining(5)) {
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});
LOG_INFO(" Entry: id=", id, " rank=", static_cast<int>(rank));
}
// Parse glyph tail: glyphSlots + glyphIds[]
if (!packet.hasRemaining(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 (", static_cast<int>(glyphSlots), "), clamping to 12");
glyphSlots = 12;
}
LOG_INFO(" GlyphSlots: ", static_cast<int>(glyphSlots));
data.glyphs.clear();
data.glyphs.reserve(glyphSlots);
for (uint8_t i = 0; i < glyphSlots; ++i) {
if (!packet.hasRemaining(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.getRemainingSize()));
return true;
}
network::Packet LearnTalentPacket::build(uint32_t talentId, uint32_t requestedRank) {
network::Packet packet(wireOpcode(Opcode::CMSG_LEARN_TALENT));
packet.writeUInt32(talentId);
packet.writeUInt32(requestedRank);
return packet;
}
network::Packet TalentWipeConfirmPacket::build(bool accept) {
network::Packet packet(wireOpcode(Opcode::MSG_TALENT_WIPE_CONFIRM));
packet.writeUInt32(accept ? 1 : 0);
return packet;
}
network::Packet ActivateTalentGroupPacket::build(uint32_t group) {
// CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE (0x4C3 in WotLK 3.3.5a)
// Payload: uint32 group (0 = primary, 1 = secondary)
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE));
packet.writeUInt32(group);
return packet;
}
// ============================================================
// Death/Respawn
// ============================================================
network::Packet RepopRequestPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_REPOP_REQUEST));
packet.writeUInt8(1); // request release (1 = manual)
return packet;
}
network::Packet ReclaimCorpsePacket::build(uint64_t guid) {
network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE));
packet.writeUInt64(guid);
return packet;
}
network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet ResurrectResponsePacket::build(uint64_t casterGuid, bool accept) {
network::Packet packet(wireOpcode(Opcode::CMSG_RESURRECT_RESPONSE));
packet.writeUInt64(casterGuid);
packet.writeUInt8(accept ? 1 : 0);
return packet;
}
// ============================================================
// Taxi / Flight Paths
// ============================================================
} // namespace game
} // namespace wowee