Kelsidavis-WoWee/src/game/world_packets.cpp
Kelsi c49bb58e47 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:31:03 -08:00

1502 lines
51 KiB
C++

#include "game/world_packets.hpp"
#include "game/opcodes.hpp"
#include "game/character.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <cstring>
namespace wowee {
namespace game {
network::Packet AuthSessionPacket::build(uint32_t build,
const std::string& accountName,
uint32_t clientSeed,
const std::vector<uint8_t>& sessionKey,
uint32_t serverSeed) {
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)
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTH_SESSION));
// Build number (uint32, little-endian)
packet.writeUInt32(build);
// Unknown uint32 (always 0)
packet.writeUInt32(0);
// Account name (null-terminated string)
packet.writeString(upperAccount);
// Unknown uint32 (always 0)
packet.writeUInt32(0);
// Client seed (uint32, little-endian)
packet.writeUInt32(clientSeed);
// Unknown fields (5x uint32, all zeros)
for (int i = 0; i < 5; ++i) {
packet.writeUInt32(0);
}
// Authentication hash (20 bytes)
packet.writeBytes(authHash.data(), authHash.size());
// Addon CRC (uint32, can be 0)
packet.writeUInt32(0);
LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes");
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());
LOG_DEBUG("Auth hash input: ", hashInput.size(), " bytes");
// Compute SHA1 hash
return auth::Crypto::sha1(hashInput);
}
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
// SMSG_AUTH_CHALLENGE format (WoW 3.3.5a):
// uint32 unknown1 (always 1?)
// uint32 serverSeed
if (packet.getSize() < 8) {
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
return false;
}
data.unknown1 = packet.readUInt32();
data.serverSeed = packet.readUInt32();
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE:");
LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
LOG_INFO(" Server seed: 0x", std::hex, data.serverSeed, std::dec);
// Note: 3.3.5a has additional data after this (seed2, etc.)
// but we only need the first seed for authentication
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";
}
}
network::Packet CharEnumPacket::build() {
// CMSG_CHAR_ENUM has no body - just the opcode
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
return packet;
}
bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) {
// 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;
// Read GUID (8 bytes, little-endian)
character.guid = packet.readUInt64();
// Read name (null-terminated string)
character.name = packet.readString();
// Read race, class, gender
character.race = static_cast<Race>(packet.readUInt8());
character.characterClass = static_cast<Class>(packet.readUInt8());
character.gender = static_cast<Gender>(packet.readUInt8());
// Read appearance data
character.appearanceBytes = packet.readUInt32();
character.facialFeatures = packet.readUInt8();
// Read level
character.level = packet.readUInt8();
// Read location
character.zoneId = packet.readUInt32();
character.mapId = packet.readUInt32();
character.x = packet.readFloat();
character.y = packet.readFloat();
character.z = packet.readFloat();
// Read affiliations
character.guildId = packet.readUInt32();
// Read flags
character.flags = packet.readUInt32();
// Skip customization flag (uint32) and unknown byte
packet.readUInt32(); // Customization
packet.readUInt8(); // Unknown
// Read pet data (always present, even if no pet)
character.pet.displayModel = packet.readUInt32();
character.pet.level = packet.readUInt32();
character.pet.family = packet.readUInt32();
// Read equipment (23 items)
character.equipment.reserve(23);
for (int j = 0; j < 23; ++j) {
EquipmentItem item;
item.displayModel = packet.readUInt32();
item.inventoryType = packet.readUInt8();
item.enchantment = packet.readUInt32();
character.equipment.push_back(item);
}
LOG_INFO(" Character ", (int)(i + 1), ": ", character.name);
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
LOG_INFO(" ", getRaceName(character.race), " ",
getClassName(character.characterClass), " (",
getGenderName(character.gender), ")");
LOG_INFO(" Level: ", (int)character.level);
LOG_INFO(" Location: Zone ", character.zoneId, ", Map ", character.mapId);
LOG_INFO(" Position: (", character.x, ", ", character.y, ", ", character.z, ")");
if (character.hasGuild()) {
LOG_INFO(" Guild ID: ", character.guildId);
}
if (character.hasPet()) {
LOG_INFO(" Pet: Model ", character.pet.displayModel,
", Level ", character.pet.level);
}
response.characters.push_back(character);
}
LOG_INFO("Successfully parsed ", response.characters.size(), " characters");
return true;
}
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PLAYER_LOGIN));
// 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) {
// SMSG_ACCOUNT_DATA_TIMES format (WoW 3.3.5a):
// uint32 serverTime (Unix timestamp)
// uint8 unknown (always 1?)
// uint32[8] accountDataTimes (timestamps for each data slot)
if (packet.getSize() < 37) {
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), " bytes");
return false;
}
data.serverTime = packet.readUInt32();
data.unknown = packet.readUInt8();
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
LOG_DEBUG(" Server time: ", data.serverTime);
LOG_DEBUG(" Unknown: ", (int)data.unknown);
for (int i = 0; i < 8; ++i) {
data.accountDataTimes[i] = packet.readUInt32();
if (data.accountDataTimes[i] != 0) {
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
}
}
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();
LOG_INFO("Parsed SMSG_MOTD:");
LOG_INFO(" Line count: ", lineCount);
data.lines.clear();
data.lines.reserve(lineCount);
for (uint32_t i = 0; i < lineCount; ++i) {
std::string line = packet.readString();
data.lines.push_back(line);
LOG_INFO(" [", i + 1, "] ", line);
}
return true;
}
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PING));
// 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;
}
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info) {
network::Packet packet(static_cast<uint16_t>(opcode));
// Movement packet format (WoW 3.3.5a):
// uint32 flags
// uint16 flags2
// uint32 time
// float x, y, z
// float orientation
// 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));
// 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));
}
// Write fall time if falling
if (info.hasFlag(MovementFlags::FALLING)) {
packet.writeUInt32(info.fallTime);
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
// Extended fall data if far falling
if (info.hasFlag(MovementFlags::FALLINGFAR)) {
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));
}
}
LOG_DEBUG("Built movement packet: opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec);
LOG_DEBUG(" Flags: 0x", std::hex, info.flags, std::dec);
LOG_DEBUG(" Position: (", info.x, ", ", info.y, ", ", info.z, ")");
LOG_DEBUG(" Orientation: ", info.orientation);
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) {
// Skip movement flags and other movement data for now
// This is a simplified implementation
// Read movement flags (not used yet)
/*uint32_t flags =*/ packet.readUInt32();
/*uint16_t flags2 =*/ packet.readUInt16();
// Read timestamp (not used yet)
/*uint32_t time =*/ packet.readUInt32();
// Read position
block.x = packet.readFloat();
block.y = packet.readFloat();
block.z = packet.readFloat();
block.orientation = packet.readFloat();
block.hasMovement = true;
LOG_DEBUG(" Movement: (", block.x, ", ", block.y, ", ", block.z, "), orientation=", block.orientation);
// TODO: Parse additional movement fields based on flags
// For now, we'll skip them to keep this simple
return true;
}
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
// 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
}
LOG_DEBUG(" Parsing ", (int)blockCount, " field blocks");
// Read update mask
std::vector<uint32_t> updateMask(blockCount);
for (int i = 0; i < blockCount; ++i) {
updateMask[i] = packet.readUInt32();
}
// Read field values for each bit set in mask
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
uint32_t mask = updateMask[blockIdx];
for (int bit = 0; bit < 32; ++bit) {
if (mask & (1 << bit)) {
uint16_t fieldIndex = blockIdx * 32 + bit;
uint32_t value = packet.readUInt32();
block.fields[fieldIndex] = value;
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
}
}
}
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
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
block.guid = readPackedGuid(packet);
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) {
LOG_INFO("Parsing SMSG_UPDATE_OBJECT");
// Read block count
data.blockCount = packet.readUInt32();
LOG_INFO(" Block count: ", data.blockCount);
// 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)) {
// Read out-of-range GUID count
uint32_t count = packet.readUInt32();
LOG_INFO(" Out-of-range objects: ", count);
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
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)) {
LOG_ERROR("Failed to parse update block ", i + 1);
return false;
}
data.blocks.push_back(block);
}
LOG_INFO("Successfully parsed ", data.blocks.size(), " update blocks");
return true;
}
bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) {
// SMSG_DESTROY_OBJECT format:
// uint64 guid
// uint8 isDeath (0 = despawn, 1 = death)
if (packet.getSize() < 9) {
LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes");
return false;
}
data.guid = packet.readUInt64();
data.isDeath = (packet.readUInt8() != 0);
LOG_INFO("Parsed SMSG_DESTROY_OBJECT:");
LOG_INFO(" GUID: 0x", std::hex, data.guid, std::dec);
LOG_INFO(" Is death: ", data.isDeath ? "yes" : "no");
return true;
}
network::Packet MessageChatPacket::build(ChatType type,
ChatLanguage language,
const std::string& message,
const std::string& target) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_MESSAGECHAT));
// 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();
// Type-specific data
switch (data.type) {
case ChatType::MONSTER_SAY:
case ChatType::MONSTER_YELL:
case ChatType::MONSTER_EMOTE: {
// Read sender name length + name
uint32_t nameLen = packet.readUInt32();
if (nameLen > 0 && nameLen < 256) {
std::vector<char> nameBuffer(nameLen);
for (uint32_t i = 0; i < nameLen; ++i) {
nameBuffer[i] = static_cast<char>(packet.readUInt8());
}
data.senderName = std::string(nameBuffer.begin(), nameBuffer.end());
}
// Read receiver GUID (usually 0 for monsters)
data.receiverGuid = packet.readUInt64();
break;
}
case ChatType::WHISPER_INFORM: {
// Read receiver name
data.receiverName = packet.readString();
break;
}
case ChatType::CHANNEL: {
// Read channel name
data.channelName = packet.readString();
break;
}
case ChatType::ACHIEVEMENT:
case ChatType::GUILD_ACHIEVEMENT: {
// Read achievement ID
packet.readUInt32();
break;
}
default:
// No additional data for most types
break;
}
// Read message length
uint32_t messageLen = packet.readUInt32();
// Read message
if (messageLen > 0 && messageLen < 8192) {
std::vector<char> msgBuffer(messageLen);
for (uint32_t i = 0; i < messageLen; ++i) {
msgBuffer[i] = static_cast<char>(packet.readUInt8());
}
data.message = std::string(msgBuffer.begin(), msgBuffer.end());
}
// 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";
}
}
// ============================================================
// Phase 1: Foundation — Targeting, Name Queries
// ============================================================
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_SELECTION));
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) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_ACTIVE_MOVER));
packet.writeUInt64(guid);
LOG_DEBUG("Built CMSG_SET_ACTIVE_MOVER: guid=0x", std::hex, guid, std::dec);
return packet;
}
network::Packet NameQueryPacket::build(uint64_t playerGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_NAME_QUERY));
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
data.guid = UpdateObjectParser::readPackedGuid(packet);
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
}
data.name = packet.readString();
data.realmName = packet.readString();
data.race = packet.readUInt8();
data.gender = packet.readUInt8();
data.classId = packet.readUInt8();
LOG_INFO("Name query response: ", data.name, " (race=", (int)data.race,
" class=", (int)data.classId, ")");
return true;
}
network::Packet CreatureQueryPacket::build(uint32_t entry, uint64_t guid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CREATURE_QUERY));
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) {
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();
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
LOG_INFO("Creature query response: ", data.name, " (type=", data.creatureType,
" rank=", data.rank, ")");
return true;
}
// ============================================================
// Phase 2: Combat Core
// ============================================================
network::Packet AttackSwingPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSWING));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_ATTACKSWING: target=0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet AttackStopPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSTOP));
LOG_DEBUG("Built CMSG_ATTACKSTOP");
return packet;
}
bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) {
if (packet.getSize() < 16) return false;
data.attackerGuid = packet.readUInt64();
data.victimGuid = packet.readUInt64();
LOG_INFO("Attack started: 0x", std::hex, data.attackerGuid,
" -> 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();
}
LOG_INFO("Attack stopped: 0x", std::hex, data.attackerGuid, std::dec);
return true;
}
bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) {
data.hitInfo = packet.readUInt32();
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
data.subDamageCount = packet.readUInt8();
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
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);
}
data.victimState = packet.readUInt32();
data.overkill = static_cast<int32_t>(packet.readUInt32());
// Read blocked amount
if (packet.getReadPos() < packet.getSize()) {
data.blocked = packet.readUInt32();
}
LOG_INFO("Melee hit: ", data.totalDamage, " damage",
data.isCrit() ? " (CRIT)" : "",
data.isMiss() ? " (MISS)" : "");
return true;
}
bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) {
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet);
data.spellId = packet.readUInt32();
data.damage = packet.readUInt32();
data.overkill = packet.readUInt32();
data.schoolMask = packet.readUInt8();
data.absorbed = packet.readUInt32();
data.resisted = packet.readUInt32();
// Skip remaining fields
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;
LOG_INFO("Spell damage: spellId=", data.spellId, " dmg=", data.damage,
data.isCrit ? " CRIT" : "");
return true;
}
bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) {
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
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);
LOG_INFO("Spell heal: spellId=", data.spellId, " heal=", data.heal,
data.isCrit ? " CRIT" : "");
return true;
}
// ============================================================
// Phase 3: Spells, Action Bar, Auras
// ============================================================
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data) {
data.talentSpec = packet.readUInt8();
uint16_t spellCount = packet.readUInt16();
data.spellIds.reserve(spellCount);
for (uint16_t i = 0; i < spellCount; ++i) {
uint32_t spellId = packet.readUInt32();
packet.readUInt16(); // unknown (always 0)
if (spellId != 0) {
data.spellIds.push_back(spellId);
}
}
uint16_t cooldownCount = packet.readUInt16();
data.cooldowns.reserve(cooldownCount);
for (uint16_t i = 0; i < cooldownCount; ++i) {
SpellCooldownEntry entry;
entry.spellId = packet.readUInt32();
entry.itemId = packet.readUInt16();
entry.categoryId = packet.readUInt16();
entry.cooldownMs = packet.readUInt32();
entry.categoryCooldownMs = packet.readUInt32();
data.cooldowns.push_back(entry);
}
LOG_INFO("Initial spells: ", data.spellIds.size(), " spells, ",
data.cooldowns.size(), " cooldowns");
return true;
}
network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CAST_SPELL));
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 CancelCastPacket::build(uint32_t spellId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_CAST));
packet.writeUInt32(0); // sequence
packet.writeUInt32(spellId);
return packet;
}
network::Packet CancelAuraPacket::build(uint32_t spellId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_AURA));
packet.writeUInt32(spellId);
return packet;
}
bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) {
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) {
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
data.castCount = packet.readUInt8();
data.spellId = packet.readUInt32();
data.castFlags = packet.readUInt32();
data.castTime = packet.readUInt32();
// Read target flags and target (simplified)
if (packet.getReadPos() < packet.getSize()) {
uint32_t targetFlags = packet.readUInt32();
if (targetFlags & 0x02) { // TARGET_FLAG_UNIT
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
}
}
LOG_INFO("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
return true;
}
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
data.castCount = packet.readUInt8();
data.spellId = packet.readUInt32();
data.castFlags = packet.readUInt32();
// Timestamp in 3.3.5a
packet.readUInt32();
data.hitCount = packet.readUInt8();
data.hitTargets.reserve(data.hitCount);
for (uint8_t i = 0; i < data.hitCount; ++i) {
data.hitTargets.push_back(packet.readUInt64());
}
data.missCount = packet.readUInt8();
// Skip miss details for now
LOG_INFO("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
" misses=", (int)data.missCount);
return true;
}
bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool isAll) {
data.guid = UpdateObjectParser::readPackedGuid(packet);
while (packet.getReadPos() < packet.getSize()) {
uint8_t slot = packet.readUInt8();
uint32_t spellId = packet.readUInt32();
AuraSlot aura;
if (spellId != 0) {
aura.spellId = spellId;
aura.flags = packet.readUInt8();
aura.level = packet.readUInt8();
aura.charges = packet.readUInt8();
if (!(aura.flags & 0x08)) { // NOT_CASTER flag
aura.casterGuid = UpdateObjectParser::readPackedGuid(packet);
}
if (aura.flags & 0x20) { // DURATION
aura.maxDurationMs = static_cast<int32_t>(packet.readUInt32());
aura.durationMs = static_cast<int32_t>(packet.readUInt32());
}
if (aura.flags & 0x40) { // EFFECT_AMOUNTS - skip
// 3 effect amounts
for (int i = 0; i < 3; ++i) {
if (packet.getReadPos() < packet.getSize()) {
packet.readUInt32();
}
}
}
}
data.updates.push_back({slot, aura});
// For single update, only one entry
if (!isAll) break;
}
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) {
data.guid = packet.readUInt64();
data.flags = packet.readUInt8();
while (packet.getReadPos() + 8 <= packet.getSize()) {
uint32_t spellId = packet.readUInt32();
uint32_t cooldownMs = packet.readUInt32();
data.cooldowns.push_back({spellId, cooldownMs});
}
LOG_DEBUG("Spell cooldowns: ", data.cooldowns.size(), " entries");
return true;
}
// ============================================================
// Phase 4: Group/Party System
// ============================================================
network::Packet GroupInvitePacket::build(const std::string& playerName) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_INVITE));
packet.writeString(playerName);
packet.writeUInt32(0); // unused
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
return packet;
}
bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteResponseData& data) {
data.canAccept = packet.readUInt8();
data.inviterName = packet.readString();
LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", (int)data.canAccept, ")");
return true;
}
network::Packet GroupAcceptPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_ACCEPT));
packet.writeUInt32(0); // unused in 3.3.5a
return packet;
}
network::Packet GroupDeclinePacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DECLINE));
return packet;
}
network::Packet GroupDisbandPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DISBAND));
return packet;
}
bool GroupListParser::parse(network::Packet& packet, GroupListData& data) {
data.groupType = packet.readUInt8();
data.subGroup = packet.readUInt8();
data.flags = packet.readUInt8();
data.roles = packet.readUInt8();
// Skip LFG data if present
if (data.groupType & 0x04) {
packet.readUInt8(); // lfg state
packet.readUInt32(); // lfg entry
packet.readUInt8(); // lfg flags (3.3.5a may not have this)
}
packet.readUInt64(); // group GUID
packet.readUInt32(); // counter
data.memberCount = packet.readUInt32();
data.members.reserve(data.memberCount);
for (uint32_t i = 0; i < data.memberCount; ++i) {
GroupMember member;
member.name = packet.readString();
member.guid = packet.readUInt64();
member.isOnline = packet.readUInt8();
member.subGroup = packet.readUInt8();
member.flags = packet.readUInt8();
member.roles = packet.readUInt8();
data.members.push_back(member);
}
data.leaderGuid = packet.readUInt64();
if (data.memberCount > 0 && packet.getReadPos() < packet.getSize()) {
data.lootMethod = packet.readUInt8();
data.looterGuid = packet.readUInt64();
data.lootThreshold = packet.readUInt8();
data.difficultyId = packet.readUInt8();
data.raidDifficultyId = packet.readUInt8();
if (packet.getReadPos() < packet.getSize()) {
packet.readUInt8(); // unknown byte
}
}
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) {
data.command = static_cast<PartyCommand>(packet.readUInt32());
data.name = packet.readString();
data.result = static_cast<PartyResult>(packet.readUInt32());
LOG_INFO("Party command result: ", (int)data.result);
return true;
}
bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData& data) {
data.playerName = packet.readString();
LOG_INFO("Group decline from: ", data.playerName);
return true;
}
// ============================================================
// Phase 5: Loot System
// ============================================================
network::Packet LootPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
packet.writeUInt8(slotIndex);
return packet;
}
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_RELEASE));
packet.writeUInt64(lootGuid);
return packet;
}
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) {
data.lootGuid = packet.readUInt64();
data.lootType = packet.readUInt8();
data.gold = packet.readUInt32();
uint8_t itemCount = packet.readUInt8();
data.items.reserve(itemCount);
for (uint8_t i = 0; i < itemCount; ++i) {
LootItem item;
item.slotIndex = packet.readUInt8();
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
item.randomSuffix = packet.readUInt32();
item.randomPropertyId = packet.readUInt32();
item.lootSlotType = packet.readUInt8();
data.items.push_back(item);
}
LOG_INFO("Loot response: ", (int)itemCount, " items, ", data.gold, " copper");
return true;
}
// ============================================================
// Phase 5: NPC Gossip
// ============================================================
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_HELLO));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optionId, const std::string& code) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
packet.writeUInt64(npcGuid);
packet.writeUInt32(optionId);
if (!code.empty()) {
packet.writeString(code);
}
return packet;
}
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
data.npcGuid = packet.readUInt64();
data.menuId = packet.readUInt32();
data.titleTextId = packet.readUInt32();
uint32_t optionCount = packet.readUInt32();
data.options.reserve(optionCount);
for (uint32_t i = 0; i < optionCount; ++i) {
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);
}
uint32_t questCount = packet.readUInt32();
data.quests.reserve(questCount);
for (uint32_t i = 0; i < questCount; ++i) {
GossipQuestItem quest;
quest.questId = packet.readUInt32();
quest.questIcon = packet.readUInt32();
quest.questLevel = static_cast<int32_t>(packet.readUInt32());
quest.questFlags = packet.readUInt32();
quest.isRepeatable = packet.readUInt8();
quest.title = packet.readString();
data.quests.push_back(quest);
}
LOG_INFO("Gossip: ", optionCount, " options, ", questCount, " quests");
return true;
}
// ============================================================
// Phase 5: Vendor
// ============================================================
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LIST_INVENTORY));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_BUY_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt32(itemId);
packet.writeUInt32(slot);
packet.writeUInt8(count);
return packet;
}
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SELL_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt64(itemGuid);
packet.writeUInt8(count);
return packet;
}
bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) {
data.vendorGuid = packet.readUInt64();
uint8_t itemCount = packet.readUInt8();
if (itemCount == 0) {
LOG_INFO("Vendor has nothing for sale");
return true;
}
data.items.reserve(itemCount);
for (uint8_t i = 0; i < itemCount; ++i) {
VendorItem item;
item.slot = packet.readUInt32();
item.itemId = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
item.maxCount = static_cast<int32_t>(packet.readUInt32());
item.buyPrice = packet.readUInt32();
item.durability = packet.readUInt32();
item.stackCount = packet.readUInt32();
item.extendedCost = packet.readUInt32();
data.items.push_back(item);
}
LOG_INFO("Vendor inventory: ", (int)itemCount, " items");
return true;
}
} // namespace game
} // namespace wowee