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

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

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

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

1214 lines
45 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

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

#include "game/world_packets.hpp"
#include "game/packet_parsers.hpp"
#include "game/opcodes.hpp"
#include "game/character.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <sstream>
#include <iomanip>
#include <zlib.h>
namespace wowee {
namespace game {
network::Packet MessageChatPacket::build(ChatType type,
ChatLanguage language,
const std::string& message,
const std::string& target) {
network::Packet packet(wireOpcode(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();
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.hasRemaining(static_cast<size_t>(len) + minTrailingBytes)) {
packet.setReadPos(start);
return false;
}
// Stack buffer for typical messages; heap fallback for oversized ones.
static constexpr uint32_t kStackBufSize = 256;
std::array<char, kStackBufSize> stackBuf;
std::string heapBuf;
char* buf;
if (len <= kStackBufSize) {
buf = stackBuf.data();
} else {
heapBuf.resize(len);
buf = heapBuf.data();
}
for (uint32_t i = 0; i < len; ++i) {
buf[i] = static_cast<char>(packet.readUInt8());
}
if (buf[len - 1] != '\0') {
packet.setReadPos(start);
return false;
}
// len >= 2 guaranteed above, so len-1 >= 1 — string body is non-empty.
for (uint32_t i = 0; i < len - 1; ++i) {
auto uc = static_cast<unsigned char>(buf[i]);
if (uc < 32 || uc > 126) {
packet.setReadPos(start);
return false;
}
}
out.assign(buf, len - 1);
return true;
};
// Type-specific data
// WoW 3.3.5 SMSG_MESSAGECHAT format: after senderGuid+unk, most types
// have a receiverGuid (uint64). Some types have extra fields before it.
switch (data.type) {
case ChatType::MONSTER_SAY:
case ChatType::MONSTER_YELL:
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)
uint32_t nameLen = packet.readUInt32();
if (nameLen > packet.getRemainingSize()) return false;
if (nameLen > 0 && nameLen < 256) {
data.senderName.resize(nameLen);
for (uint32_t i = 0; i < nameLen; ++i) {
data.senderName[i] = static_cast<char>(packet.readUInt8());
}
// Strip trailing null (server includes it in nameLen)
if (!data.senderName.empty() && data.senderName.back() == '\0') {
data.senderName.pop_back();
}
}
// Read receiver GUID (NamedGuid: guid + optional name for non-player targets)
data.receiverGuid = packet.readUInt64();
if (data.receiverGuid != 0) {
// WoW GUID type encoding: bits 48-63 identify entity type.
// Players have highGuid=0x0000. Pets use 0xF040 (active pet) or
// 0xF014 (creature treated as pet). Mask 0xF0FF isolates the type
// nibbles while ignoring the server-specific middle bits.
constexpr uint16_t kGuidTypeMask = 0xF0FF;
constexpr uint16_t kGuidTypePet = 0xF040;
constexpr uint16_t kGuidTypeVehicle = 0xF014;
uint16_t highGuid = static_cast<uint16_t>(data.receiverGuid >> 48);
bool isPlayer = (highGuid == 0x0000);
bool isPet = ((highGuid & kGuidTypeMask) == kGuidTypePet) ||
((highGuid & kGuidTypeMask) == kGuidTypeVehicle);
if (!isPlayer && !isPet) {
// Read receiver name (SizedCString)
uint32_t recvNameLen = packet.readUInt32();
if (recvNameLen > 0 && recvNameLen < 256) {
packet.setReadPos(packet.getReadPos() + recvNameLen);
}
}
}
break;
}
case ChatType::CHANNEL: {
// Read channel name, then receiver GUID
data.channelName = packet.readString();
data.receiverGuid = packet.readUInt64();
break;
}
case ChatType::ACHIEVEMENT:
case ChatType::GUILD_ACHIEVEMENT: {
// Read target GUID
data.receiverGuid = packet.readUInt64();
break;
}
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;
}
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;
default:
// SAY, GUILD, PARTY, YELL, WHISPER, WHISPER_INFORM, RAID, etc.
// All have receiverGuid (typically senderGuid repeated)
data.receiverGuid = packet.readUInt64();
break;
}
// Read message length
uint32_t messageLen = packet.readUInt32();
if (messageLen > packet.getRemainingSize()) return false;
// Read message
if (messageLen > 0 && messageLen < 8192) {
data.message.resize(messageLen);
for (uint32_t i = 0; i < messageLen; ++i) {
data.message[i] = static_cast<char>(packet.readUInt8());
}
// Strip trailing null terminator (servers include it in messageLen)
if (!data.message.empty() && data.message.back() == '\0') {
data.message.pop_back();
}
}
// 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, static_cast<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";
}
}
// ============================================================
// 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;
}
bool TextEmoteParser::parse(network::Packet& packet, TextEmoteData& data, bool legacyFormat) {
size_t bytesLeft = packet.getRemainingSize();
if (bytesLeft < 20) {
LOG_WARNING("SMSG_TEXT_EMOTE too short: ", bytesLeft, " bytes");
return false;
}
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();
}
uint32_t nameLen = packet.readUInt32();
if (nameLen > 0 && nameLen <= 256) {
data.targetName = packet.readString();
} else if (nameLen > 0) {
// Implausible name length — misaligned read
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.getRemainingSize();
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.getRemainingSize();
if (bytesLeft >= 8) {
data.senderGuid = packet.readUInt64();
}
return true;
}
// ============================================================
// Foundation — Targeting, Name Queries
// ============================================================
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
network::Packet packet(wireOpcode(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(wireOpcode(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 InspectPacket::build(uint64_t targetGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_INSPECT));
packet.writePackedGuid(targetGuid);
LOG_DEBUG("Built CMSG_INSPECT: target=0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet QueryInspectAchievementsPacket::build(uint64_t targetGuid) {
// CMSG_QUERY_INSPECT_ACHIEVEMENTS: PackedGuid targetGuid
network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_INSPECT_ACHIEVEMENTS));
packet.writePackedGuid(targetGuid);
LOG_DEBUG("Built CMSG_QUERY_INSPECT_ACHIEVEMENTS: target=0x", std::hex, targetGuid, std::dec);
return packet;
}
// ============================================================
// Server Info Commands
// ============================================================
network::Packet QueryTimePacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_TIME));
LOG_DEBUG("Built CMSG_QUERY_TIME");
return packet;
}
bool QueryTimeResponseParser::parse(network::Packet& packet, QueryTimeResponseData& data) {
// Validate minimum packet size: serverTime(4) + timeOffset(4)
if (!packet.hasRemaining(8)) {
LOG_WARNING("SMSG_QUERY_TIME_RESPONSE: packet too small (", packet.getSize(), " bytes)");
return false;
}
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) {
network::Packet packet(wireOpcode(Opcode::CMSG_PLAYED_TIME));
packet.writeUInt8(sendToChat ? 1 : 0);
LOG_DEBUG("Built CMSG_PLAYED_TIME: sendToChat=", sendToChat);
return packet;
}
bool PlayedTimeParser::parse(network::Packet& packet, PlayedTimeData& data) {
// Classic/Turtle may omit the trailing trigger-message byte and send only
// totalTime(4) + levelTime(4). Later expansions append triggerMsg(1).
if (!packet.hasRemaining(8)) {
LOG_WARNING("SMSG_PLAYED_TIME: packet too small (", packet.getSize(), " bytes)");
return false;
}
data.totalTimePlayed = packet.readUInt32();
data.levelTimePlayed = packet.readUInt32();
data.triggerMessage = (packet.hasRemaining(1)) && (packet.readUInt8() != 0);
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) {
network::Packet packet(wireOpcode(Opcode::CMSG_WHO));
packet.writeUInt32(minLevel);
packet.writeUInt32(maxLevel);
packet.writeString(playerName);
packet.writeString(guildName);
packet.writeUInt32(raceMask);
packet.writeUInt32(classMask);
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
LOG_DEBUG("Built CMSG_WHO: player=", playerName);
return packet;
}
// ============================================================
// Social Commands
// ============================================================
network::Packet AddFriendPacket::build(const std::string& playerName, const std::string& note) {
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_FRIEND));
packet.writeString(playerName);
packet.writeString(note);
LOG_DEBUG("Built CMSG_ADD_FRIEND: player=", playerName);
return packet;
}
network::Packet DelFriendPacket::build(uint64_t friendGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_DEL_FRIEND));
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) {
network::Packet packet(wireOpcode(Opcode::CMSG_SET_CONTACT_NOTES));
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) {
// Validate minimum packet size: status(1) + guid(8)
if (!packet.hasRemaining(9)) {
LOG_WARNING("SMSG_FRIEND_STATUS: packet too small (", packet.getSize(), " bytes)");
return false;
}
data.status = packet.readUInt8();
data.guid = packet.readUInt64();
if (data.status == 1) { // Online
// Conditional: note (string) + chatFlag (1)
if (packet.hasData()) {
data.note = packet.readString();
if (packet.hasRemaining(1)) {
data.chatFlag = packet.readUInt8();
}
}
}
LOG_DEBUG("Parsed SMSG_FRIEND_STATUS: status=", static_cast<int>(data.status), " guid=0x", std::hex, data.guid, std::dec);
return true;
}
network::Packet AddIgnorePacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_IGNORE));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_ADD_IGNORE: player=", playerName);
return packet;
}
network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_DEL_IGNORE));
packet.writeUInt64(ignoreGuid);
LOG_DEBUG("Built CMSG_DEL_IGNORE: guid=0x", std::hex, ignoreGuid, std::dec);
return packet;
}
network::Packet ComplainPacket::build(uint64_t targetGuid, const std::string& reason) {
network::Packet packet(wireOpcode(Opcode::CMSG_COMPLAIN));
packet.writeUInt8(1); // complaintType: 1 = spam
packet.writeUInt64(targetGuid);
packet.writeUInt32(0); // unk
packet.writeUInt32(0); // messageType
packet.writeUInt32(0); // channelId
packet.writeUInt32(static_cast<uint32_t>(time(nullptr))); // timestamp
packet.writeString(reason);
LOG_DEBUG("Built CMSG_COMPLAIN: target=0x", std::hex, targetGuid, std::dec, " reason=", reason);
return packet;
}
// ============================================================
// Logout Commands
// ============================================================
network::Packet LogoutRequestPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_REQUEST));
LOG_DEBUG("Built CMSG_LOGOUT_REQUEST");
return packet;
}
network::Packet LogoutCancelPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_CANCEL));
LOG_DEBUG("Built CMSG_LOGOUT_CANCEL");
return packet;
}
bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& data) {
// Validate minimum packet size: result(4) + instant(1)
if (!packet.hasRemaining(5)) {
LOG_WARNING("SMSG_LOGOUT_RESPONSE: packet too small (", packet.getSize(), " bytes)");
return false;
}
data.result = packet.readUInt32();
data.instant = packet.readUInt8();
LOG_DEBUG("Parsed SMSG_LOGOUT_RESPONSE: result=", data.result, " instant=", static_cast<int>(data.instant));
return true;
}
// ============================================================
// Stand State
// ============================================================
network::Packet StandStateChangePacket::build(uint8_t state) {
network::Packet packet(wireOpcode(Opcode::CMSG_STANDSTATECHANGE));
packet.writeUInt32(state);
LOG_DEBUG("Built CMSG_STANDSTATECHANGE: state=", static_cast<int>(state));
return packet;
}
// ============================================================
// 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=", static_cast<int>(button),
" id=", id, " type=", static_cast<int>(classicType));
} else {
// TBC/WotLK: type in bits 2431, id in bits 023; 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=", static_cast<int>(button),
" packed=0x", std::hex, packed, std::dec);
}
return packet;
}
// ============================================================
// Display Toggles
// ============================================================
network::Packet ShowingHelmPacket::build(bool show) {
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_HELM));
packet.writeUInt8(show ? 1 : 0);
LOG_DEBUG("Built CMSG_SHOWING_HELM: show=", show);
return packet;
}
network::Packet ShowingCloakPacket::build(bool show) {
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_CLOAK));
packet.writeUInt8(show ? 1 : 0);
LOG_DEBUG("Built CMSG_SHOWING_CLOAK: show=", show);
return packet;
}
// ============================================================
// PvP
// ============================================================
network::Packet TogglePvpPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_TOGGLE_PVP));
LOG_DEBUG("Built CMSG_TOGGLE_PVP");
return packet;
}
// ============================================================
// Guild Commands
// ============================================================
network::Packet GuildInfoPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INFO));
LOG_DEBUG("Built CMSG_GUILD_INFO");
return packet;
}
network::Packet GuildRosterPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ROSTER));
LOG_DEBUG("Built CMSG_GUILD_ROSTER");
return packet;
}
network::Packet GuildMotdPacket::build(const std::string& motd) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_MOTD));
packet.writeString(motd);
LOG_DEBUG("Built CMSG_GUILD_MOTD: ", motd);
return packet;
}
network::Packet GuildPromotePacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_PROMOTE));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_PROMOTE: ", playerName);
return packet;
}
network::Packet GuildDemotePacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DEMOTE));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_DEMOTE: ", playerName);
return packet;
}
network::Packet GuildLeavePacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_LEAVE));
LOG_DEBUG("Built CMSG_GUILD_LEAVE");
return packet;
}
network::Packet GuildInvitePacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INVITE));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_INVITE: ", playerName);
return packet;
}
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;
}
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;
}
network::Packet GuildAcceptPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ACCEPT));
LOG_DEBUG("Built CMSG_GUILD_ACCEPT");
return packet;
}
network::Packet GuildDeclineInvitationPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DECLINE));
LOG_DEBUG("Built CMSG_GUILD_DECLINE");
return packet;
}
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.hasRemaining(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;
}
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();
// Validate before reading guild name
if (!packet.hasData()) {
LOG_WARNING("GuildQueryResponseParser: truncated before guild name");
data.guildName.clear();
return true;
}
data.guildName = packet.readString();
// Read 10 rank names with validation
for (int i = 0; i < 10; ++i) {
if (!packet.hasData()) {
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.hasRemaining(20)) {
LOG_WARNING("GuildQueryResponseParser: truncated before emblem data");
data.emblemStyle = 0;
data.emblemColor = 0;
data.borderStyle = 0;
data.borderColor = 0;
data.backgroundColor = 0;
return true;
}
data.emblemStyle = packet.readUInt32();
data.emblemColor = packet.readUInt32();
data.borderStyle = packet.readUInt32();
data.borderColor = packet.readUInt32();
data.backgroundColor = packet.readUInt32();
if (packet.hasRemaining(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();
// 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;
}
data.motd = packet.readString();
data.guildInfo = packet.readString();
if (!packet.hasRemaining(4)) {
LOG_WARNING("GuildRosterParser: truncated before rankCount");
data.ranks.clear();
data.members.clear();
return true;
}
uint32_t rankCount = packet.readUInt32();
// 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;
}
data.ranks.resize(rankCount);
for (uint32_t i = 0; i < rankCount; ++i) {
// Validate 4 bytes before each rank rights read
if (!packet.hasRemaining(4)) {
LOG_WARNING("GuildRosterParser: truncated rank at index ", i);
break;
}
data.ranks[i].rights = packet.readUInt32();
if (!packet.hasRemaining(4)) {
data.ranks[i].goldLimit = 0;
} else {
data.ranks[i].goldLimit = packet.readUInt32();
}
// 6 bank tab flags + 6 bank tab items per day
for (int t = 0; t < 6; ++t) {
if (!packet.hasRemaining(8)) break;
packet.readUInt32(); // tabFlags
packet.readUInt32(); // tabItemsPerDay
}
}
data.members.resize(numMembers);
for (uint32_t i = 0; i < numMembers; ++i) {
// Validate minimum bytes before reading member (guid+online+name at minimum is 9+ bytes)
if (!packet.hasRemaining(9)) {
LOG_WARNING("GuildRosterParser: truncated member at index ", i);
break;
}
auto& m = data.members[i];
m.guid = packet.readUInt64();
m.online = (packet.readUInt8() != 0);
// Validate before reading name string
if (!packet.hasData()) {
m.name.clear();
} else {
m.name = packet.readString();
}
// Validate before reading rank/level/class/gender/zone
if (!packet.hasRemaining(1)) {
m.rankIndex = 0;
m.level = 1;
m.classId = 0;
m.gender = 0;
m.zoneId = 0;
} else {
m.rankIndex = packet.readUInt32();
if (!packet.hasRemaining(3)) {
m.level = 1;
m.classId = 0;
m.gender = 0;
} else {
m.level = packet.readUInt8();
m.classId = packet.readUInt8();
m.gender = packet.readUInt8();
}
if (!packet.hasRemaining(4)) {
m.zoneId = 0;
} else {
m.zoneId = packet.readUInt32();
}
}
// Online status affects next fields
if (!m.online) {
if (!packet.hasRemaining(4)) {
m.lastOnline = 0.0f;
} else {
m.lastOnline = packet.readFloat();
}
}
// Read notes
if (!packet.hasData()) {
m.publicNote.clear();
m.officerNote.clear();
} else {
m.publicNote = packet.readString();
if (!packet.hasData()) {
m.officerNote.clear();
} else {
m.officerNote = packet.readString();
}
}
}
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.hasRemaining(8)) {
data.guid = packet.readUInt64();
}
LOG_INFO("Parsed SMSG_GUILD_EVENT: type=", static_cast<int>(data.eventType), " strings=", static_cast<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;
}
// ============================================================
// Ready Check
// ============================================================
network::Packet ReadyCheckPacket::build() {
network::Packet packet(wireOpcode(Opcode::MSG_RAID_READY_CHECK));
LOG_DEBUG("Built MSG_RAID_READY_CHECK");
return packet;
}
network::Packet ReadyCheckConfirmPacket::build(bool ready) {
network::Packet packet(wireOpcode(Opcode::MSG_RAID_READY_CHECK_CONFIRM));
packet.writeUInt8(ready ? 1 : 0);
LOG_DEBUG("Built MSG_RAID_READY_CHECK_CONFIRM: ready=", ready);
return packet;
}
// ============================================================
// Duel
// ============================================================
network::Packet DuelAcceptPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_ACCEPTED));
LOG_DEBUG("Built CMSG_DUEL_ACCEPTED");
return packet;
}
network::Packet DuelCancelPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED));
LOG_DEBUG("Built CMSG_DUEL_CANCELLED");
return packet;
}
// ============================================================
// Party/Raid Management
// ============================================================
network::Packet GroupUninvitePacket::build(const std::string& playerName) {
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_UNINVITE_GUID));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GROUP_UNINVITE_GUID for player: ", playerName);
return packet;
}
network::Packet GroupDisbandPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DISBAND));
LOG_DEBUG("Built CMSG_GROUP_DISBAND");
return packet;
}
network::Packet GroupRaidConvertPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_RAID_CONVERT));
LOG_DEBUG("Built CMSG_GROUP_RAID_CONVERT");
return packet;
}
network::Packet SetLootMethodPacket::build(uint32_t method, uint32_t threshold, uint64_t masterLooterGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_METHOD));
packet.writeUInt32(method);
packet.writeUInt32(threshold);
packet.writeUInt64(masterLooterGuid);
LOG_DEBUG("Built CMSG_LOOT_METHOD: method=", method, " threshold=", threshold,
" masterLooter=0x", std::hex, masterLooterGuid, std::dec);
return packet;
}
network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targetGuid) {
network::Packet packet(wireOpcode(Opcode::MSG_RAID_TARGET_UPDATE));
packet.writeUInt8(targetIndex);
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", static_cast<uint32_t>(targetIndex), ", guid: 0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet RequestRaidInfoPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_REQUEST_RAID_INFO));
LOG_DEBUG("Built CMSG_REQUEST_RAID_INFO");
return packet;
}
// ============================================================
// Combat and Trade
// ============================================================
network::Packet DuelProposedPacket::build(uint64_t targetGuid) {
// 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.
auto packet = CastSpellPacket::build(7266, targetGuid, 0);
LOG_DEBUG("Built duel request (spell 7266) for target: 0x", std::hex, targetGuid, std::dec);
return packet;
}
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;
}
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=", static_cast<int>(tradeSlot), " bag=", static_cast<int>(bag), " bagSlot=", static_cast<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=", static_cast<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;
}
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_INITIATE_TRADE for target: 0x", std::hex, targetGuid, std::dec);
return packet;
}
} // namespace game
} // namespace wowee