2026-02-02 12:24:50 -08:00
|
|
|
|
#include "game/world_packets.hpp"
|
2026-02-16 18:46:44 -08:00
|
|
|
|
#include "game/packet_parsers.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include "game/opcodes.hpp"
|
|
|
|
|
|
#include "game/character.hpp"
|
|
|
|
|
|
#include "auth/crypto.hpp"
|
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
#include <cctype>
|
|
|
|
|
|
#include <cstring>
|
2026-02-10 13:16:38 -08:00
|
|
|
|
#include <sstream>
|
|
|
|
|
|
#include <iomanip>
|
2026-02-05 21:03:11 -08:00
|
|
|
|
#include <zlib.h>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-23 16:30:49 +01:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-03-14 10:17:19 -07:00
|
|
|
|
|
|
|
|
|
|
bool hasFullPackedGuid(const wowee::network::Packet& packet) {
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& rawData = packet.getData();
|
|
|
|
|
|
const uint8_t mask = rawData[packet.getReadPos()];
|
|
|
|
|
|
size_t guidBytes = 1;
|
|
|
|
|
|
for (int bit = 0; bit < 8; ++bit) {
|
|
|
|
|
|
if ((mask & (1u << bit)) != 0) {
|
|
|
|
|
|
++guidBytes;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return packet.getSize() - packet.getReadPos() >= guidBytes;
|
|
|
|
|
|
}
|
2026-03-15 03:40:58 -07:00
|
|
|
|
|
|
|
|
|
|
const char* updateTypeName(wowee::game::UpdateType type) {
|
|
|
|
|
|
using wowee::game::UpdateType;
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case UpdateType::VALUES: return "VALUES";
|
|
|
|
|
|
case UpdateType::MOVEMENT: return "MOVEMENT";
|
|
|
|
|
|
case UpdateType::CREATE_OBJECT: return "CREATE_OBJECT";
|
|
|
|
|
|
case UpdateType::CREATE_OBJECT2: return "CREATE_OBJECT2";
|
|
|
|
|
|
case UpdateType::OUT_OF_RANGE_OBJECTS: return "OUT_OF_RANGE_OBJECTS";
|
|
|
|
|
|
case UpdateType::NEAR_OBJECTS: return "NEAR_OBJECTS";
|
|
|
|
|
|
default: return "UNKNOWN";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-23 16:30:49 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
namespace wowee {
|
|
|
|
|
|
namespace game {
|
|
|
|
|
|
|
2026-02-20 23:20:02 -08:00
|
|
|
|
std::string normalizeWowTextTokens(std::string text) {
|
|
|
|
|
|
if (text.empty()) return text;
|
|
|
|
|
|
|
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
|
while ((pos = text.find('$', pos)) != std::string::npos) {
|
|
|
|
|
|
if (pos + 1 >= text.size()) break;
|
|
|
|
|
|
const char code = text[pos + 1];
|
|
|
|
|
|
if (code == 'b' || code == 'B') {
|
|
|
|
|
|
text.replace(pos, 2, "\n");
|
|
|
|
|
|
++pos;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
++pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pos = 0;
|
|
|
|
|
|
while ((pos = text.find("|n", pos)) != std::string::npos) {
|
|
|
|
|
|
text.replace(pos, 2, "\n");
|
|
|
|
|
|
++pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
pos = 0;
|
|
|
|
|
|
while ((pos = text.find("|N", pos)) != std::string::npos) {
|
|
|
|
|
|
text.replace(pos, 2, "\n");
|
|
|
|
|
|
++pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
network::Packet AuthSessionPacket::build(uint32_t build,
|
|
|
|
|
|
const std::string& accountName,
|
|
|
|
|
|
uint32_t clientSeed,
|
|
|
|
|
|
const std::vector<uint8_t>& sessionKey,
|
2026-02-05 21:03:11 -08:00
|
|
|
|
uint32_t serverSeed,
|
|
|
|
|
|
uint32_t realmId) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (sessionKey.size() != 40) {
|
|
|
|
|
|
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Convert account name to uppercase
|
|
|
|
|
|
std::string upperAccount = accountName;
|
|
|
|
|
|
std::transform(upperAccount.begin(), upperAccount.end(),
|
|
|
|
|
|
upperAccount.begin(), ::toupper);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Building CMSG_AUTH_SESSION for account: ", upperAccount);
|
|
|
|
|
|
|
|
|
|
|
|
// Compute authentication hash
|
|
|
|
|
|
auto authHash = computeAuthHash(upperAccount, clientSeed, serverSeed, sessionKey);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG(" Build: ", build);
|
|
|
|
|
|
LOG_DEBUG(" Client seed: 0x", std::hex, clientSeed, std::dec);
|
|
|
|
|
|
LOG_DEBUG(" Server seed: 0x", std::hex, serverSeed, std::dec);
|
|
|
|
|
|
LOG_DEBUG(" Auth hash: ", authHash.size(), " bytes");
|
|
|
|
|
|
|
|
|
|
|
|
// Create packet (opcode will be added by WorldSocket)
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_AUTH_SESSION));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
|
bool isTbc = (build <= 8606); // TBC 2.4.3 = 8606, WotLK starts at 11159+
|
2026-02-05 21:03:11 -08:00
|
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
|
if (isTbc) {
|
|
|
|
|
|
// TBC 2.4.3 format (6 fields):
|
|
|
|
|
|
// Build, ServerID, Account, ClientSeed, Digest, AddonInfo
|
|
|
|
|
|
packet.writeUInt32(build);
|
|
|
|
|
|
packet.writeUInt32(realmId); // server_id
|
|
|
|
|
|
packet.writeString(upperAccount);
|
|
|
|
|
|
packet.writeUInt32(clientSeed);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// WotLK 3.3.5a format (11 fields):
|
|
|
|
|
|
// Build, LoginServerID, Account, LoginServerType, LocalChallenge,
|
|
|
|
|
|
// RegionID, BattlegroupID, RealmID, DosResponse, Digest, AddonInfo
|
|
|
|
|
|
packet.writeUInt32(build);
|
|
|
|
|
|
packet.writeUInt32(0); // LoginServerID
|
|
|
|
|
|
packet.writeString(upperAccount);
|
|
|
|
|
|
packet.writeUInt32(0); // LoginServerType
|
|
|
|
|
|
packet.writeUInt32(clientSeed);
|
2026-02-13 16:53:28 -08:00
|
|
|
|
// AzerothCore ignores these fields; other cores may validate them.
|
|
|
|
|
|
// Use 0 for maximum compatibility.
|
|
|
|
|
|
packet.writeUInt32(0); // RegionID
|
|
|
|
|
|
packet.writeUInt32(0); // BattlegroupID
|
|
|
|
|
|
packet.writeUInt32(realmId); // RealmID
|
2026-02-12 22:56:36 -08:00
|
|
|
|
LOG_DEBUG(" Realm ID: ", realmId);
|
|
|
|
|
|
packet.writeUInt32(0); // DOS response (uint64)
|
|
|
|
|
|
packet.writeUInt32(0);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-05 21:03:11 -08:00
|
|
|
|
// Authentication hash/digest (20 bytes)
|
|
|
|
|
|
packet.writeBytes(authHash.data(), authHash.size());
|
|
|
|
|
|
|
Fix Classic field extraction, Warden PE patches, and auth/opcode corrections
Update field extraction in both CREATE_OBJECT and VALUES handlers to check
specific fields (maxHealth, level, faction, etc.) before power/maxpower range
checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28),
and maxPower indices 29-33 are adjacent to level (34) and faction (35), so
range checks like "key >= powerBase && key < powerBase+7" were incorrectly
capturing those fields.
Add build-aware WoW.exe selection and runtime global patching for Warden
SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix
Classic opcodes and auth session addon data format for CMaNGOS compatibility.
2026-02-20 00:18:03 -08:00
|
|
|
|
// Addon info - compressed block
|
|
|
|
|
|
// Format differs between expansions:
|
|
|
|
|
|
// Vanilla/TBC (CMaNGOS): while-loop of {string name, uint8 flags, uint32 modulusCRC, uint32 urlCRC}
|
|
|
|
|
|
// WotLK (AzerothCore): uint32 addonCount + {string name, uint8 enabled, uint32 crc, uint32 unk} + uint32 clientTime
|
|
|
|
|
|
std::vector<uint8_t> addonData;
|
|
|
|
|
|
if (isTbc) {
|
|
|
|
|
|
// Vanilla/TBC: each addon entry = null-terminated name + uint8 flags + uint32 modulusCRC + uint32 urlCRC
|
|
|
|
|
|
// Send standard Blizzard addons that CMaNGOS anticheat expects for fingerprinting
|
|
|
|
|
|
static const char* vanillaAddons[] = {
|
|
|
|
|
|
"Blizzard_AuctionUI", "Blizzard_BattlefieldMinimap", "Blizzard_BindingUI",
|
|
|
|
|
|
"Blizzard_CombatText", "Blizzard_CraftUI", "Blizzard_GMSurveyUI",
|
|
|
|
|
|
"Blizzard_InspectUI", "Blizzard_MacroUI", "Blizzard_RaidUI",
|
|
|
|
|
|
"Blizzard_TalentUI", "Blizzard_TradeSkillUI", "Blizzard_TrainerUI"
|
|
|
|
|
|
};
|
|
|
|
|
|
static const uint32_t standardModulusCRC = 0x4C1C776D;
|
|
|
|
|
|
for (const char* name : vanillaAddons) {
|
|
|
|
|
|
// string (null-terminated)
|
|
|
|
|
|
size_t len = strlen(name);
|
|
|
|
|
|
addonData.insert(addonData.end(), reinterpret_cast<const uint8_t*>(name),
|
|
|
|
|
|
reinterpret_cast<const uint8_t*>(name) + len + 1);
|
|
|
|
|
|
// uint8 flags = 1 (enabled)
|
|
|
|
|
|
addonData.push_back(0x01);
|
|
|
|
|
|
// uint32 modulusCRC (little-endian)
|
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>(standardModulusCRC & 0xFF));
|
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>((standardModulusCRC >> 8) & 0xFF));
|
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>((standardModulusCRC >> 16) & 0xFF));
|
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>((standardModulusCRC >> 24) & 0xFF));
|
|
|
|
|
|
// uint32 urlCRC = 0
|
|
|
|
|
|
addonData.push_back(0); addonData.push_back(0);
|
|
|
|
|
|
addonData.push_back(0); addonData.push_back(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// WotLK: uint32 addonCount + entries + uint32 clientTime
|
|
|
|
|
|
// Send 0 addons
|
|
|
|
|
|
addonData = { 0, 0, 0, 0, // addonCount = 0
|
|
|
|
|
|
0, 0, 0, 0 }; // clientTime = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
uint32_t decompressedSize = static_cast<uint32_t>(addonData.size());
|
2026-02-05 21:03:11 -08:00
|
|
|
|
|
|
|
|
|
|
// Compress with zlib
|
|
|
|
|
|
uLongf compressedSize = compressBound(decompressedSize);
|
|
|
|
|
|
std::vector<uint8_t> compressed(compressedSize);
|
Fix Classic field extraction, Warden PE patches, and auth/opcode corrections
Update field extraction in both CREATE_OBJECT and VALUES handlers to check
specific fields (maxHealth, level, faction, etc.) before power/maxpower range
checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28),
and maxPower indices 29-33 are adjacent to level (34) and faction (35), so
range checks like "key >= powerBase && key < powerBase+7" were incorrectly
capturing those fields.
Add build-aware WoW.exe selection and runtime global patching for Warden
SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix
Classic opcodes and auth session addon data format for CMaNGOS compatibility.
2026-02-20 00:18:03 -08:00
|
|
|
|
int ret = compress(compressed.data(), &compressedSize, addonData.data(), decompressedSize);
|
2026-02-05 21:03:11 -08:00
|
|
|
|
if (ret == Z_OK) {
|
|
|
|
|
|
compressed.resize(compressedSize);
|
|
|
|
|
|
// Write decompressedSize, then compressed bytes
|
|
|
|
|
|
packet.writeUInt32(decompressedSize);
|
|
|
|
|
|
packet.writeBytes(compressed.data(), compressed.size());
|
|
|
|
|
|
LOG_DEBUG("Addon info: decompressedSize=", decompressedSize,
|
Fix Classic field extraction, Warden PE patches, and auth/opcode corrections
Update field extraction in both CREATE_OBJECT and VALUES handlers to check
specific fields (maxHealth, level, faction, etc.) before power/maxpower range
checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28),
and maxPower indices 29-33 are adjacent to level (34) and faction (35), so
range checks like "key >= powerBase && key < powerBase+7" were incorrectly
capturing those fields.
Add build-aware WoW.exe selection and runtime global patching for Warden
SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix
Classic opcodes and auth session addon data format for CMaNGOS compatibility.
2026-02-20 00:18:03 -08:00
|
|
|
|
" compressedSize=", compressedSize, " addons=",
|
|
|
|
|
|
isTbc ? "12 vanilla" : "0 wotlk");
|
2026-02-05 21:03:11 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
LOG_ERROR("zlib compress failed with code: ", ret);
|
|
|
|
|
|
packet.writeUInt32(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes");
|
|
|
|
|
|
|
2026-02-05 21:03:11 -08:00
|
|
|
|
// Dump full packet for protocol debugging
|
|
|
|
|
|
const auto& data = packet.getData();
|
|
|
|
|
|
std::string hexDump;
|
|
|
|
|
|
for (size_t i = 0; i < data.size(); ++i) {
|
|
|
|
|
|
char buf[4];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%02x ", data[i]);
|
|
|
|
|
|
hexDump += buf;
|
|
|
|
|
|
if ((i + 1) % 16 == 0) hexDump += "\n";
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_DEBUG("CMSG_AUTH_SESSION full dump:\n", hexDump);
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
|
|
|
|
|
|
const std::string& accountName,
|
|
|
|
|
|
uint32_t clientSeed,
|
|
|
|
|
|
uint32_t serverSeed,
|
|
|
|
|
|
const std::vector<uint8_t>& sessionKey) {
|
|
|
|
|
|
|
|
|
|
|
|
// Build hash input:
|
|
|
|
|
|
// account_name + [0,0,0,0] + client_seed + server_seed + session_key
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> hashInput;
|
|
|
|
|
|
hashInput.reserve(accountName.size() + 4 + 4 + 4 + sessionKey.size());
|
|
|
|
|
|
|
|
|
|
|
|
// Account name (as bytes)
|
|
|
|
|
|
hashInput.insert(hashInput.end(), accountName.begin(), accountName.end());
|
|
|
|
|
|
|
|
|
|
|
|
// 4 null bytes
|
|
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
|
|
hashInput.push_back(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Client seed (little-endian)
|
|
|
|
|
|
hashInput.push_back(clientSeed & 0xFF);
|
|
|
|
|
|
hashInput.push_back((clientSeed >> 8) & 0xFF);
|
|
|
|
|
|
hashInput.push_back((clientSeed >> 16) & 0xFF);
|
|
|
|
|
|
hashInput.push_back((clientSeed >> 24) & 0xFF);
|
|
|
|
|
|
|
|
|
|
|
|
// Server seed (little-endian)
|
|
|
|
|
|
hashInput.push_back(serverSeed & 0xFF);
|
|
|
|
|
|
hashInput.push_back((serverSeed >> 8) & 0xFF);
|
|
|
|
|
|
hashInput.push_back((serverSeed >> 16) & 0xFF);
|
|
|
|
|
|
hashInput.push_back((serverSeed >> 24) & 0xFF);
|
|
|
|
|
|
|
|
|
|
|
|
// Session key (40 bytes)
|
|
|
|
|
|
hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end());
|
|
|
|
|
|
|
2026-02-13 16:53:28 -08:00
|
|
|
|
// Diagnostic: dump auth hash inputs for debugging AUTH_REJECT
|
|
|
|
|
|
{
|
|
|
|
|
|
auto toHex = [](const uint8_t* data, size_t len) {
|
|
|
|
|
|
std::string s;
|
|
|
|
|
|
for (size_t i = 0; i < len; ++i) {
|
|
|
|
|
|
char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
return s;
|
|
|
|
|
|
};
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_DEBUG("AUTH HASH: account='", accountName, "' clientSeed=0x", std::hex, clientSeed,
|
|
|
|
|
|
" serverSeed=0x", serverSeed, std::dec);
|
|
|
|
|
|
LOG_DEBUG("AUTH HASH: sessionKey=", toHex(sessionKey.data(), sessionKey.size()));
|
|
|
|
|
|
LOG_DEBUG("AUTH HASH: input(", hashInput.size(), ")=", toHex(hashInput.data(), hashInput.size()));
|
2026-02-13 16:53:28 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Compute SHA1 hash
|
2026-02-13 16:53:28 -08:00
|
|
|
|
auto result = auth::Crypto::sha1(hashInput);
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
auto toHex = [](const uint8_t* data, size_t len) {
|
|
|
|
|
|
std::string s;
|
|
|
|
|
|
for (size_t i = 0; i < len; ++i) {
|
|
|
|
|
|
char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
return s;
|
|
|
|
|
|
};
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_DEBUG("AUTH HASH: digest=", toHex(result.data(), result.size()));
|
2026-02-13 16:53:28 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
// SMSG_AUTH_CHALLENGE format varies by expansion:
|
|
|
|
|
|
// TBC 2.4.3: uint32 serverSeed (4 bytes)
|
|
|
|
|
|
// WotLK 3.3.5a: uint32 one + uint32 serverSeed + seeds (40 bytes)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
|
if (packet.getSize() < 4) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix Classic field extraction, Warden PE patches, and auth/opcode corrections
Update field extraction in both CREATE_OBJECT and VALUES handlers to check
specific fields (maxHealth, level, faction, etc.) before power/maxpower range
checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28),
and maxPower indices 29-33 are adjacent to level (34) and faction (35), so
range checks like "key >= powerBase && key < powerBase+7" were incorrectly
capturing those fields.
Add build-aware WoW.exe selection and runtime global patching for Warden
SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix
Classic opcodes and auth session addon data format for CMaNGOS compatibility.
2026-02-20 00:18:03 -08:00
|
|
|
|
if (packet.getSize() <= 4) {
|
|
|
|
|
|
// Original vanilla/TBC format: just the server seed (4 bytes)
|
|
|
|
|
|
data.unknown1 = 0;
|
|
|
|
|
|
data.serverSeed = packet.readUInt32();
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_INFO("SMSG_AUTH_CHALLENGE: TBC format (", packet.getSize(), " bytes)");
|
Fix Classic field extraction, Warden PE patches, and auth/opcode corrections
Update field extraction in both CREATE_OBJECT and VALUES handlers to check
specific fields (maxHealth, level, faction, etc.) before power/maxpower range
checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28),
and maxPower indices 29-33 are adjacent to level (34) and faction (35), so
range checks like "key >= powerBase && key < powerBase+7" were incorrectly
capturing those fields.
Add build-aware WoW.exe selection and runtime global patching for Warden
SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix
Classic opcodes and auth session addon data format for CMaNGOS compatibility.
2026-02-20 00:18:03 -08:00
|
|
|
|
} else if (packet.getSize() < 40) {
|
|
|
|
|
|
// Vanilla with encryption seeds (36 bytes): serverSeed + 32 bytes seeds
|
|
|
|
|
|
// No "unknown1" prefix — first uint32 IS the server seed
|
2026-02-12 22:56:36 -08:00
|
|
|
|
data.unknown1 = 0;
|
|
|
|
|
|
data.serverSeed = packet.readUInt32();
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_INFO("SMSG_AUTH_CHALLENGE: Classic+seeds format (", packet.getSize(), " bytes)");
|
2026-02-12 22:56:36 -08:00
|
|
|
|
} else {
|
Fix Classic field extraction, Warden PE patches, and auth/opcode corrections
Update field extraction in both CREATE_OBJECT and VALUES handlers to check
specific fields (maxHealth, level, faction, etc.) before power/maxpower range
checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28),
and maxPower indices 29-33 are adjacent to level (34) and faction (35), so
range checks like "key >= powerBase && key < powerBase+7" were incorrectly
capturing those fields.
Add build-aware WoW.exe selection and runtime global patching for Warden
SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix
Classic opcodes and auth session addon data format for CMaNGOS compatibility.
2026-02-20 00:18:03 -08:00
|
|
|
|
// WotLK format (40+ bytes): unknown1 + serverSeed + 32 bytes encryption seeds
|
2026-02-12 22:56:36 -08:00
|
|
|
|
data.unknown1 = packet.readUInt32();
|
|
|
|
|
|
data.serverSeed = packet.readUInt32();
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_INFO("SMSG_AUTH_CHALLENGE: WotLK format (", packet.getSize(), " bytes)");
|
|
|
|
|
|
LOG_DEBUG(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
|
2026-02-12 22:56:36 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_DEBUG(" Server seed: 0x", std::hex, data.serverSeed, std::dec);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AuthResponseParser::parse(network::Packet& packet, AuthResponseData& response) {
|
|
|
|
|
|
// SMSG_AUTH_RESPONSE format:
|
|
|
|
|
|
// uint8 result
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getSize() < 1) {
|
|
|
|
|
|
LOG_ERROR("SMSG_AUTH_RESPONSE packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint8_t resultCode = packet.readUInt8();
|
|
|
|
|
|
response.result = static_cast<AuthResult>(resultCode);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_AUTH_RESPONSE: ", getAuthResultString(response.result));
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* getAuthResultString(AuthResult result) {
|
|
|
|
|
|
switch (result) {
|
|
|
|
|
|
case AuthResult::OK:
|
|
|
|
|
|
return "OK - Authentication successful";
|
|
|
|
|
|
case AuthResult::FAILED:
|
|
|
|
|
|
return "FAILED - Authentication failed";
|
|
|
|
|
|
case AuthResult::REJECT:
|
|
|
|
|
|
return "REJECT - Connection rejected";
|
|
|
|
|
|
case AuthResult::BAD_SERVER_PROOF:
|
|
|
|
|
|
return "BAD_SERVER_PROOF - Invalid server proof";
|
|
|
|
|
|
case AuthResult::UNAVAILABLE:
|
|
|
|
|
|
return "UNAVAILABLE - Server unavailable";
|
|
|
|
|
|
case AuthResult::SYSTEM_ERROR:
|
|
|
|
|
|
return "SYSTEM_ERROR - System error occurred";
|
|
|
|
|
|
case AuthResult::BILLING_ERROR:
|
|
|
|
|
|
return "BILLING_ERROR - Billing error";
|
|
|
|
|
|
case AuthResult::BILLING_EXPIRED:
|
|
|
|
|
|
return "BILLING_EXPIRED - Subscription expired";
|
|
|
|
|
|
case AuthResult::VERSION_MISMATCH:
|
|
|
|
|
|
return "VERSION_MISMATCH - Client version mismatch";
|
|
|
|
|
|
case AuthResult::UNKNOWN_ACCOUNT:
|
|
|
|
|
|
return "UNKNOWN_ACCOUNT - Account not found";
|
|
|
|
|
|
case AuthResult::INCORRECT_PASSWORD:
|
|
|
|
|
|
return "INCORRECT_PASSWORD - Wrong password";
|
|
|
|
|
|
case AuthResult::SESSION_EXPIRED:
|
|
|
|
|
|
return "SESSION_EXPIRED - Session has expired";
|
|
|
|
|
|
case AuthResult::SERVER_SHUTTING_DOWN:
|
|
|
|
|
|
return "SERVER_SHUTTING_DOWN - Server is shutting down";
|
|
|
|
|
|
case AuthResult::ALREADY_LOGGING_IN:
|
|
|
|
|
|
return "ALREADY_LOGGING_IN - Already logging in";
|
|
|
|
|
|
case AuthResult::LOGIN_SERVER_NOT_FOUND:
|
|
|
|
|
|
return "LOGIN_SERVER_NOT_FOUND - Can't contact login server";
|
|
|
|
|
|
case AuthResult::WAIT_QUEUE:
|
|
|
|
|
|
return "WAIT_QUEUE - Waiting in queue";
|
|
|
|
|
|
case AuthResult::BANNED:
|
|
|
|
|
|
return "BANNED - Account is banned";
|
|
|
|
|
|
case AuthResult::ALREADY_ONLINE:
|
|
|
|
|
|
return "ALREADY_ONLINE - Character already logged in";
|
|
|
|
|
|
case AuthResult::NO_TIME:
|
|
|
|
|
|
return "NO_TIME - No game time remaining";
|
|
|
|
|
|
case AuthResult::DB_BUSY:
|
|
|
|
|
|
return "DB_BUSY - Database is busy";
|
|
|
|
|
|
case AuthResult::SUSPENDED:
|
|
|
|
|
|
return "SUSPENDED - Account is suspended";
|
|
|
|
|
|
case AuthResult::PARENTAL_CONTROL:
|
|
|
|
|
|
return "PARENTAL_CONTROL - Parental controls active";
|
|
|
|
|
|
case AuthResult::LOCKED_ENFORCED:
|
|
|
|
|
|
return "LOCKED_ENFORCED - Account is locked";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "UNKNOWN - Unknown result code";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 14:13:48 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Character Creation
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet CharCreatePacket::build(const CharCreateData& data) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_CREATE));
|
2026-02-05 14:13:48 -08:00
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Convert nonbinary gender to server-compatible value (servers only support male/female)
|
|
|
|
|
|
Gender serverGender = toServerGender(data.gender);
|
|
|
|
|
|
|
2026-02-05 14:13:48 -08:00
|
|
|
|
packet.writeString(data.name); // null-terminated name
|
|
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(data.race));
|
|
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(data.characterClass));
|
2026-02-09 17:39:21 -08:00
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(serverGender));
|
2026-02-05 14:13:48 -08:00
|
|
|
|
packet.writeUInt8(data.skin);
|
|
|
|
|
|
packet.writeUInt8(data.face);
|
|
|
|
|
|
packet.writeUInt8(data.hairStyle);
|
|
|
|
|
|
packet.writeUInt8(data.hairColor);
|
|
|
|
|
|
packet.writeUInt8(data.facialHair);
|
|
|
|
|
|
packet.writeUInt8(0); // outfitId, always 0
|
2026-02-17 04:16:27 -08:00
|
|
|
|
// Turtle WoW / 1.12.1 clients send 4 extra zero bytes after outfitId.
|
|
|
|
|
|
// Servers may validate packet length and silently drop undersized packets.
|
|
|
|
|
|
packet.writeUInt32(0);
|
2026-02-05 14:13:48 -08:00
|
|
|
|
|
2026-02-05 21:03:11 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_CHAR_CREATE: name=", data.name,
|
|
|
|
|
|
" race=", static_cast<int>(data.race),
|
|
|
|
|
|
" class=", static_cast<int>(data.characterClass),
|
|
|
|
|
|
" gender=", static_cast<int>(data.gender),
|
2026-02-09 17:39:21 -08:00
|
|
|
|
" (server gender=", static_cast<int>(serverGender), ")",
|
2026-02-05 21:03:11 -08:00
|
|
|
|
" skin=", static_cast<int>(data.skin),
|
|
|
|
|
|
" face=", static_cast<int>(data.face),
|
|
|
|
|
|
" hair=", static_cast<int>(data.hairStyle),
|
|
|
|
|
|
" hairColor=", static_cast<int>(data.hairColor),
|
|
|
|
|
|
" facial=", static_cast<int>(data.facialHair));
|
|
|
|
|
|
|
|
|
|
|
|
// Dump full packet for protocol debugging
|
|
|
|
|
|
const auto& pktData = packet.getData();
|
|
|
|
|
|
std::string hexDump;
|
|
|
|
|
|
for (size_t i = 0; i < pktData.size(); ++i) {
|
|
|
|
|
|
char buf[4];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%02x ", pktData[i]);
|
|
|
|
|
|
hexDump += buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_DEBUG("CMSG_CHAR_CREATE full dump: ", hexDump);
|
2026-02-05 14:13:48 -08:00
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponseData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: result(1)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
|
|
|
|
|
LOG_WARNING("SMSG_CHAR_CREATE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 14:13:48 -08:00
|
|
|
|
data.result = static_cast<CharCreateResult>(packet.readUInt8());
|
|
|
|
|
|
LOG_INFO("SMSG_CHAR_CREATE result: ", static_cast<int>(data.result));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
network::Packet CharEnumPacket::build() {
|
|
|
|
|
|
// CMSG_CHAR_ENUM has no body - just the opcode
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_ENUM));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
|
|
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) {
|
2026-03-11 14:40:07 -07:00
|
|
|
|
// Upfront validation: count(1) + at least minimal character data
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) return false;
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Read character count
|
|
|
|
|
|
uint8_t count = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
|
|
|
|
|
|
|
|
|
|
|
|
response.characters.clear();
|
|
|
|
|
|
response.characters.reserve(count);
|
|
|
|
|
|
|
|
|
|
|
|
for (uint8_t i = 0; i < count; ++i) {
|
|
|
|
|
|
Character character;
|
|
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
|
// Validate minimum bytes for this character entry before reading:
|
|
|
|
|
|
// GUID(8) + name(>=1 for empty string) + race(1) + class(1) + gender(1) +
|
|
|
|
|
|
// appearanceBytes(4) + facialFeatures(1) + level(1) + zoneId(4) + mapId(4) +
|
|
|
|
|
|
// x(4) + y(4) + z(4) + guildId(4) + flags(4) + customization(4) + unknown(1) +
|
|
|
|
|
|
// petDisplayModel(4) + petLevel(4) + petFamily(4) + 23items*(dispModel(4)+invType(1)+enchant(4)) = 207 bytes
|
|
|
|
|
|
const size_t minCharacterSize = 8 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 1 + 4 + 4 + 4 + (23 * 9);
|
|
|
|
|
|
if (packet.getReadPos() + minCharacterSize > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("CharEnumParser: truncated character at index ", (int)i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Read GUID (8 bytes, little-endian)
|
|
|
|
|
|
character.guid = packet.readUInt64();
|
|
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
|
// Read name (null-terminated string) - validate before reading
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("CharEnumParser: no bytes for name at index ", (int)i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
character.name = packet.readString();
|
|
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
|
// Validate remaining bytes before reading fixed-size fields
|
|
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("CharEnumParser: truncated before race/class/gender at index ", (int)i);
|
|
|
|
|
|
character.race = Race::HUMAN;
|
|
|
|
|
|
character.characterClass = Class::WARRIOR;
|
|
|
|
|
|
character.gender = Gender::MALE;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Read race, class, gender
|
|
|
|
|
|
character.race = static_cast<Race>(packet.readUInt8());
|
|
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
character.characterClass = Class::WARRIOR;
|
|
|
|
|
|
character.gender = Gender::MALE;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.characterClass = static_cast<Class>(packet.readUInt8());
|
|
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
character.gender = Gender::MALE;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.gender = static_cast<Gender>(packet.readUInt8());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
|
// Validate before reading appearance data
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
character.appearanceBytes = 0;
|
|
|
|
|
|
character.facialFeatures = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Read appearance data
|
|
|
|
|
|
character.appearanceBytes = packet.readUInt32();
|
|
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
character.facialFeatures = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.facialFeatures = packet.readUInt8();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read level
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
character.level = 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.level = packet.readUInt8();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read location
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) {
|
|
|
|
|
|
character.zoneId = 0;
|
|
|
|
|
|
character.mapId = 0;
|
|
|
|
|
|
character.x = 0.0f;
|
|
|
|
|
|
character.y = 0.0f;
|
|
|
|
|
|
character.z = 0.0f;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.zoneId = packet.readUInt32();
|
|
|
|
|
|
character.mapId = packet.readUInt32();
|
|
|
|
|
|
character.x = packet.readFloat();
|
|
|
|
|
|
character.y = packet.readFloat();
|
|
|
|
|
|
character.z = packet.readFloat();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read affiliations
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
character.guildId = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.guildId = packet.readUInt32();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read flags
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
character.flags = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.flags = packet.readUInt32();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Skip customization flag (uint32) and unknown byte
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
// Customization missing, skip unknown
|
|
|
|
|
|
} else {
|
|
|
|
|
|
packet.readUInt32(); // Customization
|
|
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
// Unknown missing
|
|
|
|
|
|
} else {
|
|
|
|
|
|
packet.readUInt8(); // Unknown
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read pet data (always present, even if no pet)
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) {
|
|
|
|
|
|
character.pet.displayModel = 0;
|
|
|
|
|
|
character.pet.level = 0;
|
|
|
|
|
|
character.pet.family = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
character.pet.displayModel = packet.readUInt32();
|
|
|
|
|
|
character.pet.level = packet.readUInt32();
|
|
|
|
|
|
character.pet.family = packet.readUInt32();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read equipment (23 items)
|
|
|
|
|
|
character.equipment.reserve(23);
|
|
|
|
|
|
for (int j = 0; j < 23; ++j) {
|
2026-03-11 14:40:07 -07:00
|
|
|
|
if (packet.getReadPos() + 9 > packet.getSize()) break;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
EquipmentItem item;
|
|
|
|
|
|
item.displayModel = packet.readUInt32();
|
|
|
|
|
|
item.inventoryType = packet.readUInt8();
|
|
|
|
|
|
item.enchantment = packet.readUInt32();
|
|
|
|
|
|
character.equipment.push_back(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 04:25:45 -07:00
|
|
|
|
LOG_DEBUG(" Character ", (int)(i + 1), ": ", character.name,
|
|
|
|
|
|
" (", getRaceName(character.race), " ", getClassName(character.characterClass),
|
|
|
|
|
|
" level ", (int)character.level, " zone ", character.zoneId, ")");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
response.characters.push_back(character);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Successfully parsed ", response.characters.size(), " characters");
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PLAYER_LOGIN));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Write character GUID (8 bytes, little-endian)
|
|
|
|
|
|
packet.writeUInt64(characterGuid);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Built CMSG_PLAYER_LOGIN packet");
|
|
|
|
|
|
LOG_INFO(" Character GUID: 0x", std::hex, characterGuid, std::dec);
|
|
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData& data) {
|
|
|
|
|
|
// SMSG_LOGIN_VERIFY_WORLD format (WoW 3.3.5a):
|
|
|
|
|
|
// uint32 mapId
|
|
|
|
|
|
// float x, y, z (position)
|
|
|
|
|
|
// float orientation
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getSize() < 20) {
|
|
|
|
|
|
LOG_ERROR("SMSG_LOGIN_VERIFY_WORLD packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.mapId = packet.readUInt32();
|
|
|
|
|
|
data.x = packet.readFloat();
|
|
|
|
|
|
data.y = packet.readFloat();
|
|
|
|
|
|
data.z = packet.readFloat();
|
|
|
|
|
|
data.orientation = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_LOGIN_VERIFY_WORLD:");
|
|
|
|
|
|
LOG_INFO(" Map ID: ", data.mapId);
|
|
|
|
|
|
LOG_INFO(" Position: (", data.x, ", ", data.y, ", ", data.z, ")");
|
|
|
|
|
|
LOG_INFO(" Orientation: ", data.orientation, " radians");
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData& data) {
|
2026-02-22 07:44:32 -08:00
|
|
|
|
// Common layouts seen in the wild:
|
|
|
|
|
|
// - WotLK-like: uint32 serverTime, uint8 unk, uint32 mask, uint32[up to 8] slotTimes
|
|
|
|
|
|
// - Older/variant: uint32 serverTime, uint8 unk, uint32[up to 8] slotTimes
|
|
|
|
|
|
// Some servers only send a subset of slots.
|
|
|
|
|
|
if (packet.getSize() < 5) {
|
|
|
|
|
|
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(),
|
|
|
|
|
|
" bytes (need at least 5)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 07:44:32 -08:00
|
|
|
|
for (uint32_t& t : data.accountDataTimes) {
|
|
|
|
|
|
t = 0;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
data.serverTime = packet.readUInt32();
|
|
|
|
|
|
data.unknown = packet.readUInt8();
|
|
|
|
|
|
|
2026-02-22 07:44:32 -08:00
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
uint32_t mask = 0xFF;
|
|
|
|
|
|
if (remaining >= 4 && ((remaining - 4) % 4) == 0) {
|
|
|
|
|
|
// Treat first dword as slot mask when payload shape matches.
|
|
|
|
|
|
mask = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
size_t slotWords = std::min<size_t>(8, remaining / 4);
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
|
|
|
|
|
|
LOG_DEBUG(" Server time: ", data.serverTime);
|
|
|
|
|
|
LOG_DEBUG(" Unknown: ", (int)data.unknown);
|
2026-02-22 07:44:32 -08:00
|
|
|
|
LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-22 07:44:32 -08:00
|
|
|
|
for (size_t i = 0; i < slotWords; ++i) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
data.accountDataTimes[i] = packet.readUInt32();
|
2026-02-22 07:44:32 -08:00
|
|
|
|
if (data.accountDataTimes[i] != 0 || ((mask & (1u << i)) != 0)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-22 07:44:32 -08:00
|
|
|
|
if (packet.getReadPos() != packet.getSize()) {
|
|
|
|
|
|
LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getSize() - packet.getReadPos());
|
|
|
|
|
|
packet.setReadPos(packet.getSize());
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool MotdParser::parse(network::Packet& packet, MotdData& data) {
|
|
|
|
|
|
// SMSG_MOTD format (WoW 3.3.5a):
|
|
|
|
|
|
// uint32 lineCount
|
|
|
|
|
|
// string[lineCount] lines (null-terminated strings)
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_MOTD packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t lineCount = packet.readUInt32();
|
|
|
|
|
|
|
2026-03-11 14:41:25 -07:00
|
|
|
|
// Cap lineCount to prevent unbounded memory allocation
|
|
|
|
|
|
const uint32_t MAX_MOTD_LINES = 64;
|
|
|
|
|
|
if (lineCount > MAX_MOTD_LINES) {
|
|
|
|
|
|
LOG_WARNING("MotdParser: lineCount capped (requested=", lineCount, ")");
|
|
|
|
|
|
lineCount = MAX_MOTD_LINES;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_INFO("Parsed SMSG_MOTD: ", lineCount, " line(s)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
data.lines.clear();
|
|
|
|
|
|
data.lines.reserve(lineCount);
|
|
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < lineCount; ++i) {
|
2026-03-11 14:41:25 -07:00
|
|
|
|
// Validate at least 1 byte available for the string
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("MotdParser: truncated at line ", i + 1);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
std::string line = packet.readString();
|
|
|
|
|
|
data.lines.push_back(line);
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_DEBUG(" MOTD[", i + 1, "]: ", line);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PING));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Write sequence number (uint32, little-endian)
|
|
|
|
|
|
packet.writeUInt32(sequence);
|
|
|
|
|
|
|
|
|
|
|
|
// Write latency (uint32, little-endian, in milliseconds)
|
|
|
|
|
|
packet.writeUInt32(latency);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_PING packet");
|
|
|
|
|
|
LOG_DEBUG(" Sequence: ", sequence);
|
|
|
|
|
|
LOG_DEBUG(" Latency: ", latency, " ms");
|
|
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool PongParser::parse(network::Packet& packet, PongData& data) {
|
|
|
|
|
|
// SMSG_PONG format (WoW 3.3.5a):
|
|
|
|
|
|
// uint32 sequence (echoed from CMSG_PING)
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_PONG packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.sequence = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Parsed SMSG_PONG:");
|
|
|
|
|
|
LOG_DEBUG(" Sequence: ", data.sequence);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
|
void MovementPacket::writePackedGuid(network::Packet& packet, uint64_t guid) {
|
2026-02-06 03:24:46 -08:00
|
|
|
|
uint8_t mask = 0;
|
|
|
|
|
|
uint8_t guidBytes[8];
|
|
|
|
|
|
int guidByteCount = 0;
|
|
|
|
|
|
for (int i = 0; i < 8; i++) {
|
2026-02-11 21:14:35 -08:00
|
|
|
|
uint8_t byte = static_cast<uint8_t>((guid >> (i * 8)) & 0xFF);
|
2026-02-06 03:24:46 -08:00
|
|
|
|
if (byte != 0) {
|
|
|
|
|
|
mask |= (1 << i);
|
|
|
|
|
|
guidBytes[guidByteCount++] = byte;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
packet.writeUInt8(mask);
|
|
|
|
|
|
for (int i = 0; i < guidByteCount; i++) {
|
|
|
|
|
|
packet.writeUInt8(guidBytes[i]);
|
|
|
|
|
|
}
|
2026-02-11 21:14:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void MovementPacket::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
|
|
|
|
|
|
// Movement packet format (WoW 3.3.5a) payload:
|
|
|
|
|
|
// uint32 flags
|
|
|
|
|
|
// uint16 flags2
|
|
|
|
|
|
// uint32 time
|
|
|
|
|
|
// float x, y, z
|
|
|
|
|
|
// float orientation
|
2026-02-06 03:24:46 -08:00
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Write movement flags
|
|
|
|
|
|
packet.writeUInt32(info.flags);
|
|
|
|
|
|
packet.writeUInt16(info.flags2);
|
|
|
|
|
|
|
|
|
|
|
|
// Write timestamp
|
|
|
|
|
|
packet.writeUInt32(info.time);
|
|
|
|
|
|
|
|
|
|
|
|
// Write position
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.x), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.y), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.z), sizeof(float));
|
|
|
|
|
|
|
|
|
|
|
|
// Write orientation
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
|
|
|
|
|
|
|
2026-02-11 15:24:05 -08:00
|
|
|
|
// Write transport data if on transport.
|
|
|
|
|
|
// 3.3.5a ordering: transport block appears before pitch/fall/jump.
|
2026-02-11 02:23:37 -08:00
|
|
|
|
if (info.hasFlag(MovementFlags::ONTRANSPORT)) {
|
|
|
|
|
|
// Write packed transport GUID
|
|
|
|
|
|
uint8_t transMask = 0;
|
|
|
|
|
|
uint8_t transGuidBytes[8];
|
|
|
|
|
|
int transGuidByteCount = 0;
|
|
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
|
|
|
|
uint8_t byte = static_cast<uint8_t>((info.transportGuid >> (i * 8)) & 0xFF);
|
|
|
|
|
|
if (byte != 0) {
|
|
|
|
|
|
transMask |= (1 << i);
|
|
|
|
|
|
transGuidBytes[transGuidByteCount++] = byte;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
packet.writeUInt8(transMask);
|
|
|
|
|
|
for (int i = 0; i < transGuidByteCount; i++) {
|
|
|
|
|
|
packet.writeUInt8(transGuidBytes[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write transport local position
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportX), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportY), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportZ), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportO), sizeof(float));
|
|
|
|
|
|
|
|
|
|
|
|
// Write transport time
|
|
|
|
|
|
packet.writeUInt32(info.transportTime);
|
2026-02-11 15:24:05 -08:00
|
|
|
|
|
|
|
|
|
|
// Transport seat is always present in ONTRANSPORT movement info.
|
|
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(info.transportSeat));
|
|
|
|
|
|
|
|
|
|
|
|
// Optional second transport time for interpolated movement.
|
|
|
|
|
|
if (info.flags2 & 0x0200) {
|
|
|
|
|
|
packet.writeUInt32(info.transportTime2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write pitch if swimming/flying
|
|
|
|
|
|
if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) {
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fall time is ALWAYS present in the packet (server reads it unconditionally).
|
|
|
|
|
|
// Jump velocity/angle data is only present when FALLING flag is set.
|
|
|
|
|
|
packet.writeUInt32(info.fallTime);
|
|
|
|
|
|
|
|
|
|
|
|
if (info.hasFlag(MovementFlags::FALLING)) {
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
|
|
|
|
|
|
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
2026-02-11 02:23:37 -08:00
|
|
|
|
}
|
2026-02-11 21:14:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(opcode));
|
2026-02-11 21:14:35 -08:00
|
|
|
|
|
|
|
|
|
|
// Movement packet format (WoW 3.3.5a):
|
|
|
|
|
|
// packed GUID + movement payload
|
|
|
|
|
|
writePackedGuid(packet, playerGuid);
|
|
|
|
|
|
writeMovementPayload(packet, info);
|
2026-02-11 02:23:37 -08:00
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
|
// Detailed hex dump for debugging
|
|
|
|
|
|
static int mvLog = 5;
|
2026-02-06 09:14:22 -08:00
|
|
|
|
if (mvLog-- > 0) {
|
2026-02-06 13:47:03 -08:00
|
|
|
|
const auto& raw = packet.getData();
|
|
|
|
|
|
std::string hex;
|
|
|
|
|
|
for (size_t i = 0; i < raw.size(); i++) {
|
|
|
|
|
|
char b[4]; snprintf(b, sizeof(b), "%02x ", raw[i]);
|
|
|
|
|
|
hex += b;
|
|
|
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
|
LOG_DEBUG("MOVEPKT opcode=0x", std::hex, wireOpcode(opcode), std::dec,
|
2026-02-06 13:47:03 -08:00
|
|
|
|
" guid=0x", std::hex, playerGuid, std::dec,
|
|
|
|
|
|
" payload=", raw.size(), " bytes",
|
|
|
|
|
|
" flags=0x", std::hex, info.flags, std::dec,
|
|
|
|
|
|
" flags2=0x", std::hex, info.flags2, std::dec,
|
|
|
|
|
|
" pos=(", info.x, ",", info.y, ",", info.z, ",", info.orientation, ")",
|
2026-02-11 02:23:37 -08:00
|
|
|
|
" fallTime=", info.fallTime,
|
|
|
|
|
|
(info.hasFlag(MovementFlags::ONTRANSPORT) ?
|
|
|
|
|
|
" ONTRANSPORT guid=0x" + std::to_string(info.transportGuid) +
|
|
|
|
|
|
" localPos=(" + std::to_string(info.transportX) + "," +
|
|
|
|
|
|
std::to_string(info.transportY) + "," + std::to_string(info.transportZ) + ")" : ""));
|
2026-02-21 01:26:16 -08:00
|
|
|
|
LOG_DEBUG("MOVEPKT hex: ", hex);
|
2026-02-06 09:14:22 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint64_t UpdateObjectParser::readPackedGuid(network::Packet& packet) {
|
|
|
|
|
|
// Read packed GUID format:
|
|
|
|
|
|
// First byte is a mask indicating which bytes are present
|
|
|
|
|
|
uint8_t mask = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (mask == 0) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint64_t guid = 0;
|
|
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
|
|
|
|
if (mask & (1 << i)) {
|
|
|
|
|
|
uint8_t byte = packet.readUInt8();
|
|
|
|
|
|
guid |= (static_cast<uint64_t>(byte) << (i * 8));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return guid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// WoW 3.3.5a UPDATE_OBJECT movement block structure:
|
|
|
|
|
|
// 1. UpdateFlags (1 byte, sometimes 2)
|
|
|
|
|
|
// 2. Movement data depends on update flags
|
|
|
|
|
|
|
|
|
|
|
|
// Update flags (3.3.5a uses 2 bytes for flags)
|
|
|
|
|
|
uint16_t updateFlags = packet.readUInt16();
|
2026-02-08 00:59:40 -08:00
|
|
|
|
block.updateFlags = updateFlags;
|
2026-02-05 21:55:52 -08:00
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec);
|
|
|
|
|
|
|
2026-02-11 00:54:38 -08:00
|
|
|
|
// Log transport-related flag combinations
|
|
|
|
|
|
if (updateFlags & 0x0002) { // UPDATEFLAG_TRANSPORT
|
2026-02-21 01:26:16 -08:00
|
|
|
|
static int transportFlagLogCount = 0;
|
|
|
|
|
|
if (transportFlagLogCount < 12) {
|
|
|
|
|
|
LOG_INFO(" Transport flags detected: 0x", std::hex, updateFlags, std::dec,
|
|
|
|
|
|
" (TRANSPORT=", !!(updateFlags & 0x0002),
|
|
|
|
|
|
", POSITION=", !!(updateFlags & 0x0100),
|
|
|
|
|
|
", ROTATION=", !!(updateFlags & 0x0200),
|
|
|
|
|
|
", STATIONARY=", !!(updateFlags & 0x0040), ")");
|
|
|
|
|
|
transportFlagLogCount++;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
LOG_DEBUG(" Transport flags detected: 0x", std::hex, updateFlags, std::dec);
|
|
|
|
|
|
}
|
2026-02-11 00:54:38 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// UpdateFlags bit meanings:
|
|
|
|
|
|
// 0x0001 = UPDATEFLAG_SELF
|
|
|
|
|
|
// 0x0002 = UPDATEFLAG_TRANSPORT
|
|
|
|
|
|
// 0x0004 = UPDATEFLAG_HAS_TARGET
|
|
|
|
|
|
// 0x0008 = UPDATEFLAG_LOWGUID
|
|
|
|
|
|
// 0x0010 = UPDATEFLAG_HIGHGUID
|
|
|
|
|
|
// 0x0020 = UPDATEFLAG_LIVING
|
|
|
|
|
|
// 0x0040 = UPDATEFLAG_STATIONARY_POSITION
|
|
|
|
|
|
// 0x0080 = UPDATEFLAG_VEHICLE
|
|
|
|
|
|
// 0x0100 = UPDATEFLAG_POSITION (transport)
|
|
|
|
|
|
// 0x0200 = UPDATEFLAG_ROTATION
|
|
|
|
|
|
|
|
|
|
|
|
const uint16_t UPDATEFLAG_LIVING = 0x0020;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_STATIONARY_POSITION = 0x0040;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_HAS_TARGET = 0x0004;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_TRANSPORT = 0x0002;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_POSITION = 0x0100;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_VEHICLE = 0x0080;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_ROTATION = 0x0200;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_LOWGUID = 0x0008;
|
|
|
|
|
|
const uint16_t UPDATEFLAG_HIGHGUID = 0x0010;
|
|
|
|
|
|
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_LIVING) {
|
|
|
|
|
|
// Full movement block for living units
|
|
|
|
|
|
uint32_t moveFlags = packet.readUInt32();
|
|
|
|
|
|
uint16_t moveFlags2 = packet.readUInt16();
|
|
|
|
|
|
/*uint32_t time =*/ packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// Position
|
|
|
|
|
|
block.x = packet.readFloat();
|
|
|
|
|
|
block.y = packet.readFloat();
|
|
|
|
|
|
block.z = packet.readFloat();
|
|
|
|
|
|
block.orientation = packet.readFloat();
|
|
|
|
|
|
block.hasMovement = true;
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG(" LIVING movement: (", block.x, ", ", block.y, ", ", block.z,
|
|
|
|
|
|
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
|
|
|
|
|
|
|
|
|
|
|
|
// Transport data (if on transport)
|
|
|
|
|
|
if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT
|
2026-02-08 00:59:40 -08:00
|
|
|
|
block.onTransport = true;
|
|
|
|
|
|
block.transportGuid = readPackedGuid(packet);
|
|
|
|
|
|
block.transportX = packet.readFloat();
|
|
|
|
|
|
block.transportY = packet.readFloat();
|
|
|
|
|
|
block.transportZ = packet.readFloat();
|
|
|
|
|
|
block.transportO = packet.readFloat();
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*uint32_t tTime =*/ packet.readUInt32();
|
|
|
|
|
|
/*int8_t tSeat =*/ packet.readUInt8();
|
|
|
|
|
|
|
2026-02-08 00:59:40 -08:00
|
|
|
|
LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec,
|
|
|
|
|
|
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
|
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT
|
|
|
|
|
|
/*uint32_t tTime2 =*/ packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// Swimming/flying pitch
|
2026-03-10 11:16:40 -07:00
|
|
|
|
// WotLK 3.3.5a movement flags relevant here:
|
|
|
|
|
|
// SWIMMING = 0x00200000
|
|
|
|
|
|
// FLYING = 0x01000000 (player/creature actively flying)
|
|
|
|
|
|
// SPLINE_ELEVATION = 0x02000000 (smooth vertical spline offset — no pitch field)
|
|
|
|
|
|
// MovementFlags2:
|
|
|
|
|
|
// MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0010
|
|
|
|
|
|
//
|
|
|
|
|
|
// Pitch is present when SWIMMING or FLYING are set, or the always-allow flag is set.
|
|
|
|
|
|
// The original code checked 0x02000000 (SPLINE_ELEVATION) which neither covers SWIMMING
|
|
|
|
|
|
// nor FLYING, causing misaligned reads for swimming/flying entities in SMSG_UPDATE_OBJECT.
|
2026-03-10 11:03:33 -07:00
|
|
|
|
if ((moveFlags & 0x00200000) /* SWIMMING */ ||
|
2026-03-10 11:16:40 -07:00
|
|
|
|
(moveFlags & 0x01000000) /* FLYING */ ||
|
2026-03-10 11:03:33 -07:00
|
|
|
|
(moveFlags2 & 0x0010) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) {
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*float pitch =*/ packet.readFloat();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// Fall time
|
|
|
|
|
|
/*uint32_t fallTime =*/ packet.readUInt32();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// Jumping
|
|
|
|
|
|
if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING
|
|
|
|
|
|
/*float jumpVelocity =*/ packet.readFloat();
|
|
|
|
|
|
/*float jumpSinAngle =*/ packet.readFloat();
|
|
|
|
|
|
/*float jumpCosAngle =*/ packet.readFloat();
|
|
|
|
|
|
/*float jumpXYSpeed =*/ packet.readFloat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Spline elevation
|
|
|
|
|
|
if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION
|
|
|
|
|
|
/*float splineElevation =*/ packet.readFloat();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
fix: harden Turtle movement block parser with bounds checks
The Turtle parseMovementBlock had no bounds checking on any reads.
Since Packet::readUInt8() returns 0 past the end without failing, the
parser could "succeed" with all-zero garbage data, then subsequent
parseUpdateFields would read from wrong positions, producing
"truncated field value" and "truncated update mask" errors.
Added bounds checks before every conditional read section (transport,
swimming pitch, fall time, jumping, spline elevation, speeds, spline
data, tail flags). Also removed the WotLK movement block fallback from
the Turtle parser chain — WotLK format is fundamentally incompatible
(uint16 flags, 9 speeds) and false-positive parses corrupt NPC data.
Also changed spline pointCount > 256 from cap-to-zero to return false
so the parser correctly fails instead of silently dropping waypoints.
2026-03-18 07:39:40 -07:00
|
|
|
|
// Speeds (9 values in WotLK: walk/run/runBack/swim/swimBack/flight/flightBack/turn/pitch)
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*float walkSpeed =*/ packet.readFloat();
|
2026-02-07 20:24:25 -08:00
|
|
|
|
float runSpeed = packet.readFloat();
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*float runBackSpeed =*/ packet.readFloat();
|
|
|
|
|
|
/*float swimSpeed =*/ packet.readFloat();
|
|
|
|
|
|
/*float swimBackSpeed =*/ packet.readFloat();
|
|
|
|
|
|
/*float flightSpeed =*/ packet.readFloat();
|
|
|
|
|
|
/*float flightBackSpeed =*/ packet.readFloat();
|
|
|
|
|
|
/*float turnRate =*/ packet.readFloat();
|
|
|
|
|
|
/*float pitchRate =*/ packet.readFloat();
|
|
|
|
|
|
|
2026-02-07 20:24:25 -08:00
|
|
|
|
block.runSpeed = runSpeed;
|
2026-03-10 11:14:58 -07:00
|
|
|
|
block.moveFlags = moveFlags;
|
2026-02-07 20:24:25 -08:00
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// Spline data
|
|
|
|
|
|
if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED
|
2026-02-22 08:27:17 -08:00
|
|
|
|
auto bytesAvailable = [&](size_t n) -> bool { return packet.getReadPos() + n <= packet.getSize(); };
|
|
|
|
|
|
if (!bytesAvailable(4)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
|
uint32_t splineFlags = packet.readUInt32();
|
2026-02-06 15:18:50 -08:00
|
|
|
|
LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec);
|
2026-02-05 21:55:52 -08:00
|
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
|
if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT
|
2026-02-22 08:27:17 -08:00
|
|
|
|
if (!bytesAvailable(12)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*float finalX =*/ packet.readFloat();
|
|
|
|
|
|
/*float finalY =*/ packet.readFloat();
|
|
|
|
|
|
/*float finalZ =*/ packet.readFloat();
|
2026-02-06 15:18:50 -08:00
|
|
|
|
} else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET
|
2026-02-22 08:27:17 -08:00
|
|
|
|
if (!bytesAvailable(8)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*uint64_t finalTarget =*/ packet.readUInt64();
|
2026-02-06 15:18:50 -08:00
|
|
|
|
} else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE
|
2026-02-22 08:27:17 -08:00
|
|
|
|
if (!bytesAvailable(4)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*float finalAngle =*/ packet.readFloat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 07:23:51 -07:00
|
|
|
|
// Spline data layout varies by expansion:
|
|
|
|
|
|
// Classic/Vanilla: timePassed(4)+duration(4)+splineId(4)+pointCount(4)+points+mode(1)+endPoint(12)
|
|
|
|
|
|
// WotLK: timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4)
|
|
|
|
|
|
// +[ANIMATION(5)]+[PARABOLIC(8)]+pointCount(4)+points+mode(1)+endPoint(12)
|
|
|
|
|
|
// Since the parser has no expansion context, auto-detect by trying Classic first.
|
2026-02-22 08:27:17 -08:00
|
|
|
|
const size_t legacyStart = packet.getReadPos();
|
2026-03-18 07:23:51 -07:00
|
|
|
|
if (!bytesAvailable(16)) return false; // minimum: 12 common + 4 pointCount
|
2026-02-05 21:55:52 -08:00
|
|
|
|
/*uint32_t timePassed =*/ packet.readUInt32();
|
|
|
|
|
|
/*uint32_t duration =*/ packet.readUInt32();
|
|
|
|
|
|
/*uint32_t splineId =*/ packet.readUInt32();
|
2026-03-18 07:23:51 -07:00
|
|
|
|
const size_t afterSplineId = packet.getReadPos();
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: try to parse uncompressed spline points from current read position.
|
|
|
|
|
|
auto tryParseUncompressedSpline = [&](const char* tag) -> bool {
|
|
|
|
|
|
if (!bytesAvailable(4)) return false;
|
|
|
|
|
|
uint32_t pc = packet.readUInt32();
|
|
|
|
|
|
if (pc > 256) return false;
|
|
|
|
|
|
size_t needed = static_cast<size_t>(pc) * 12ull + 13ull;
|
|
|
|
|
|
if (!bytesAvailable(needed)) return false;
|
|
|
|
|
|
for (uint32_t i = 0; i < pc; i++) {
|
|
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
2026-02-22 08:27:17 -08:00
|
|
|
|
}
|
2026-03-18 07:23:51 -07:00
|
|
|
|
packet.readUInt8(); // splineMode
|
|
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat(); // endPoint
|
|
|
|
|
|
LOG_DEBUG(" Spline pointCount=", pc, " (", tag, ")");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// --- Try 1: Classic format (pointCount immediately after splineId) ---
|
|
|
|
|
|
bool splineParsed = tryParseUncompressedSpline("classic");
|
|
|
|
|
|
|
|
|
|
|
|
// --- Try 2: WotLK format (durationMod+durationModNext+conditional+pointCount) ---
|
|
|
|
|
|
if (!splineParsed) {
|
|
|
|
|
|
packet.setReadPos(afterSplineId);
|
|
|
|
|
|
bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext
|
|
|
|
|
|
if (wotlkOk) {
|
|
|
|
|
|
/*float durationMod =*/ packet.readFloat();
|
|
|
|
|
|
/*float durationModNext =*/ packet.readFloat();
|
|
|
|
|
|
if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION
|
|
|
|
|
|
if (!bytesAvailable(5)) { wotlkOk = false; }
|
|
|
|
|
|
else { packet.readUInt8(); packet.readUInt32(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (wotlkOk && (splineFlags & 0x00000800)) { // SPLINEFLAG_PARABOLIC
|
|
|
|
|
|
if (!bytesAvailable(8)) { wotlkOk = false; }
|
|
|
|
|
|
else { packet.readFloat(); packet.readUInt32(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (wotlkOk) {
|
|
|
|
|
|
splineParsed = tryParseUncompressedSpline("wotlk");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Try 3: Compact layout (compressed points) as final recovery ---
|
|
|
|
|
|
if (!splineParsed) {
|
2026-02-22 08:27:17 -08:00
|
|
|
|
packet.setReadPos(legacyStart);
|
|
|
|
|
|
const size_t afterFinalFacingPos = packet.getReadPos();
|
|
|
|
|
|
if (splineFlags & 0x00400000) { // Animation
|
|
|
|
|
|
if (!bytesAvailable(5)) return false;
|
|
|
|
|
|
/*uint8_t animType =*/ packet.readUInt8();
|
|
|
|
|
|
/*uint32_t animStart =*/ packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!bytesAvailable(4)) return false;
|
|
|
|
|
|
/*uint32_t duration =*/ packet.readUInt32();
|
|
|
|
|
|
if (splineFlags & 0x00000800) { // Parabolic
|
|
|
|
|
|
if (!bytesAvailable(8)) return false;
|
|
|
|
|
|
/*float verticalAccel =*/ packet.readFloat();
|
|
|
|
|
|
/*uint32_t effectStartTime =*/ packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!bytesAvailable(4)) return false;
|
|
|
|
|
|
const uint32_t compactPointCount = packet.readUInt32();
|
|
|
|
|
|
if (compactPointCount > 16384) {
|
2026-02-21 04:05:53 -08:00
|
|
|
|
static uint32_t badSplineCount = 0;
|
|
|
|
|
|
++badSplineCount;
|
|
|
|
|
|
if (badSplineCount <= 5 || (badSplineCount % 100) == 0) {
|
2026-03-18 07:23:51 -07:00
|
|
|
|
LOG_WARNING(" Spline invalid (classic+wotlk+compact) at readPos=",
|
2026-02-22 08:27:17 -08:00
|
|
|
|
afterFinalFacingPos, "/", packet.getSize(),
|
|
|
|
|
|
", occurrence=", badSplineCount);
|
2026-02-21 04:05:53 -08:00
|
|
|
|
}
|
2026-02-22 08:27:17 -08:00
|
|
|
|
return false;
|
2026-02-06 15:18:50 -08:00
|
|
|
|
}
|
2026-02-22 08:27:17 -08:00
|
|
|
|
const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0;
|
|
|
|
|
|
size_t compactPayloadBytes = 0;
|
|
|
|
|
|
if (compactPointCount > 0) {
|
|
|
|
|
|
if (uncompressed) {
|
|
|
|
|
|
compactPayloadBytes = static_cast<size_t>(compactPointCount) * 12ull;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
compactPayloadBytes = 12ull;
|
|
|
|
|
|
if (compactPointCount > 1) {
|
|
|
|
|
|
compactPayloadBytes += static_cast<size_t>(compactPointCount - 1) * 4ull;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!bytesAvailable(compactPayloadBytes)) return false;
|
|
|
|
|
|
packet.setReadPos(packet.getReadPos() + compactPayloadBytes);
|
2026-02-05 21:55:52 -08:00
|
|
|
|
}
|
2026-03-18 07:23:51 -07:00
|
|
|
|
} // end compact fallback
|
2026-02-05 21:55:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (updateFlags & UPDATEFLAG_POSITION) {
|
2026-02-11 00:54:38 -08:00
|
|
|
|
// Transport position update (UPDATEFLAG_POSITION = 0x0100)
|
|
|
|
|
|
uint64_t transportGuid = readPackedGuid(packet);
|
2026-02-05 21:55:52 -08:00
|
|
|
|
block.x = packet.readFloat();
|
|
|
|
|
|
block.y = packet.readFloat();
|
|
|
|
|
|
block.z = packet.readFloat();
|
2026-02-11 15:24:05 -08:00
|
|
|
|
block.onTransport = (transportGuid != 0);
|
|
|
|
|
|
block.transportGuid = transportGuid;
|
2026-02-11 21:14:35 -08:00
|
|
|
|
float tx = packet.readFloat();
|
|
|
|
|
|
float ty = packet.readFloat();
|
|
|
|
|
|
float tz = packet.readFloat();
|
|
|
|
|
|
if (block.onTransport) {
|
|
|
|
|
|
block.transportX = tx;
|
|
|
|
|
|
block.transportY = ty;
|
|
|
|
|
|
block.transportZ = tz;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
block.transportX = 0.0f;
|
|
|
|
|
|
block.transportY = 0.0f;
|
|
|
|
|
|
block.transportZ = 0.0f;
|
|
|
|
|
|
}
|
2026-02-05 21:55:52 -08:00
|
|
|
|
block.orientation = packet.readFloat();
|
|
|
|
|
|
/*float corpseOrientation =*/ packet.readFloat();
|
|
|
|
|
|
block.hasMovement = true;
|
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
|
if (block.onTransport) {
|
2026-03-10 04:51:01 -07:00
|
|
|
|
LOG_DEBUG(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec,
|
|
|
|
|
|
" pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation,
|
|
|
|
|
|
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
2026-02-11 21:14:35 -08:00
|
|
|
|
}
|
2026-02-05 21:55:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) {
|
|
|
|
|
|
// Simple stationary position (4 floats)
|
|
|
|
|
|
block.x = packet.readFloat();
|
|
|
|
|
|
block.y = packet.readFloat();
|
|
|
|
|
|
block.z = packet.readFloat();
|
|
|
|
|
|
block.orientation = packet.readFloat();
|
|
|
|
|
|
block.hasMovement = true;
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG(" STATIONARY: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// Target GUID (for units with target)
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
|
|
|
|
|
|
/*uint64_t targetGuid =*/ readPackedGuid(packet);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
|
// Transport time
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
|
|
|
|
|
/*uint32_t transportTime =*/ packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Vehicle
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_VEHICLE) {
|
|
|
|
|
|
/*uint32_t vehicleId =*/ packet.readUInt32();
|
|
|
|
|
|
/*float vehicleOrientation =*/ packet.readFloat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Rotation (GameObjects)
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_ROTATION) {
|
|
|
|
|
|
/*int64_t rotation =*/ packet.readUInt64();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Low GUID
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_LOWGUID) {
|
|
|
|
|
|
/*uint32_t lowGuid =*/ packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// High GUID
|
|
|
|
|
|
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
|
|
|
|
|
/*uint32_t highGuid =*/ packet.readUInt32();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
|
2026-02-10 01:24:37 -08:00
|
|
|
|
size_t startPos = packet.getReadPos();
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Read number of blocks (each block is 32 fields = 32 bits)
|
|
|
|
|
|
uint8_t blockCount = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (blockCount == 0) {
|
|
|
|
|
|
return true; // No fields to update
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
uint32_t fieldsCapacity = blockCount * 32;
|
2026-02-11 21:14:35 -08:00
|
|
|
|
LOG_DEBUG(" UPDATE MASK PARSE:");
|
|
|
|
|
|
LOG_DEBUG(" maskBlockCount = ", (int)blockCount);
|
|
|
|
|
|
LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-22 08:30:18 -08:00
|
|
|
|
// Read update mask into a reused scratch buffer to avoid per-block allocations.
|
|
|
|
|
|
static thread_local std::vector<uint32_t> updateMask;
|
|
|
|
|
|
updateMask.resize(blockCount);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (int i = 0; i < blockCount; ++i) {
|
2026-03-11 14:41:25 -07:00
|
|
|
|
// Validate 4 bytes available before each block read
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
2026-03-15 03:40:58 -07:00
|
|
|
|
LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i,
|
|
|
|
|
|
" type=", updateTypeName(block.updateType),
|
|
|
|
|
|
" objectType=", static_cast<int>(block.objectType),
|
|
|
|
|
|
" guid=0x", std::hex, block.guid, std::dec,
|
|
|
|
|
|
" readPos=", packet.getReadPos(),
|
|
|
|
|
|
" size=", packet.getSize(),
|
|
|
|
|
|
" maskBlockCount=", static_cast<int>(blockCount));
|
2026-03-11 14:41:25 -07:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
updateMask[i] = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Find highest set bit
|
|
|
|
|
|
uint16_t highestSetBit = 0;
|
|
|
|
|
|
uint32_t valuesReadCount = 0;
|
|
|
|
|
|
|
2026-02-22 08:30:18 -08:00
|
|
|
|
// Read only set bits in each mask block (faster than scanning all 32 bits).
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
|
|
|
|
|
|
uint32_t mask = updateMask[blockIdx];
|
2026-02-22 08:30:18 -08:00
|
|
|
|
while (mask != 0) {
|
|
|
|
|
|
const uint16_t fieldIndex =
|
|
|
|
|
|
#if defined(__GNUC__) || defined(__clang__)
|
|
|
|
|
|
static_cast<uint16_t>(blockIdx * 32 + __builtin_ctz(mask));
|
|
|
|
|
|
#else
|
|
|
|
|
|
static_cast<uint16_t>(blockIdx * 32 + [] (uint32_t v) -> uint16_t {
|
|
|
|
|
|
uint16_t b = 0;
|
|
|
|
|
|
while ((v & 1u) == 0u) { v >>= 1u; ++b; }
|
|
|
|
|
|
return b;
|
|
|
|
|
|
}(mask));
|
|
|
|
|
|
#endif
|
|
|
|
|
|
if (fieldIndex > highestSetBit) {
|
|
|
|
|
|
highestSetBit = fieldIndex;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-03-11 14:41:25 -07:00
|
|
|
|
// Validate 4 bytes available before reading field value
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
2026-03-15 03:40:58 -07:00
|
|
|
|
LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex,
|
|
|
|
|
|
" type=", updateTypeName(block.updateType),
|
|
|
|
|
|
" objectType=", static_cast<int>(block.objectType),
|
|
|
|
|
|
" guid=0x", std::hex, block.guid, std::dec,
|
|
|
|
|
|
" readPos=", packet.getReadPos(),
|
|
|
|
|
|
" size=", packet.getSize(),
|
|
|
|
|
|
" maskBlockIndex=", blockIdx,
|
|
|
|
|
|
" maskBlock=0x", std::hex, updateMask[blockIdx], std::dec);
|
2026-03-11 14:41:25 -07:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-22 08:30:18 -08:00
|
|
|
|
uint32_t value = packet.readUInt32();
|
2026-02-22 08:37:02 -08:00
|
|
|
|
// fieldIndex is monotonically increasing here, so end() is a good insertion hint.
|
|
|
|
|
|
block.fields.emplace_hint(block.fields.end(), fieldIndex, value);
|
2026-02-22 08:30:18 -08:00
|
|
|
|
valuesReadCount++;
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
|
|
|
|
|
|
mask &= (mask - 1u);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
size_t endPos = packet.getReadPos();
|
|
|
|
|
|
size_t bytesUsed = endPos - startPos;
|
|
|
|
|
|
size_t bytesRemaining = packet.getSize() - endPos;
|
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
|
LOG_DEBUG(" highestSetBitIndex = ", highestSetBit);
|
|
|
|
|
|
LOG_DEBUG(" valuesReadCount = ", valuesReadCount);
|
|
|
|
|
|
LOG_DEBUG(" bytesUsedForFields = ", bytesUsed);
|
|
|
|
|
|
LOG_DEBUG(" bytesRemainingInPacket = ", bytesRemaining);
|
|
|
|
|
|
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) {
|
|
|
|
|
|
// Read update type
|
|
|
|
|
|
uint8_t updateTypeVal = packet.readUInt8();
|
|
|
|
|
|
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
|
|
|
|
|
|
|
|
|
|
|
|
switch (block.updateType) {
|
|
|
|
|
|
case UpdateType::VALUES: {
|
|
|
|
|
|
// Partial update - changed fields only
|
|
|
|
|
|
block.guid = readPackedGuid(packet);
|
|
|
|
|
|
LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec);
|
|
|
|
|
|
|
|
|
|
|
|
return parseUpdateFields(packet, block);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case UpdateType::MOVEMENT: {
|
|
|
|
|
|
// Movement update
|
2026-03-15 03:40:58 -07:00
|
|
|
|
block.guid = packet.readUInt64();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec);
|
|
|
|
|
|
|
|
|
|
|
|
return parseMovementBlock(packet, block);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case UpdateType::CREATE_OBJECT:
|
|
|
|
|
|
case UpdateType::CREATE_OBJECT2: {
|
|
|
|
|
|
// Create new object with full data
|
|
|
|
|
|
block.guid = readPackedGuid(packet);
|
|
|
|
|
|
LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec);
|
|
|
|
|
|
|
|
|
|
|
|
// Read object type
|
|
|
|
|
|
uint8_t objectTypeVal = packet.readUInt8();
|
|
|
|
|
|
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
|
|
|
|
|
LOG_DEBUG(" Object type: ", (int)objectTypeVal);
|
|
|
|
|
|
|
|
|
|
|
|
// Parse movement if present
|
|
|
|
|
|
bool hasMovement = parseMovementBlock(packet, block);
|
|
|
|
|
|
if (!hasMovement) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse update fields
|
|
|
|
|
|
return parseUpdateFields(packet, block);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case UpdateType::OUT_OF_RANGE_OBJECTS: {
|
|
|
|
|
|
// Objects leaving view range - handled differently
|
|
|
|
|
|
LOG_DEBUG(" OUT_OF_RANGE_OBJECTS (skipping in block parser)");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case UpdateType::NEAR_OBJECTS: {
|
|
|
|
|
|
// Objects entering view range - handled differently
|
|
|
|
|
|
LOG_DEBUG(" NEAR_OBJECTS (skipping in block parser)");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
2026-03-15 01:21:23 -07:00
|
|
|
|
// Keep worst-case packet parsing bounded. Extremely large counts are typically
|
|
|
|
|
|
// malformed/desynced and can stall a frame long enough to trigger disconnects.
|
|
|
|
|
|
constexpr uint32_t kMaxReasonableUpdateBlocks = 1024;
|
|
|
|
|
|
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 4096;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Read block count
|
|
|
|
|
|
data.blockCount = packet.readUInt32();
|
2026-02-22 07:26:54 -08:00
|
|
|
|
if (data.blockCount > kMaxReasonableUpdateBlocks) {
|
|
|
|
|
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable blockCount=", data.blockCount,
|
|
|
|
|
|
" packetSize=", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
|
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
|
|
|
|
|
|
LOG_DEBUG(" objectCount = ", data.blockCount);
|
|
|
|
|
|
LOG_DEBUG(" packetSize = ", packet.getSize());
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
2026-03-15 03:40:58 -07:00
|
|
|
|
uint32_t remainingBlockCount = data.blockCount;
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Check for out-of-range objects first
|
|
|
|
|
|
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
|
|
|
|
|
uint8_t firstByte = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
2026-03-15 03:40:58 -07:00
|
|
|
|
if (remainingBlockCount == 0) {
|
|
|
|
|
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: OUT_OF_RANGE_OBJECTS with zero blockCount");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
--remainingBlockCount;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Read out-of-range GUID count
|
|
|
|
|
|
uint32_t count = packet.readUInt32();
|
2026-02-22 07:26:54 -08:00
|
|
|
|
if (count > kMaxReasonableOutOfRangeGuids) {
|
|
|
|
|
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable outOfRange count=", count,
|
|
|
|
|
|
" packetSize=", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
|
|
|
|
uint64_t guid = readPackedGuid(packet);
|
|
|
|
|
|
data.outOfRangeGuids.push_back(guid);
|
|
|
|
|
|
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Done - packet may have more blocks after this
|
|
|
|
|
|
// Reset read position to after the first byte if needed
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Not out-of-range, rewind
|
|
|
|
|
|
packet.setReadPos(packet.getReadPos() - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse update blocks
|
2026-03-15 03:40:58 -07:00
|
|
|
|
data.blockCount = remainingBlockCount;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
data.blocks.reserve(data.blockCount);
|
|
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < data.blockCount; ++i) {
|
|
|
|
|
|
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
|
|
|
|
|
|
|
|
|
|
|
|
UpdateBlock block;
|
|
|
|
|
|
if (!parseUpdateBlock(packet, block)) {
|
2026-02-23 04:32:58 -08:00
|
|
|
|
static int parseBlockErrors = 0;
|
|
|
|
|
|
if (++parseBlockErrors <= 5) {
|
2026-03-10 08:38:39 -07:00
|
|
|
|
LOG_ERROR("Failed to parse update block ", i + 1, " of ", data.blockCount,
|
|
|
|
|
|
" (", i, " blocks parsed successfully before failure)");
|
2026-02-23 04:32:58 -08:00
|
|
|
|
if (parseBlockErrors == 5)
|
|
|
|
|
|
LOG_ERROR("(suppressing further update block parse errors)");
|
|
|
|
|
|
}
|
2026-03-10 08:38:39 -07:00
|
|
|
|
// Cannot reliably re-sync to the next block after a parse failure,
|
|
|
|
|
|
// but still return true so the blocks already parsed are processed.
|
|
|
|
|
|
break;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 08:37:02 -08:00
|
|
|
|
data.blocks.emplace_back(std::move(block));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) {
|
|
|
|
|
|
// SMSG_DESTROY_OBJECT format:
|
|
|
|
|
|
// uint64 guid
|
2026-02-13 18:59:09 -08:00
|
|
|
|
// uint8 isDeath (0 = despawn, 1 = death) — WotLK only; vanilla/TBC omit this
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-13 18:59:09 -08:00
|
|
|
|
if (packet.getSize() < 8) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.guid = packet.readUInt64();
|
2026-02-13 18:59:09 -08:00
|
|
|
|
// WotLK adds isDeath byte; vanilla/TBC packets are exactly 8 bytes
|
|
|
|
|
|
if (packet.getReadPos() < packet.getSize()) {
|
|
|
|
|
|
data.isDeath = (packet.readUInt8() != 0);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.isDeath = false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-15 14:00:41 -08:00
|
|
|
|
LOG_DEBUG("Parsed SMSG_DESTROY_OBJECT:");
|
|
|
|
|
|
LOG_DEBUG(" GUID: 0x", std::hex, data.guid, std::dec);
|
|
|
|
|
|
LOG_DEBUG(" Is death: ", data.isDeath ? "yes" : "no");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet MessageChatPacket::build(ChatType type,
|
|
|
|
|
|
ChatLanguage language,
|
|
|
|
|
|
const std::string& message,
|
|
|
|
|
|
const std::string& target) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_MESSAGECHAT));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Write chat type
|
|
|
|
|
|
packet.writeUInt32(static_cast<uint32_t>(type));
|
|
|
|
|
|
|
|
|
|
|
|
// Write language
|
|
|
|
|
|
packet.writeUInt32(static_cast<uint32_t>(language));
|
|
|
|
|
|
|
|
|
|
|
|
// Write target (for whispers) or channel name
|
|
|
|
|
|
if (type == ChatType::WHISPER) {
|
|
|
|
|
|
packet.writeString(target);
|
|
|
|
|
|
} else if (type == ChatType::CHANNEL) {
|
|
|
|
|
|
packet.writeString(target); // Channel name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write message
|
|
|
|
|
|
packet.writeString(message);
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_MESSAGECHAT packet");
|
|
|
|
|
|
LOG_DEBUG(" Type: ", static_cast<int>(type));
|
|
|
|
|
|
LOG_DEBUG(" Language: ", static_cast<int>(language));
|
|
|
|
|
|
LOG_DEBUG(" Message: ", message);
|
|
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
|
|
|
|
|
|
// SMSG_MESSAGECHAT format (WoW 3.3.5a):
|
|
|
|
|
|
// uint8 type
|
|
|
|
|
|
// uint32 language
|
|
|
|
|
|
// uint64 senderGuid
|
|
|
|
|
|
// uint32 unknown (always 0)
|
|
|
|
|
|
// [type-specific data]
|
|
|
|
|
|
// uint32 messageLength
|
|
|
|
|
|
// string message
|
|
|
|
|
|
// uint8 chatTag
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getSize() < 15) {
|
|
|
|
|
|
LOG_ERROR("SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read chat type
|
|
|
|
|
|
uint8_t typeVal = packet.readUInt8();
|
|
|
|
|
|
data.type = static_cast<ChatType>(typeVal);
|
|
|
|
|
|
|
|
|
|
|
|
// Read language
|
|
|
|
|
|
uint32_t langVal = packet.readUInt32();
|
|
|
|
|
|
data.language = static_cast<ChatLanguage>(langVal);
|
|
|
|
|
|
|
|
|
|
|
|
// Read sender GUID
|
|
|
|
|
|
data.senderGuid = packet.readUInt64();
|
|
|
|
|
|
|
|
|
|
|
|
// Read unknown field
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
|
2026-03-14 06:56:16 -07:00
|
|
|
|
auto tryReadSizedCString = [&](std::string& out, uint32_t maxLen, size_t minTrailingBytes) -> bool {
|
|
|
|
|
|
size_t start = packet.getReadPos();
|
|
|
|
|
|
size_t remaining = packet.getSize() - start;
|
|
|
|
|
|
if (remaining < 4 + minTrailingBytes) return false;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t len = packet.readUInt32();
|
|
|
|
|
|
if (len < 2 || len > maxLen) {
|
|
|
|
|
|
packet.setReadPos(start);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((packet.getSize() - packet.getReadPos()) < (static_cast<size_t>(len) + minTrailingBytes)) {
|
|
|
|
|
|
packet.setReadPos(start);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string tmp;
|
|
|
|
|
|
tmp.resize(len);
|
|
|
|
|
|
for (uint32_t i = 0; i < len; ++i) {
|
|
|
|
|
|
tmp[i] = static_cast<char>(packet.readUInt8());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tmp.empty() || tmp.back() != '\0') {
|
|
|
|
|
|
packet.setReadPos(start);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
tmp.pop_back();
|
|
|
|
|
|
if (tmp.empty()) {
|
|
|
|
|
|
packet.setReadPos(start);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (char c : tmp) {
|
|
|
|
|
|
unsigned char uc = static_cast<unsigned char>(c);
|
|
|
|
|
|
if (uc < 32 || uc > 126) {
|
|
|
|
|
|
packet.setReadPos(start);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
out = std::move(tmp);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Type-specific data
|
2026-02-14 14:37:53 -08:00
|
|
|
|
// WoW 3.3.5 SMSG_MESSAGECHAT format: after senderGuid+unk, most types
|
|
|
|
|
|
// have a receiverGuid (uint64). Some types have extra fields before it.
|
2026-02-02 12:24:50 -08:00
|
|
|
|
switch (data.type) {
|
|
|
|
|
|
case ChatType::MONSTER_SAY:
|
|
|
|
|
|
case ChatType::MONSTER_YELL:
|
2026-03-02 08:31:34 -08:00
|
|
|
|
case ChatType::MONSTER_EMOTE:
|
|
|
|
|
|
case ChatType::MONSTER_WHISPER:
|
|
|
|
|
|
case ChatType::MONSTER_PARTY:
|
|
|
|
|
|
case ChatType::RAID_BOSS_EMOTE:
|
|
|
|
|
|
case ChatType::RAID_BOSS_WHISPER: {
|
|
|
|
|
|
// Read sender name (SizedCString: uint32 len including null + chars)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
uint32_t nameLen = packet.readUInt32();
|
|
|
|
|
|
if (nameLen > 0 && nameLen < 256) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
data.senderName.resize(nameLen);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (uint32_t i = 0; i < nameLen; ++i) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
data.senderName[i] = static_cast<char>(packet.readUInt8());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-03-02 08:31:34 -08:00
|
|
|
|
// Strip trailing null (server includes it in nameLen)
|
|
|
|
|
|
if (!data.senderName.empty() && data.senderName.back() == '\0') {
|
|
|
|
|
|
data.senderName.pop_back();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-03-02 08:31:34 -08:00
|
|
|
|
// Read receiver GUID (NamedGuid: guid + optional name for non-player targets)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
data.receiverGuid = packet.readUInt64();
|
2026-03-02 08:31:34 -08:00
|
|
|
|
if (data.receiverGuid != 0) {
|
|
|
|
|
|
// Non-player, non-pet GUIDs have high type bits set (0xF1xx/0xF0xx range)
|
|
|
|
|
|
uint16_t highGuid = static_cast<uint16_t>(data.receiverGuid >> 48);
|
|
|
|
|
|
bool isPlayer = (highGuid == 0x0000);
|
|
|
|
|
|
bool isPet = ((highGuid & 0xF0FF) == 0xF040) || ((highGuid & 0xF0FF) == 0xF014);
|
|
|
|
|
|
if (!isPlayer && !isPet) {
|
|
|
|
|
|
// Read receiver name (SizedCString)
|
|
|
|
|
|
uint32_t recvNameLen = packet.readUInt32();
|
|
|
|
|
|
if (recvNameLen > 0 && recvNameLen < 256) {
|
|
|
|
|
|
packet.setReadPos(packet.getReadPos() + recvNameLen);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case ChatType::CHANNEL: {
|
2026-02-14 14:37:53 -08:00
|
|
|
|
// Read channel name, then receiver GUID
|
2026-02-02 12:24:50 -08:00
|
|
|
|
data.channelName = packet.readString();
|
2026-02-14 14:37:53 -08:00
|
|
|
|
data.receiverGuid = packet.readUInt64();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case ChatType::ACHIEVEMENT:
|
|
|
|
|
|
case ChatType::GUILD_ACHIEVEMENT: {
|
2026-02-14 14:37:53 -08:00
|
|
|
|
// Read target GUID
|
|
|
|
|
|
data.receiverGuid = packet.readUInt64();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 06:56:16 -07:00
|
|
|
|
case ChatType::WHISPER:
|
|
|
|
|
|
case ChatType::WHISPER_INFORM: {
|
|
|
|
|
|
// Some cores include an explicit sized sender/receiver name for whisper chat.
|
|
|
|
|
|
// Consume it when present so /r has a reliable last whisper sender.
|
|
|
|
|
|
if (data.type == ChatType::WHISPER) {
|
|
|
|
|
|
tryReadSizedCString(data.senderName, 128, 8 + 4 + 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tryReadSizedCString(data.receiverName, 128, 8 + 4 + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.receiverGuid = packet.readUInt64();
|
|
|
|
|
|
|
|
|
|
|
|
// Optional trailing whisper target/source name on some formats.
|
|
|
|
|
|
if (data.type == ChatType::WHISPER && data.receiverName.empty()) {
|
|
|
|
|
|
tryReadSizedCString(data.receiverName, 128, 4 + 1);
|
|
|
|
|
|
} else if (data.type == ChatType::WHISPER_INFORM && data.senderName.empty()) {
|
|
|
|
|
|
tryReadSizedCString(data.senderName, 128, 4 + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 15:32:04 -07:00
|
|
|
|
case ChatType::BG_SYSTEM_NEUTRAL:
|
|
|
|
|
|
case ChatType::BG_SYSTEM_ALLIANCE:
|
|
|
|
|
|
case ChatType::BG_SYSTEM_HORDE:
|
|
|
|
|
|
// BG/Arena system messages — no sender GUID or name field, just message.
|
|
|
|
|
|
// Reclassify as SYSTEM for consistent display.
|
|
|
|
|
|
data.type = ChatType::SYSTEM;
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
default:
|
2026-02-14 14:37:53 -08:00
|
|
|
|
// SAY, GUILD, PARTY, YELL, WHISPER, WHISPER_INFORM, RAID, etc.
|
|
|
|
|
|
// All have receiverGuid (typically senderGuid repeated)
|
|
|
|
|
|
data.receiverGuid = packet.readUInt64();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read message length
|
|
|
|
|
|
uint32_t messageLen = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// Read message
|
|
|
|
|
|
if (messageLen > 0 && messageLen < 8192) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
data.message.resize(messageLen);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (uint32_t i = 0; i < messageLen; ++i) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
data.message[i] = static_cast<char>(packet.readUInt8());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-14 18:27:59 -08:00
|
|
|
|
// Strip trailing null terminator (servers include it in messageLen)
|
|
|
|
|
|
if (!data.message.empty() && data.message.back() == '\0') {
|
|
|
|
|
|
data.message.pop_back();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read chat tag
|
|
|
|
|
|
data.chatTag = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Parsed SMSG_MESSAGECHAT:");
|
|
|
|
|
|
LOG_DEBUG(" Type: ", getChatTypeString(data.type));
|
|
|
|
|
|
LOG_DEBUG(" Language: ", static_cast<int>(data.language));
|
|
|
|
|
|
LOG_DEBUG(" Sender GUID: 0x", std::hex, data.senderGuid, std::dec);
|
|
|
|
|
|
if (!data.senderName.empty()) {
|
|
|
|
|
|
LOG_DEBUG(" Sender name: ", data.senderName);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!data.channelName.empty()) {
|
|
|
|
|
|
LOG_DEBUG(" Channel: ", data.channelName);
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_DEBUG(" Message: ", data.message);
|
|
|
|
|
|
LOG_DEBUG(" Chat tag: 0x", std::hex, (int)data.chatTag, std::dec);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const char* getChatTypeString(ChatType type) {
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case ChatType::SAY: return "SAY";
|
|
|
|
|
|
case ChatType::PARTY: return "PARTY";
|
|
|
|
|
|
case ChatType::RAID: return "RAID";
|
|
|
|
|
|
case ChatType::GUILD: return "GUILD";
|
|
|
|
|
|
case ChatType::OFFICER: return "OFFICER";
|
|
|
|
|
|
case ChatType::YELL: return "YELL";
|
|
|
|
|
|
case ChatType::WHISPER: return "WHISPER";
|
|
|
|
|
|
case ChatType::WHISPER_INFORM: return "WHISPER_INFORM";
|
|
|
|
|
|
case ChatType::EMOTE: return "EMOTE";
|
|
|
|
|
|
case ChatType::TEXT_EMOTE: return "TEXT_EMOTE";
|
|
|
|
|
|
case ChatType::SYSTEM: return "SYSTEM";
|
|
|
|
|
|
case ChatType::MONSTER_SAY: return "MONSTER_SAY";
|
|
|
|
|
|
case ChatType::MONSTER_YELL: return "MONSTER_YELL";
|
|
|
|
|
|
case ChatType::MONSTER_EMOTE: return "MONSTER_EMOTE";
|
|
|
|
|
|
case ChatType::CHANNEL: return "CHANNEL";
|
|
|
|
|
|
case ChatType::CHANNEL_JOIN: return "CHANNEL_JOIN";
|
|
|
|
|
|
case ChatType::CHANNEL_LEAVE: return "CHANNEL_LEAVE";
|
|
|
|
|
|
case ChatType::CHANNEL_LIST: return "CHANNEL_LIST";
|
|
|
|
|
|
case ChatType::CHANNEL_NOTICE: return "CHANNEL_NOTICE";
|
|
|
|
|
|
case ChatType::CHANNEL_NOTICE_USER: return "CHANNEL_NOTICE_USER";
|
|
|
|
|
|
case ChatType::AFK: return "AFK";
|
|
|
|
|
|
case ChatType::DND: return "DND";
|
|
|
|
|
|
case ChatType::IGNORED: return "IGNORED";
|
|
|
|
|
|
case ChatType::SKILL: return "SKILL";
|
|
|
|
|
|
case ChatType::LOOT: return "LOOT";
|
|
|
|
|
|
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
|
|
|
|
|
|
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
|
|
|
|
|
|
case ChatType::RAID_LEADER: return "RAID_LEADER";
|
|
|
|
|
|
case ChatType::RAID_WARNING: return "RAID_WARNING";
|
|
|
|
|
|
case ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
|
|
|
|
|
|
case ChatType::GUILD_ACHIEVEMENT: return "GUILD_ACHIEVEMENT";
|
|
|
|
|
|
default: return "UNKNOWN";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Text Emotes
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet TextEmotePacket::build(uint32_t textEmoteId, uint64_t targetGuid) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_TEXT_EMOTE));
|
|
|
|
|
|
packet.writeUInt32(textEmoteId);
|
|
|
|
|
|
packet.writeUInt32(0); // emoteNum (unused)
|
|
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_TEXT_EMOTE: emoteId=", textEmoteId, " target=0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 01:05:23 -07:00
|
|
|
|
bool TextEmoteParser::parse(network::Packet& packet, TextEmoteData& data, bool legacyFormat) {
|
2026-02-14 14:30:09 -08:00
|
|
|
|
size_t bytesLeft = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (bytesLeft < 20) {
|
|
|
|
|
|
LOG_WARNING("SMSG_TEXT_EMOTE too short: ", bytesLeft, " bytes");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-10 01:05:23 -07:00
|
|
|
|
|
|
|
|
|
|
if (legacyFormat) {
|
|
|
|
|
|
// Classic 1.12 / TBC 2.4.3: textEmoteId(u32) + emoteNum(u32) + senderGuid(u64)
|
|
|
|
|
|
data.textEmoteId = packet.readUInt32();
|
|
|
|
|
|
data.emoteNum = packet.readUInt32();
|
|
|
|
|
|
data.senderGuid = packet.readUInt64();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// WotLK 3.3.5a: senderGuid(u64) + textEmoteId(u32) + emoteNum(u32)
|
|
|
|
|
|
data.senderGuid = packet.readUInt64();
|
|
|
|
|
|
data.textEmoteId = packet.readUInt32();
|
|
|
|
|
|
data.emoteNum = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 14:30:09 -08:00
|
|
|
|
uint32_t nameLen = packet.readUInt32();
|
|
|
|
|
|
if (nameLen > 0 && nameLen <= 256) {
|
|
|
|
|
|
data.targetName = packet.readString();
|
|
|
|
|
|
} else if (nameLen > 0) {
|
2026-03-10 01:05:23 -07:00
|
|
|
|
// Implausible name length — misaligned read
|
2026-02-14 14:30:09 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Channel System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet JoinChannelPacket::build(const std::string& channelName, const std::string& password) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL));
|
|
|
|
|
|
packet.writeUInt32(0); // channelId (unused)
|
|
|
|
|
|
packet.writeUInt8(0); // hasVoice
|
|
|
|
|
|
packet.writeUInt8(0); // joinedByZone
|
|
|
|
|
|
packet.writeString(channelName);
|
|
|
|
|
|
packet.writeString(password);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_JOIN_CHANNEL: channel=", channelName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet LeaveChannelPacket::build(const std::string& channelName) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL));
|
|
|
|
|
|
packet.writeUInt32(0); // channelId (unused)
|
|
|
|
|
|
packet.writeString(channelName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_LEAVE_CHANNEL: channel=", channelName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ChannelNotifyParser::parse(network::Packet& packet, ChannelNotifyData& data) {
|
|
|
|
|
|
size_t bytesLeft = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (bytesLeft < 2) {
|
|
|
|
|
|
LOG_WARNING("SMSG_CHANNEL_NOTIFY too short");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.notifyType = static_cast<ChannelNotifyType>(packet.readUInt8());
|
|
|
|
|
|
data.channelName = packet.readString();
|
|
|
|
|
|
// Some notification types have additional fields (guid, etc.)
|
|
|
|
|
|
bytesLeft = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (bytesLeft >= 8) {
|
|
|
|
|
|
data.senderGuid = packet.readUInt64();
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 1: Foundation — Targeting, Name Queries
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_SELECTION));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_SELECTION: target=0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SetActiveMoverPacket::build(uint64_t guid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTIVE_MOVER));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_ACTIVE_MOVER: guid=0x", std::hex, guid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:37:13 -08:00
|
|
|
|
network::Packet InspectPacket::build(uint64_t targetGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_INSPECT));
|
2026-02-07 12:37:13 -08:00
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_INSPECT: target=0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 23:23:02 -07:00
|
|
|
|
network::Packet QueryInspectAchievementsPacket::build(uint64_t targetGuid) {
|
|
|
|
|
|
// CMSG_QUERY_INSPECT_ACHIEVEMENTS: uint64 targetGuid + uint8 unk (always 0)
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_INSPECT_ACHIEVEMENTS));
|
|
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
packet.writeUInt8(0); // unk / achievementSlot — always 0 for WotLK
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_QUERY_INSPECT_ACHIEVEMENTS: target=0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Server Info Commands
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet QueryTimePacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_TIME));
|
2026-02-07 12:43:32 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_QUERY_TIME");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool QueryTimeResponseParser::parse(network::Packet& packet, QueryTimeResponseData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: serverTime(4) + timeOffset(4)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
|
|
|
|
|
LOG_WARNING("SMSG_QUERY_TIME_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
|
data.serverTime = packet.readUInt32();
|
|
|
|
|
|
data.timeOffset = packet.readUInt32();
|
|
|
|
|
|
LOG_DEBUG("Parsed SMSG_QUERY_TIME_RESPONSE: time=", data.serverTime, " offset=", data.timeOffset);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet RequestPlayedTimePacket::build(bool sendToChat) {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PLAYED_TIME));
|
2026-02-07 12:43:32 -08:00
|
|
|
|
packet.writeUInt8(sendToChat ? 1 : 0);
|
2026-02-20 02:50:59 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_PLAYED_TIME: sendToChat=", sendToChat);
|
2026-02-07 12:43:32 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool PlayedTimeParser::parse(network::Packet& packet, PlayedTimeData& data) {
|
2026-03-15 06:13:36 -07:00
|
|
|
|
// Classic/Turtle may omit the trailing trigger-message byte and send only
|
|
|
|
|
|
// totalTime(4) + levelTime(4). Later expansions append triggerMsg(1).
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
LOG_WARNING("SMSG_PLAYED_TIME: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:43:32 -08:00
|
|
|
|
data.totalTimePlayed = packet.readUInt32();
|
|
|
|
|
|
data.levelTimePlayed = packet.readUInt32();
|
2026-03-15 06:13:36 -07:00
|
|
|
|
data.triggerMessage = (packet.getSize() - packet.getReadPos() >= 1) && (packet.readUInt8() != 0);
|
2026-02-07 12:43:32 -08:00
|
|
|
|
LOG_DEBUG("Parsed SMSG_PLAYED_TIME: total=", data.totalTimePlayed, " level=", data.levelTimePlayed);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel,
|
|
|
|
|
|
const std::string& playerName,
|
|
|
|
|
|
const std::string& guildName,
|
|
|
|
|
|
uint32_t raceMask, uint32_t classMask,
|
|
|
|
|
|
uint32_t zones) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_WHO));
|
2026-02-07 12:43:32 -08:00
|
|
|
|
packet.writeUInt32(minLevel);
|
|
|
|
|
|
packet.writeUInt32(maxLevel);
|
|
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
packet.writeString(guildName);
|
|
|
|
|
|
packet.writeUInt32(raceMask);
|
|
|
|
|
|
packet.writeUInt32(classMask);
|
2026-02-14 15:05:18 -08:00
|
|
|
|
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
|
2026-02-07 12:43:32 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_WHO: player=", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Social Commands
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AddFriendPacket::build(const std::string& playerName, const std::string& note) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_FRIEND));
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
packet.writeString(note);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_ADD_FRIEND: player=", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet DelFriendPacket::build(uint64_t friendGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_DEL_FRIEND));
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
packet.writeUInt64(friendGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_DEL_FRIEND: guid=0x", std::hex, friendGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SetContactNotesPacket::build(uint64_t friendGuid, const std::string& note) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_CONTACT_NOTES));
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
packet.writeUInt64(friendGuid);
|
|
|
|
|
|
packet.writeString(note);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_CONTACT_NOTES: guid=0x", std::hex, friendGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: status(1) + guid(8)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 9) {
|
|
|
|
|
|
LOG_WARNING("SMSG_FRIEND_STATUS: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
data.status = packet.readUInt8();
|
|
|
|
|
|
data.guid = packet.readUInt64();
|
|
|
|
|
|
if (data.status == 1) { // Online
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Conditional: note (string) + chatFlag (1)
|
|
|
|
|
|
if (packet.getReadPos() < packet.getSize()) {
|
|
|
|
|
|
data.note = packet.readString();
|
|
|
|
|
|
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
|
|
|
|
|
data.chatFlag = packet.readUInt8();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
LOG_DEBUG("Parsed SMSG_FRIEND_STATUS: status=", (int)data.status, " guid=0x", std::hex, data.guid, std::dec);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:58:11 -08:00
|
|
|
|
network::Packet AddIgnorePacket::build(const std::string& playerName) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_IGNORE));
|
2026-02-07 12:58:11 -08:00
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_ADD_IGNORE: player=", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_DEL_IGNORE));
|
2026-02-07 12:58:11 -08:00
|
|
|
|
packet.writeUInt64(ignoreGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_DEL_IGNORE: guid=0x", std::hex, ignoreGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Logout Commands
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet LogoutRequestPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_REQUEST));
|
2026-02-07 12:58:11 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_LOGOUT_REQUEST");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet LogoutCancelPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_CANCEL));
|
2026-02-07 12:58:11 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_LOGOUT_CANCEL");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: result(4) + instant(1)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 5) {
|
|
|
|
|
|
LOG_WARNING("SMSG_LOGOUT_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 12:58:11 -08:00
|
|
|
|
data.result = packet.readUInt32();
|
|
|
|
|
|
data.instant = packet.readUInt8();
|
|
|
|
|
|
LOG_DEBUG("Parsed SMSG_LOGOUT_RESPONSE: result=", data.result, " instant=", (int)data.instant);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Stand State
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet StandStateChangePacket::build(uint8_t state) {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_STANDSTATECHANGE));
|
2026-02-07 12:58:11 -08:00
|
|
|
|
packet.writeUInt32(state);
|
2026-02-20 02:50:59 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_STANDSTATECHANGE: state=", (int)state);
|
2026-02-07 12:58:11 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 04:25:05 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Action Bar
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SetActionButtonPacket::build(uint8_t button, uint8_t type, uint32_t id, bool isClassic) {
|
|
|
|
|
|
// Classic/Turtle (1.12): uint8 button + uint16 id + uint8 type + uint8 misc(0)
|
|
|
|
|
|
// type encoding: 0=spell, 1=item, 64=macro
|
|
|
|
|
|
// TBC/WotLK: uint8 button + uint32 packed (type<<24 | id)
|
|
|
|
|
|
// type encoding: 0x00=spell, 0x80=item, 0x40=macro
|
|
|
|
|
|
// packed=0 means clear the slot
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTION_BUTTON));
|
|
|
|
|
|
packet.writeUInt8(button);
|
|
|
|
|
|
if (isClassic) {
|
|
|
|
|
|
// Classic: 16-bit id, 8-bit type code, 8-bit misc
|
|
|
|
|
|
// Map ActionBarSlot::Type (0=EMPTY,1=SPELL,2=ITEM,3=MACRO) → classic type byte
|
|
|
|
|
|
uint8_t classicType = 0; // 0 = spell
|
|
|
|
|
|
if (type == 2 /* ITEM */) classicType = 1;
|
|
|
|
|
|
if (type == 3 /* MACRO */) classicType = 64;
|
|
|
|
|
|
packet.writeUInt16(static_cast<uint16_t>(id));
|
|
|
|
|
|
packet.writeUInt8(classicType);
|
|
|
|
|
|
packet.writeUInt8(0); // misc
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (Classic): button=", (int)button,
|
|
|
|
|
|
" id=", id, " type=", (int)classicType);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// TBC/WotLK: type in bits 24–31, id in bits 0–23; packed=0 clears slot
|
|
|
|
|
|
uint8_t packedType = 0x00; // spell
|
|
|
|
|
|
if (type == 2 /* ITEM */) packedType = 0x80;
|
|
|
|
|
|
if (type == 3 /* MACRO */) packedType = 0x40;
|
|
|
|
|
|
uint32_t packed = (id == 0) ? 0 : (static_cast<uint32_t>(packedType) << 24) | (id & 0x00FFFFFF);
|
|
|
|
|
|
packet.writeUInt32(packed);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (TBC/WotLK): button=", (int)button,
|
|
|
|
|
|
" packed=0x", std::hex, packed, std::dec);
|
|
|
|
|
|
}
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:03:21 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Display Toggles
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ShowingHelmPacket::build(bool show) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_HELM));
|
2026-02-07 13:03:21 -08:00
|
|
|
|
packet.writeUInt8(show ? 1 : 0);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SHOWING_HELM: show=", show);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ShowingCloakPacket::build(bool show) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_CLOAK));
|
2026-02-07 13:03:21 -08:00
|
|
|
|
packet.writeUInt8(show ? 1 : 0);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SHOWING_CLOAK: show=", show);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// PvP
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet TogglePvpPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_TOGGLE_PVP));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_TOGGLE_PVP");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Guild Commands
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildInfoPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INFO));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_INFO");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildRosterPacket::build() {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ROSTER));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_ROSTER");
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildMotdPacket::build(const std::string& motd) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_MOTD));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
packet.writeString(motd);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_MOTD: ", motd);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildPromotePacket::build(const std::string& playerName) {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_PROMOTE));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
packet.writeString(playerName);
|
2026-02-20 02:50:59 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_PROMOTE: ", playerName);
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildDemotePacket::build(const std::string& playerName) {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DEMOTE));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
packet.writeString(playerName);
|
2026-02-20 02:50:59 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_DEMOTE: ", playerName);
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildLeavePacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_LEAVE));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_LEAVE");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildInvitePacket::build(const std::string& playerName) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INVITE));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_INVITE: ", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
network::Packet GuildQueryPacket::build(uint32_t guildId) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_QUERY));
|
|
|
|
|
|
packet.writeUInt32(guildId);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_QUERY: guildId=", guildId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildRemovePacket::build(const std::string& playerName) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_REMOVE));
|
|
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_REMOVE: ", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 20:16:14 -08:00
|
|
|
|
network::Packet GuildDisbandPacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DISBAND));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_DISBAND");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildLeaderPacket::build(const std::string& playerName) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_LEADER));
|
|
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_LEADER: ", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildSetPublicNotePacket::build(const std::string& playerName, const std::string& note) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_SET_PUBLIC_NOTE));
|
|
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
packet.writeString(note);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_SET_PUBLIC_NOTE: ", playerName, " -> ", note);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildSetOfficerNotePacket::build(const std::string& playerName, const std::string& note) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_SET_OFFICER_NOTE));
|
|
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
packet.writeString(note);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_SET_OFFICER_NOTE: ", playerName, " -> ", note);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
network::Packet GuildAcceptPacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ACCEPT));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_ACCEPT");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildDeclineInvitationPacket::build() {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DECLINE));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_DECLINE");
|
2026-02-13 21:39:48 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
network::Packet GuildCreatePacket::build(const std::string& guildName) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_CREATE));
|
|
|
|
|
|
packet.writeString(guildName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_CREATE: ", guildName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildAddRankPacket::build(const std::string& rankName) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ADD_RANK));
|
|
|
|
|
|
packet.writeString(rankName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_ADD_RANK: ", rankName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildDelRankPacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DEL_RANK));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GUILD_DEL_RANK");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet PetitionShowlistPacket::build(uint64_t npcGuid) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PETITION_SHOWLIST));
|
|
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_PETITION_SHOWLIST: guid=", npcGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet PetitionBuyPacket::build(uint64_t npcGuid, const std::string& guildName) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PETITION_BUY));
|
|
|
|
|
|
packet.writeUInt64(npcGuid); // NPC GUID
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
packet.writeUInt64(0); // unk
|
|
|
|
|
|
packet.writeString(guildName); // guild name
|
|
|
|
|
|
packet.writeUInt32(0); // body text (empty)
|
|
|
|
|
|
packet.writeUInt32(0); // min sigs
|
|
|
|
|
|
packet.writeUInt32(0); // max sigs
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
packet.writeUInt16(0); // unk
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
packet.writeUInt32(0); // unk index
|
|
|
|
|
|
packet.writeUInt32(0); // unk
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_PETITION_BUY: npcGuid=", npcGuid, " name=", guildName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool PetitionShowlistParser::parse(network::Packet& packet, PetitionShowlistData& data) {
|
|
|
|
|
|
if (packet.getSize() < 12) {
|
|
|
|
|
|
LOG_ERROR("SMSG_PETITION_SHOWLIST too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.npcGuid = packet.readUInt64();
|
|
|
|
|
|
uint32_t count = packet.readUInt32();
|
|
|
|
|
|
if (count > 0) {
|
|
|
|
|
|
data.itemId = packet.readUInt32();
|
|
|
|
|
|
data.displayId = packet.readUInt32();
|
|
|
|
|
|
data.cost = packet.readUInt32();
|
|
|
|
|
|
// Skip unused fields if present
|
|
|
|
|
|
if ((packet.getSize() - packet.getReadPos()) >= 8) {
|
|
|
|
|
|
data.charterType = packet.readUInt32();
|
|
|
|
|
|
data.requiredSigs = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_PETITION_SHOWLIST: npcGuid=", data.npcGuid, " cost=", data.cost);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TurnInPetitionResultsParser::parse(network::Packet& packet, uint32_t& result) {
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_TURN_IN_PETITION_RESULTS too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
result = packet.readUInt32();
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_TURN_IN_PETITION_RESULTS: result=", result);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponseData& data) {
|
|
|
|
|
|
if (packet.getSize() < 8) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GUILD_QUERY_RESPONSE too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.guildId = packet.readUInt32();
|
2026-03-11 14:46:44 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate before reading guild name
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildQueryResponseParser: truncated before guild name");
|
|
|
|
|
|
data.guildName.clear();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
data.guildName = packet.readString();
|
2026-03-11 14:46:44 -07:00
|
|
|
|
|
|
|
|
|
|
// Read 10 rank names with validation
|
2026-02-13 21:39:48 -08:00
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
2026-03-11 14:46:44 -07:00
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildQueryResponseParser: truncated at rank name ", i);
|
|
|
|
|
|
data.rankNames[i].clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.rankNames[i] = packet.readString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate before reading emblem fields (5 uint32s = 20 bytes)
|
|
|
|
|
|
if (packet.getReadPos() + 20 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildQueryResponseParser: truncated before emblem data");
|
|
|
|
|
|
data.emblemStyle = 0;
|
|
|
|
|
|
data.emblemColor = 0;
|
|
|
|
|
|
data.borderStyle = 0;
|
|
|
|
|
|
data.borderColor = 0;
|
|
|
|
|
|
data.backgroundColor = 0;
|
|
|
|
|
|
return true;
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
2026-03-11 14:46:44 -07:00
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
data.emblemStyle = packet.readUInt32();
|
|
|
|
|
|
data.emblemColor = packet.readUInt32();
|
|
|
|
|
|
data.borderStyle = packet.readUInt32();
|
|
|
|
|
|
data.borderColor = packet.readUInt32();
|
|
|
|
|
|
data.backgroundColor = packet.readUInt32();
|
2026-03-11 14:46:44 -07:00
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
if ((packet.getSize() - packet.getReadPos()) >= 4) {
|
|
|
|
|
|
data.rankCount = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_GUILD_QUERY_RESPONSE: guild=", data.guildName, " id=", data.guildId);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GuildInfoParser::parse(network::Packet& packet, GuildInfoData& data) {
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GUILD_INFO too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.guildName = packet.readString();
|
|
|
|
|
|
data.creationDay = packet.readUInt32();
|
|
|
|
|
|
data.creationMonth = packet.readUInt32();
|
|
|
|
|
|
data.creationYear = packet.readUInt32();
|
|
|
|
|
|
data.numMembers = packet.readUInt32();
|
|
|
|
|
|
data.numAccounts = packet.readUInt32();
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_GUILD_INFO: ", data.guildName, " members=", data.numMembers);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) {
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GUILD_ROSTER too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint32_t numMembers = packet.readUInt32();
|
2026-03-11 14:42:09 -07:00
|
|
|
|
|
|
|
|
|
|
// Cap members and ranks to prevent unbounded memory allocation
|
|
|
|
|
|
const uint32_t MAX_GUILD_MEMBERS = 1000;
|
|
|
|
|
|
if (numMembers > MAX_GUILD_MEMBERS) {
|
|
|
|
|
|
LOG_WARNING("GuildRosterParser: numMembers capped (requested=", numMembers, ")");
|
|
|
|
|
|
numMembers = MAX_GUILD_MEMBERS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
data.motd = packet.readString();
|
|
|
|
|
|
data.guildInfo = packet.readString();
|
|
|
|
|
|
|
2026-03-11 14:42:09 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildRosterParser: truncated before rankCount");
|
|
|
|
|
|
data.ranks.clear();
|
|
|
|
|
|
data.members.clear();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
uint32_t rankCount = packet.readUInt32();
|
2026-03-11 14:42:09 -07:00
|
|
|
|
|
|
|
|
|
|
// Cap rank count to prevent unbounded allocation
|
|
|
|
|
|
const uint32_t MAX_GUILD_RANKS = 20;
|
|
|
|
|
|
if (rankCount > MAX_GUILD_RANKS) {
|
|
|
|
|
|
LOG_WARNING("GuildRosterParser: rankCount capped (requested=", rankCount, ")");
|
|
|
|
|
|
rankCount = MAX_GUILD_RANKS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:39:48 -08:00
|
|
|
|
data.ranks.resize(rankCount);
|
|
|
|
|
|
for (uint32_t i = 0; i < rankCount; ++i) {
|
2026-03-11 14:42:09 -07:00
|
|
|
|
// Validate 4 bytes before each rank rights read
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildRosterParser: truncated rank at index ", i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
data.ranks[i].rights = packet.readUInt32();
|
2026-03-11 14:42:09 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
data.ranks[i].goldLimit = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.ranks[i].goldLimit = packet.readUInt32();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
// 6 bank tab flags + 6 bank tab items per day
|
|
|
|
|
|
for (int t = 0; t < 6; ++t) {
|
2026-03-11 14:42:09 -07:00
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) break;
|
2026-02-13 21:39:48 -08:00
|
|
|
|
packet.readUInt32(); // tabFlags
|
|
|
|
|
|
packet.readUInt32(); // tabItemsPerDay
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.members.resize(numMembers);
|
|
|
|
|
|
for (uint32_t i = 0; i < numMembers; ++i) {
|
2026-03-11 14:42:09 -07:00
|
|
|
|
// Validate minimum bytes before reading member (guid+online+name at minimum is 9+ bytes)
|
|
|
|
|
|
if (packet.getReadPos() + 9 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildRosterParser: truncated member at index ", i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
auto& m = data.members[i];
|
|
|
|
|
|
m.guid = packet.readUInt64();
|
|
|
|
|
|
m.online = (packet.readUInt8() != 0);
|
2026-03-11 14:42:09 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate before reading name string
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
m.name.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.name = packet.readString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate before reading rank/level/class/gender/zone
|
|
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
m.rankIndex = 0;
|
|
|
|
|
|
m.level = 1;
|
|
|
|
|
|
m.classId = 0;
|
|
|
|
|
|
m.gender = 0;
|
|
|
|
|
|
m.zoneId = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.rankIndex = packet.readUInt32();
|
|
|
|
|
|
if (packet.getReadPos() + 3 > packet.getSize()) {
|
|
|
|
|
|
m.level = 1;
|
|
|
|
|
|
m.classId = 0;
|
|
|
|
|
|
m.gender = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.level = packet.readUInt8();
|
|
|
|
|
|
m.classId = packet.readUInt8();
|
|
|
|
|
|
m.gender = packet.readUInt8();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
m.zoneId = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.zoneId = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Online status affects next fields
|
2026-02-13 21:39:48 -08:00
|
|
|
|
if (!m.online) {
|
2026-03-11 14:42:09 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
m.lastOnline = 0.0f;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.lastOnline = packet.readFloat();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read notes
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
m.publicNote.clear();
|
|
|
|
|
|
m.officerNote.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.publicNote = packet.readString();
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
m.officerNote.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
m.officerNote = packet.readString();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_GUILD_ROSTER: ", numMembers, " members, motd=", data.motd);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GuildEventParser::parse(network::Packet& packet, GuildEventData& data) {
|
|
|
|
|
|
if (packet.getSize() < 2) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GUILD_EVENT too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.eventType = packet.readUInt8();
|
|
|
|
|
|
data.numStrings = packet.readUInt8();
|
|
|
|
|
|
for (uint8_t i = 0; i < data.numStrings && i < 3; ++i) {
|
|
|
|
|
|
data.strings[i] = packet.readString();
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((packet.getSize() - packet.getReadPos()) >= 8) {
|
|
|
|
|
|
data.guid = packet.readUInt64();
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_GUILD_EVENT: type=", (int)data.eventType, " strings=", (int)data.numStrings);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GuildInviteResponseParser::parse(network::Packet& packet, GuildInviteResponseData& data) {
|
|
|
|
|
|
if (packet.getSize() < 2) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GUILD_INVITE too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.inviterName = packet.readString();
|
|
|
|
|
|
data.guildName = packet.readString();
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_GUILD_INVITE: from=", data.inviterName, " guild=", data.guildName);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GuildCommandResultParser::parse(network::Packet& packet, GuildCommandResultData& data) {
|
|
|
|
|
|
if (packet.getSize() < 8) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GUILD_COMMAND_RESULT too small: ", packet.getSize());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.command = packet.readUInt32();
|
|
|
|
|
|
data.name = packet.readString();
|
|
|
|
|
|
data.errorCode = packet.readUInt32();
|
|
|
|
|
|
LOG_INFO("Parsed SMSG_GUILD_COMMAND_RESULT: cmd=", data.command, " error=", data.errorCode);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Ready Check
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ReadyCheckPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::MSG_RAID_READY_CHECK));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
LOG_DEBUG("Built MSG_RAID_READY_CHECK");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ReadyCheckConfirmPacket::build(bool ready) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::MSG_RAID_READY_CHECK_CONFIRM));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
packet.writeUInt8(ready ? 1 : 0);
|
|
|
|
|
|
LOG_DEBUG("Built MSG_RAID_READY_CHECK_CONFIRM: ready=", ready);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Duel
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-09 13:58:02 -07:00
|
|
|
|
network::Packet DuelAcceptPacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_ACCEPTED));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_DUEL_ACCEPTED");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
network::Packet DuelCancelPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_DUEL_CANCELLED");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:28:46 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Party/Raid Management
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GroupUninvitePacket::build(const std::string& playerName) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_UNINVITE_GUID));
|
2026-02-07 13:28:46 -08:00
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GROUP_UNINVITE_GUID for player: ", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GroupDisbandPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DISBAND));
|
2026-02-07 13:28:46 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_GROUP_DISBAND");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targetGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::MSG_RAID_TARGET_UPDATE));
|
2026-02-07 13:28:46 -08:00
|
|
|
|
packet.writeUInt8(targetIndex);
|
|
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", (uint32_t)targetIndex, ", guid: 0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet RequestRaidInfoPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_REQUEST_RAID_INFO));
|
2026-02-07 13:28:46 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_REQUEST_RAID_INFO");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:36:50 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Combat and Trade
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet DuelProposedPacket::build(uint64_t targetGuid) {
|
2026-03-09 16:30:42 -07:00
|
|
|
|
// Duels are initiated via CMSG_CAST_SPELL with spell 7266 (Duel) targeted at the opponent.
|
|
|
|
|
|
// There is no separate CMSG_DUEL_PROPOSED opcode in WoW.
|
2026-02-20 01:13:01 -08:00
|
|
|
|
auto packet = CastSpellPacket::build(7266, targetGuid, 0);
|
|
|
|
|
|
LOG_DEBUG("Built duel request (spell 7266) for target: 0x", std::hex, targetGuid, std::dec);
|
2026-02-07 13:36:50 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Implement basic trade request/accept/decline flow
- Parse SMSG_TRADE_STATUS for all 20+ status codes: incoming request,
open/cancel/complete/accept notifications, error conditions (too far,
wrong faction, stunned, dead, trial account, etc.)
- SMSG_TRADE_STATUS_EXTENDED consumed via shared handler (no full item
window yet; state tracking sufficient for accept/decline flow)
- Add acceptTradeRequest() (CMSG_BEGIN_TRADE), declineTradeRequest(),
acceptTrade() (CMSG_ACCEPT_TRADE), cancelTrade() (CMSG_CANCEL_TRADE)
- Add BeginTradePacket, CancelTradePacket, AcceptTradePacket builders
- Add renderTradeRequestPopup(): shows "X wants to trade" with
Accept/Decline buttons when tradeStatus_ == PendingIncoming
- TradeStatus enum tracks None/PendingIncoming/Open/Accepted/Complete
2026-03-09 14:05:42 -07:00
|
|
|
|
network::Packet BeginTradePacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_BEGIN_TRADE));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_BEGIN_TRADE");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet CancelTradePacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_TRADE));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CANCEL_TRADE");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AcceptTradePacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ACCEPT_TRADE));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_ACCEPT_TRADE");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 00:44:07 -07:00
|
|
|
|
network::Packet SetTradeItemPacket::build(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_ITEM));
|
|
|
|
|
|
packet.writeUInt8(tradeSlot);
|
|
|
|
|
|
packet.writeUInt8(bag);
|
|
|
|
|
|
packet.writeUInt8(bagSlot);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_TRADE_ITEM slot=", (int)tradeSlot, " bag=", (int)bag, " bagSlot=", (int)bagSlot);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ClearTradeItemPacket::build(uint8_t tradeSlot) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CLEAR_TRADE_ITEM));
|
|
|
|
|
|
packet.writeUInt8(tradeSlot);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CLEAR_TRADE_ITEM slot=", (int)tradeSlot);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SetTradeGoldPacket::build(uint64_t copper) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SET_TRADE_GOLD));
|
|
|
|
|
|
packet.writeUInt64(copper);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_SET_TRADE_GOLD copper=", copper);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet UnacceptTradePacket::build() {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_UNACCEPT_TRADE));
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_UNACCEPT_TRADE");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 13:36:50 -08:00
|
|
|
|
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
|
2026-02-07 13:36:50 -08:00
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_INITIATE_TRADE for target: 0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AttackSwingPacket::build(uint64_t targetGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ATTACKSWING));
|
2026-02-07 13:36:50 -08:00
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_ATTACKSWING for target: 0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AttackStopPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ATTACKSTOP));
|
2026-02-07 13:36:50 -08:00
|
|
|
|
LOG_DEBUG("Built CMSG_ATTACKSTOP");
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet CancelCastPacket::build(uint32_t spellId) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_CAST));
|
2026-02-07 13:36:50 -08:00
|
|
|
|
packet.writeUInt32(0); // cast count/sequence
|
|
|
|
|
|
packet.writeUInt32(spellId);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CANCEL_CAST for spell: ", spellId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Random Roll
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet RandomRollPacket::build(uint32_t minRoll, uint32_t maxRoll) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::MSG_RANDOM_ROLL));
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
packet.writeUInt32(minRoll);
|
|
|
|
|
|
packet.writeUInt32(maxRoll);
|
|
|
|
|
|
LOG_DEBUG("Built MSG_RANDOM_ROLL: ", minRoll, "-", maxRoll);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool RandomRollParser::parse(network::Packet& packet, RandomRollData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: rollerGuid(8) + targetGuid(8) + minRoll(4) + maxRoll(4) + result(4)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 28) {
|
|
|
|
|
|
LOG_WARNING("SMSG_RANDOM_ROLL: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
data.rollerGuid = packet.readUInt64();
|
|
|
|
|
|
data.targetGuid = packet.readUInt64();
|
|
|
|
|
|
data.minRoll = packet.readUInt32();
|
|
|
|
|
|
data.maxRoll = packet.readUInt32();
|
|
|
|
|
|
data.result = packet.readUInt32();
|
|
|
|
|
|
LOG_DEBUG("Parsed SMSG_RANDOM_ROLL: roller=0x", std::hex, data.rollerGuid, std::dec,
|
|
|
|
|
|
" result=", data.result, " (", data.minRoll, "-", data.maxRoll, ")");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
network::Packet NameQueryPacket::build(uint64_t playerGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_NAME_QUERY));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(playerGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_NAME_QUERY: guid=0x", std::hex, playerGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseData& data) {
|
|
|
|
|
|
// 3.3.5a: packedGuid, uint8 found
|
|
|
|
|
|
// If found==0: CString name, CString realmName, uint8 race, uint8 gender, uint8 classId
|
2026-03-11 14:27:39 -07:00
|
|
|
|
// Validation: packed GUID (1-8 bytes) + found flag (1 byte minimum)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 2) return false; // At least 1 for packed GUID + 1 for found
|
|
|
|
|
|
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-11 14:27:39 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate found flag read
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.found = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.found != 0) {
|
|
|
|
|
|
LOG_DEBUG("Name query: player not found for GUID 0x", std::hex, data.guid, std::dec);
|
|
|
|
|
|
return true; // Valid response, just not found
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:27:39 -07:00
|
|
|
|
// Validate strings: need at least 2 null terminators for empty strings
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 2) {
|
|
|
|
|
|
data.name.clear();
|
|
|
|
|
|
data.realmName.clear();
|
|
|
|
|
|
return !data.name.empty(); // Fail if name was required
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.name = packet.readString();
|
|
|
|
|
|
data.realmName = packet.readString();
|
2026-03-11 14:27:39 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate final 3 uint8 fields (race, gender, classId)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 3) {
|
|
|
|
|
|
LOG_WARNING("Name query: truncated fields after realmName, expected 3 uint8s");
|
|
|
|
|
|
data.race = 0;
|
|
|
|
|
|
data.gender = 0;
|
|
|
|
|
|
data.classId = 0;
|
|
|
|
|
|
return !data.name.empty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.race = packet.readUInt8();
|
|
|
|
|
|
data.gender = packet.readUInt8();
|
|
|
|
|
|
data.classId = packet.readUInt8();
|
|
|
|
|
|
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Name query response: ", data.name, " (race=", (int)data.race,
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
" class=", (int)data.classId, ")");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet CreatureQueryPacket::build(uint32_t entry, uint64_t guid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CREATURE_QUERY));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt32(entry);
|
|
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CREATURE_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryResponseData& data) {
|
2026-03-11 14:19:58 -07:00
|
|
|
|
// Validate minimum packet size: entry(4)
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_CREATURE_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.entry = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// High bit set means creature not found
|
|
|
|
|
|
if (data.entry & 0x80000000) {
|
|
|
|
|
|
data.entry &= ~0x80000000;
|
|
|
|
|
|
LOG_DEBUG("Creature query: entry ", data.entry, " not found");
|
|
|
|
|
|
data.name = "";
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4 name strings (only first is usually populated)
|
|
|
|
|
|
data.name = packet.readString();
|
|
|
|
|
|
packet.readString(); // name2
|
|
|
|
|
|
packet.readString(); // name3
|
|
|
|
|
|
packet.readString(); // name4
|
|
|
|
|
|
data.subName = packet.readString();
|
|
|
|
|
|
data.iconName = packet.readString();
|
2026-03-11 14:19:58 -07:00
|
|
|
|
|
|
|
|
|
|
// WotLK: 4 fixed fields after iconName (typeFlags, creatureType, family, rank)
|
|
|
|
|
|
// Validate minimum size for these fields: 4×4 = 16 bytes
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 16) {
|
|
|
|
|
|
LOG_WARNING("SMSG_CREATURE_QUERY_RESPONSE: truncated before typeFlags (entry=", data.entry, ")");
|
|
|
|
|
|
data.typeFlags = 0;
|
|
|
|
|
|
data.creatureType = 0;
|
|
|
|
|
|
data.family = 0;
|
|
|
|
|
|
data.rank = 0;
|
|
|
|
|
|
return true; // Have name/sub/icon; base fields are important but optional
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.typeFlags = packet.readUInt32();
|
|
|
|
|
|
data.creatureType = packet.readUInt32();
|
|
|
|
|
|
data.family = packet.readUInt32();
|
|
|
|
|
|
data.rank = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.)
|
|
|
|
|
|
// We've got what we need for display purposes
|
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
|
LOG_DEBUG("Creature query response: ", data.name, " (type=", data.creatureType,
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
" rank=", data.rank, ")");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 00:59:40 -08:00
|
|
|
|
// ---- GameObject Query ----
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GameObjectQueryPacket::build(uint32_t entry, uint64_t guid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJECT_QUERY));
|
2026-02-08 00:59:40 -08:00
|
|
|
|
packet.writeUInt32(entry);
|
|
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GAMEOBJECT_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQueryResponseData& data) {
|
2026-03-11 14:11:45 -07:00
|
|
|
|
// Validate minimum packet size: entry(4)
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GAMEOBJECT_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 00:59:40 -08:00
|
|
|
|
data.entry = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// High bit set means gameobject not found
|
|
|
|
|
|
if (data.entry & 0x80000000) {
|
|
|
|
|
|
data.entry &= ~0x80000000;
|
|
|
|
|
|
LOG_DEBUG("GameObject query: entry ", data.entry, " not found");
|
|
|
|
|
|
data.name = "";
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:11:45 -07:00
|
|
|
|
// Validate minimum size for fixed fields: type(4) + displayId(4)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
|
|
|
|
|
LOG_ERROR("SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 00:59:40 -08:00
|
|
|
|
data.type = packet.readUInt32(); // GameObjectType
|
2026-02-14 20:20:43 -08:00
|
|
|
|
data.displayId = packet.readUInt32();
|
2026-02-08 00:59:40 -08:00
|
|
|
|
// 4 name strings (only first is usually populated)
|
|
|
|
|
|
data.name = packet.readString();
|
|
|
|
|
|
// name2, name3, name4
|
|
|
|
|
|
packet.readString();
|
|
|
|
|
|
packet.readString();
|
|
|
|
|
|
packet.readString();
|
|
|
|
|
|
|
2026-02-14 20:20:43 -08:00
|
|
|
|
// WotLK: 3 extra strings before data[] (iconName, castBarCaption, unk1)
|
|
|
|
|
|
packet.readString(); // iconName
|
|
|
|
|
|
packet.readString(); // castBarCaption
|
|
|
|
|
|
packet.readString(); // unk1
|
|
|
|
|
|
|
|
|
|
|
|
// Read 24 type-specific data fields
|
|
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (remaining >= 24 * 4) {
|
|
|
|
|
|
for (int i = 0; i < 24; i++) {
|
|
|
|
|
|
data.data[i] = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
data.hasData = true;
|
2026-03-11 14:11:45 -07:00
|
|
|
|
} else if (remaining > 0) {
|
|
|
|
|
|
// Partial data field; read what we can
|
|
|
|
|
|
uint32_t fieldsToRead = remaining / 4;
|
|
|
|
|
|
for (uint32_t i = 0; i < fieldsToRead && i < 24; i++) {
|
|
|
|
|
|
data.data[i] = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fieldsToRead < 24) {
|
|
|
|
|
|
LOG_WARNING("SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated in data fields (", fieldsToRead,
|
|
|
|
|
|
" of 24 read, entry=", data.entry, ")");
|
|
|
|
|
|
}
|
2026-02-14 20:20:43 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
|
LOG_DEBUG("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
|
2026-02-08 00:59:40 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 23:31:30 -08:00
|
|
|
|
network::Packet PageTextQueryPacket::build(uint32_t pageId, uint64_t guid) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PAGE_TEXT_QUERY));
|
|
|
|
|
|
packet.writeUInt32(pageId);
|
|
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool PageTextQueryResponseParser::parse(network::Packet& packet, PageTextQueryResponseData& data) {
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 4) return false;
|
|
|
|
|
|
data.pageId = packet.readUInt32();
|
|
|
|
|
|
data.text = normalizeWowTextTokens(packet.readString());
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
|
|
|
|
|
data.nextPageId = packet.readUInt32();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.nextPageId = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
return data.isValid();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:11:43 -08:00
|
|
|
|
// ---- Item Query ----
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ITEM_QUERY_SINGLE));
|
2026-02-06 03:11:43 -08:00
|
|
|
|
packet.writeUInt32(entry);
|
|
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_ITEM_QUERY_SINGLE: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass) {
|
|
|
|
|
|
if (itemClass == 2) { // Weapon
|
|
|
|
|
|
switch (subClass) {
|
|
|
|
|
|
case 0: return "Axe"; case 1: return "Axe";
|
|
|
|
|
|
case 2: return "Bow"; case 3: return "Gun";
|
|
|
|
|
|
case 4: return "Mace"; case 5: return "Mace";
|
|
|
|
|
|
case 6: return "Polearm"; case 7: return "Sword";
|
|
|
|
|
|
case 8: return "Sword"; case 9: return "Obsolete";
|
|
|
|
|
|
case 10: return "Staff"; case 13: return "Fist Weapon";
|
|
|
|
|
|
case 15: return "Dagger"; case 16: return "Thrown";
|
|
|
|
|
|
case 18: return "Crossbow"; case 19: return "Wand";
|
|
|
|
|
|
case 20: return "Fishing Pole";
|
|
|
|
|
|
default: return "Weapon";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (itemClass == 4) { // Armor
|
|
|
|
|
|
switch (subClass) {
|
|
|
|
|
|
case 0: return "Miscellaneous"; case 1: return "Cloth";
|
|
|
|
|
|
case 2: return "Leather"; case 3: return "Mail";
|
|
|
|
|
|
case 4: return "Plate"; case 6: return "Shield";
|
|
|
|
|
|
default: return "Armor";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseData& data) {
|
2026-03-11 14:08:59 -07:00
|
|
|
|
// Validate minimum packet size: entry(4) + item not found check
|
|
|
|
|
|
if (packet.getSize() < 4) {
|
|
|
|
|
|
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:11:43 -08:00
|
|
|
|
data.entry = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// High bit set means item not found
|
|
|
|
|
|
if (data.entry & 0x80000000) {
|
|
|
|
|
|
data.entry &= ~0x80000000;
|
|
|
|
|
|
LOG_DEBUG("Item query: entry ", data.entry, " not found");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:08:59 -07:00
|
|
|
|
// Validate minimum size for fixed fields before reading: itemClass(4) + subClass(4) + soundOverride(4)
|
|
|
|
|
|
// + 4 name strings + displayInfoId(4) + quality(4) = at least 24 bytes more
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 24) {
|
|
|
|
|
|
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before displayInfoId (entry=", data.entry, ")");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:11:43 -08:00
|
|
|
|
uint32_t itemClass = packet.readUInt32();
|
|
|
|
|
|
uint32_t subClass = packet.readUInt32();
|
2026-02-18 03:46:03 -08:00
|
|
|
|
data.itemClass = itemClass;
|
|
|
|
|
|
data.subClass = subClass;
|
2026-02-06 03:11:43 -08:00
|
|
|
|
packet.readUInt32(); // SoundOverrideSubclass
|
|
|
|
|
|
|
|
|
|
|
|
data.subclassName = getItemSubclassName(itemClass, subClass);
|
|
|
|
|
|
|
|
|
|
|
|
// 4 name strings
|
|
|
|
|
|
data.name = packet.readString();
|
|
|
|
|
|
packet.readString(); // name2
|
|
|
|
|
|
packet.readString(); // name3
|
|
|
|
|
|
packet.readString(); // name4
|
|
|
|
|
|
|
|
|
|
|
|
data.displayInfoId = packet.readUInt32();
|
|
|
|
|
|
data.quality = packet.readUInt32();
|
|
|
|
|
|
|
2026-02-19 17:54:32 -08:00
|
|
|
|
// WotLK 3.3.5a (TrinityCore/AzerothCore): Flags, Flags2, BuyCount, BuyPrice, SellPrice
|
|
|
|
|
|
// Some server variants omit BuyCount (4 fields instead of 5).
|
|
|
|
|
|
// Read 5 fields and validate InventoryType; if it looks implausible, rewind and try 4.
|
|
|
|
|
|
const size_t postQualityPos = packet.getReadPos();
|
2026-03-11 14:08:59 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 24) {
|
|
|
|
|
|
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before flags (entry=", data.entry, ")");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-13 11:32:32 -07:00
|
|
|
|
data.itemFlags = packet.readUInt32(); // Flags
|
2026-02-06 03:11:43 -08:00
|
|
|
|
packet.readUInt32(); // Flags2
|
2026-02-19 17:54:32 -08:00
|
|
|
|
packet.readUInt32(); // BuyCount
|
2026-02-06 03:11:43 -08:00
|
|
|
|
packet.readUInt32(); // BuyPrice
|
2026-02-06 13:47:03 -08:00
|
|
|
|
data.sellPrice = packet.readUInt32(); // SellPrice
|
2026-02-06 03:11:43 -08:00
|
|
|
|
data.inventoryType = packet.readUInt32();
|
|
|
|
|
|
|
2026-02-19 17:54:32 -08:00
|
|
|
|
if (data.inventoryType > 28) {
|
|
|
|
|
|
// inventoryType out of range — BuyCount probably not present; rewind and try 4 fields
|
|
|
|
|
|
packet.setReadPos(postQualityPos);
|
2026-03-13 11:32:32 -07:00
|
|
|
|
data.itemFlags = packet.readUInt32(); // Flags
|
2026-02-19 17:54:32 -08:00
|
|
|
|
packet.readUInt32(); // Flags2
|
|
|
|
|
|
packet.readUInt32(); // BuyPrice
|
|
|
|
|
|
data.sellPrice = packet.readUInt32(); // SellPrice
|
|
|
|
|
|
data.inventoryType = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:08:59 -07:00
|
|
|
|
// Validate minimum size for remaining fixed fields before inventoryType through containerSlots: 13×4 = 52 bytes
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 52) {
|
|
|
|
|
|
LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-13 11:11:33 -07:00
|
|
|
|
data.allowableClass = packet.readUInt32(); // AllowableClass
|
|
|
|
|
|
data.allowableRace = packet.readUInt32(); // AllowableRace
|
2026-03-10 16:26:20 -07:00
|
|
|
|
data.itemLevel = packet.readUInt32();
|
|
|
|
|
|
data.requiredLevel = packet.readUInt32();
|
2026-03-13 11:11:33 -07:00
|
|
|
|
data.requiredSkill = packet.readUInt32(); // RequiredSkill
|
|
|
|
|
|
data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank
|
2026-02-06 03:11:43 -08:00
|
|
|
|
packet.readUInt32(); // RequiredSpell
|
|
|
|
|
|
packet.readUInt32(); // RequiredHonorRank
|
|
|
|
|
|
packet.readUInt32(); // RequiredCityRank
|
2026-03-13 11:11:33 -07:00
|
|
|
|
data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction
|
|
|
|
|
|
data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank
|
2026-03-13 11:32:32 -07:00
|
|
|
|
data.maxCount = static_cast<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
|
2026-02-06 03:11:43 -08:00
|
|
|
|
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
|
|
|
|
|
data.containerSlots = packet.readUInt32();
|
|
|
|
|
|
|
2026-03-11 14:08:59 -07:00
|
|
|
|
// Read statsCount with bounds validation
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 4) {
|
|
|
|
|
|
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated at statsCount (entry=", data.entry, ")");
|
|
|
|
|
|
return true; // Have enough for core fields; stats are optional
|
|
|
|
|
|
}
|
2026-02-06 03:11:43 -08:00
|
|
|
|
uint32_t statsCount = packet.readUInt32();
|
2026-03-11 14:08:59 -07:00
|
|
|
|
|
|
|
|
|
|
// Cap statsCount to prevent excessive iteration
|
|
|
|
|
|
constexpr uint32_t kMaxItemStats = 10;
|
|
|
|
|
|
if (statsCount > kMaxItemStats) {
|
|
|
|
|
|
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: statsCount=", statsCount, " exceeds max ",
|
|
|
|
|
|
kMaxItemStats, " (entry=", data.entry, "), capping");
|
|
|
|
|
|
statsCount = kMaxItemStats;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
|
// Server sends exactly statsCount stat pairs (not always 10).
|
|
|
|
|
|
uint32_t statsToRead = std::min(statsCount, 10u);
|
|
|
|
|
|
for (uint32_t i = 0; i < statsToRead; i++) {
|
2026-03-11 14:08:59 -07:00
|
|
|
|
// Each stat is 2 uint32s (type + value) = 8 bytes
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
|
|
|
|
|
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-06 03:11:43 -08:00
|
|
|
|
uint32_t statType = packet.readUInt32();
|
|
|
|
|
|
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
2026-02-26 00:59:07 -08:00
|
|
|
|
switch (statType) {
|
|
|
|
|
|
case 3: data.agility = statValue; break;
|
|
|
|
|
|
case 4: data.strength = statValue; break;
|
|
|
|
|
|
case 5: data.intellect = statValue; break;
|
|
|
|
|
|
case 6: data.spirit = statValue; break;
|
|
|
|
|
|
case 7: data.stamina = statValue; break;
|
feat: capture and display all item stat types in tooltips
Previously only the 5 primary stats (Str/Agi/Sta/Int/Spi) were stored,
discarding hit rating, crit, haste, attack power, spell power, resilience,
expertise, armor penetration, MP5, and many others.
Changes:
- Add ItemDef::ExtraStat and ItemQueryResponseData::ExtraStat arrays
- All three expansion parsers (WotLK/TBC/Classic) now capture non-primary
stat type/value pairs into extraStats instead of silently dropping them
- All 5 rebuildOnlineInventory paths propagate extraStats to ItemDef
- Tooltip now renders each extra stat on its own line with a name lookup
covering all common WotLK stat types (hit, crit, haste, AP, SP, etc.)
- Also fix Classic/TBC bag-content and bank-bag paths that were missing
bindType, description propagation from previous commits
2026-03-10 17:00:24 -07:00
|
|
|
|
default:
|
|
|
|
|
|
if (statValue != 0)
|
|
|
|
|
|
data.extraStats.push_back({statType, statValue});
|
|
|
|
|
|
break;
|
2026-02-06 03:11:43 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:08:59 -07:00
|
|
|
|
// ScalingStatDistribution and ScalingStatValue
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
|
|
|
|
|
LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before scaling stats (entry=", data.entry, ")");
|
|
|
|
|
|
return true; // Have core fields; scaling is optional
|
|
|
|
|
|
}
|
2026-02-06 03:11:43 -08:00
|
|
|
|
packet.readUInt32(); // ScalingStatDistribution
|
|
|
|
|
|
packet.readUInt32(); // ScalingStatValue
|
|
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
|
// WotLK 3.3.5a: MAX_ITEM_PROTO_DAMAGES = 2
|
|
|
|
|
|
bool haveWeaponDamage = false;
|
|
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
|
|
|
|
float dmgMin = packet.readFloat();
|
|
|
|
|
|
float dmgMax = packet.readFloat();
|
|
|
|
|
|
uint32_t damageType = packet.readUInt32();
|
|
|
|
|
|
if (!haveWeaponDamage && dmgMax > 0.0f) {
|
|
|
|
|
|
if (damageType == 0 || data.damageMax <= 0.0f) {
|
|
|
|
|
|
data.damageMin = dmgMin;
|
|
|
|
|
|
data.damageMax = dmgMax;
|
|
|
|
|
|
haveWeaponDamage = (damageType == 0);
|
2026-02-18 03:46:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 00:59:07 -08:00
|
|
|
|
}
|
2026-02-06 03:11:43 -08:00
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
|
data.armor = static_cast<int32_t>(packet.readUInt32());
|
2026-03-13 11:23:55 -07:00
|
|
|
|
data.holyRes = static_cast<int32_t>(packet.readUInt32()); // HolyRes
|
|
|
|
|
|
data.fireRes = static_cast<int32_t>(packet.readUInt32()); // FireRes
|
|
|
|
|
|
data.natureRes = static_cast<int32_t>(packet.readUInt32()); // NatureRes
|
|
|
|
|
|
data.frostRes = static_cast<int32_t>(packet.readUInt32()); // FrostRes
|
|
|
|
|
|
data.shadowRes = static_cast<int32_t>(packet.readUInt32()); // ShadowRes
|
|
|
|
|
|
data.arcaneRes = static_cast<int32_t>(packet.readUInt32()); // ArcaneRes
|
2026-02-26 00:59:07 -08:00
|
|
|
|
data.delayMs = packet.readUInt32();
|
|
|
|
|
|
packet.readUInt32(); // AmmoType
|
|
|
|
|
|
packet.readFloat(); // RangedModRange
|
|
|
|
|
|
|
|
|
|
|
|
// 5 item spells: SpellId, SpellTrigger, SpellCharges, SpellCooldown, SpellCategory, SpellCategoryCooldown
|
|
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
|
|
|
|
if (packet.getReadPos() + 24 > packet.getSize()) break;
|
|
|
|
|
|
data.spells[i].spellId = packet.readUInt32();
|
|
|
|
|
|
data.spells[i].spellTrigger = packet.readUInt32();
|
|
|
|
|
|
packet.readUInt32(); // SpellCharges
|
|
|
|
|
|
packet.readUInt32(); // SpellCooldown
|
|
|
|
|
|
packet.readUInt32(); // SpellCategory
|
|
|
|
|
|
packet.readUInt32(); // SpellCategoryCooldown
|
|
|
|
|
|
}
|
2026-02-06 03:11:43 -08:00
|
|
|
|
|
2026-03-10 16:47:55 -07:00
|
|
|
|
// Bonding type (0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ)
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
|
|
|
|
|
data.bindType = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// Flavor/lore text (Description cstring)
|
|
|
|
|
|
if (packet.getReadPos() < packet.getSize())
|
|
|
|
|
|
data.description = packet.readString();
|
|
|
|
|
|
|
2026-03-10 17:05:04 -07:00
|
|
|
|
// Post-description fields: PageText, LanguageID, PageMaterial, StartQuest
|
|
|
|
|
|
if (packet.getReadPos() + 16 <= packet.getSize()) {
|
|
|
|
|
|
packet.readUInt32(); // PageText
|
|
|
|
|
|
packet.readUInt32(); // LanguageID
|
|
|
|
|
|
packet.readUInt32(); // PageMaterial
|
|
|
|
|
|
data.startQuestId = packet.readUInt32(); // StartQuest
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 12:15:08 -07:00
|
|
|
|
// WotLK 3.3.5a: additional fields after StartQuest (read up to socket data)
|
|
|
|
|
|
// LockID(4), Material(4), Sheath(4), RandomProperty(4), RandomSuffix(4),
|
|
|
|
|
|
// Block(4), ItemSet(4), MaxDurability(4), Area(4), Map(4), BagFamily(4),
|
|
|
|
|
|
// TotemCategory(4) = 48 bytes before sockets
|
|
|
|
|
|
constexpr size_t kPreSocketSkip = 48;
|
|
|
|
|
|
if (packet.getReadPos() + kPreSocketSkip + 28 <= packet.getSize()) {
|
2026-03-12 12:35:56 -07:00
|
|
|
|
// LockID(0), Material(1), Sheath(2), RandomProperty(3), RandomSuffix(4), Block(5)
|
|
|
|
|
|
for (size_t i = 0; i < 6; ++i) packet.readUInt32();
|
|
|
|
|
|
data.itemSetId = packet.readUInt32(); // ItemSet(6)
|
|
|
|
|
|
// MaxDurability(7), Area(8), Map(9), BagFamily(10), TotemCategory(11)
|
|
|
|
|
|
for (size_t i = 0; i < 5; ++i) packet.readUInt32();
|
2026-03-12 12:15:08 -07:00
|
|
|
|
// 3 socket slots: socketColor (4 bytes each)
|
|
|
|
|
|
data.socketColor[0] = packet.readUInt32();
|
|
|
|
|
|
data.socketColor[1] = packet.readUInt32();
|
|
|
|
|
|
data.socketColor[2] = packet.readUInt32();
|
|
|
|
|
|
// 3 socket content (gem enchantment IDs — skip, not currently displayed)
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
// socketBonus (enchantmentId)
|
|
|
|
|
|
data.socketBonus = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 03:11:43 -08:00
|
|
|
|
data.valid = !data.name.empty();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Creature Movement
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
|
|
|
|
|
// PackedGuid
|
|
|
|
|
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
if (data.guid == 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// uint8 unk (toggle for MOVEMENTFLAG2_UNK7)
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
|
|
|
|
|
packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
// Current position (server coords: float x, y, z)
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
|
|
|
|
|
data.x = packet.readFloat();
|
|
|
|
|
|
data.y = packet.readFloat();
|
|
|
|
|
|
data.z = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
|
|
// uint32 splineId
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// uint8 moveType
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
|
|
|
|
|
data.moveType = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.moveType == 1) {
|
|
|
|
|
|
// Stop - no more required data
|
|
|
|
|
|
data.destX = data.x;
|
|
|
|
|
|
data.destY = data.y;
|
|
|
|
|
|
data.destZ = data.z;
|
|
|
|
|
|
data.hasDest = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read facing data based on move type
|
|
|
|
|
|
if (data.moveType == 2) {
|
|
|
|
|
|
// FacingSpot: float x, y, z
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
|
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
|
|
|
|
|
} else if (data.moveType == 3) {
|
|
|
|
|
|
// FacingTarget: uint64 guid
|
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
|
|
|
|
|
data.facingTarget = packet.readUInt64();
|
|
|
|
|
|
} else if (data.moveType == 4) {
|
|
|
|
|
|
// FacingAngle: float angle
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
data.facingAngle = packet.readFloat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// uint32 splineFlags
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
data.splineFlags = packet.readUInt32();
|
|
|
|
|
|
|
2026-02-17 18:16:53 -08:00
|
|
|
|
// WotLK 3.3.5a SplineFlags (from TrinityCore/MaNGOS MoveSplineFlag.h):
|
|
|
|
|
|
// Animation = 0x00400000
|
|
|
|
|
|
// Parabolic = 0x00000800
|
|
|
|
|
|
// Catmullrom = 0x00080000 \ either means uncompressed (absolute) waypoints
|
|
|
|
|
|
// Flying = 0x00002000 /
|
|
|
|
|
|
|
|
|
|
|
|
// [if Animation] uint8 animationType + int32 effectStartTime (5 bytes)
|
|
|
|
|
|
if (data.splineFlags & 0x00400000) {
|
|
|
|
|
|
if (packet.getReadPos() + 5 > packet.getSize()) return false;
|
|
|
|
|
|
packet.readUInt8(); // animationType
|
|
|
|
|
|
packet.readUInt32(); // effectStartTime (int32, read as uint32 same size)
|
2026-02-06 13:47:03 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// uint32 duration
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
data.duration = packet.readUInt32();
|
|
|
|
|
|
|
2026-02-17 18:16:53 -08:00
|
|
|
|
// [if Parabolic] float verticalAcceleration + int32 effectStartTime (8 bytes)
|
|
|
|
|
|
if (data.splineFlags & 0x00000800) {
|
2026-02-06 13:47:03 -08:00
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
2026-02-17 18:16:53 -08:00
|
|
|
|
packet.readFloat(); // verticalAcceleration
|
2026-02-06 13:47:03 -08:00
|
|
|
|
packet.readUInt32(); // effectStartTime
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// uint32 pointCount
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
uint32_t pointCount = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
if (pointCount == 0) return true;
|
|
|
|
|
|
|
2026-03-14 22:01:26 -07:00
|
|
|
|
// Cap pointCount to prevent excessive iteration from malformed packets.
|
2026-03-11 14:13:09 -07:00
|
|
|
|
constexpr uint32_t kMaxSplinePoints = 1000;
|
|
|
|
|
|
if (pointCount > kMaxSplinePoints) {
|
2026-03-14 22:01:26 -07:00
|
|
|
|
LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints,
|
|
|
|
|
|
" (guid=0x", std::hex, data.guid, std::dec, "), capping");
|
|
|
|
|
|
pointCount = kMaxSplinePoints;
|
2026-03-11 14:13:09 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-17 18:16:53 -08:00
|
|
|
|
// Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed).
|
|
|
|
|
|
// Otherwise: first float3 is final destination, remaining are packed deltas.
|
|
|
|
|
|
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
|
2026-02-06 13:47:03 -08:00
|
|
|
|
|
|
|
|
|
|
if (uncompressed) {
|
|
|
|
|
|
// Read last point as destination
|
|
|
|
|
|
// Skip to last point: each point is 12 bytes
|
|
|
|
|
|
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
2026-03-14 22:01:26 -07:00
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
2026-02-06 13:47:03 -08:00
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
|
|
|
|
|
}
|
2026-03-14 22:01:26 -07:00
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
2026-02-06 13:47:03 -08:00
|
|
|
|
data.destX = packet.readFloat();
|
|
|
|
|
|
data.destY = packet.readFloat();
|
|
|
|
|
|
data.destZ = packet.readFloat();
|
|
|
|
|
|
data.hasDest = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Compressed: first 3 floats are the destination (final point)
|
2026-03-14 22:01:26 -07:00
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return true;
|
2026-02-06 13:47:03 -08:00
|
|
|
|
data.destX = packet.readFloat();
|
|
|
|
|
|
data.destY = packet.readFloat();
|
|
|
|
|
|
data.destZ = packet.readFloat();
|
|
|
|
|
|
data.hasDest = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("MonsterMove: guid=0x", std::hex, data.guid, std::dec,
|
|
|
|
|
|
" type=", (int)data.moveType, " dur=", data.duration, "ms",
|
|
|
|
|
|
" dest=(", data.destX, ",", data.destY, ",", data.destZ, ")");
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-18 03:13:17 -08:00
|
|
|
|
bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& data) {
|
|
|
|
|
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
if (data.guid == 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
|
|
|
|
|
data.x = packet.readFloat();
|
|
|
|
|
|
data.y = packet.readFloat();
|
|
|
|
|
|
data.z = packet.readFloat();
|
|
|
|
|
|
|
|
|
|
|
|
// Turtle WoW movement payload uses a spline-style layout after XYZ:
|
|
|
|
|
|
// uint32 splineIdOrTick
|
|
|
|
|
|
// uint8 moveType
|
|
|
|
|
|
// [if moveType 2/3/4] facing payload
|
|
|
|
|
|
// uint32 splineFlags
|
|
|
|
|
|
// [if Animation] uint8 + uint32
|
|
|
|
|
|
// uint32 duration
|
|
|
|
|
|
// [if Parabolic] float + uint32
|
|
|
|
|
|
// uint32 pointCount
|
|
|
|
|
|
// float[3] dest
|
|
|
|
|
|
// uint32 packedPoints[pointCount-1]
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
/*uint32_t splineIdOrTick =*/ packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
|
|
|
|
|
data.moveType = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.moveType == 1) {
|
|
|
|
|
|
data.destX = data.x;
|
|
|
|
|
|
data.destY = data.y;
|
|
|
|
|
|
data.destZ = data.z;
|
|
|
|
|
|
data.hasDest = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.moveType == 2) {
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) return false;
|
|
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
|
|
|
|
|
} else if (data.moveType == 3) {
|
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
|
|
|
|
|
data.facingTarget = packet.readUInt64();
|
|
|
|
|
|
} else if (data.moveType == 4) {
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
data.facingAngle = packet.readFloat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
data.splineFlags = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// Animation flag (same bit as WotLK MoveSplineFlag::Animation)
|
|
|
|
|
|
if (data.splineFlags & 0x00400000) {
|
|
|
|
|
|
if (packet.getReadPos() + 5 > packet.getSize()) return false;
|
|
|
|
|
|
packet.readUInt8();
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
data.duration = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
// Parabolic flag (same bit as WotLK MoveSplineFlag::Parabolic)
|
|
|
|
|
|
if (data.splineFlags & 0x00000800) {
|
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return false;
|
|
|
|
|
|
packet.readFloat();
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return false;
|
|
|
|
|
|
uint32_t pointCount = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
if (pointCount == 0) return true;
|
2026-03-11 14:13:09 -07:00
|
|
|
|
|
2026-03-14 21:49:30 -07:00
|
|
|
|
// Reject extreme point counts from malformed packets.
|
2026-03-11 14:13:09 -07:00
|
|
|
|
constexpr uint32_t kMaxSplinePoints = 1000;
|
|
|
|
|
|
if (pointCount > kMaxSplinePoints) {
|
2026-03-14 21:49:30 -07:00
|
|
|
|
return false;
|
2026-03-11 14:13:09 -07:00
|
|
|
|
}
|
2026-02-18 03:13:17 -08:00
|
|
|
|
|
2026-03-14 21:49:30 -07:00
|
|
|
|
size_t requiredBytes = 12;
|
|
|
|
|
|
if (pointCount > 1) {
|
|
|
|
|
|
requiredBytes += static_cast<size_t>(pointCount - 1) * 4ull;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (packet.getReadPos() + requiredBytes > packet.getSize()) return false;
|
|
|
|
|
|
|
2026-02-18 03:13:17 -08:00
|
|
|
|
// First float[3] is destination.
|
|
|
|
|
|
data.destX = packet.readFloat();
|
|
|
|
|
|
data.destY = packet.readFloat();
|
|
|
|
|
|
data.destZ = packet.readFloat();
|
|
|
|
|
|
data.hasDest = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Remaining waypoints are packed as uint32 deltas.
|
|
|
|
|
|
if (pointCount > 1) {
|
|
|
|
|
|
size_t skipBytes = static_cast<size_t>(pointCount - 1) * 4;
|
|
|
|
|
|
size_t newPos = packet.getReadPos() + skipBytes;
|
2026-03-14 21:49:30 -07:00
|
|
|
|
if (newPos > packet.getSize()) return false;
|
|
|
|
|
|
packet.setReadPos(newPos);
|
2026-02-18 03:13:17 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,
|
|
|
|
|
|
" type=", (int)data.moveType, " dur=", data.duration, "ms",
|
|
|
|
|
|
" dest=(", data.destX, ",", data.destY, ",", data.destZ, ")");
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 2: Combat Core
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) {
|
|
|
|
|
|
if (packet.getSize() < 16) return false;
|
|
|
|
|
|
data.attackerGuid = packet.readUInt64();
|
|
|
|
|
|
data.victimGuid = packet.readUInt64();
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Attack started: 0x", std::hex, data.attackerGuid,
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
" -> 0x", data.victimGuid, std::dec);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) {
|
|
|
|
|
|
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
data.victimGuid = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
if (packet.getReadPos() < packet.getSize()) {
|
|
|
|
|
|
data.unknown = packet.readUInt32();
|
|
|
|
|
|
}
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Attack stopped: 0x", std::hex, data.attackerGuid, std::dec);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
|
2026-03-11 14:32:03 -07:00
|
|
|
|
// Upfront validation: hitInfo(4) + packed GUIDs(1-8 each) + totalDamage(4) + subDamageCount(1) = 13 bytes minimum
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 13) return false;
|
|
|
|
|
|
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.hitInfo = packet.readUInt32();
|
2026-03-14 10:48:20 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-14 10:48:20 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-11 14:32:03 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate totalDamage + subDamageCount can be read (5 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 5) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
|
|
|
|
|
data.subDamageCount = packet.readUInt8();
|
|
|
|
|
|
|
2026-03-14 14:21:55 -07:00
|
|
|
|
// Cap subDamageCount: each entry is 20 bytes. If the claimed count
|
2026-03-13 02:47:40 -07:00
|
|
|
|
// exceeds what the remaining bytes can hold, a GUID was mis-parsed
|
|
|
|
|
|
// (off by one byte), causing the school-mask byte to be read as count.
|
2026-03-14 14:21:55 -07:00
|
|
|
|
// In that case clamp to the number of full entries that fit.
|
2026-03-13 02:47:40 -07:00
|
|
|
|
{
|
|
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
size_t maxFit = remaining / 20;
|
|
|
|
|
|
if (data.subDamageCount > maxFit) {
|
2026-03-14 14:21:55 -07:00
|
|
|
|
data.subDamageCount = static_cast<uint8_t>(std::min<size_t>(maxFit, 64));
|
2026-03-13 02:47:40 -07:00
|
|
|
|
} else if (data.subDamageCount > 64) {
|
|
|
|
|
|
data.subDamageCount = 64;
|
|
|
|
|
|
}
|
2026-03-11 14:32:03 -07:00
|
|
|
|
}
|
2026-03-13 02:47:40 -07:00
|
|
|
|
if (data.subDamageCount == 0) return false;
|
2026-03-11 14:32:03 -07:00
|
|
|
|
|
|
|
|
|
|
data.subDamages.reserve(data.subDamageCount);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
|
2026-03-11 14:32:03 -07:00
|
|
|
|
// Each sub-damage entry needs 20 bytes: schoolMask(4) + damage(4) + intDamage(4) + absorbed(4) + resisted(4)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 20) {
|
|
|
|
|
|
data.subDamageCount = i;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
SubDamage sub;
|
|
|
|
|
|
sub.schoolMask = packet.readUInt32();
|
|
|
|
|
|
sub.damage = packet.readFloat();
|
|
|
|
|
|
sub.intDamage = packet.readUInt32();
|
|
|
|
|
|
sub.absorbed = packet.readUInt32();
|
|
|
|
|
|
sub.resisted = packet.readUInt32();
|
|
|
|
|
|
data.subDamages.push_back(sub);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:32:03 -07:00
|
|
|
|
// Validate victimState + overkill fields (8 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
|
|
|
|
|
data.victimState = 0;
|
|
|
|
|
|
data.overkill = 0;
|
|
|
|
|
|
return !data.subDamages.empty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.victimState = packet.readUInt32();
|
2026-03-13 02:47:40 -07:00
|
|
|
|
// WotLK (AzerothCore): two unknown uint32 fields follow victimState before overkill.
|
|
|
|
|
|
// Older parsers omitted these, reading overkill from the wrong offset.
|
|
|
|
|
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
|
|
|
|
|
if (rem() >= 4) packet.readUInt32(); // unk1 (always 0)
|
|
|
|
|
|
if (rem() >= 4) packet.readUInt32(); // unk2 (melee spell ID, 0 for auto-attack)
|
|
|
|
|
|
data.overkill = (rem() >= 4) ? static_cast<int32_t>(packet.readUInt32()) : -1;
|
|
|
|
|
|
|
|
|
|
|
|
// hitInfo-conditional fields: HITINFO_BLOCK(0x2000), RAGE_GAIN(0x20000), FAKE_DAMAGE(0x40)
|
|
|
|
|
|
if ((data.hitInfo & 0x2000) && rem() >= 4) data.blocked = packet.readUInt32();
|
|
|
|
|
|
else data.blocked = 0;
|
|
|
|
|
|
// RAGE_GAIN and FAKE_DAMAGE both add a uint32 we can skip
|
|
|
|
|
|
if ((data.hitInfo & 0x20000) && rem() >= 4) packet.readUInt32(); // rage gain
|
|
|
|
|
|
if ((data.hitInfo & 0x40) && rem() >= 4) packet.readUInt32(); // fake damage total
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
2026-03-10 04:25:45 -07:00
|
|
|
|
LOG_DEBUG("Melee hit: ", data.totalDamage, " damage",
|
|
|
|
|
|
data.isCrit() ? " (CRIT)" : "",
|
|
|
|
|
|
data.isMiss() ? " (MISS)" : "");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) {
|
2026-03-14 15:13:49 -07:00
|
|
|
|
// Upfront validation:
|
|
|
|
|
|
// packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1)
|
|
|
|
|
|
// + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4)
|
|
|
|
|
|
// = 33 bytes minimum.
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 33) return false;
|
2026-03-11 14:32:03 -07:00
|
|
|
|
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
2026-03-14 13:51:37 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-14 13:51:37 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-11 14:32:03 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate core fields (spellId + damage + overkill + schoolMask + absorbed + resisted = 21 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 21) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.spellId = packet.readUInt32();
|
|
|
|
|
|
data.damage = packet.readUInt32();
|
|
|
|
|
|
data.overkill = packet.readUInt32();
|
|
|
|
|
|
data.schoolMask = packet.readUInt8();
|
|
|
|
|
|
data.absorbed = packet.readUInt32();
|
|
|
|
|
|
data.resisted = packet.readUInt32();
|
|
|
|
|
|
|
2026-03-14 15:13:49 -07:00
|
|
|
|
// Remaining fields are required for a complete event.
|
|
|
|
|
|
// Reject truncated packets so we do not emit partial/incorrect combat entries.
|
2026-03-11 14:32:03 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 10) {
|
2026-03-14 15:13:49 -07:00
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
2026-03-11 14:32:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
uint8_t periodicLog = packet.readUInt8();
|
|
|
|
|
|
(void)periodicLog;
|
|
|
|
|
|
packet.readUInt8(); // unused
|
|
|
|
|
|
packet.readUInt32(); // blocked
|
|
|
|
|
|
uint32_t flags = packet.readUInt32();
|
|
|
|
|
|
(void)flags;
|
|
|
|
|
|
// Check crit flag
|
|
|
|
|
|
data.isCrit = (flags & 0x02) != 0;
|
|
|
|
|
|
|
2026-03-10 04:25:45 -07:00
|
|
|
|
LOG_DEBUG("Spell damage: spellId=", data.spellId, " dmg=", data.damage,
|
|
|
|
|
|
data.isCrit ? " CRIT" : "");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) {
|
2026-03-11 14:32:03 -07:00
|
|
|
|
// Upfront validation: packed GUIDs(1-8 each) + spellId(4) + heal(4) + overheal(4) + absorbed(4) + critFlag(1) = 21 bytes minimum
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 21) return false;
|
|
|
|
|
|
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
2026-03-14 13:51:37 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-14 13:51:37 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-11 14:32:03 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate remaining fields (spellId + heal + overheal + absorbed + critFlag = 17 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 17) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.spellId = packet.readUInt32();
|
|
|
|
|
|
data.heal = packet.readUInt32();
|
|
|
|
|
|
data.overheal = packet.readUInt32();
|
|
|
|
|
|
data.absorbed = packet.readUInt32();
|
|
|
|
|
|
uint8_t critFlag = packet.readUInt8();
|
|
|
|
|
|
data.isCrit = (critFlag != 0);
|
|
|
|
|
|
|
2026-03-10 04:25:45 -07:00
|
|
|
|
LOG_DEBUG("Spell heal: spellId=", data.spellId, " heal=", data.heal,
|
|
|
|
|
|
data.isCrit ? " CRIT" : "");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 12:07:58 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// XP Gain
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: victimGuid(8) + totalXp(4) + type(1)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 13) {
|
|
|
|
|
|
LOG_WARNING("SMSG_LOG_XPGAIN: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 12:07:58 -08:00
|
|
|
|
data.victimGuid = packet.readUInt64();
|
|
|
|
|
|
data.totalXp = packet.readUInt32();
|
|
|
|
|
|
data.type = packet.readUInt8();
|
|
|
|
|
|
if (data.type == 0) {
|
2026-02-06 15:41:29 -08:00
|
|
|
|
// Kill XP: float groupRate (1.0 = solo) + uint8 RAF flag
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate before reading conditional fields
|
|
|
|
|
|
if (packet.getReadPos() + 5 <= packet.getSize()) {
|
|
|
|
|
|
float groupRate = packet.readFloat();
|
|
|
|
|
|
packet.readUInt8(); // RAF bonus flag
|
|
|
|
|
|
// Group bonus = total - (total / rate); only if grouped (rate > 1)
|
|
|
|
|
|
if (groupRate > 1.0f) {
|
|
|
|
|
|
data.groupBonus = data.totalXp - static_cast<uint32_t>(data.totalXp / groupRate);
|
|
|
|
|
|
}
|
2026-02-06 15:41:29 -08:00
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
}
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("XP gain: ", data.totalXp, " xp (type=", static_cast<int>(data.type), ")");
|
2026-02-05 12:07:58 -08:00
|
|
|
|
return data.totalXp > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 3: Spells, Action Bar, Auras
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-11 04:38:30 -07:00
|
|
|
|
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data,
|
|
|
|
|
|
bool vanillaFormat) {
|
2026-03-11 14:10:20 -07:00
|
|
|
|
// Validate minimum packet size for header: talentSpec(1) + spellCount(2)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 3) {
|
|
|
|
|
|
LOG_ERROR("SMSG_INITIAL_SPELLS: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.talentSpec = packet.readUInt8();
|
|
|
|
|
|
uint16_t spellCount = packet.readUInt16();
|
|
|
|
|
|
|
2026-03-11 14:10:20 -07:00
|
|
|
|
// Cap spell count to prevent excessive iteration
|
|
|
|
|
|
constexpr uint16_t kMaxSpells = 256;
|
|
|
|
|
|
if (spellCount > kMaxSpells) {
|
|
|
|
|
|
LOG_WARNING("SMSG_INITIAL_SPELLS: spellCount=", spellCount, " exceeds max ", kMaxSpells,
|
|
|
|
|
|
", capping");
|
|
|
|
|
|
spellCount = kMaxSpells;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 04:38:30 -07:00
|
|
|
|
LOG_DEBUG("SMSG_INITIAL_SPELLS: spellCount=", spellCount,
|
|
|
|
|
|
vanillaFormat ? " (vanilla uint16 format)" : " (TBC/WotLK uint32 format)");
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.spellIds.reserve(spellCount);
|
|
|
|
|
|
for (uint16_t i = 0; i < spellCount; ++i) {
|
2026-03-11 14:10:20 -07:00
|
|
|
|
// Vanilla spell: spellId(2) + slot(2) = 4 bytes
|
|
|
|
|
|
// TBC/WotLK spell: spellId(4) + unknown(2) = 6 bytes
|
|
|
|
|
|
size_t spellEntrySize = vanillaFormat ? 4 : 6;
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < spellEntrySize) {
|
|
|
|
|
|
LOG_WARNING("SMSG_INITIAL_SPELLS: spell ", i, " truncated (", spellCount, " expected)");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 18:59:09 -08:00
|
|
|
|
uint32_t spellId;
|
|
|
|
|
|
if (vanillaFormat) {
|
|
|
|
|
|
spellId = packet.readUInt16();
|
|
|
|
|
|
packet.readUInt16(); // slot
|
|
|
|
|
|
} else {
|
|
|
|
|
|
spellId = packet.readUInt32();
|
|
|
|
|
|
packet.readUInt16(); // unknown (always 0)
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
if (spellId != 0) {
|
|
|
|
|
|
data.spellIds.push_back(spellId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:10:20 -07:00
|
|
|
|
// Validate minimum packet size for cooldownCount (2 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 2) {
|
|
|
|
|
|
LOG_WARNING("SMSG_INITIAL_SPELLS: truncated before cooldownCount (parsed ", data.spellIds.size(),
|
|
|
|
|
|
" spells)");
|
|
|
|
|
|
return true; // Have spells; cooldowns are optional
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
uint16_t cooldownCount = packet.readUInt16();
|
2026-03-11 14:10:20 -07:00
|
|
|
|
|
|
|
|
|
|
// Cap cooldown count to prevent excessive iteration
|
|
|
|
|
|
constexpr uint16_t kMaxCooldowns = 256;
|
|
|
|
|
|
if (cooldownCount > kMaxCooldowns) {
|
|
|
|
|
|
LOG_WARNING("SMSG_INITIAL_SPELLS: cooldownCount=", cooldownCount, " exceeds max ", kMaxCooldowns,
|
|
|
|
|
|
", capping");
|
|
|
|
|
|
cooldownCount = kMaxCooldowns;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.cooldowns.reserve(cooldownCount);
|
|
|
|
|
|
for (uint16_t i = 0; i < cooldownCount; ++i) {
|
2026-03-11 14:10:20 -07:00
|
|
|
|
// Vanilla cooldown: spellId(2) + itemId(2) + categoryId(2) + cooldownMs(4) + categoryCooldownMs(4) = 14 bytes
|
|
|
|
|
|
// TBC/WotLK cooldown: spellId(4) + itemId(2) + categoryId(2) + cooldownMs(4) + categoryCooldownMs(4) = 16 bytes
|
|
|
|
|
|
size_t cooldownEntrySize = vanillaFormat ? 14 : 16;
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < cooldownEntrySize) {
|
|
|
|
|
|
LOG_WARNING("SMSG_INITIAL_SPELLS: cooldown ", i, " truncated (", cooldownCount, " expected)");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
SpellCooldownEntry entry;
|
2026-02-13 18:59:09 -08:00
|
|
|
|
if (vanillaFormat) {
|
|
|
|
|
|
entry.spellId = packet.readUInt16();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
entry.spellId = packet.readUInt32();
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
entry.itemId = packet.readUInt16();
|
|
|
|
|
|
entry.categoryId = packet.readUInt16();
|
|
|
|
|
|
entry.cooldownMs = packet.readUInt32();
|
|
|
|
|
|
entry.categoryCooldownMs = packet.readUInt32();
|
|
|
|
|
|
data.cooldowns.push_back(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
LOG_INFO("Initial spells parsed: ", data.spellIds.size(), " spells, ",
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.cooldowns.size(), " cooldowns");
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
|
|
|
|
|
if (!data.spellIds.empty()) {
|
|
|
|
|
|
std::string first10;
|
|
|
|
|
|
for (size_t i = 0; i < std::min(size_t(10), data.spellIds.size()); ++i) {
|
|
|
|
|
|
if (!first10.empty()) first10 += ", ";
|
|
|
|
|
|
first10 += std::to_string(data.spellIds[i]);
|
|
|
|
|
|
}
|
2026-03-10 04:52:22 -07:00
|
|
|
|
LOG_DEBUG("Initial spell IDs (first 10): ", first10);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CAST_SPELL));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt8(castCount);
|
|
|
|
|
|
packet.writeUInt32(spellId);
|
|
|
|
|
|
packet.writeUInt8(0x00); // castFlags = 0 for normal cast
|
|
|
|
|
|
|
|
|
|
|
|
// SpellCastTargets
|
|
|
|
|
|
if (targetGuid != 0) {
|
|
|
|
|
|
packet.writeUInt32(0x02); // TARGET_FLAG_UNIT
|
|
|
|
|
|
|
|
|
|
|
|
// Write packed GUID
|
|
|
|
|
|
uint8_t mask = 0;
|
|
|
|
|
|
uint8_t bytes[8];
|
|
|
|
|
|
int byteCount = 0;
|
|
|
|
|
|
uint64_t g = targetGuid;
|
|
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
|
|
|
|
uint8_t b = g & 0xFF;
|
|
|
|
|
|
if (b != 0) {
|
|
|
|
|
|
mask |= (1 << i);
|
|
|
|
|
|
bytes[byteCount++] = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
g >>= 8;
|
|
|
|
|
|
}
|
|
|
|
|
|
packet.writeUInt8(mask);
|
|
|
|
|
|
for (int i = 0; i < byteCount; ++i) {
|
|
|
|
|
|
packet.writeUInt8(bytes[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
packet.writeUInt32(0x00); // TARGET_FLAG_SELF
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CAST_SPELL: spell=", spellId, " target=0x",
|
|
|
|
|
|
std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet CancelAuraPacket::build(uint32_t spellId) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_AURA));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt32(spellId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 22:53:09 -07:00
|
|
|
|
network::Packet PetActionPacket::build(uint64_t petGuid, uint32_t action, uint64_t targetGuid) {
|
|
|
|
|
|
// CMSG_PET_ACTION: petGuid(8) + action(4) + targetGuid(8)
|
2026-02-26 10:41:29 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PET_ACTION));
|
|
|
|
|
|
packet.writeUInt64(petGuid);
|
|
|
|
|
|
packet.writeUInt32(action);
|
2026-03-09 22:53:09 -07:00
|
|
|
|
packet.writeUInt64(targetGuid);
|
2026-02-26 10:41:29 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) {
|
2026-03-11 14:35:29 -07:00
|
|
|
|
// WotLK format: castCount(1) + spellId(4) + result(1) = 6 bytes minimum
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 6) return false;
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.castCount = packet.readUInt8();
|
|
|
|
|
|
data.spellId = packet.readUInt32();
|
|
|
|
|
|
data.result = packet.readUInt8();
|
|
|
|
|
|
LOG_INFO("Cast failed: spell=", data.spellId, " result=", (int)data.result);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
|
2026-03-14 14:43:15 -07:00
|
|
|
|
data = SpellStartData{};
|
|
|
|
|
|
|
2026-03-14 14:06:19 -07:00
|
|
|
|
// Packed GUIDs are variable-length; only require minimal packet shape up front:
|
|
|
|
|
|
// two GUID masks + castCount(1) + spellId(4) + castFlags(4) + castTime(4).
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 15) return false;
|
2026-03-11 14:28:41 -07:00
|
|
|
|
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
2026-03-14 10:33:48 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-14 10:33:48 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-11 14:28:41 -07:00
|
|
|
|
|
2026-03-14 11:03:02 -07:00
|
|
|
|
// Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 13 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 13) {
|
2026-03-11 14:28:41 -07:00
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.castCount = packet.readUInt8();
|
|
|
|
|
|
data.spellId = packet.readUInt32();
|
|
|
|
|
|
data.castFlags = packet.readUInt32();
|
|
|
|
|
|
data.castTime = packet.readUInt32();
|
|
|
|
|
|
|
2026-03-14 14:43:15 -07:00
|
|
|
|
// SpellCastTargets starts with target flags and is mandatory.
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 4) {
|
|
|
|
|
|
LOG_WARNING("Spell start: missing targetFlags");
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 22:20:03 -07:00
|
|
|
|
// WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that
|
|
|
|
|
|
// subsequent fields (e.g. school mask, cast flags 0x20 extra data) are not
|
|
|
|
|
|
// misaligned for ground-targeted or AoE spells.
|
2026-03-14 14:43:15 -07:00
|
|
|
|
uint32_t targetFlags = packet.readUInt32();
|
2026-03-17 22:20:03 -07:00
|
|
|
|
|
|
|
|
|
|
auto readPackedTarget = [&](uint64_t* out) -> bool {
|
|
|
|
|
|
if (!hasFullPackedGuid(packet)) return false;
|
|
|
|
|
|
uint64_t g = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
if (out) *out = g;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
auto skipPackedAndFloats3 = [&]() -> bool {
|
|
|
|
|
|
if (!hasFullPackedGuid(packet)) return false;
|
|
|
|
|
|
UpdateObjectParser::readPackedGuid(packet); // transport GUID (may be zero)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 12) return false;
|
|
|
|
|
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// UNIT/UNIT_MINIPET/CORPSE_ALLY/GAMEOBJECT share a single object target GUID
|
|
|
|
|
|
if (targetFlags & (0x0002u | 0x0004u | 0x0400u | 0x0800u)) {
|
|
|
|
|
|
readPackedTarget(&data.targetGuid); // best-effort; ignore failure
|
|
|
|
|
|
}
|
|
|
|
|
|
// ITEM/TRADE_ITEM share a single 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.getReadPos() < packet.getSize() && packet.readUInt8() != 0) {}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 14:00:41 -08:00
|
|
|
|
LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
2026-03-14 13:21:38 -07:00
|
|
|
|
// Always reset output to avoid stale targets when callers reuse buffers.
|
|
|
|
|
|
data = SpellGoData{};
|
|
|
|
|
|
|
2026-03-13 21:16:24 -07:00
|
|
|
|
// Packed GUIDs are variable-length, so only require the smallest possible
|
2026-03-14 14:06:19 -07:00
|
|
|
|
// shape up front: 2 GUID masks + fixed fields through hitCount.
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 16) return false;
|
2026-03-11 14:28:41 -07:00
|
|
|
|
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
2026-03-14 10:40:54 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-14 10:40:54 -07:00
|
|
|
|
if (!hasFullPackedGuid(packet)) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
2026-03-11 14:28:41 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate remaining fixed fields up to hitCount/missCount
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 14) { // castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1)
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.castCount = packet.readUInt8();
|
|
|
|
|
|
data.spellId = packet.readUInt32();
|
|
|
|
|
|
data.castFlags = packet.readUInt32();
|
|
|
|
|
|
// Timestamp in 3.3.5a
|
|
|
|
|
|
packet.readUInt32();
|
|
|
|
|
|
|
2026-03-14 10:09:04 -07:00
|
|
|
|
const uint8_t rawHitCount = packet.readUInt8();
|
|
|
|
|
|
if (rawHitCount > 128) {
|
|
|
|
|
|
LOG_WARNING("Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
|
2026-03-11 14:28:41 -07:00
|
|
|
|
}
|
2026-03-14 10:09:04 -07:00
|
|
|
|
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
|
2026-03-11 14:28:41 -07:00
|
|
|
|
|
2026-03-14 10:56:04 -07:00
|
|
|
|
bool truncatedTargets = false;
|
|
|
|
|
|
|
2026-03-14 10:09:04 -07:00
|
|
|
|
data.hitTargets.reserve(storedHitLimit);
|
|
|
|
|
|
for (uint16_t i = 0; i < rawHitCount; ++i) {
|
2026-03-18 07:23:51 -07:00
|
|
|
|
// WotLK 3.3.5a hit targets are full uint64 GUIDs (not PackedGuid).
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) {
|
2026-03-14 10:09:04 -07:00
|
|
|
|
LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount);
|
2026-03-14 10:56:04 -07:00
|
|
|
|
truncatedTargets = true;
|
2026-03-11 14:28:41 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-18 07:23:51 -07:00
|
|
|
|
const uint64_t targetGuid = packet.readUInt64();
|
2026-03-14 10:09:04 -07:00
|
|
|
|
if (i < storedHitLimit) {
|
|
|
|
|
|
data.hitTargets.push_back(targetGuid);
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
2026-03-14 10:56:04 -07:00
|
|
|
|
if (truncatedTargets) {
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-14 10:09:04 -07:00
|
|
|
|
data.hitCount = static_cast<uint8_t>(data.hitTargets.size());
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
2026-03-14 13:37:28 -07:00
|
|
|
|
// missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation.
|
2026-03-11 14:28:41 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
2026-03-14 13:37:28 -07:00
|
|
|
|
LOG_WARNING("Spell go: missing missCount after hit target list");
|
|
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
return false;
|
2026-03-11 14:28:41 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 06:57:15 -07:00
|
|
|
|
const size_t missCountPos = packet.getReadPos();
|
2026-03-14 10:09:04 -07:00
|
|
|
|
const uint8_t rawMissCount = packet.readUInt8();
|
2026-03-18 06:57:15 -07:00
|
|
|
|
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=", (int)rawMissCount,
|
|
|
|
|
|
" spell=", data.spellId, " hits=", (int)data.hitCount,
|
|
|
|
|
|
" castFlags=0x", std::hex, data.castFlags, std::dec,
|
|
|
|
|
|
" missCountPos=", missCountPos, " pktSize=", packet.getSize(),
|
|
|
|
|
|
" ctx=", hexCtx);
|
|
|
|
|
|
}
|
2026-03-14 10:09:04 -07:00
|
|
|
|
if (rawMissCount > 128) {
|
2026-03-18 06:23:03 -07:00
|
|
|
|
LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount,
|
|
|
|
|
|
") spell=", data.spellId, " hits=", (int)data.hitCount,
|
|
|
|
|
|
" remaining=", packet.getSize() - packet.getReadPos());
|
2026-03-11 14:28:41 -07:00
|
|
|
|
}
|
2026-03-14 10:09:04 -07:00
|
|
|
|
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
|
2026-03-11 14:28:41 -07:00
|
|
|
|
|
2026-03-14 10:09:04 -07:00
|
|
|
|
data.missTargets.reserve(storedMissLimit);
|
|
|
|
|
|
for (uint16_t i = 0; i < rawMissCount; ++i) {
|
2026-03-18 07:23:51 -07:00
|
|
|
|
// WotLK 3.3.5a miss targets are full uint64 GUIDs + uint8 missType.
|
2026-03-18 06:23:03 -07:00
|
|
|
|
// REFLECT additionally appends uint8 reflectResult.
|
2026-03-18 07:23:51 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 9) { // 8 GUID + 1 missType
|
2026-03-18 06:23:03 -07:00
|
|
|
|
LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount,
|
|
|
|
|
|
" spell=", data.spellId, " hits=", (int)data.hitCount);
|
2026-03-14 10:56:04 -07:00
|
|
|
|
truncatedTargets = true;
|
2026-03-11 14:28:41 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-09 23:00:21 -07:00
|
|
|
|
SpellGoMissEntry m;
|
2026-03-18 07:23:51 -07:00
|
|
|
|
m.targetGuid = packet.readUInt64();
|
2026-03-14 10:17:19 -07:00
|
|
|
|
m.missType = packet.readUInt8();
|
2026-03-18 06:20:18 -07:00
|
|
|
|
if (m.missType == 11) { // SPELL_MISS_REFLECT
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
2026-03-14 10:09:04 -07:00
|
|
|
|
LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount);
|
2026-03-14 10:56:04 -07:00
|
|
|
|
truncatedTargets = true;
|
2026-03-13 23:15:56 -07:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-18 06:20:18 -07:00
|
|
|
|
(void)packet.readUInt8(); // reflectResult
|
2026-03-13 23:15:56 -07:00
|
|
|
|
}
|
2026-03-14 10:09:04 -07:00
|
|
|
|
if (i < storedMissLimit) {
|
|
|
|
|
|
data.missTargets.push_back(m);
|
|
|
|
|
|
}
|
2026-03-09 23:00:21 -07:00
|
|
|
|
}
|
2026-03-18 06:23:03 -07:00
|
|
|
|
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).
|
2026-03-14 10:56:04 -07:00
|
|
|
|
if (truncatedTargets) {
|
2026-03-18 06:23:03 -07:00
|
|
|
|
LOG_DEBUG("Spell go: salvaging ", (int)data.hitCount, " hits despite miss truncation");
|
|
|
|
|
|
packet.setReadPos(packet.getSize()); // consume remaining bytes
|
|
|
|
|
|
return true;
|
2026-03-14 10:56:04 -07:00
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
2026-03-17 22:26:05 -07:00
|
|
|
|
// 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.getReadPos() < packet.getSize()) {
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
|
|
|
|
|
uint32_t targetFlags = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
auto readPackedTarget = [&](uint64_t* out) -> bool {
|
|
|
|
|
|
if (!hasFullPackedGuid(packet)) return false;
|
|
|
|
|
|
uint64_t g = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
if (out) *out = g;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
auto skipPackedAndFloats3 = [&]() -> bool {
|
|
|
|
|
|
if (!hasFullPackedGuid(packet)) return false;
|
|
|
|
|
|
UpdateObjectParser::readPackedGuid(packet); // transport GUID
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 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.getReadPos() < packet.getSize() && packet.readUInt8() != 0) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 14:00:41 -08:00
|
|
|
|
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
" misses=", (int)data.missCount);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool isAll) {
|
2026-03-11 14:30:57 -07:00
|
|
|
|
// Validation: packed GUID (1-8 bytes minimum for reading)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) return false;
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
|
2026-03-11 14:30:57 -07:00
|
|
|
|
// Cap number of aura entries to prevent unbounded loop DoS
|
|
|
|
|
|
uint32_t maxAuras = isAll ? 512 : 1;
|
|
|
|
|
|
uint32_t auraCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (packet.getReadPos() < packet.getSize() && auraCount < maxAuras) {
|
|
|
|
|
|
// Validate we can read slot (1) + spellId (4) = 5 bytes minimum
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 5) {
|
|
|
|
|
|
LOG_DEBUG("Aura update: truncated entry at position ", auraCount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
uint8_t slot = packet.readUInt8();
|
|
|
|
|
|
uint32_t spellId = packet.readUInt32();
|
2026-03-11 14:30:57 -07:00
|
|
|
|
auraCount++;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
|
|
|
|
|
AuraSlot aura;
|
|
|
|
|
|
if (spellId != 0) {
|
|
|
|
|
|
aura.spellId = spellId;
|
2026-03-11 14:30:57 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate flags + level + charges (3 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 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();
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
|
|
|
|
|
if (!(aura.flags & 0x08)) { // NOT_CASTER flag
|
2026-03-11 14:30:57 -07:00
|
|
|
|
// Validate space for packed GUID read (minimum 1 byte)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
|
|
|
|
|
aura.casterGuid = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
aura.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:30:57 -07:00
|
|
|
|
if (aura.flags & 0x20) { // DURATION - need 8 bytes (two uint32s)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 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());
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 00:06:25 -08:00
|
|
|
|
if (aura.flags & 0x40) { // EFFECT_AMOUNTS
|
|
|
|
|
|
// Only read amounts for active effect indices (flags 0x01, 0x02, 0x04)
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
2026-02-08 00:06:25 -08:00
|
|
|
|
if (aura.flags & (1 << i)) {
|
2026-03-11 14:30:57 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
2026-02-08 00:06:25 -08:00
|
|
|
|
packet.readUInt32();
|
2026-03-11 14:30:57 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
LOG_WARNING("Aura update: truncated effect amount ", i, " at entry ", auraCount);
|
|
|
|
|
|
break;
|
2026-02-08 00:06:25 -08:00
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.updates.push_back({slot, aura});
|
|
|
|
|
|
|
|
|
|
|
|
// For single update, only one entry
|
|
|
|
|
|
if (!isAll) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:30:57 -07:00
|
|
|
|
if (auraCount >= maxAuras && packet.getReadPos() < packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("Aura update: capped at ", maxAuras, " entries, remaining data ignored");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
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) {
|
2026-03-11 14:33:02 -07:00
|
|
|
|
// Upfront validation: guid(8) + flags(1) = 9 bytes minimum
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 9) return false;
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.guid = packet.readUInt64();
|
|
|
|
|
|
data.flags = packet.readUInt8();
|
|
|
|
|
|
|
2026-03-11 14:33:02 -07:00
|
|
|
|
// Cap cooldown entries to prevent unbounded memory allocation (each entry is 8 bytes)
|
|
|
|
|
|
uint32_t maxCooldowns = 512;
|
|
|
|
|
|
uint32_t cooldownCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (packet.getReadPos() + 8 <= packet.getSize() && cooldownCount < maxCooldowns) {
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
uint32_t spellId = packet.readUInt32();
|
|
|
|
|
|
uint32_t cooldownMs = packet.readUInt32();
|
|
|
|
|
|
data.cooldowns.push_back({spellId, cooldownMs});
|
2026-03-11 14:33:02 -07:00
|
|
|
|
cooldownCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cooldownCount >= maxCooldowns && packet.getReadPos() + 8 <= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("Spell cooldowns: capped at ", maxCooldowns, " entries, remaining data ignored");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("Spell cooldowns: ", data.cooldowns.size(), " entries");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 4: Group/Party System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GroupInvitePacket::build(const std::string& playerName) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_INVITE));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeString(playerName);
|
|
|
|
|
|
packet.writeUInt32(0); // unused
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteResponseData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Validate minimum packet size: canAccept(1)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
|
|
|
|
|
LOG_WARNING("SMSG_GROUP_INVITE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.canAccept = packet.readUInt8();
|
2026-03-11 15:08:48 -07:00
|
|
|
|
// Note: inviterName is a string, which is always safe to read even if empty
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.inviterName = packet.readString();
|
|
|
|
|
|
LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", (int)data.canAccept, ")");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GroupAcceptPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_ACCEPT));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt32(0); // unused in 3.3.5a
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GroupDeclinePacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DECLINE));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 00:58:56 -07:00
|
|
|
|
bool GroupListParser::parse(network::Packet& packet, GroupListData& data, bool hasRoles) {
|
|
|
|
|
|
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
|
|
|
|
|
|
|
|
|
|
|
if (rem() < 3) return false;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.groupType = packet.readUInt8();
|
2026-03-10 00:58:56 -07:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
2026-03-10 00:58:56 -07:00
|
|
|
|
// 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
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.readUInt32(); // lfg entry
|
2026-03-10 00:58:56 -07:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 00:58:56 -07:00
|
|
|
|
if (rem() < 12) return false;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.readUInt64(); // group GUID
|
2026-03-10 00:58:56 -07:00
|
|
|
|
packet.readUInt32(); // update counter
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
|
2026-03-10 00:58:56 -07:00
|
|
|
|
if (rem() < 4) return false;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.memberCount = packet.readUInt32();
|
2026-03-10 00:58:56 -07:00
|
|
|
|
if (data.memberCount > 40) {
|
|
|
|
|
|
LOG_WARNING("GroupListParser: implausible memberCount=", data.memberCount, ", clamping");
|
|
|
|
|
|
data.memberCount = 40;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.members.reserve(data.memberCount);
|
|
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < data.memberCount; ++i) {
|
2026-03-10 00:58:56 -07:00
|
|
|
|
if (rem() == 0) break;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
GroupMember member;
|
2026-03-10 00:58:56 -07:00
|
|
|
|
member.name = packet.readString();
|
|
|
|
|
|
if (rem() < 8) break;
|
|
|
|
|
|
member.guid = packet.readUInt64();
|
|
|
|
|
|
if (rem() < 3) break;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
member.isOnline = packet.readUInt8();
|
|
|
|
|
|
member.subGroup = packet.readUInt8();
|
2026-03-10 00:58:56 -07:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.members.push_back(member);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 00:58:56 -07:00
|
|
|
|
if (rem() < 8) {
|
|
|
|
|
|
LOG_INFO("Group list: ", data.memberCount, " members (no leader GUID in packet)");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.leaderGuid = packet.readUInt64();
|
|
|
|
|
|
|
2026-03-10 00:58:56 -07:00
|
|
|
|
if (data.memberCount > 0 && rem() >= 10) {
|
|
|
|
|
|
data.lootMethod = packet.readUInt8();
|
|
|
|
|
|
data.looterGuid = packet.readUInt64();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.lootThreshold = packet.readUInt8();
|
2026-03-10 00:58:56 -07:00
|
|
|
|
// 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();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-03-11 14:38:11 -07:00
|
|
|
|
// Upfront validation: command(4) + name(var) + result(4) = 8 bytes minimum (plus name string)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 8) return false;
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.command = static_cast<PartyCommand>(packet.readUInt32());
|
|
|
|
|
|
data.name = packet.readString();
|
2026-03-11 14:38:11 -07:00
|
|
|
|
|
|
|
|
|
|
// Validate result field exists (4 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 4) {
|
|
|
|
|
|
data.result = static_cast<PartyResult>(0);
|
|
|
|
|
|
return true; // Partial read is acceptable
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.result = static_cast<PartyResult>(packet.readUInt32());
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Party command result: ", (int)data.result);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData& data) {
|
2026-03-11 14:38:11 -07:00
|
|
|
|
// Upfront validation: playerName is a CString (minimum 1 null terminator)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 1) return false;
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.playerName = packet.readString();
|
|
|
|
|
|
LOG_INFO("Group decline from: ", data.playerName);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 5: Loot System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet LootPacket::build(uint64_t targetGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(targetGuid);
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt8(slotIndex);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
|
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM));
|
2026-02-06 18:34:45 -08:00
|
|
|
|
packet.writeUInt8(bagIndex);
|
|
|
|
|
|
packet.writeUInt8(slotIndex);
|
2026-02-08 00:59:40 -08:00
|
|
|
|
packet.writeUInt8(0); // cast count
|
2026-02-26 00:59:07 -08:00
|
|
|
|
packet.writeUInt32(spellId); // spell id from item data
|
|
|
|
|
|
packet.writeUInt64(itemGuid); // full 8-byte GUID
|
2026-02-06 18:34:45 -08:00
|
|
|
|
packet.writeUInt32(0); // glyph index
|
|
|
|
|
|
packet.writeUInt8(0); // cast flags
|
|
|
|
|
|
// SpellCastTargets: self
|
|
|
|
|
|
packet.writeUInt32(0x00);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 06:06:29 -07:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 19:13:38 -08:00
|
|
|
|
network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOEQUIP_ITEM));
|
2026-02-06 19:13:38 -08:00
|
|
|
|
packet.writeUInt8(srcBag);
|
|
|
|
|
|
packet.writeUInt8(srcSlot);
|
2026-02-06 18:34:45 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 14:55:27 -08:00
|
|
|
|
network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_ITEM));
|
2026-02-12 14:55:27 -08:00
|
|
|
|
packet.writeUInt8(dstBag);
|
|
|
|
|
|
packet.writeUInt8(dstSlot);
|
|
|
|
|
|
packet.writeUInt8(srcBag);
|
|
|
|
|
|
packet.writeUInt8(srcSlot);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM));
|
2026-02-12 14:55:27 -08:00
|
|
|
|
packet.writeUInt8(srcSlot);
|
|
|
|
|
|
packet.writeUInt8(dstSlot);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 19:24:44 -08:00
|
|
|
|
network::Packet LootMoneyPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_MONEY));
|
2026-02-06 19:24:44 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_RELEASE));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(lootGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 04:01:07 -07:00
|
|
|
|
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat) {
|
2026-02-18 04:06:14 -08:00
|
|
|
|
data = LootResponseData{};
|
2026-03-13 01:29:21 -07:00
|
|
|
|
size_t avail = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
|
|
|
|
|
|
// 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)");
|
2026-02-18 04:06:14 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.lootGuid = packet.readUInt64();
|
|
|
|
|
|
data.lootType = packet.readUInt8();
|
2026-03-13 01:29:21 -07:00
|
|
|
|
|
|
|
|
|
|
// Short failure packet — no gold/item data follows.
|
|
|
|
|
|
avail = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (avail < 5) {
|
|
|
|
|
|
LOG_DEBUG("LootResponseParser: lootType=", (int)data.lootType, " (empty/failure response)");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.gold = packet.readUInt32();
|
|
|
|
|
|
uint8_t itemCount = packet.readUInt8();
|
|
|
|
|
|
|
2026-03-17 09:04:40 -07:00
|
|
|
|
// 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;
|
2026-03-11 04:01:07 -07:00
|
|
|
|
|
2026-02-18 23:46:11 -08:00
|
|
|
|
auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool {
|
|
|
|
|
|
for (uint8_t i = 0; i < listCount; ++i) {
|
|
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
2026-03-11 04:01:07 -07:00
|
|
|
|
if (remaining < kItemSize) {
|
2026-02-18 04:06:14 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-18 23:46:11 -08:00
|
|
|
|
|
2026-02-18 04:06:14 -08:00
|
|
|
|
LootItem item;
|
2026-03-17 09:04:40 -07:00
|
|
|
|
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;
|
2026-02-18 04:06:14 -08:00
|
|
|
|
data.items.push_back(item);
|
|
|
|
|
|
}
|
2026-02-18 23:46:11 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
data.items.reserve(itemCount);
|
|
|
|
|
|
if (!parseLootItemList(itemCount, false)) {
|
|
|
|
|
|
LOG_WARNING("LootResponseParser: truncated regular item list");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 09:04:40 -07:00
|
|
|
|
// Quest item section only present in WotLK 3.3.5a
|
2026-02-18 23:46:11 -08:00
|
|
|
|
uint8_t questItemCount = 0;
|
2026-03-17 09:04:40 -07:00
|
|
|
|
if (isWotlkFormat && packet.getSize() - packet.getReadPos() >= 1) {
|
2026-02-18 23:46:11 -08:00
|
|
|
|
questItemCount = packet.readUInt8();
|
|
|
|
|
|
data.items.reserve(data.items.size() + questItemCount);
|
|
|
|
|
|
if (!parseLootItemList(questItemCount, true)) {
|
|
|
|
|
|
LOG_WARNING("LootResponseParser: truncated quest item list");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-18 04:06:14 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Loot response: ", (int)itemCount, " regular + ", (int)questItemCount,
|
2026-02-18 04:06:14 -08:00
|
|
|
|
" quest items, ", data.gold, " copper");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 5: NPC Gossip
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_HELLO));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 21:12:54 -08:00
|
|
|
|
network::Packet QuestgiverHelloPacket::build(uint64_t npcGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_HELLO));
|
2026-02-07 21:12:54 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 11:45:35 -08:00
|
|
|
|
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
2026-02-06 11:45:35 -08:00
|
|
|
|
packet.writeUInt32(menuId);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt32(optionId);
|
|
|
|
|
|
if (!code.empty()) {
|
|
|
|
|
|
packet.writeString(code);
|
|
|
|
|
|
}
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 11:45:35 -08:00
|
|
|
|
network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
|
2026-02-06 11:45:35 -08:00
|
|
|
|
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) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
2026-02-06 11:45:35 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
packet.writeUInt32(questId);
|
2026-02-20 23:20:02 -08:00
|
|
|
|
packet.writeUInt32(0); // AzerothCore/WotLK expects trailing unk1
|
2026-02-06 11:45:35 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
|
bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) {
|
2026-02-17 05:27:03 -08:00
|
|
|
|
if (packet.getSize() < 20) return false;
|
2026-02-06 11:59:51 -08:00
|
|
|
|
data.npcGuid = packet.readUInt64();
|
2026-02-17 05:27:03 -08:00
|
|
|
|
|
|
|
|
|
|
// 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();
|
2026-02-06 11:59:51 -08:00
|
|
|
|
/*informUnit*/ packet.readUInt64();
|
|
|
|
|
|
data.questId = packet.readUInt32();
|
2026-02-20 23:20:02 -08:00
|
|
|
|
data.title = normalizeWowTextTokens(packet.readString());
|
2026-02-17 05:27:03 -08:00
|
|
|
|
if (data.title.empty() || data.questId > 100000) {
|
|
|
|
|
|
// Likely vanilla format — rewind past informUnit
|
|
|
|
|
|
packet.setReadPos(preInform);
|
|
|
|
|
|
data.questId = packet.readUInt32();
|
2026-02-20 23:20:02 -08:00
|
|
|
|
data.title = normalizeWowTextTokens(packet.readString());
|
2026-02-17 05:27:03 -08:00
|
|
|
|
}
|
2026-02-20 23:20:02 -08:00
|
|
|
|
data.details = normalizeWowTextTokens(packet.readString());
|
|
|
|
|
|
data.objectives = normalizeWowTextTokens(packet.readString());
|
2026-02-06 12:08:47 -08:00
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 10 > packet.getSize()) {
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Quest details (short): id=", data.questId, " title='", data.title, "'");
|
2026-02-06 12:08:47 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 11:59:51 -08:00
|
|
|
|
/*activateAccept*/ packet.readUInt8();
|
|
|
|
|
|
/*flags*/ packet.readUInt32();
|
|
|
|
|
|
data.suggestedPlayers = packet.readUInt32();
|
|
|
|
|
|
/*isFinished*/ packet.readUInt8();
|
|
|
|
|
|
|
2026-02-06 12:08:47 -08:00
|
|
|
|
// Reward choice items: server always writes 6 entries (QUEST_REWARD_CHOICES_COUNT)
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
|
|
|
|
|
/*choiceCount*/ packet.readUInt32();
|
|
|
|
|
|
for (int i = 0; i < 6; i++) {
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
2026-03-10 19:05:34 -07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-02-06 12:08:47 -08:00
|
|
|
|
}
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 12:08:47 -08:00
|
|
|
|
// Reward items: server always writes 4 entries (QUEST_REWARDS_COUNT)
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize()) {
|
|
|
|
|
|
/*rewardCount*/ packet.readUInt32();
|
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
2026-03-10 19:05:34 -07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-02-06 12:08:47 -08:00
|
|
|
|
}
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 12:08:47 -08:00
|
|
|
|
// Money and XP rewards
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
|
|
|
|
|
data.rewardMoney = packet.readUInt32();
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
2026-02-06 11:59:51 -08:00
|
|
|
|
data.rewardXp = packet.readUInt32();
|
|
|
|
|
|
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Quest details: id=", data.questId, " title='", data.title, "'");
|
2026-02-06 11:59:51 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
|
2026-03-11 14:34:20 -07:00
|
|
|
|
// Upfront validation: npcGuid(8) + menuId(4) + titleTextId(4) + optionCount(4) = 20 bytes minimum
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 20) return false;
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.npcGuid = packet.readUInt64();
|
|
|
|
|
|
data.menuId = packet.readUInt32();
|
|
|
|
|
|
data.titleTextId = packet.readUInt32();
|
|
|
|
|
|
uint32_t optionCount = packet.readUInt32();
|
|
|
|
|
|
|
2026-03-11 14:34:20 -07:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 19:55:32 -08:00
|
|
|
|
data.options.clear();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.options.reserve(optionCount);
|
|
|
|
|
|
for (uint32_t i = 0; i < optionCount; ++i) {
|
2026-03-11 14:34:20 -07:00
|
|
|
|
// 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.getSize() - packet.getReadPos() < 12) {
|
|
|
|
|
|
LOG_WARNING("GossipMessageParser: truncated options at index ", i, "/", optionCount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:34:20 -07:00
|
|
|
|
// Validate questCount field exists (4 bytes)
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 4) {
|
|
|
|
|
|
LOG_DEBUG("Gossip: ", data.options.size(), " options (no quest data)");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
uint32_t questCount = packet.readUInt32();
|
2026-03-11 14:34:20 -07:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 19:55:32 -08:00
|
|
|
|
data.quests.clear();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.quests.reserve(questCount);
|
|
|
|
|
|
for (uint32_t i = 0; i < questCount; ++i) {
|
2026-03-11 14:34:20 -07:00
|
|
|
|
// 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.getSize() - packet.getReadPos() < 18) {
|
|
|
|
|
|
LOG_WARNING("GossipMessageParser: truncated quests at index ", i, "/", questCount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
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();
|
2026-02-20 23:20:02 -08:00
|
|
|
|
quest.title = normalizeWowTextTokens(packet.readString());
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.quests.push_back(quest);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:34:20 -07:00
|
|
|
|
LOG_DEBUG("Gossip: ", data.options.size(), " options, ", data.quests.size(), " quests");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 03:32:00 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Bind Point (Hearthstone)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet BinderActivatePacket::build(uint64_t npcGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet pkt(wireOpcode(Opcode::CMSG_BINDER_ACTIVATE));
|
2026-02-08 03:32:00 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 21:50:15 -08:00
|
|
|
|
bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsData& data) {
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 20) return false;
|
|
|
|
|
|
data.npcGuid = packet.readUInt64();
|
|
|
|
|
|
data.questId = packet.readUInt32();
|
2026-02-20 23:20:02 -08:00
|
|
|
|
data.title = normalizeWowTextTokens(packet.readString());
|
|
|
|
|
|
data.completionText = normalizeWowTextTokens(packet.readString());
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-02-17 17:15:48 -08:00
|
|
|
|
if (packet.getReadPos() + 9 > packet.getSize()) {
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Quest request items (short): id=", data.questId, " title='", data.title, "'");
|
2026-02-06 21:50:15 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 01:12:14 -08:00
|
|
|
|
struct ParsedTail {
|
|
|
|
|
|
uint32_t requiredMoney = 0;
|
|
|
|
|
|
uint32_t completableFlags = 0;
|
|
|
|
|
|
std::vector<QuestRewardItem> requiredItems;
|
|
|
|
|
|
bool ok = false;
|
|
|
|
|
|
int score = -1;
|
|
|
|
|
|
};
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
|
auto parseTail = [&](size_t startPos, size_t prefixSkip) -> ParsedTail {
|
2026-02-19 01:12:14 -08:00
|
|
|
|
ParsedTail out;
|
|
|
|
|
|
packet.setReadPos(startPos);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
|
if (packet.getReadPos() + prefixSkip > packet.getSize()) return out;
|
|
|
|
|
|
packet.setReadPos(packet.getReadPos() + prefixSkip);
|
2026-02-19 01:12:14 -08:00
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) 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.getReadPos() + 12 > packet.getSize()) 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.getReadPos() + 4 > packet.getSize()) 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;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
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.getSize() - packet.getReadPos();
|
|
|
|
|
|
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;
|
2026-02-19 01:12:14 -08:00
|
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
size_t tailStart = packet.getReadPos();
|
2026-02-19 16:17:06 -08:00
|
|
|
|
std::vector<ParsedTail> candidates;
|
|
|
|
|
|
candidates.reserve(25);
|
|
|
|
|
|
for (size_t skip = 0; skip <= 24; ++skip) {
|
|
|
|
|
|
candidates.push_back(parseTail(tailStart, skip));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 01:12:14 -08:00
|
|
|
|
const ParsedTail* chosen = nullptr;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
for (const auto& cand : candidates) {
|
|
|
|
|
|
if (!cand.ok) continue;
|
|
|
|
|
|
if (!chosen || cand.score > chosen->score) chosen = &cand;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!chosen) {
|
2026-02-19 01:12:14 -08:00
|
|
|
|
return true;
|
2026-02-06 21:50:15 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 01:12:14 -08:00
|
|
|
|
data.requiredMoney = chosen->requiredMoney;
|
|
|
|
|
|
data.completableFlags = chosen->completableFlags;
|
|
|
|
|
|
data.requiredItems = chosen->requiredItems;
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Quest request items: id=", data.questId, " title='", data.title,
|
2026-02-06 21:50:15 -08:00
|
|
|
|
"' items=", data.requiredItems.size(), " completable=", data.isCompletable());
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData& data) {
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 20) return false;
|
|
|
|
|
|
data.npcGuid = packet.readUInt64();
|
|
|
|
|
|
data.questId = packet.readUInt32();
|
2026-02-20 23:20:02 -08:00
|
|
|
|
data.title = normalizeWowTextTokens(packet.readString());
|
|
|
|
|
|
data.rewardText = normalizeWowTextTokens(packet.readString());
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-11 01:00:08 -07:00
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) {
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Quest offer reward (short): id=", data.questId, " title='", data.title, "'");
|
2026-02-06 21:50:15 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 01:00:08 -07:00
|
|
|
|
// 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.
|
|
|
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
|
struct ParsedTail {
|
|
|
|
|
|
uint32_t rewardMoney = 0;
|
|
|
|
|
|
uint32_t rewardXp = 0;
|
|
|
|
|
|
std::vector<QuestRewardItem> choiceRewards;
|
|
|
|
|
|
std::vector<QuestRewardItem> fixedRewards;
|
|
|
|
|
|
bool ok = false;
|
|
|
|
|
|
int score = -1000;
|
2026-03-11 01:00:08 -07:00
|
|
|
|
size_t prefixSkip = 0;
|
|
|
|
|
|
bool fixedArrays = false;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-11 01:00:08 -07:00
|
|
|
|
auto parseTail = [&](size_t startPos, size_t prefixSkip, bool fixedArrays) -> ParsedTail {
|
2026-02-19 16:17:06 -08:00
|
|
|
|
ParsedTail out;
|
2026-03-11 01:00:08 -07:00
|
|
|
|
out.prefixSkip = prefixSkip;
|
|
|
|
|
|
out.fixedArrays = fixedArrays;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
packet.setReadPos(startPos);
|
|
|
|
|
|
|
2026-03-11 01:00:08 -07:00
|
|
|
|
// Skip the prefix bytes (autoFinish + optional suggestedPlayers before emoteCount)
|
|
|
|
|
|
if (packet.getReadPos() + prefixSkip > packet.getSize()) return out;
|
|
|
|
|
|
packet.setReadPos(packet.getReadPos() + prefixSkip);
|
2026-02-19 16:17:06 -08:00
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
|
|
|
|
|
uint32_t emoteCount = packet.readUInt32();
|
2026-03-11 01:00:08 -07:00
|
|
|
|
if (emoteCount > 32) return out; // guard against misalignment
|
2026-02-19 16:17:06 -08:00
|
|
|
|
for (uint32_t i = 0; i < emoteCount; ++i) {
|
|
|
|
|
|
if (packet.getReadPos() + 8 > packet.getSize()) return out;
|
|
|
|
|
|
packet.readUInt32(); // delay
|
2026-03-11 01:00:08 -07:00
|
|
|
|
packet.readUInt32(); // emote type
|
2026-02-19 16:17:06 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) 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.getReadPos() + 12 > packet.getSize()) 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);
|
2026-03-11 01:00:08 -07:00
|
|
|
|
++nonZeroChoice;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) 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.getReadPos() + 12 > packet.getSize()) 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);
|
2026-03-11 01:00:08 -07:00
|
|
|
|
++nonZeroFixed;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
|
|
|
|
|
out.rewardMoney = packet.readUInt32();
|
|
|
|
|
|
if (packet.getReadPos() + 4 <= packet.getSize())
|
|
|
|
|
|
out.rewardXp = packet.readUInt32();
|
|
|
|
|
|
|
|
|
|
|
|
out.ok = true;
|
|
|
|
|
|
out.score = 0;
|
2026-03-11 01:00:08 -07:00
|
|
|
|
// 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
|
2026-02-19 16:17:06 -08:00
|
|
|
|
if (choiceCount <= 6) out.score += 3;
|
|
|
|
|
|
if (rewardCount <= 4) out.score += 3;
|
2026-03-11 01:00:08 -07:00
|
|
|
|
// 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)
|
2026-02-19 16:17:06 -08:00
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
2026-03-11 01:00:08 -07:00
|
|
|
|
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
|
2026-02-19 16:17:06 -08:00
|
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
size_t tailStart = packet.getReadPos();
|
2026-03-11 01:00:08 -07:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-02-19 16:17:06 -08:00
|
|
|
|
|
|
|
|
|
|
const ParsedTail* best = nullptr;
|
2026-03-11 01:00:08 -07:00
|
|
|
|
for (const auto& cand : candidates) {
|
|
|
|
|
|
if (!cand.ok) continue;
|
|
|
|
|
|
if (!best || cand.score > best->score) best = &cand;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (best) {
|
|
|
|
|
|
data.choiceRewards = best->choiceRewards;
|
2026-03-11 01:00:08 -07:00
|
|
|
|
data.fixedRewards = best->fixedRewards;
|
|
|
|
|
|
data.rewardMoney = best->rewardMoney;
|
|
|
|
|
|
data.rewardXp = best->rewardXp;
|
2026-02-19 16:17:06 -08:00
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Quest offer reward: id=", data.questId, " title='", data.title,
|
2026-03-11 01:00:08 -07:00
|
|
|
|
"' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size(),
|
|
|
|
|
|
" prefix=", (best ? best->prefixSkip : size_t(0)),
|
|
|
|
|
|
(best && best->fixedArrays ? " fixed" : " var"));
|
2026-02-06 21:50:15 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet QuestgiverCompleteQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_COMPLETE_QUEST));
|
2026-02-06 21:50:15 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
packet.writeUInt32(questId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 01:12:14 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 21:50:15 -08:00
|
|
|
|
network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t questId, uint32_t rewardIndex) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
|
2026-02-06 21:50:15 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
packet.writeUInt32(questId);
|
|
|
|
|
|
packet.writeUInt32(rewardIndex);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Phase 5: Vendor
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LIST_INVENTORY));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
|
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_BUY_ITEM));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(vendorGuid);
|
2026-02-17 17:44:48 -08:00
|
|
|
|
packet.writeUInt32(itemId); // item entry
|
2026-02-19 16:17:06 -08:00
|
|
|
|
packet.writeUInt32(slot); // vendor slot index from SMSG_LIST_INVENTORY
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
packet.writeUInt32(count);
|
2026-03-11 04:49:18 -07:00
|
|
|
|
// 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.
|
2026-02-19 16:17:06 -08:00
|
|
|
|
packet.writeUInt8(0);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 19:50:22 -08:00
|
|
|
|
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SELL_ITEM));
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
packet.writeUInt64(vendorGuid);
|
|
|
|
|
|
packet.writeUInt64(itemGuid);
|
2026-02-06 19:50:22 -08:00
|
|
|
|
packet.writeUInt32(count);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) {
|
2026-02-18 03:40:59 -08:00
|
|
|
|
data = ListInventoryData{};
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 9) {
|
|
|
|
|
|
LOG_WARNING("ListInventoryParser: packet too short");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.vendorGuid = packet.readUInt64();
|
|
|
|
|
|
uint8_t itemCount = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (itemCount == 0) {
|
|
|
|
|
|
LOG_INFO("Vendor has nothing for sale");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-17 18:08:00 -08:00
|
|
|
|
// 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.getSize() - packet.getReadPos();
|
2026-02-18 03:40:59 -08:00
|
|
|
|
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=", (int)itemCount,
|
|
|
|
|
|
", remaining=", remaining, ")");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (remaining >= static_cast<size_t>(itemCount) * bytesPerItemWithExt) {
|
|
|
|
|
|
hasExtendedCost = true;
|
|
|
|
|
|
}
|
2026-02-17 18:08:00 -08:00
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.items.reserve(itemCount);
|
|
|
|
|
|
for (uint8_t i = 0; i < itemCount; ++i) {
|
2026-02-18 03:40:59 -08:00
|
|
|
|
const size_t perItemBytes = hasExtendedCost ? bytesPerItemWithExt : bytesPerItemNoExt;
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < perItemBytes) {
|
|
|
|
|
|
LOG_WARNING("ListInventoryParser: item ", (int)i, " truncated");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
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();
|
2026-02-17 18:08:00 -08:00
|
|
|
|
item.extendedCost = hasExtendedCost ? packet.readUInt32() : 0;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
data.items.push_back(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 05:09:43 -07:00
|
|
|
|
LOG_DEBUG("Vendor inventory: ", (int)itemCount, " items (extendedCost: ", hasExtendedCost ? "yes" : "no", ")");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 14:33:39 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Trainer
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-10 01:20:41 -07:00
|
|
|
|
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
|
2026-02-08 14:33:39 -08:00
|
|
|
|
data = TrainerListData{};
|
2026-03-11 14:44:52 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 16) return false; // guid(8) + type(4) + count(4)
|
|
|
|
|
|
|
2026-02-08 14:33:39 -08:00
|
|
|
|
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) {
|
2026-03-11 14:44:52 -07:00
|
|
|
|
// Validate minimum entry size before reading
|
|
|
|
|
|
const size_t minEntrySize = isClassic ? 34 : 38;
|
|
|
|
|
|
if (packet.getReadPos() + minEntrySize > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("TrainerListParser: truncated at spell ", i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 14:33:39 -08:00
|
|
|
|
TrainerSpell spell;
|
2026-03-10 01:20:41 -07:00
|
|
|
|
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();
|
2026-02-08 14:33:39 -08:00
|
|
|
|
spell.reqSkillValue = packet.readUInt32();
|
2026-03-10 01:20:41 -07:00
|
|
|
|
spell.chainNode1 = packet.readUInt32();
|
|
|
|
|
|
spell.chainNode2 = packet.readUInt32();
|
|
|
|
|
|
spell.chainNode3 = packet.readUInt32();
|
|
|
|
|
|
if (isClassic) {
|
|
|
|
|
|
packet.readUInt32(); // trailing unk / sort index
|
|
|
|
|
|
}
|
2026-02-08 14:33:39 -08:00
|
|
|
|
data.spells.push_back(spell);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:44:52 -07:00
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("TrainerListParser: truncated before greeting");
|
|
|
|
|
|
data.greeting.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.greeting = packet.readString();
|
|
|
|
|
|
}
|
2026-02-08 14:33:39 -08:00
|
|
|
|
|
2026-03-10 01:20:41 -07:00
|
|
|
|
LOG_INFO("Trainer list (", isClassic ? "Classic" : "TBC/WotLK", "): ",
|
|
|
|
|
|
spellCount, " spells, type=", data.trainerType,
|
2026-02-08 14:33:39 -08:00
|
|
|
|
", greeting=\"", data.greeting, "\"");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spellId) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_TRAINER_BUY_SPELL));
|
2026-02-08 14:33:39 -08:00
|
|
|
|
packet.writeUInt64(trainerGuid);
|
|
|
|
|
|
packet.writeUInt32(spellId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Talents
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
|
2026-02-10 13:16:38 -08:00
|
|
|
|
// 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, ")");
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:16:38 -08:00
|
|
|
|
data = TalentsInfoData{};
|
|
|
|
|
|
|
|
|
|
|
|
// Read header
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
data.talentSpec = packet.readUInt8();
|
|
|
|
|
|
data.unspentPoints = packet.readUInt8();
|
2026-02-10 13:16:38 -08:00
|
|
|
|
|
|
|
|
|
|
// These two counts are big-endian (network byte order)
|
|
|
|
|
|
uint32_t talentCountBE = packet.readUInt32();
|
2026-02-23 16:30:49 +01:00
|
|
|
|
uint32_t talentCount = bswap32(talentCountBE);
|
2026-02-10 13:16:38 -08:00
|
|
|
|
|
|
|
|
|
|
uint16_t entryCountBE = packet.readUInt16();
|
2026-02-23 16:30:49 +01:00
|
|
|
|
uint16_t entryCount = bswap16(entryCountBE);
|
2026-02-10 13:16:38 -08:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_TALENTS_INFO: spec=", (int)data.talentSpec,
|
2026-02-10 13:16:38 -08:00
|
|
|
|
" unspent=", (int)data.unspentPoints,
|
|
|
|
|
|
" talentCount=", talentCount,
|
|
|
|
|
|
" entryCount=", entryCount);
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
|
2026-02-10 13:16:38 -08:00
|
|
|
|
// 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) {
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 5) {
|
2026-02-10 13:16:38 -08:00
|
|
|
|
LOG_ERROR("SMSG_TALENTS_INFO: truncated entry list at i=", i);
|
|
|
|
|
|
return false;
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
}
|
2026-02-10 13:16:38 -08:00
|
|
|
|
uint32_t id = packet.readUInt32(); // LE
|
|
|
|
|
|
uint8_t rank = packet.readUInt8();
|
|
|
|
|
|
data.talents.push_back({id, rank});
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
|
2026-02-10 13:16:38 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
|
2026-02-10 13:16:38 -08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:16:38 -08:00
|
|
|
|
LOG_INFO("SMSG_TALENTS_INFO: bytesConsumed=", (packet.getReadPos() - startPos),
|
|
|
|
|
|
" bytesRemaining=", (packet.getSize() - packet.getReadPos()));
|
|
|
|
|
|
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet LearnTalentPacket::build(uint32_t talentId, uint32_t requestedRank) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_LEARN_TALENT));
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
packet.writeUInt32(talentId);
|
|
|
|
|
|
packet.writeUInt32(requestedRank);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet TalentWipeConfirmPacket::build(bool accept) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::MSG_TALENT_WIPE_CONFIRM));
|
Implement complete talent system with dual spec support
Network Protocol:
- Add SMSG_TALENTS_INFO (0x4C0) packet parsing for talent data
- Add CMSG_LEARN_TALENT (0x251) to request learning talents
- Add MSG_TALENT_WIPE_CONFIRM (0x2AB) opcode for spec switching
- Parse talent spec, unspent points, and learned talent ranks
DBC Parsing:
- Load Talent.dbc: talent grid positions, ranks, prerequisites, spell IDs
- Load TalentTab.dbc: talent tree definitions with correct field indices
- Fix localized string field handling (17 fields per string)
- Load Spell.dbc and SpellIcon.dbc for talent icons and tooltips
- Class mask filtering using bitwise operations (1 << (class - 1))
UI Implementation:
- Complete talent tree UI with tabbed interface for specs
- Display talent icons from spell data with proper tinting/borders
- Enhanced tooltips: spell name, rank, current/next descriptions, prereqs
- Visual states: green (maxed), yellow (partial), white (available), gray (locked)
- Tier unlock system (5 points per tier requirement)
- Rank overlay on icons with shadow text
- Click to learn talents with validation
Dual Spec Support:
- Store unspent points and learned talents per spec (0 and 1)
- Track active spec and display its talents
- Spec switching UI with buttons for Spec 1/Spec 2
- Handle both SMSG_TALENTS_INFO packets from server at login
- Display unspent points for both specs in header
- Independent talent trees for each specialization
2026-02-10 02:00:13 -08:00
|
|
|
|
packet.writeUInt32(accept ? 1 : 0);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 22:49:23 -07:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 17:27:20 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Death/Respawn
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet RepopRequestPacket::build() {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_REPOP_REQUEST));
|
2026-02-07 21:47:14 -08:00
|
|
|
|
packet.writeUInt8(1); // request release (1 = manual)
|
2026-02-06 17:27:20 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 06:43:49 -07:00
|
|
|
|
network::Packet ReclaimCorpsePacket::build(uint64_t guid) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE));
|
|
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
|
network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
|
2026-02-06 18:34:45 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 21:47:14 -08:00
|
|
|
|
network::Packet ResurrectResponsePacket::build(uint64_t casterGuid, bool accept) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_RESURRECT_RESPONSE));
|
2026-02-07 21:47:14 -08:00
|
|
|
|
packet.writeUInt64(casterGuid);
|
|
|
|
|
|
packet.writeUInt8(accept ? 1 : 0);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 16:59:20 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Taxi / Flight Paths
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data) {
|
2026-02-14 16:47:21 -08:00
|
|
|
|
// Minimum: windowInfo(4) + npcGuid(8) + nearestNode(4) + at least 1 mask uint32(4)
|
|
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (remaining < 4 + 8 + 4 + 4) {
|
|
|
|
|
|
LOG_ERROR("ShowTaxiNodesParser: packet too short (", remaining, " bytes)");
|
2026-02-07 16:59:20 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.windowInfo = packet.readUInt32();
|
|
|
|
|
|
data.npcGuid = packet.readUInt64();
|
|
|
|
|
|
data.nearestNode = packet.readUInt32();
|
2026-02-14 16:47:21 -08:00
|
|
|
|
// Read as many mask uint32s as available (Classic/Vanilla=4, WotLK=12)
|
|
|
|
|
|
size_t maskBytes = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
uint32_t maskCount = static_cast<uint32_t>(maskBytes / 4);
|
|
|
|
|
|
if (maskCount > TLK_TAXI_MASK_SIZE) maskCount = TLK_TAXI_MASK_SIZE;
|
|
|
|
|
|
for (uint32_t i = 0; i < maskCount; ++i) {
|
2026-02-07 16:59:20 -08:00
|
|
|
|
data.nodeMask[i] = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("ShowTaxiNodes: window=", data.windowInfo, " npc=0x", std::hex, data.npcGuid, std::dec,
|
2026-02-14 16:47:21 -08:00
|
|
|
|
" nearest=", data.nearestNode, " maskSlots=", maskCount);
|
2026-02-07 16:59:20 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyData& data) {
|
2026-02-08 03:05:38 -08:00
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (remaining >= 4) {
|
|
|
|
|
|
data.result = packet.readUInt32();
|
|
|
|
|
|
} else if (remaining >= 1) {
|
|
|
|
|
|
data.result = packet.readUInt8();
|
|
|
|
|
|
} else {
|
2026-02-07 16:59:20 -08:00
|
|
|
|
LOG_ERROR("ActivateTaxiReplyParser: packet too short");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("ActivateTaxiReply: result=", data.result);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 03:05:38 -08:00
|
|
|
|
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXIEXPRESS));
|
2026-02-07 16:59:20 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
2026-02-08 03:05:38 -08:00
|
|
|
|
packet.writeUInt32(totalCost);
|
2026-02-07 16:59:20 -08:00
|
|
|
|
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
|
|
|
|
|
|
for (uint32_t nodeId : pathNodes) {
|
|
|
|
|
|
packet.writeUInt32(nodeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_INFO("ActivateTaxiExpress: npc=0x", std::hex, npcGuid, std::dec,
|
2026-02-08 03:05:38 -08:00
|
|
|
|
" cost=", totalCost, " nodes=", pathNodes.size());
|
2026-02-07 16:59:20 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 19:44:03 -08:00
|
|
|
|
network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode) {
|
2026-02-12 22:56:36 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXI));
|
2026-02-07 19:44:03 -08:00
|
|
|
|
packet.writeUInt64(npcGuid);
|
|
|
|
|
|
packet.writeUInt32(srcNode);
|
|
|
|
|
|
packet.writeUInt32(destNode);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GameObjectUsePacket::build(uint64_t guid) {
|
2026-02-20 02:50:59 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJ_USE));
|
2026-02-07 19:44:03 -08:00
|
|
|
|
packet.writeUInt64(guid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 14:00:41 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Mail System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GetMailListPacket::build(uint64_t mailboxGuid) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_GET_MAIL_LIST));
|
|
|
|
|
|
packet.writeUInt64(mailboxGuid);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet SendMailPacket::build(uint64_t mailboxGuid, const std::string& recipient,
|
|
|
|
|
|
const std::string& subject, const std::string& body,
|
2026-02-25 14:11:09 -08:00
|
|
|
|
uint32_t money, uint32_t cod,
|
|
|
|
|
|
const std::vector<uint64_t>& itemGuids) {
|
2026-02-16 18:46:44 -08:00
|
|
|
|
// WotLK 3.3.5a format
|
2026-02-15 14:00:41 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
|
|
|
|
|
|
packet.writeUInt64(mailboxGuid);
|
|
|
|
|
|
packet.writeString(recipient);
|
|
|
|
|
|
packet.writeString(subject);
|
|
|
|
|
|
packet.writeString(body);
|
2026-02-16 18:46:44 -08:00
|
|
|
|
packet.writeUInt32(0); // stationery
|
2026-02-15 14:00:41 -08:00
|
|
|
|
packet.writeUInt32(0); // unknown
|
2026-02-25 14:11:09 -08:00
|
|
|
|
uint8_t attachCount = static_cast<uint8_t>(itemGuids.size());
|
|
|
|
|
|
packet.writeUInt8(attachCount);
|
|
|
|
|
|
for (uint8_t i = 0; i < attachCount; ++i) {
|
|
|
|
|
|
packet.writeUInt8(i); // attachment slot index
|
|
|
|
|
|
packet.writeUInt64(itemGuids[i]);
|
|
|
|
|
|
}
|
2026-02-15 14:00:41 -08:00
|
|
|
|
packet.writeUInt32(money);
|
|
|
|
|
|
packet.writeUInt32(cod);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_MONEY));
|
|
|
|
|
|
packet.writeUInt64(mailboxGuid);
|
|
|
|
|
|
packet.writeUInt32(mailId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 07:11:18 -07:00
|
|
|
|
network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) {
|
2026-02-15 14:00:41 -08:00
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
|
|
|
|
|
|
packet.writeUInt64(mailboxGuid);
|
|
|
|
|
|
packet.writeUInt32(mailId);
|
2026-03-14 07:11:18 -07:00
|
|
|
|
// WotLK expects attachment item GUID low, not attachment slot index.
|
|
|
|
|
|
packet.writeUInt32(itemGuidLow);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet MailDeletePacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE));
|
|
|
|
|
|
packet.writeUInt64(mailboxGuid);
|
|
|
|
|
|
packet.writeUInt32(mailId);
|
|
|
|
|
|
packet.writeUInt32(mailTemplateId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailId) {
|
|
|
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_MARK_AS_READ));
|
|
|
|
|
|
packet.writeUInt64(mailboxGuid);
|
|
|
|
|
|
packet.writeUInt32(mailId);
|
|
|
|
|
|
return packet;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 18:46:44 -08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// PacketParsers::parseMailList — WotLK 3.3.5a format (base/default)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
bool PacketParsers::parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) {
|
|
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (remaining < 5) return false;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t totalCount = packet.readUInt32();
|
|
|
|
|
|
uint8_t shownCount = packet.readUInt8();
|
|
|
|
|
|
(void)totalCount;
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("SMSG_MAIL_LIST_RESULT (WotLK): total=", totalCount, " shown=", (int)shownCount);
|
|
|
|
|
|
|
|
|
|
|
|
inbox.clear();
|
|
|
|
|
|
inbox.reserve(shownCount);
|
|
|
|
|
|
|
|
|
|
|
|
for (uint8_t i = 0; i < shownCount; ++i) {
|
|
|
|
|
|
remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (remaining < 2) break;
|
|
|
|
|
|
|
|
|
|
|
|
uint16_t msgSize = packet.readUInt16();
|
|
|
|
|
|
size_t startPos = packet.getReadPos();
|
|
|
|
|
|
|
|
|
|
|
|
MailMessage msg;
|
|
|
|
|
|
if (remaining < static_cast<size_t>(msgSize) + 2) {
|
|
|
|
|
|
LOG_WARNING("Mail entry ", i, " truncated");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
msg.messageId = packet.readUInt32();
|
|
|
|
|
|
msg.messageType = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
switch (msg.messageType) {
|
|
|
|
|
|
case 0: msg.senderGuid = packet.readUInt64(); break;
|
|
|
|
|
|
case 2: case 3: case 4: case 5:
|
|
|
|
|
|
msg.senderEntry = packet.readUInt32(); break;
|
|
|
|
|
|
default: msg.senderEntry = packet.readUInt32(); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
msg.cod = packet.readUInt32();
|
|
|
|
|
|
packet.readUInt32(); // item text id
|
|
|
|
|
|
packet.readUInt32(); // unknown
|
|
|
|
|
|
msg.stationeryId = packet.readUInt32();
|
|
|
|
|
|
msg.money = packet.readUInt32();
|
|
|
|
|
|
msg.flags = packet.readUInt32();
|
|
|
|
|
|
msg.expirationTime = packet.readFloat();
|
|
|
|
|
|
msg.mailTemplateId = packet.readUInt32();
|
|
|
|
|
|
msg.subject = packet.readString();
|
2026-03-14 07:14:15 -07:00
|
|
|
|
// WotLK 3.3.5a always includes body text in SMSG_MAIL_LIST_RESULT.
|
|
|
|
|
|
// mailTemplateId != 0 still carries a (possibly empty) body string.
|
|
|
|
|
|
msg.body = packet.readString();
|
2026-02-16 18:46:44 -08:00
|
|
|
|
|
|
|
|
|
|
uint8_t attachCount = packet.readUInt8();
|
|
|
|
|
|
msg.attachments.reserve(attachCount);
|
|
|
|
|
|
for (uint8_t j = 0; j < attachCount; ++j) {
|
|
|
|
|
|
MailAttachment att;
|
|
|
|
|
|
att.slot = packet.readUInt8();
|
|
|
|
|
|
att.itemGuidLow = packet.readUInt32();
|
|
|
|
|
|
att.itemId = packet.readUInt32();
|
|
|
|
|
|
for (int e = 0; e < 7; ++e) {
|
|
|
|
|
|
uint32_t enchId = packet.readUInt32();
|
|
|
|
|
|
packet.readUInt32(); // duration
|
|
|
|
|
|
packet.readUInt32(); // charges
|
|
|
|
|
|
if (e == 0) att.enchantId = enchId;
|
|
|
|
|
|
}
|
|
|
|
|
|
att.randomPropertyId = packet.readUInt32();
|
|
|
|
|
|
att.randomSuffix = packet.readUInt32();
|
|
|
|
|
|
att.stackCount = packet.readUInt32();
|
|
|
|
|
|
att.chargesOrDurability = packet.readUInt32();
|
|
|
|
|
|
att.maxDurability = packet.readUInt32();
|
2026-03-14 07:14:15 -07:00
|
|
|
|
packet.readUInt32(); // durability/current durability
|
|
|
|
|
|
packet.readUInt8(); // unknown WotLK trailing byte per attachment
|
2026-02-16 18:46:44 -08:00
|
|
|
|
msg.attachments.push_back(att);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
msg.read = (msg.flags & 0x01) != 0;
|
|
|
|
|
|
inbox.push_back(std::move(msg));
|
|
|
|
|
|
|
|
|
|
|
|
// Skip unread bytes
|
|
|
|
|
|
size_t consumed = packet.getReadPos() - startPos;
|
|
|
|
|
|
if (consumed < msgSize) {
|
|
|
|
|
|
size_t skip = msgSize - consumed;
|
|
|
|
|
|
for (size_t s = 0; s < skip && packet.getReadPos() < packet.getSize(); ++s)
|
|
|
|
|
|
packet.readUInt8();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Parsed ", inbox.size(), " mail messages");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Bank System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet BankerActivatePacket::build(uint64_t guid) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_BANKER_ACTIVATE));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet BuyBankSlotPacket::build(uint64_t guid) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_BUY_BANK_SLOT));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AutoBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUTOBANK_ITEM));
|
|
|
|
|
|
p.writeUInt8(srcBag);
|
|
|
|
|
|
p.writeUInt8(srcSlot);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AutoStoreBankItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUTOSTORE_BANK_ITEM));
|
|
|
|
|
|
p.writeUInt8(srcBag);
|
|
|
|
|
|
p.writeUInt8(srcSlot);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Guild Bank System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankerActivatePacket::build(uint64_t guid) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANKER_ACTIVATE));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt8(0); // full slots update
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankQueryTabPacket::build(uint64_t guid, uint8_t tabId, bool fullUpdate) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_QUERY_TAB));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt8(tabId);
|
|
|
|
|
|
p.writeUInt8(fullUpdate ? 1 : 0);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankBuyTabPacket::build(uint64_t guid, uint8_t tabId) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_BUY_TAB));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt8(tabId);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankDepositMoneyPacket::build(uint64_t guid, uint32_t amount) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_DEPOSIT_MONEY));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt32(amount);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankWithdrawMoneyPacket::build(uint64_t guid, uint32_t amount) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_WITHDRAW_MONEY));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt32(amount);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankSwapItemsPacket::buildBankToInventory(
|
|
|
|
|
|
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
|
|
|
|
|
|
uint8_t destBag, uint8_t destSlot, uint32_t splitCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt8(0); // bankToCharacter = false -> bank source
|
|
|
|
|
|
p.writeUInt8(tabId);
|
|
|
|
|
|
p.writeUInt8(bankSlot);
|
|
|
|
|
|
p.writeUInt32(0); // itemEntry (unused client side)
|
|
|
|
|
|
p.writeUInt8(0); // autoStore = false
|
|
|
|
|
|
if (splitCount > 0) {
|
|
|
|
|
|
p.writeUInt8(splitCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
p.writeUInt8(destBag);
|
|
|
|
|
|
p.writeUInt8(destSlot);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet GuildBankSwapItemsPacket::buildInventoryToBank(
|
|
|
|
|
|
uint64_t guid, uint8_t tabId, uint8_t bankSlot,
|
|
|
|
|
|
uint8_t srcBag, uint8_t srcSlot, uint32_t splitCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_GUILD_BANK_SWAP_ITEMS));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt8(1); // bankToCharacter = true -> char to bank
|
|
|
|
|
|
p.writeUInt8(tabId);
|
|
|
|
|
|
p.writeUInt8(bankSlot);
|
|
|
|
|
|
p.writeUInt32(0); // itemEntry
|
|
|
|
|
|
p.writeUInt8(0); // autoStore
|
|
|
|
|
|
if (splitCount > 0) {
|
|
|
|
|
|
p.writeUInt8(splitCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
p.writeUInt8(srcBag);
|
|
|
|
|
|
p.writeUInt8(srcSlot);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) {
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 14) return false;
|
|
|
|
|
|
|
|
|
|
|
|
data.money = packet.readUInt64();
|
|
|
|
|
|
data.tabId = packet.readUInt8();
|
|
|
|
|
|
data.withdrawAmount = static_cast<int32_t>(packet.readUInt32());
|
|
|
|
|
|
uint8_t fullUpdate = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
|
|
if (fullUpdate) {
|
2026-03-11 14:43:03 -07:00
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated before tabCount");
|
|
|
|
|
|
data.tabs.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uint8_t tabCount = packet.readUInt8();
|
|
|
|
|
|
// Cap at 8 (normal guild bank tab limit in WoW)
|
|
|
|
|
|
if (tabCount > 8) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: tabCount capped (requested=", (int)tabCount, ")");
|
|
|
|
|
|
tabCount = 8;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.tabs.resize(tabCount);
|
|
|
|
|
|
for (uint8_t i = 0; i < tabCount; ++i) {
|
|
|
|
|
|
// Validate before reading strings
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated tab at index ", (int)i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.tabs[i].tabName = packet.readString();
|
|
|
|
|
|
if (packet.getReadPos() >= packet.getSize()) {
|
|
|
|
|
|
data.tabs[i].tabIcon.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.tabs[i].tabIcon = packet.readString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:43:03 -07:00
|
|
|
|
if (packet.getReadPos() + 1 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated before numSlots");
|
|
|
|
|
|
data.tabItems.clear();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
uint8_t numSlots = packet.readUInt8();
|
|
|
|
|
|
data.tabItems.clear();
|
|
|
|
|
|
for (uint8_t i = 0; i < numSlots; ++i) {
|
2026-03-11 14:43:03 -07:00
|
|
|
|
// Validate minimum bytes before reading slot (slotId(1) + itemEntry(4) = 5)
|
|
|
|
|
|
if (packet.getReadPos() + 5 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated slot at index ", (int)i);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
GuildBankItemSlot slot;
|
|
|
|
|
|
slot.slotId = packet.readUInt8();
|
|
|
|
|
|
slot.itemEntry = packet.readUInt32();
|
|
|
|
|
|
if (slot.itemEntry != 0) {
|
2026-03-11 14:43:03 -07:00
|
|
|
|
// Validate before reading enchant mask
|
|
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) break;
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
// Enchant info
|
|
|
|
|
|
uint32_t enchantMask = packet.readUInt32();
|
|
|
|
|
|
for (int bit = 0; bit < 10; ++bit) {
|
|
|
|
|
|
if (enchantMask & (1u << bit)) {
|
2026-03-11 14:43:03 -07:00
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated enchant data");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
uint32_t enchId = packet.readUInt32();
|
|
|
|
|
|
uint32_t enchDur = packet.readUInt32();
|
|
|
|
|
|
uint32_t enchCharges = packet.readUInt32();
|
|
|
|
|
|
if (bit == 0) slot.enchantId = enchId;
|
|
|
|
|
|
(void)enchDur; (void)enchCharges;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 14:43:03 -07:00
|
|
|
|
// Validate before reading remaining item fields
|
|
|
|
|
|
if (packet.getReadPos() + 12 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated item fields");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
slot.stackCount = packet.readUInt32();
|
|
|
|
|
|
/*spare=*/ packet.readUInt32();
|
|
|
|
|
|
slot.randomPropertyId = packet.readUInt32();
|
|
|
|
|
|
if (slot.randomPropertyId) {
|
2026-03-11 14:43:03 -07:00
|
|
|
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
|
|
|
|
|
LOG_WARNING("GuildBankListParser: truncated suffix factor");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
/*suffixFactor=*/ packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
data.tabItems.push_back(slot);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Auction House System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionHelloPacket::build(uint64_t guid) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::MSG_AUCTION_HELLO));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AuctionHelloParser::parse(network::Packet& packet, AuctionHelloData& data) {
|
|
|
|
|
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
|
|
|
|
|
if (remaining < 12) {
|
|
|
|
|
|
LOG_WARNING("AuctionHelloParser: too small, remaining=", remaining);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
data.auctioneerGuid = packet.readUInt64();
|
|
|
|
|
|
data.auctionHouseId = packet.readUInt32();
|
|
|
|
|
|
// WotLK has an extra uint8 enabled field; Vanilla does not
|
|
|
|
|
|
if (packet.getReadPos() < packet.getSize()) {
|
|
|
|
|
|
data.enabled = packet.readUInt8();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
data.enabled = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionListItemsPacket::build(
|
|
|
|
|
|
uint64_t guid, uint32_t offset,
|
|
|
|
|
|
const std::string& searchName,
|
|
|
|
|
|
uint8_t levelMin, uint8_t levelMax,
|
|
|
|
|
|
uint32_t invTypeMask, uint32_t itemClass,
|
|
|
|
|
|
uint32_t itemSubClass, uint32_t quality,
|
|
|
|
|
|
uint8_t usableOnly, uint8_t exactMatch)
|
|
|
|
|
|
{
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_ITEMS));
|
|
|
|
|
|
p.writeUInt64(guid);
|
|
|
|
|
|
p.writeUInt32(offset);
|
|
|
|
|
|
p.writeString(searchName);
|
|
|
|
|
|
p.writeUInt8(levelMin);
|
|
|
|
|
|
p.writeUInt8(levelMax);
|
|
|
|
|
|
p.writeUInt32(invTypeMask);
|
|
|
|
|
|
p.writeUInt32(itemClass);
|
|
|
|
|
|
p.writeUInt32(itemSubClass);
|
|
|
|
|
|
p.writeUInt32(quality);
|
|
|
|
|
|
p.writeUInt8(usableOnly);
|
|
|
|
|
|
p.writeUInt8(0); // getAll (0 = normal search)
|
2026-02-23 19:16:47 -08:00
|
|
|
|
p.writeUInt8(exactMatch);
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
// Sort columns (0 = none)
|
|
|
|
|
|
p.writeUInt8(0);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionSellItemPacket::build(
|
|
|
|
|
|
uint64_t auctioneerGuid, uint64_t itemGuid,
|
|
|
|
|
|
uint32_t stackCount, uint32_t bid,
|
|
|
|
|
|
uint32_t buyout, uint32_t duration)
|
|
|
|
|
|
{
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_SELL_ITEM));
|
|
|
|
|
|
p.writeUInt64(auctioneerGuid);
|
|
|
|
|
|
p.writeUInt32(1); // item count (WotLK supports multiple, we send 1)
|
|
|
|
|
|
p.writeUInt64(itemGuid);
|
|
|
|
|
|
p.writeUInt32(stackCount);
|
|
|
|
|
|
p.writeUInt32(bid);
|
|
|
|
|
|
p.writeUInt32(buyout);
|
|
|
|
|
|
p.writeUInt32(duration);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionPlaceBidPacket::build(uint64_t auctioneerGuid, uint32_t auctionId, uint32_t amount) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_PLACE_BID));
|
|
|
|
|
|
p.writeUInt64(auctioneerGuid);
|
|
|
|
|
|
p.writeUInt32(auctionId);
|
|
|
|
|
|
p.writeUInt32(amount);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionRemoveItemPacket::build(uint64_t auctioneerGuid, uint32_t auctionId) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_REMOVE_ITEM));
|
|
|
|
|
|
p.writeUInt64(auctioneerGuid);
|
|
|
|
|
|
p.writeUInt32(auctionId);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionListOwnerItemsPacket::build(uint64_t auctioneerGuid, uint32_t offset) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_OWNER_ITEMS));
|
|
|
|
|
|
p.writeUInt64(auctioneerGuid);
|
|
|
|
|
|
p.writeUInt32(offset);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet AuctionListBidderItemsPacket::build(
|
|
|
|
|
|
uint64_t auctioneerGuid, uint32_t offset,
|
|
|
|
|
|
const std::vector<uint32_t>& outbiddedIds)
|
|
|
|
|
|
{
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_AUCTION_LIST_BIDDER_ITEMS));
|
|
|
|
|
|
p.writeUInt64(auctioneerGuid);
|
|
|
|
|
|
p.writeUInt32(offset);
|
|
|
|
|
|
p.writeUInt32(static_cast<uint32_t>(outbiddedIds.size()));
|
|
|
|
|
|
for (uint32_t id : outbiddedIds)
|
|
|
|
|
|
p.writeUInt32(id);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 01:25:27 -07:00
|
|
|
|
bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data, int numEnchantSlots) {
|
|
|
|
|
|
// Per-entry fixed size: auctionId(4) + itemEntry(4) + enchantSlots×3×4 +
|
|
|
|
|
|
// randProp(4) + suffix(4) + stack(4) + charges(4) + flags(4) +
|
|
|
|
|
|
// ownerGuid(8) + startBid(4) + outbid(4) + buyout(4) + expire(4) +
|
|
|
|
|
|
// bidderGuid(8) + curBid(4)
|
|
|
|
|
|
// Classic: numEnchantSlots=1 → 80 bytes/entry
|
|
|
|
|
|
// TBC/WotLK: numEnchantSlots=3 → 104 bytes/entry
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 4) return false;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t count = packet.readUInt32();
|
2026-03-11 14:37:27 -07:00
|
|
|
|
// Cap auction count to prevent unbounded memory allocation
|
|
|
|
|
|
const uint32_t MAX_AUCTION_RESULTS = 256;
|
|
|
|
|
|
if (count > MAX_AUCTION_RESULTS) {
|
|
|
|
|
|
LOG_WARNING("AuctionListResultParser: count capped (requested=", count, ")");
|
|
|
|
|
|
count = MAX_AUCTION_RESULTS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
data.auctions.clear();
|
|
|
|
|
|
data.auctions.reserve(count);
|
|
|
|
|
|
|
2026-03-10 01:25:27 -07:00
|
|
|
|
const size_t minPerEntry = static_cast<size_t>(8 + numEnchantSlots * 12 + 28 + 8 + 8);
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
2026-03-10 01:25:27 -07:00
|
|
|
|
if (packet.getReadPos() + minPerEntry > packet.getSize()) break;
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
AuctionEntry e;
|
|
|
|
|
|
e.auctionId = packet.readUInt32();
|
|
|
|
|
|
e.itemEntry = packet.readUInt32();
|
2026-03-10 01:25:27 -07:00
|
|
|
|
// First enchant slot always present
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
e.enchantId = packet.readUInt32();
|
2026-03-10 01:25:27 -07:00
|
|
|
|
packet.readUInt32(); // enchant1 duration
|
|
|
|
|
|
packet.readUInt32(); // enchant1 charges
|
|
|
|
|
|
// Extra enchant slots for TBC/WotLK
|
|
|
|
|
|
for (int s = 1; s < numEnchantSlots; ++s) {
|
|
|
|
|
|
packet.readUInt32(); // enchant N id
|
|
|
|
|
|
packet.readUInt32(); // enchant N duration
|
|
|
|
|
|
packet.readUInt32(); // enchant N charges
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
e.randomPropertyId = packet.readUInt32();
|
2026-03-10 01:25:27 -07:00
|
|
|
|
e.suffixFactor = packet.readUInt32();
|
|
|
|
|
|
e.stackCount = packet.readUInt32();
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
packet.readUInt32(); // item charges
|
|
|
|
|
|
packet.readUInt32(); // item flags (unused)
|
2026-03-10 01:25:27 -07:00
|
|
|
|
e.ownerGuid = packet.readUInt64();
|
|
|
|
|
|
e.startBid = packet.readUInt32();
|
|
|
|
|
|
e.minBidIncrement = packet.readUInt32();
|
|
|
|
|
|
e.buyoutPrice = packet.readUInt32();
|
|
|
|
|
|
e.timeLeftMs = packet.readUInt32();
|
|
|
|
|
|
e.bidderGuid = packet.readUInt64();
|
|
|
|
|
|
e.currentBid = packet.readUInt32();
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
data.auctions.push_back(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 01:25:27 -07:00
|
|
|
|
if (packet.getSize() - packet.getReadPos() >= 8) {
|
|
|
|
|
|
data.totalCount = packet.readUInt32();
|
|
|
|
|
|
data.searchDelay = packet.readUInt32();
|
|
|
|
|
|
}
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandResult& data) {
|
|
|
|
|
|
if (packet.getSize() - packet.getReadPos() < 12) return false;
|
|
|
|
|
|
data.auctionId = packet.readUInt32();
|
|
|
|
|
|
data.action = packet.readUInt32();
|
|
|
|
|
|
data.errorCode = packet.readUInt32();
|
|
|
|
|
|
if (data.errorCode != 0 && data.action == 2 && packet.getReadPos() + 4 <= packet.getSize()) {
|
|
|
|
|
|
data.bidError = packet.readUInt32();
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: implement pet stable system (MSG_LIST_STABLED_PETS, CMSG_STABLE_PET, CMSG_UNSTABLE_PET)
- Parse MSG_LIST_STABLED_PETS (SMSG): populate StabledPet list with
petNumber, entry, level, name, displayId, and active status
- Detect stable master via gossip option text/keyword matching and
auto-send MSG_LIST_STABLED_PETS request to open the stable UI
- Refresh list automatically after SMSG_STABLE_RESULT to reflect state
- New packet builders: ListStabledPetsPacket, StablePetPacket, UnstablePetPacket
- New public API: requestStabledPetList(), stablePet(slot), unstablePet(petNumber)
- Stable window UI: shows active/stabled pets with store/retrieve buttons,
slot count, refresh, and close; opens when server sends pet list
- Clear stable state on world logout/disconnect
2026-03-12 19:15:52 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Pet Stable System
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet ListStabledPetsPacket::build(uint64_t stableMasterGuid) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::MSG_LIST_STABLED_PETS));
|
|
|
|
|
|
p.writeUInt64(stableMasterGuid);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet StablePetPacket::build(uint64_t stableMasterGuid, uint8_t slot) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_STABLE_PET));
|
|
|
|
|
|
p.writeUInt64(stableMasterGuid);
|
|
|
|
|
|
p.writeUInt8(slot);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
network::Packet UnstablePetPacket::build(uint64_t stableMasterGuid, uint32_t petNumber) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_UNSTABLE_PET));
|
|
|
|
|
|
p.writeUInt64(stableMasterGuid);
|
|
|
|
|
|
p.writeUInt32(petNumber);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 19:42:31 -07:00
|
|
|
|
network::Packet PetRenamePacket::build(uint64_t petGuid, const std::string& name, uint8_t isDeclined) {
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_PET_RENAME));
|
|
|
|
|
|
p.writeUInt64(petGuid);
|
|
|
|
|
|
p.writeString(name); // null-terminated
|
|
|
|
|
|
p.writeUInt8(isDeclined);
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 20:23:36 -07:00
|
|
|
|
network::Packet SetTitlePacket::build(int32_t titleBit) {
|
|
|
|
|
|
// CMSG_SET_TITLE: int32 titleBit (-1 = remove active title)
|
|
|
|
|
|
network::Packet p(wireOpcode(Opcode::CMSG_SET_TITLE));
|
|
|
|
|
|
p.writeUInt32(static_cast<uint32_t>(titleBit));
|
|
|
|
|
|
return p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
} // namespace game
|
|
|
|
|
|
} // namespace wowee
|