Add multi-expansion support with data-driven protocol layer

Replace hardcoded WotLK protocol constants with a data-driven architecture
supporting Classic 1.12.1, TBC 2.4.3, and WotLK 3.3.5a. Each expansion
has JSON profiles for opcodes, update fields, and DBC layouts, plus C++
polymorphic packet parsers for binary format differences (movement flags,
speed fields, transport data, spline format, char enum layout).

Key components:
- ExpansionRegistry: scans Data/expansions/*/expansion.json at startup
- OpcodeTable: logical enum <-> wire values loaded from JSON
- UpdateFieldTable: field indices loaded from JSON per expansion
- DBCLayout: schema-driven DBC field lookups replacing magic numbers
- PacketParsers: WotLK/TBC/Classic parsers with correct flag positions
- Multi-manifest AssetManager: layered manifests with priority ordering
- HDPackManager: overlay texture packs with expansion compatibility
- Auth screen expansion picker replacing hardcoded version dropdown
This commit is contained in:
Kelsi 2026-02-12 22:56:36 -08:00
parent aa16a687c2
commit 7092844b5e
51 changed files with 5258 additions and 887 deletions

View file

@ -0,0 +1,185 @@
#include "game/expansion_profile.hpp"
#include "core/logger.hpp"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <algorithm>
// Minimal JSON parsing (no external dependency) — expansion.json is tiny and flat.
// We parse the subset we need: strings, integers, arrays of integers.
namespace {
std::string trim(const std::string& s) {
size_t start = s.find_first_not_of(" \t\r\n\"");
size_t end = s.find_last_not_of(" \t\r\n\",");
if (start == std::string::npos) return "";
return s.substr(start, end - start + 1);
}
// Quick-and-dirty JSON value extractor for flat objects.
// Returns the raw value string for a given key, or empty.
std::string jsonValue(const std::string& json, const std::string& key) {
std::string needle = "\"" + key + "\"";
auto pos = json.find(needle);
if (pos == std::string::npos) return "";
pos = json.find(':', pos + needle.size());
if (pos == std::string::npos) return "";
++pos;
// Skip whitespace
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || json[pos] == '\r' || json[pos] == '\n'))
++pos;
if (pos >= json.size()) return "";
if (json[pos] == '"') {
// String value
size_t end = json.find('"', pos + 1);
return (end != std::string::npos) ? json.substr(pos + 1, end - pos - 1) : "";
}
if (json[pos] == '{') {
// Nested object — return content between braces
size_t depth = 1;
size_t start = pos + 1;
for (size_t i = start; i < json.size() && depth > 0; ++i) {
if (json[i] == '{') ++depth;
else if (json[i] == '}') { --depth; if (depth == 0) return json.substr(start, i - start); }
}
return "";
}
if (json[pos] == '[') {
// Array — return content between brackets (including brackets)
size_t end = json.find(']', pos);
return (end != std::string::npos) ? json.substr(pos, end - pos + 1) : "";
}
// Number or other literal
size_t end = json.find_first_of(",}\n\r", pos);
return trim(json.substr(pos, end - pos));
}
int jsonInt(const std::string& json, const std::string& key, int def = 0) {
std::string v = jsonValue(json, key);
if (v.empty()) return def;
try { return std::stoi(v); } catch (...) { return def; }
}
std::vector<uint32_t> jsonUintArray(const std::string& json, const std::string& key) {
std::vector<uint32_t> result;
std::string arr = jsonValue(json, key);
if (arr.empty() || arr.front() != '[') return result;
// Strip brackets
arr = arr.substr(1, arr.size() - 2);
std::istringstream ss(arr);
std::string tok;
while (std::getline(ss, tok, ',')) {
std::string t = trim(tok);
if (!t.empty()) {
try { result.push_back(static_cast<uint32_t>(std::stoul(t))); } catch (...) {}
}
}
return result;
}
} // namespace
namespace wowee {
namespace game {
std::string ExpansionProfile::versionString() const {
std::ostringstream ss;
ss << (int)majorVersion << "." << (int)minorVersion << "." << (int)patchVersion;
// Append letter suffix for known builds
if (majorVersion == 3 && minorVersion == 3 && patchVersion == 5) ss << "a";
else if (majorVersion == 2 && minorVersion == 4 && patchVersion == 3) ss << "";
else if (majorVersion == 1 && minorVersion == 12 && patchVersion == 1) ss << "";
return ss.str();
}
size_t ExpansionRegistry::initialize(const std::string& dataRoot) {
profiles_.clear();
activeId_.clear();
std::string expansionsDir = dataRoot + "/expansions";
std::error_code ec;
if (!std::filesystem::is_directory(expansionsDir, ec)) {
LOG_WARNING("ExpansionRegistry: no expansions/ directory at ", expansionsDir);
return 0;
}
for (auto& entry : std::filesystem::directory_iterator(expansionsDir, ec)) {
if (!entry.is_directory()) continue;
std::string jsonPath = entry.path().string() + "/expansion.json";
if (std::filesystem::exists(jsonPath, ec)) {
loadProfile(jsonPath, entry.path().string());
}
}
// Sort by build number (ascending: classic < tbc < wotlk < cata)
std::sort(profiles_.begin(), profiles_.end(),
[](const ExpansionProfile& a, const ExpansionProfile& b) { return a.build < b.build; });
// Default to WotLK if available, otherwise the last (highest build)
if (!profiles_.empty()) {
auto it = std::find_if(profiles_.begin(), profiles_.end(),
[](const ExpansionProfile& p) { return p.id == "wotlk"; });
activeId_ = (it != profiles_.end()) ? it->id : profiles_.back().id;
}
LOG_INFO("ExpansionRegistry: discovered ", profiles_.size(), " expansion(s), active=", activeId_);
return profiles_.size();
}
const ExpansionProfile* ExpansionRegistry::getProfile(const std::string& id) const {
for (auto& p : profiles_) {
if (p.id == id) return &p;
}
return nullptr;
}
bool ExpansionRegistry::setActive(const std::string& id) {
if (!getProfile(id)) return false;
activeId_ = id;
return true;
}
const ExpansionProfile* ExpansionRegistry::getActive() const {
return getProfile(activeId_);
}
bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::string& dirPath) {
std::ifstream f(jsonPath);
if (!f.is_open()) return false;
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
ExpansionProfile p;
p.id = jsonValue(json, "id");
p.name = jsonValue(json, "name");
p.shortName = jsonValue(json, "shortName");
p.dataPath = dirPath;
// Version nested object
std::string ver = jsonValue(json, "version");
if (!ver.empty()) {
p.majorVersion = static_cast<uint8_t>(jsonInt(ver, "major"));
p.minorVersion = static_cast<uint8_t>(jsonInt(ver, "minor"));
p.patchVersion = static_cast<uint8_t>(jsonInt(ver, "patch"));
}
p.build = static_cast<uint16_t>(jsonInt(json, "build"));
p.protocolVersion = static_cast<uint8_t>(jsonInt(json, "protocolVersion"));
p.maxLevel = static_cast<uint32_t>(jsonInt(json, "maxLevel", 60));
p.races = jsonUintArray(json, "races");
p.classes = jsonUintArray(json, "classes");
if (p.id.empty() || p.build == 0) {
LOG_WARNING("ExpansionRegistry: skipping invalid profile at ", jsonPath);
return false;
}
LOG_INFO("ExpansionRegistry: loaded '", p.name, "' (", p.shortName,
") v", p.versionString(), " build=", p.build);
profiles_.push_back(std::move(p));
return true;
}
} // namespace game
} // namespace wowee

View file

@ -1,8 +1,11 @@
#include "game/game_handler.hpp"
#include "game/packet_parsers.hpp"
#include "game/transport_manager.hpp"
#include "game/warden_crypto.hpp"
#include "game/warden_module.hpp"
#include "game/opcodes.hpp"
#include "game/update_field_table.hpp"
#include "pipeline/dbc_layout.hpp"
#include "network/world_socket.hpp"
#include "network/packet.hpp"
#include "auth/crypto.hpp"
@ -50,16 +53,16 @@ const char* worldStateName(WorldState state) {
return "UNKNOWN";
}
bool isAuthCharPipelineOpcode(uint16_t opcode) {
switch (opcode) {
case static_cast<uint16_t>(Opcode::SMSG_AUTH_CHALLENGE):
case static_cast<uint16_t>(Opcode::SMSG_AUTH_RESPONSE):
case static_cast<uint16_t>(Opcode::SMSG_CLIENTCACHE_VERSION):
case static_cast<uint16_t>(Opcode::SMSG_TUTORIAL_FLAGS):
case static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA):
case static_cast<uint16_t>(Opcode::SMSG_CHAR_ENUM):
case static_cast<uint16_t>(Opcode::SMSG_CHAR_CREATE):
case static_cast<uint16_t>(Opcode::SMSG_CHAR_DELETE):
bool isAuthCharPipelineOpcode(LogicalOpcode op) {
switch (op) {
case Opcode::SMSG_AUTH_CHALLENGE:
case Opcode::SMSG_AUTH_RESPONSE:
case Opcode::SMSG_CLIENTCACHE_VERSION:
case Opcode::SMSG_TUTORIAL_FLAGS:
case Opcode::SMSG_WARDEN_DATA:
case Opcode::SMSG_CHAR_ENUM:
case Opcode::SMSG_CHAR_CREATE:
case Opcode::SMSG_CHAR_DELETE:
return true;
default:
return false;
@ -71,6 +74,17 @@ bool isAuthCharPipelineOpcode(uint16_t opcode) {
GameHandler::GameHandler() {
LOG_DEBUG("GameHandler created");
// Initialize opcode table with WotLK defaults (may be overridden from JSON later)
opcodeTable_.loadWotlkDefaults();
setActiveOpcodeTable(&opcodeTable_);
// Initialize update field table with WotLK defaults (may be overridden from JSON later)
updateFieldTable_.loadWotlkDefaults();
setActiveUpdateFieldTable(&updateFieldTable_);
// Initialize packet parsers (WotLK default, may be replaced for other expansions)
packetParsers_ = std::make_unique<WotlkPacketParsers>();
// Initialize transport manager
transportManager_ = std::make_unique<TransportManager>();
@ -92,6 +106,10 @@ GameHandler::~GameHandler() {
disconnect();
}
void GameHandler::setPacketParsers(std::unique_ptr<PacketParsers> parsers) {
packetParsers_ = std::move(parsers);
}
bool GameHandler::connect(const std::string& host,
uint16_t port,
const std::vector<uint8_t>& sessionKey,
@ -542,10 +560,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
uint16_t opcode = packet.getOpcode();
if (wardenGateSeen_ && opcode != static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA)) {
auto preLogicalOp = opcodeTable_.fromWire(opcode);
if (wardenGateSeen_ && (!preLogicalOp || *preLogicalOp != Opcode::SMSG_WARDEN_DATA)) {
++wardenPacketsAfterGate_;
}
if (isAuthCharPipelineOpcode(opcode)) {
if (preLogicalOp && isAuthCharPipelineOpcode(*preLogicalOp)) {
LOG_INFO("AUTH/CHAR RX opcode=0x", std::hex, opcode, std::dec,
" state=", worldStateName(state),
" size=", packet.getSize());
@ -554,10 +573,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
LOG_DEBUG("Received world packet: opcode=0x", std::hex, opcode, std::dec,
" size=", packet.getSize(), " bytes");
// Route packet based on opcode
Opcode opcodeEnum = static_cast<Opcode>(opcode);
// Translate wire opcode to logical opcode via expansion table
auto logicalOp = opcodeTable_.fromWire(opcode);
if (!logicalOp) {
LOG_DEBUG("Unknown wire opcode 0x", std::hex, opcode, std::dec, " - ignoring");
return;
}
switch (opcodeEnum) {
switch (*logicalOp) {
case Opcode::SMSG_AUTH_CHALLENGE:
if (state == WorldState::CONNECTED) {
handleAuthChallenge(packet);
@ -1132,7 +1155,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (entity->getType() != ObjectType::UNIT) continue;
auto unit = std::static_pointer_cast<Unit>(entity);
if (unit->getNpcFlags() & 0x02) {
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(guid);
socket->send(qsPkt);
}
@ -1629,7 +1652,7 @@ void GameHandler::deleteCharacter(uint64_t characterGuid) {
return;
}
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_DELETE));
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_DELETE));
packet.writeUInt64(characterGuid);
socket->send(packet);
LOG_INFO("CMSG_CHAR_DELETE sent for GUID: 0x", std::hex, characterGuid, std::dec);
@ -1938,7 +1961,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
std::vector<uint8_t> encryptedResponse = wardenCrypto_->encrypt(hashResponse);
// Send HASH_RESULT response
network::Packet response(static_cast<uint16_t>(Opcode::CMSG_WARDEN_DATA));
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t byte : encryptedResponse) {
response.writeUInt8(byte);
}
@ -2151,7 +2174,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
LOG_INFO("Warden: Response encrypted (", encrypted.size(), " bytes): ", respEncHex);
// Build and send response packet
network::Packet response(static_cast<uint16_t>(Opcode::CMSG_WARDEN_DATA));
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t byte : encrypted) {
response.writeUInt8(byte);
}
@ -2344,7 +2367,7 @@ void GameHandler::sendMovement(Opcode opcode) {
}
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
static_cast<uint16_t>(opcode), std::dec,
wireOpcode(opcode), std::dec,
(isOnTransport() ? " ONTRANSPORT" : ""));
// Convert canonical → server coordinates for the wire
@ -2584,9 +2607,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if (block.objectType == ObjectType::PLAYER) {
queryPlayerName(block.guid);
} else if (block.objectType == ObjectType::UNIT) {
// Extract creature entry from fields (UNIT_FIELD_ENTRY = index 54 in 3.3.5a,
// but the OBJECT_FIELD_ENTRY is at index 3)
auto it = block.fields.find(3); // OBJECT_FIELD_ENTRY
auto it = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
if (it != block.fields.end() && it->second != 0) {
auto unit = std::static_pointer_cast<Unit>(entity);
unit->setEntry(it->second);
@ -2603,39 +2624,44 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) {
auto unit = std::static_pointer_cast<Unit>(entity);
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH);
const uint16_t ufPower = fieldIndex(UF::UNIT_FIELD_POWER1);
const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH);
const uint16_t ufMaxPower = fieldIndex(UF::UNIT_FIELD_MAXPOWER1);
const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE);
const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS);
const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS);
const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID);
const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID);
const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS);
for (const auto& [key, val] : block.fields) {
switch (key) {
case 24:
unit->setHealth(val);
// Detect dead player on login
if (block.guid == playerGuid && val == 0) {
playerDead_ = true;
LOG_INFO("Player logged in dead");
if (key == ufHealth) {
unit->setHealth(val);
if (block.guid == playerGuid && val == 0) {
playerDead_ = true;
LOG_INFO("Player logged in dead");
}
} else if (key == ufPower) { unit->setPower(val); }
else if (key == ufMaxHealth) { unit->setMaxHealth(val); }
else if (key == ufMaxPower) { unit->setMaxPower(val); }
else if (key == ufFaction) { unit->setFactionTemplate(val); }
else if (key == ufFlags) { unit->setUnitFlags(val); }
else if (key == ufDynFlags) { unit->setDynamicFlags(val); }
else if (key == ufLevel) { unit->setLevel(val); }
else if (key == ufDisplayId) { unit->setDisplayId(val); }
else if (key == ufMountDisplayId) {
if (block.guid == playerGuid) {
uint32_t old = currentMountDisplayId_;
currentMountDisplayId_ = val;
if (val != old && mountCallback_) mountCallback_(val);
if (old != 0 && val == 0) {
for (auto& a : playerAuras)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
}
break;
case 25: unit->setPower(val); break;
case 32: unit->setMaxHealth(val); break;
case 33: unit->setMaxPower(val); break;
case 55: unit->setFactionTemplate(val); break; // UNIT_FIELD_FACTIONTEMPLATE
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
case 147: unit->setDynamicFlags(val); break; // UNIT_DYNAMIC_FLAGS
case 54: unit->setLevel(val); break;
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
case 69: // UNIT_FIELD_MOUNTDISPLAYID
if (block.guid == playerGuid) {
uint32_t old = currentMountDisplayId_;
currentMountDisplayId_ = val;
if (val != old && mountCallback_) mountCallback_(val);
if (old != 0 && val == 0) {
for (auto& a : playerAuras)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
}
}
unit->setMountDisplayId(val);
break;
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
default: break;
}
}
unit->setMountDisplayId(val);
} else if (key == ufNpcFlags) { unit->setNpcFlags(val); }
}
if (block.guid == playerGuid) {
constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100;
@ -2651,11 +2677,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
playerDead_ = true;
LOG_INFO("Player logged in dead (dynamic flags)");
}
// Detect ghost state on login via PLAYER_FLAGS (field 150)
// Detect ghost state on login via PLAYER_FLAGS
if (block.guid == playerGuid) {
constexpr uint32_t PLAYER_FLAGS_IDX = 150; // UNIT_END(148) + 2
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
auto pfIt = block.fields.find(PLAYER_FLAGS_IDX);
auto pfIt = block.fields.find(fieldIndex(UF::PLAYER_FLAGS));
if (pfIt != block.fields.end() && (pfIt->second & PLAYER_FLAGS_GHOST) != 0) {
releasedSpirit_ = true;
playerDead_ = true;
@ -2674,7 +2699,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
// Query quest giver status for NPCs with questgiver flag (0x02)
if ((unit->getNpcFlags() & 0x02) && socket) {
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(block.guid);
socket->send(qsPkt);
}
@ -2683,12 +2708,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Extract displayId and entry for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8)
if (block.objectType == ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<GameObject>(entity);
auto itDisp = block.fields.find(8);
auto itDisp = block.fields.find(fieldIndex(UF::GAMEOBJECT_DISPLAYID));
if (itDisp != block.fields.end()) {
go->setDisplayId(itDisp->second);
}
// Extract entry and query name (OBJECT_FIELD_ENTRY = index 3)
auto itEntry = block.fields.find(3);
auto itEntry = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
if (itEntry != block.fields.end() && itEntry->second != 0) {
go->setEntry(itEntry->second);
auto cacheIt = gameObjectInfoCache_.find(itEntry->second);
@ -2719,8 +2743,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
// Track online item objects
if (block.objectType == ObjectType::ITEM) {
auto entryIt = block.fields.find(3); // OBJECT_FIELD_ENTRY
auto stackIt = block.fields.find(14); // ITEM_FIELD_STACK_COUNT
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
if (entryIt != block.fields.end() && entryIt->second != 0) {
OnlineItemInfo info;
info.entry = entryIt->second;
@ -2816,22 +2840,27 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
LOG_INFO(" Highest field index: ", maxField);
bool slotsChanged = false;
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
const uint16_t ufQuestEnd = ufQuestStart + 25 * 5; // 25 quest slots, stride 5
for (const auto& [key, val] : block.fields) {
if (key == 634) { playerXp_ = val; } // PLAYER_XP
else if (key == 635) { playerNextLevelXp_ = val; } // PLAYER_NEXT_LEVEL_XP
else if (key == 54) {
serverPlayerLevel_ = val; // UNIT_FIELD_LEVEL
if (key == ufPlayerXp) { playerXp_ = val; }
else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; }
else if (key == ufPlayerLevel) {
serverPlayerLevel_ = val;
for (auto& ch : characters) {
if (ch.guid == playerGuid) { ch.level = val; break; }
}
}
else if (key == 1170) {
else if (key == ufCoinage) {
playerMoneyCopper_ = val;
LOG_INFO("Money set from update fields: ", val, " copper");
} // PLAYER_FIELD_COINAGE
// Parse quest log fields (PLAYER_QUEST_LOG_1_1 = UNIT_END + 10 = 158, stride 5)
// Quest slots: 158, 163, 168, 173, ... (25 slots max = up to index 278)
else if (key >= 158 && key < 283 && (key - 158) % 5 == 0) {
}
// Parse quest log fields (stride 5, 25 slots)
else if (key >= ufQuestStart && key < ufQuestEnd && (key - ufQuestStart) % 5 == 0) {
uint32_t questId = val;
if (questId != 0) {
// Check if quest is already in log
@ -2853,7 +2882,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Request quest details from server
if (socket) {
network::Packet qPkt(static_cast<uint16_t>(Opcode::CMSG_QUEST_QUERY));
network::Packet qPkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
qPkt.writeUInt32(questId);
socket->send(qPkt);
}
@ -2907,92 +2936,89 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
uint32_t oldDisplayId = unit->getDisplayId();
bool displayIdChanged = false;
const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH);
const uint16_t ufPower = fieldIndex(UF::UNIT_FIELD_POWER1);
const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH);
const uint16_t ufMaxPower = fieldIndex(UF::UNIT_FIELD_MAXPOWER1);
const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE);
const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS);
const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS);
const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID);
const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID);
const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS);
for (const auto& [key, val] : block.fields) {
switch (key) {
case 24: {
uint32_t oldHealth = unit->getHealth();
unit->setHealth(val);
if (val == 0) {
if (block.guid == autoAttackTarget) {
stopAutoAttack();
}
hostileAttackers_.erase(block.guid);
// Player death
if (block.guid == playerGuid) {
playerDead_ = true;
releasedSpirit_ = false;
stopAutoAttack();
LOG_INFO("Player died!");
}
// Trigger death animation for NPC units
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
npcDeathCallback_(block.guid);
}
} else if (oldHealth == 0 && val > 0) {
// Player resurrection or ghost form
if (block.guid == playerGuid) {
playerDead_ = false;
if (!releasedSpirit_) {
LOG_INFO("Player resurrected!");
} else {
LOG_INFO("Player entered ghost form");
}
}
// Respawn: health went from 0 to >0, reset animation
if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) {
npcRespawnCallback_(block.guid);
}
if (key == ufHealth) {
uint32_t oldHealth = unit->getHealth();
unit->setHealth(val);
if (val == 0) {
if (block.guid == autoAttackTarget) {
stopAutoAttack();
}
break;
}
case 25: unit->setPower(val); break;
case 32: unit->setMaxHealth(val); break;
case 33: unit->setMaxPower(val); break;
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
case 147: {
uint32_t oldDyn = unit->getDynamicFlags();
unit->setDynamicFlags(val);
hostileAttackers_.erase(block.guid);
if (block.guid == playerGuid) {
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
if (!wasDead && nowDead) {
playerDead_ = true;
releasedSpirit_ = false;
LOG_INFO("Player died (dynamic flags)");
} else if (wasDead && !nowDead) {
playerDead_ = false;
releasedSpirit_ = false;
LOG_INFO("Player resurrected (dynamic flags)");
}
playerDead_ = true;
releasedSpirit_ = false;
stopAutoAttack();
LOG_INFO("Player died!");
}
break;
}
case 54: unit->setLevel(val); break;
case 55: // UNIT_FIELD_FACTIONTEMPLATE
unit->setFactionTemplate(val);
unit->setHostile(isHostileFaction(val));
break;
case 67:
if (val != unit->getDisplayId()) {
unit->setDisplayId(val);
displayIdChanged = true;
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
npcDeathCallback_(block.guid);
}
break; // UNIT_FIELD_DISPLAYID
case 69: // UNIT_FIELD_MOUNTDISPLAYID
} else if (oldHealth == 0 && val > 0) {
if (block.guid == playerGuid) {
uint32_t old = currentMountDisplayId_;
currentMountDisplayId_ = val;
if (val != old && mountCallback_) mountCallback_(val);
if (old != 0 && val == 0) {
for (auto& a : playerAuras)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
playerDead_ = false;
if (!releasedSpirit_) {
LOG_INFO("Player resurrected!");
} else {
LOG_INFO("Player entered ghost form");
}
}
unit->setMountDisplayId(val);
break;
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
default: break;
}
if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) {
npcRespawnCallback_(block.guid);
}
}
} else if (key == ufPower) { unit->setPower(val); }
else if (key == ufMaxHealth) { unit->setMaxHealth(val); }
else if (key == ufMaxPower) { unit->setMaxPower(val); }
else if (key == ufFlags) { unit->setUnitFlags(val); }
else if (key == ufDynFlags) {
uint32_t oldDyn = unit->getDynamicFlags();
unit->setDynamicFlags(val);
if (block.guid == playerGuid) {
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
if (!wasDead && nowDead) {
playerDead_ = true;
releasedSpirit_ = false;
LOG_INFO("Player died (dynamic flags)");
} else if (wasDead && !nowDead) {
playerDead_ = false;
releasedSpirit_ = false;
LOG_INFO("Player resurrected (dynamic flags)");
}
}
} else if (key == ufLevel) { unit->setLevel(val); }
else if (key == ufFaction) {
unit->setFactionTemplate(val);
unit->setHostile(isHostileFaction(val));
} else if (key == ufDisplayId) {
if (val != unit->getDisplayId()) {
unit->setDisplayId(val);
displayIdChanged = true;
}
} else if (key == ufMountDisplayId) {
if (block.guid == playerGuid) {
uint32_t old = currentMountDisplayId_;
currentMountDisplayId_ = val;
if (val != old && mountCallback_) mountCallback_(val);
if (old != 0 && val == 0) {
for (auto& a : playerAuras)
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
}
}
unit->setMountDisplayId(val);
} else if (key == ufNpcFlags) { unit->setNpcFlags(val); }
}
// Some units are created without displayId and get it later via VALUES.
@ -3005,7 +3031,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
}
if ((unit->getNpcFlags() & 0x02) && socket) {
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(block.guid);
socket->send(qsPkt);
}
@ -3031,19 +3057,23 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
detectInventorySlotBases(block.fields);
bool slotsChanged = false;
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
for (const auto& [key, val] : block.fields) {
if (key == 634) {
if (key == ufPlayerXp) {
playerXp_ = val;
LOG_INFO("XP updated: ", val);
}
else if (key == 635) {
else if (key == ufPlayerNextXp) {
playerNextLevelXp_ = val;
LOG_INFO("Next level XP updated: ", val);
}
else if (key == 54) {
else if (key == ufPlayerLevel) {
serverPlayerLevel_ = val;
LOG_INFO("Level updated: ", val);
// Update Character struct for character selection screen
for (auto& ch : characters) {
if (ch.guid == playerGuid) {
ch.level = val;
@ -3051,11 +3081,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
}
}
else if (key == 1170) {
else if (key == ufCoinage) {
playerMoneyCopper_ = val;
LOG_INFO("Money updated via VALUES: ", val, " copper");
}
else if (key == 150) { // PLAYER_FLAGS (UNIT_END+2)
else if (key == ufPlayerFlags) {
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
bool wasGhost = releasedSpirit_;
bool nowGhost = (val & PLAYER_FLAGS_GHOST) != 0;
@ -3080,7 +3110,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Update item stack count for online items
if (entity->getType() == ObjectType::ITEM) {
for (const auto& [key, val] : block.fields) {
if (key == 14) { // ITEM_FIELD_STACK_COUNT
if (key == fieldIndex(UF::ITEM_FIELD_STACK_COUNT)) {
auto it = onlineItems_.find(block.guid);
if (it != onlineItems_.end()) it->second.stackCount = val;
}
@ -3240,7 +3270,7 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
LOG_DEBUG(" Decompressed ", compressedSize, " -> ", destLen, " bytes");
// Create packet from decompressed data and parse it
network::Packet decompressedPacket(static_cast<uint16_t>(Opcode::SMSG_UPDATE_OBJECT), decompressed);
network::Packet decompressedPacket(wireOpcode(Opcode::SMSG_UPDATE_OBJECT), decompressed);
handleUpdateObject(decompressedPacket);
}
@ -3908,15 +3938,12 @@ void GameHandler::assistTarget() {
}
// Try to read target GUID from update fields (UNIT_FIELD_TARGET)
// Field offset 6 is typically UNIT_FIELD_TARGET in 3.3.5a
uint64_t assistTargetGuid = 0;
const auto& fields = target->getFields();
auto it = fields.find(6);
auto it = fields.find(fieldIndex(UF::UNIT_FIELD_TARGET_LO));
if (it != fields.end()) {
// Low 32 bits
assistTargetGuid = it->second;
// Try to get high 32 bits from next field
auto it2 = fields.find(7);
auto it2 = fields.find(fieldIndex(UF::UNIT_FIELD_TARGET_HI));
if (it2 != fields.end()) {
assistTargetGuid |= (static_cast<uint64_t>(it2->second) << 32);
}
@ -4581,8 +4608,7 @@ void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& f
// The lowest matching field is the first EQUIPPED slot (not necessarily HEAD).
// With 2+ matches we can derive the true base: all matches must be at
// even offsets from the base, spaced 2 fields per slot.
// Use the known 3.3.5a default (324) and verify matches align to it.
constexpr int knownBase = 324;
const int knownBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
constexpr int slotStride = 2;
bool allAlign = true;
for (uint16_t p : matchingPairs) {
@ -4628,10 +4654,8 @@ void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& f
bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& fields) {
bool slotsChanged = false;
// WoW 3.3.5a: PLAYER_FIELD_INV_SLOT_HEAD = UNIT_END + 0x00B0 = 324
// PLAYER_FIELD_PACK_SLOT_1 = UNIT_END + 0x00DE = 370
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : 324;
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : 370;
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1));
for (const auto& [key, val] : fields) {
if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) {
@ -4854,7 +4878,7 @@ void GameHandler::dismount() {
taxiClientActive_ = false;
LOG_INFO("Dismount desync recovery: force-cleared local mount state");
}
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_CANCEL_MOUNT_AURA));
network::Packet pkt(wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA));
socket->send(pkt);
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
}
@ -4886,7 +4910,7 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
// Always ACK the speed change to prevent server stall.
// Packet format mirrors movement packets: packed guid + counter + movement info + new speed.
if (socket) {
network::Packet ack(static_cast<uint16_t>(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
network::Packet ack(wireOpcode(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
MovementPacket::writePackedGuid(ack, playerGuid);
ack.writeUInt32(counter);
@ -5781,7 +5805,7 @@ void GameHandler::selectGossipQuest(uint32_t questId) {
if (isInLog && isCompletable) {
// Quest is ready to turn in - request reward
LOG_INFO("Turning in quest: questId=", questId, " npcGuid=", currentGossip.npcGuid);
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
packet.writeUInt64(currentGossip.npcGuid);
packet.writeUInt32(questId);
socket->send(packet);
@ -5831,7 +5855,7 @@ void GameHandler::acceptQuest() {
// Re-query quest giver status so marker updates (! → ?)
if (npcGuid) {
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(npcGuid);
socket->send(qsPkt);
}
@ -5849,7 +5873,7 @@ void GameHandler::abandonQuest(uint32_t questId) {
// Tell server to remove it (slot index in server quest log)
// We send the local index; server maps it via PLAYER_QUEST_LOG fields
if (state == WorldState::IN_WORLD && socket) {
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
pkt.writeUInt8(static_cast<uint8_t>(i));
socket->send(pkt);
}
@ -5923,7 +5947,7 @@ void GameHandler::chooseQuestReward(uint32_t rewardIndex) {
// Re-query quest giver status so markers update
if (npcGuid) {
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(npcGuid);
socket->send(qsPkt);
}
@ -6274,13 +6298,13 @@ void GameHandler::loadSpellNameCache() {
return;
}
// Fields: 0=SpellID, 136=SpellName_enUS, 153=RankText_enUS
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t count = dbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) {
uint32_t id = dbc->getUInt32(i, 0);
uint32_t id = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
if (id == 0) continue;
std::string name = dbc->getString(i, 136);
std::string rank = dbc->getString(i, 153);
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
if (!name.empty()) {
spellNameCache_[id] = {std::move(name), std::move(rank)};
}
@ -6295,12 +6319,12 @@ void GameHandler::loadSkillLineAbilityDbc() {
auto* am = core::Application::getInstance().getAssetManager();
if (!am || !am->isInitialized()) return;
// SkillLineAbility.dbc: field 1=skillLineID, field 2=spellID
auto slaDbc = am->loadDBC("SkillLineAbility.dbc");
if (slaDbc && slaDbc->isLoaded()) {
const auto* slaL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLineAbility") : nullptr;
for (uint32_t i = 0; i < slaDbc->getRecordCount(); i++) {
uint32_t skillLineId = slaDbc->getUInt32(i, 1);
uint32_t spellId = slaDbc->getUInt32(i, 2);
uint32_t skillLineId = slaDbc->getUInt32(i, slaL ? (*slaL)["SkillLineID"] : 1);
uint32_t spellId = slaDbc->getUInt32(i, slaL ? (*slaL)["SpellID"] : 2);
if (spellId > 0 && skillLineId > 0) {
spellToSkillLine_[spellId] = skillLineId;
}
@ -6380,25 +6404,34 @@ void GameHandler::loadTalentDbc() {
// 12-14: PrereqRank[0-2]
// (other fields less relevant for basic functionality)
const auto* talL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Talent") : nullptr;
const uint32_t tID = talL ? (*talL)["ID"] : 0;
const uint32_t tTabID = talL ? (*talL)["TabID"] : 1;
const uint32_t tRow = talL ? (*talL)["Row"] : 2;
const uint32_t tCol = talL ? (*talL)["Column"] : 3;
const uint32_t tRank0 = talL ? (*talL)["RankSpell0"] : 4;
const uint32_t tPrereq0 = talL ? (*talL)["PrereqTalent0"] : 9;
const uint32_t tPrereqR0 = talL ? (*talL)["PrereqRank0"] : 12;
uint32_t count = talentDbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) {
TalentEntry entry;
entry.talentId = talentDbc->getUInt32(i, 0);
entry.talentId = talentDbc->getUInt32(i, tID);
if (entry.talentId == 0) continue;
entry.tabId = talentDbc->getUInt32(i, 1);
entry.row = static_cast<uint8_t>(talentDbc->getUInt32(i, 2));
entry.column = static_cast<uint8_t>(talentDbc->getUInt32(i, 3));
entry.tabId = talentDbc->getUInt32(i, tTabID);
entry.row = static_cast<uint8_t>(talentDbc->getUInt32(i, tRow));
entry.column = static_cast<uint8_t>(talentDbc->getUInt32(i, tCol));
// Rank spells (1-5 ranks)
for (int r = 0; r < 5; ++r) {
entry.rankSpells[r] = talentDbc->getUInt32(i, 4 + r);
entry.rankSpells[r] = talentDbc->getUInt32(i, tRank0 + r);
}
// Prerequisites
for (int p = 0; p < 3; ++p) {
entry.prereqTalent[p] = talentDbc->getUInt32(i, 9 + p);
entry.prereqRank[p] = static_cast<uint8_t>(talentDbc->getUInt32(i, 12 + p));
entry.prereqTalent[p] = talentDbc->getUInt32(i, tPrereq0 + p);
entry.prereqRank[p] = static_cast<uint8_t>(talentDbc->getUInt32(i, tPrereqR0 + p));
}
// Calculate max rank
@ -6429,16 +6462,17 @@ void GameHandler::loadTalentDbc() {
// 22: OrderIndex
// 23-39: BackgroundFile (16 localized strings + flags = 17 fields)
const auto* ttL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TalentTab") : nullptr;
uint32_t count = tabDbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) {
TalentTabEntry entry;
entry.tabId = tabDbc->getUInt32(i, 0);
entry.tabId = tabDbc->getUInt32(i, ttL ? (*ttL)["ID"] : 0);
if (entry.tabId == 0) continue;
entry.name = tabDbc->getString(i, 1);
entry.classMask = tabDbc->getUInt32(i, 20);
entry.orderIndex = static_cast<uint8_t>(tabDbc->getUInt32(i, 22));
entry.backgroundFile = tabDbc->getString(i, 23);
entry.name = tabDbc->getString(i, ttL ? (*ttL)["Name"] : 1);
entry.classMask = tabDbc->getUInt32(i, ttL ? (*ttL)["ClassMask"] : 20);
entry.orderIndex = static_cast<uint8_t>(tabDbc->getUInt32(i, ttL ? (*ttL)["OrderIndex"] : 22));
entry.backgroundFile = tabDbc->getString(i, ttL ? (*ttL)["BackgroundFile"] : 23);
talentTabCache_[entry.tabId] = entry;
@ -6616,7 +6650,7 @@ void GameHandler::handleTeleportAck(network::Packet& packet) {
// Send the ack back to the server
// Client→server MSG_MOVE_TELEPORT_ACK: u64 guid + u32 counter + u32 time
if (socket) {
network::Packet ack(static_cast<uint16_t>(Opcode::MSG_MOVE_TELEPORT_ACK));
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK));
// Write packed guid
uint8_t mask = 0;
uint8_t bytes[8];
@ -6698,7 +6732,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
// Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready
if (socket) {
network::Packet ack(static_cast<uint16_t>(Opcode::MSG_MOVE_WORLDPORT_ACK));
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
socket->send(ack);
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK");
}
@ -6720,25 +6754,28 @@ void GameHandler::loadTaxiDbc() {
auto* am = core::Application::getInstance().getAssetManager();
if (!am || !am->isInitialized()) return;
// Load TaxiNodes.dbc: 0=ID, 1=mapId, 2=x, 3=y, 4=z, 5=name(enUS locale)
auto nodesDbc = am->loadDBC("TaxiNodes.dbc");
if (nodesDbc && nodesDbc->isLoaded()) {
const auto* tnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiNodes") : nullptr;
uint32_t fieldCount = nodesDbc->getFieldCount();
for (uint32_t i = 0; i < nodesDbc->getRecordCount(); i++) {
TaxiNode node;
node.id = nodesDbc->getUInt32(i, 0);
node.mapId = nodesDbc->getUInt32(i, 1);
node.x = nodesDbc->getFloat(i, 2);
node.y = nodesDbc->getFloat(i, 3);
node.z = nodesDbc->getFloat(i, 4);
node.name = nodesDbc->getString(i, 5);
// TaxiNodes.dbc (3.3.5a): last two fields are mount display IDs (Alliance, Horde)
if (fieldCount >= 24) {
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, 22);
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, 23);
if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount >= 22) {
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, 20);
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, 21);
node.id = nodesDbc->getUInt32(i, tnL ? (*tnL)["ID"] : 0);
node.mapId = nodesDbc->getUInt32(i, tnL ? (*tnL)["MapID"] : 1);
node.x = nodesDbc->getFloat(i, tnL ? (*tnL)["X"] : 2);
node.y = nodesDbc->getFloat(i, tnL ? (*tnL)["Y"] : 3);
node.z = nodesDbc->getFloat(i, tnL ? (*tnL)["Z"] : 4);
node.name = nodesDbc->getString(i, tnL ? (*tnL)["Name"] : 5);
const uint32_t mountAllianceField = tnL ? (*tnL)["MountDisplayIdAlliance"] : 22;
const uint32_t mountHordeField = tnL ? (*tnL)["MountDisplayIdHorde"] : 23;
const uint32_t mountAllianceFB = tnL ? (*tnL)["MountDisplayIdAllianceFallback"] : 20;
const uint32_t mountHordeFB = tnL ? (*tnL)["MountDisplayIdHordeFallback"] : 21;
if (fieldCount > mountHordeField) {
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceField);
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeField);
if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount > mountHordeFB) {
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceFB);
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeFB);
}
}
if (node.id > 0) {
@ -6757,15 +6794,15 @@ void GameHandler::loadTaxiDbc() {
LOG_WARNING("Could not load TaxiNodes.dbc");
}
// Load TaxiPath.dbc: 0=pathId, 1=fromNode, 2=toNode, 3=cost
auto pathDbc = am->loadDBC("TaxiPath.dbc");
if (pathDbc && pathDbc->isLoaded()) {
const auto* tpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPath") : nullptr;
for (uint32_t i = 0; i < pathDbc->getRecordCount(); i++) {
TaxiPathEdge edge;
edge.pathId = pathDbc->getUInt32(i, 0);
edge.fromNode = pathDbc->getUInt32(i, 1);
edge.toNode = pathDbc->getUInt32(i, 2);
edge.cost = pathDbc->getUInt32(i, 3);
edge.pathId = pathDbc->getUInt32(i, tpL ? (*tpL)["ID"] : 0);
edge.fromNode = pathDbc->getUInt32(i, tpL ? (*tpL)["FromNode"] : 1);
edge.toNode = pathDbc->getUInt32(i, tpL ? (*tpL)["ToNode"] : 2);
edge.cost = pathDbc->getUInt32(i, tpL ? (*tpL)["Cost"] : 3);
taxiPathEdges_.push_back(edge);
}
LOG_INFO("Loaded ", taxiPathEdges_.size(), " taxi path edges from TaxiPath.dbc");
@ -6773,19 +6810,18 @@ void GameHandler::loadTaxiDbc() {
LOG_WARNING("Could not load TaxiPath.dbc");
}
// Load TaxiPathNode.dbc: actual spline waypoints for each path
// 0=ID, 1=PathID, 2=NodeIndex, 3=MapID, 4=X, 5=Y, 6=Z
auto pathNodeDbc = am->loadDBC("TaxiPathNode.dbc");
if (pathNodeDbc && pathNodeDbc->isLoaded()) {
const auto* tpnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPathNode") : nullptr;
for (uint32_t i = 0; i < pathNodeDbc->getRecordCount(); i++) {
TaxiPathNode node;
node.id = pathNodeDbc->getUInt32(i, 0);
node.pathId = pathNodeDbc->getUInt32(i, 1);
node.nodeIndex = pathNodeDbc->getUInt32(i, 2);
node.mapId = pathNodeDbc->getUInt32(i, 3);
node.x = pathNodeDbc->getFloat(i, 4);
node.y = pathNodeDbc->getFloat(i, 5);
node.z = pathNodeDbc->getFloat(i, 6);
node.id = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["ID"] : 0);
node.pathId = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["PathID"] : 1);
node.nodeIndex = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["NodeIndex"] : 2);
node.mapId = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["MapID"] : 3);
node.x = pathNodeDbc->getFloat(i, tpnL ? (*tpnL)["X"] : 4);
node.y = pathNodeDbc->getFloat(i, tpnL ? (*tpnL)["Y"] : 5);
node.z = pathNodeDbc->getFloat(i, tpnL ? (*tpnL)["Z"] : 6);
taxiPathNodes_[node.pathId].push_back(node);
}
// Sort waypoints by nodeIndex for each path
@ -7667,10 +7703,11 @@ void GameHandler::loadSkillLineDbc() {
return;
}
const auto* slL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, 0);
uint32_t category = dbc->getUInt32(i, 1);
std::string name = dbc->getString(i, 3);
uint32_t id = dbc->getUInt32(i, slL ? (*slL)["ID"] : 0);
uint32_t category = dbc->getUInt32(i, slL ? (*slL)["Category"] : 1);
std::string name = dbc->getString(i, slL ? (*slL)["Name"] : 3);
if (id > 0 && !name.empty()) {
skillLineNames_[id] = name;
skillLineCategories_[id] = category;
@ -7682,8 +7719,7 @@ void GameHandler::loadSkillLineDbc() {
void GameHandler::extractSkillFields(const std::map<uint16_t, uint32_t>& fields) {
loadSkillLineDbc();
// PLAYER_SKILL_INFO_1_1 = field 636, 128 slots x 3 fields each (636..1019)
static constexpr uint16_t PLAYER_SKILL_INFO_START = 636;
const uint16_t PLAYER_SKILL_INFO_START = fieldIndex(UF::PLAYER_SKILL_INFO_START);
static constexpr int MAX_SKILL_SLOTS = 128;
std::map<uint32_t, PlayerSkill> newSkills;
@ -7745,7 +7781,7 @@ void GameHandler::extractExploredZoneFields(const std::map<uint16_t, uint32_t>&
bool foundAny = false;
for (size_t i = 0; i < PLAYER_EXPLORED_ZONES_COUNT; i++) {
const uint16_t fieldIdx = static_cast<uint16_t>(PLAYER_EXPLORED_ZONES_START + i);
const uint16_t fieldIdx = static_cast<uint16_t>(fieldIndex(UF::PLAYER_EXPLORED_ZONES_START) + i);
auto it = fields.find(fieldIdx);
if (it == fields.end()) continue;
playerExploredZones_[i] = it->second;

649
src/game/opcode_table.cpp Normal file
View file

@ -0,0 +1,649 @@
#include "game/opcode_table.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
namespace wowee {
namespace game {
// Global active opcode table pointer
static const OpcodeTable* g_activeOpcodeTable = nullptr;
void setActiveOpcodeTable(const OpcodeTable* table) { g_activeOpcodeTable = table; }
const OpcodeTable* getActiveOpcodeTable() { return g_activeOpcodeTable; }
// Name ↔ LogicalOpcode mapping table (generated from the enum)
struct OpcodeNameEntry {
const char* name;
LogicalOpcode op;
};
// clang-format off
static const OpcodeNameEntry kOpcodeNames[] = {
{"CMSG_PING", LogicalOpcode::CMSG_PING},
{"CMSG_AUTH_SESSION", LogicalOpcode::CMSG_AUTH_SESSION},
{"CMSG_CHAR_CREATE", LogicalOpcode::CMSG_CHAR_CREATE},
{"CMSG_CHAR_ENUM", LogicalOpcode::CMSG_CHAR_ENUM},
{"CMSG_CHAR_DELETE", LogicalOpcode::CMSG_CHAR_DELETE},
{"CMSG_PLAYER_LOGIN", LogicalOpcode::CMSG_PLAYER_LOGIN},
{"CMSG_MOVE_START_FORWARD", LogicalOpcode::CMSG_MOVE_START_FORWARD},
{"CMSG_MOVE_START_BACKWARD", LogicalOpcode::CMSG_MOVE_START_BACKWARD},
{"CMSG_MOVE_STOP", LogicalOpcode::CMSG_MOVE_STOP},
{"CMSG_MOVE_START_STRAFE_LEFT", LogicalOpcode::CMSG_MOVE_START_STRAFE_LEFT},
{"CMSG_MOVE_START_STRAFE_RIGHT", LogicalOpcode::CMSG_MOVE_START_STRAFE_RIGHT},
{"CMSG_MOVE_STOP_STRAFE", LogicalOpcode::CMSG_MOVE_STOP_STRAFE},
{"CMSG_MOVE_JUMP", LogicalOpcode::CMSG_MOVE_JUMP},
{"CMSG_MOVE_START_TURN_LEFT", LogicalOpcode::CMSG_MOVE_START_TURN_LEFT},
{"CMSG_MOVE_START_TURN_RIGHT", LogicalOpcode::CMSG_MOVE_START_TURN_RIGHT},
{"CMSG_MOVE_STOP_TURN", LogicalOpcode::CMSG_MOVE_STOP_TURN},
{"CMSG_MOVE_SET_FACING", LogicalOpcode::CMSG_MOVE_SET_FACING},
{"CMSG_MOVE_FALL_LAND", LogicalOpcode::CMSG_MOVE_FALL_LAND},
{"CMSG_MOVE_START_SWIM", LogicalOpcode::CMSG_MOVE_START_SWIM},
{"CMSG_MOVE_STOP_SWIM", LogicalOpcode::CMSG_MOVE_STOP_SWIM},
{"CMSG_MOVE_HEARTBEAT", LogicalOpcode::CMSG_MOVE_HEARTBEAT},
{"SMSG_AUTH_CHALLENGE", LogicalOpcode::SMSG_AUTH_CHALLENGE},
{"SMSG_AUTH_RESPONSE", LogicalOpcode::SMSG_AUTH_RESPONSE},
{"SMSG_CHAR_CREATE", LogicalOpcode::SMSG_CHAR_CREATE},
{"SMSG_CHAR_ENUM", LogicalOpcode::SMSG_CHAR_ENUM},
{"SMSG_CHAR_DELETE", LogicalOpcode::SMSG_CHAR_DELETE},
{"SMSG_PONG", LogicalOpcode::SMSG_PONG},
{"SMSG_LOGIN_VERIFY_WORLD", LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD},
{"SMSG_LOGIN_SETTIMESPEED", LogicalOpcode::SMSG_LOGIN_SETTIMESPEED},
{"SMSG_TUTORIAL_FLAGS", LogicalOpcode::SMSG_TUTORIAL_FLAGS},
{"SMSG_WARDEN_DATA", LogicalOpcode::SMSG_WARDEN_DATA},
{"CMSG_WARDEN_DATA", LogicalOpcode::CMSG_WARDEN_DATA},
{"SMSG_ACCOUNT_DATA_TIMES", LogicalOpcode::SMSG_ACCOUNT_DATA_TIMES},
{"SMSG_CLIENTCACHE_VERSION", LogicalOpcode::SMSG_CLIENTCACHE_VERSION},
{"SMSG_FEATURE_SYSTEM_STATUS", LogicalOpcode::SMSG_FEATURE_SYSTEM_STATUS},
{"SMSG_MOTD", LogicalOpcode::SMSG_MOTD},
{"SMSG_UPDATE_OBJECT", LogicalOpcode::SMSG_UPDATE_OBJECT},
{"SMSG_COMPRESSED_UPDATE_OBJECT", LogicalOpcode::SMSG_COMPRESSED_UPDATE_OBJECT},
{"SMSG_MONSTER_MOVE_TRANSPORT", LogicalOpcode::SMSG_MONSTER_MOVE_TRANSPORT},
{"SMSG_DESTROY_OBJECT", LogicalOpcode::SMSG_DESTROY_OBJECT},
{"CMSG_MESSAGECHAT", LogicalOpcode::CMSG_MESSAGECHAT},
{"SMSG_MESSAGECHAT", LogicalOpcode::SMSG_MESSAGECHAT},
{"CMSG_WHO", LogicalOpcode::CMSG_WHO},
{"SMSG_WHO", LogicalOpcode::SMSG_WHO},
{"CMSG_REQUEST_PLAYED_TIME", LogicalOpcode::CMSG_REQUEST_PLAYED_TIME},
{"SMSG_PLAYED_TIME", LogicalOpcode::SMSG_PLAYED_TIME},
{"CMSG_QUERY_TIME", LogicalOpcode::CMSG_QUERY_TIME},
{"SMSG_QUERY_TIME_RESPONSE", LogicalOpcode::SMSG_QUERY_TIME_RESPONSE},
{"SMSG_FRIEND_STATUS", LogicalOpcode::SMSG_FRIEND_STATUS},
{"CMSG_ADD_FRIEND", LogicalOpcode::CMSG_ADD_FRIEND},
{"CMSG_DEL_FRIEND", LogicalOpcode::CMSG_DEL_FRIEND},
{"CMSG_SET_CONTACT_NOTES", LogicalOpcode::CMSG_SET_CONTACT_NOTES},
{"CMSG_ADD_IGNORE", LogicalOpcode::CMSG_ADD_IGNORE},
{"CMSG_DEL_IGNORE", LogicalOpcode::CMSG_DEL_IGNORE},
{"CMSG_PLAYER_LOGOUT", LogicalOpcode::CMSG_PLAYER_LOGOUT},
{"CMSG_LOGOUT_REQUEST", LogicalOpcode::CMSG_LOGOUT_REQUEST},
{"CMSG_LOGOUT_CANCEL", LogicalOpcode::CMSG_LOGOUT_CANCEL},
{"SMSG_LOGOUT_RESPONSE", LogicalOpcode::SMSG_LOGOUT_RESPONSE},
{"SMSG_LOGOUT_COMPLETE", LogicalOpcode::SMSG_LOGOUT_COMPLETE},
{"CMSG_STAND_STATE_CHANGE", LogicalOpcode::CMSG_STAND_STATE_CHANGE},
{"CMSG_SHOWING_HELM", LogicalOpcode::CMSG_SHOWING_HELM},
{"CMSG_SHOWING_CLOAK", LogicalOpcode::CMSG_SHOWING_CLOAK},
{"CMSG_TOGGLE_PVP", LogicalOpcode::CMSG_TOGGLE_PVP},
{"CMSG_GUILD_INVITE", LogicalOpcode::CMSG_GUILD_INVITE},
{"CMSG_GUILD_ACCEPT", LogicalOpcode::CMSG_GUILD_ACCEPT},
{"CMSG_GUILD_DECLINE_INVITATION", LogicalOpcode::CMSG_GUILD_DECLINE_INVITATION},
{"CMSG_GUILD_INFO", LogicalOpcode::CMSG_GUILD_INFO},
{"CMSG_GUILD_GET_ROSTER", LogicalOpcode::CMSG_GUILD_GET_ROSTER},
{"CMSG_GUILD_PROMOTE_MEMBER", LogicalOpcode::CMSG_GUILD_PROMOTE_MEMBER},
{"CMSG_GUILD_DEMOTE_MEMBER", LogicalOpcode::CMSG_GUILD_DEMOTE_MEMBER},
{"CMSG_GUILD_LEAVE", LogicalOpcode::CMSG_GUILD_LEAVE},
{"CMSG_GUILD_MOTD", LogicalOpcode::CMSG_GUILD_MOTD},
{"SMSG_GUILD_INFO", LogicalOpcode::SMSG_GUILD_INFO},
{"SMSG_GUILD_ROSTER", LogicalOpcode::SMSG_GUILD_ROSTER},
{"MSG_RAID_READY_CHECK", LogicalOpcode::MSG_RAID_READY_CHECK},
{"MSG_RAID_READY_CHECK_CONFIRM", LogicalOpcode::MSG_RAID_READY_CHECK_CONFIRM},
{"CMSG_DUEL_PROPOSED", LogicalOpcode::CMSG_DUEL_PROPOSED},
{"CMSG_DUEL_ACCEPTED", LogicalOpcode::CMSG_DUEL_ACCEPTED},
{"CMSG_DUEL_CANCELLED", LogicalOpcode::CMSG_DUEL_CANCELLED},
{"SMSG_DUEL_REQUESTED", LogicalOpcode::SMSG_DUEL_REQUESTED},
{"CMSG_INITIATE_TRADE", LogicalOpcode::CMSG_INITIATE_TRADE},
{"MSG_RANDOM_ROLL", LogicalOpcode::MSG_RANDOM_ROLL},
{"CMSG_SET_SELECTION", LogicalOpcode::CMSG_SET_SELECTION},
{"CMSG_NAME_QUERY", LogicalOpcode::CMSG_NAME_QUERY},
{"SMSG_NAME_QUERY_RESPONSE", LogicalOpcode::SMSG_NAME_QUERY_RESPONSE},
{"CMSG_CREATURE_QUERY", LogicalOpcode::CMSG_CREATURE_QUERY},
{"SMSG_CREATURE_QUERY_RESPONSE", LogicalOpcode::SMSG_CREATURE_QUERY_RESPONSE},
{"CMSG_GAMEOBJECT_QUERY", LogicalOpcode::CMSG_GAMEOBJECT_QUERY},
{"SMSG_GAMEOBJECT_QUERY_RESPONSE", LogicalOpcode::SMSG_GAMEOBJECT_QUERY_RESPONSE},
{"CMSG_SET_ACTIVE_MOVER", LogicalOpcode::CMSG_SET_ACTIVE_MOVER},
{"CMSG_BINDER_ACTIVATE", LogicalOpcode::CMSG_BINDER_ACTIVATE},
{"SMSG_LOG_XPGAIN", LogicalOpcode::SMSG_LOG_XPGAIN},
{"SMSG_MONSTER_MOVE", LogicalOpcode::SMSG_MONSTER_MOVE},
{"CMSG_ATTACKSWING", LogicalOpcode::CMSG_ATTACKSWING},
{"CMSG_ATTACKSTOP", LogicalOpcode::CMSG_ATTACKSTOP},
{"SMSG_ATTACKSTART", LogicalOpcode::SMSG_ATTACKSTART},
{"SMSG_ATTACKSTOP", LogicalOpcode::SMSG_ATTACKSTOP},
{"SMSG_ATTACKERSTATEUPDATE", LogicalOpcode::SMSG_ATTACKERSTATEUPDATE},
{"SMSG_SPELLNONMELEEDAMAGELOG", LogicalOpcode::SMSG_SPELLNONMELEEDAMAGELOG},
{"SMSG_SPELLHEALLOG", LogicalOpcode::SMSG_SPELLHEALLOG},
{"SMSG_SPELLENERGIZELOG", LogicalOpcode::SMSG_SPELLENERGIZELOG},
{"SMSG_PERIODICAURALOG", LogicalOpcode::SMSG_PERIODICAURALOG},
{"SMSG_ENVIRONMENTALDAMAGELOG", LogicalOpcode::SMSG_ENVIRONMENTALDAMAGELOG},
{"CMSG_CAST_SPELL", LogicalOpcode::CMSG_CAST_SPELL},
{"CMSG_CANCEL_CAST", LogicalOpcode::CMSG_CANCEL_CAST},
{"CMSG_CANCEL_AURA", LogicalOpcode::CMSG_CANCEL_AURA},
{"SMSG_CAST_FAILED", LogicalOpcode::SMSG_CAST_FAILED},
{"SMSG_SPELL_START", LogicalOpcode::SMSG_SPELL_START},
{"SMSG_SPELL_GO", LogicalOpcode::SMSG_SPELL_GO},
{"SMSG_SPELL_FAILURE", LogicalOpcode::SMSG_SPELL_FAILURE},
{"SMSG_SPELL_COOLDOWN", LogicalOpcode::SMSG_SPELL_COOLDOWN},
{"SMSG_COOLDOWN_EVENT", LogicalOpcode::SMSG_COOLDOWN_EVENT},
{"SMSG_UPDATE_AURA_DURATION", LogicalOpcode::SMSG_UPDATE_AURA_DURATION},
{"SMSG_INITIAL_SPELLS", LogicalOpcode::SMSG_INITIAL_SPELLS},
{"SMSG_LEARNED_SPELL", LogicalOpcode::SMSG_LEARNED_SPELL},
{"SMSG_SUPERCEDED_SPELL", LogicalOpcode::SMSG_SUPERCEDED_SPELL},
{"SMSG_REMOVED_SPELL", LogicalOpcode::SMSG_REMOVED_SPELL},
{"SMSG_SEND_UNLEARN_SPELLS", LogicalOpcode::SMSG_SEND_UNLEARN_SPELLS},
{"SMSG_SPELL_DELAYED", LogicalOpcode::SMSG_SPELL_DELAYED},
{"SMSG_AURA_UPDATE", LogicalOpcode::SMSG_AURA_UPDATE},
{"SMSG_AURA_UPDATE_ALL", LogicalOpcode::SMSG_AURA_UPDATE_ALL},
{"SMSG_SET_FLAT_SPELL_MODIFIER", LogicalOpcode::SMSG_SET_FLAT_SPELL_MODIFIER},
{"SMSG_SET_PCT_SPELL_MODIFIER", LogicalOpcode::SMSG_SET_PCT_SPELL_MODIFIER},
{"SMSG_TALENTS_INFO", LogicalOpcode::SMSG_TALENTS_INFO},
{"CMSG_LEARN_TALENT", LogicalOpcode::CMSG_LEARN_TALENT},
{"MSG_TALENT_WIPE_CONFIRM", LogicalOpcode::MSG_TALENT_WIPE_CONFIRM},
{"CMSG_GROUP_INVITE", LogicalOpcode::CMSG_GROUP_INVITE},
{"SMSG_GROUP_INVITE", LogicalOpcode::SMSG_GROUP_INVITE},
{"CMSG_GROUP_ACCEPT", LogicalOpcode::CMSG_GROUP_ACCEPT},
{"CMSG_GROUP_DECLINE", LogicalOpcode::CMSG_GROUP_DECLINE},
{"SMSG_GROUP_DECLINE", LogicalOpcode::SMSG_GROUP_DECLINE},
{"CMSG_GROUP_UNINVITE_GUID", LogicalOpcode::CMSG_GROUP_UNINVITE_GUID},
{"SMSG_GROUP_UNINVITE", LogicalOpcode::SMSG_GROUP_UNINVITE},
{"CMSG_GROUP_SET_LEADER", LogicalOpcode::CMSG_GROUP_SET_LEADER},
{"SMSG_GROUP_SET_LEADER", LogicalOpcode::SMSG_GROUP_SET_LEADER},
{"CMSG_GROUP_DISBAND", LogicalOpcode::CMSG_GROUP_DISBAND},
{"SMSG_GROUP_LIST", LogicalOpcode::SMSG_GROUP_LIST},
{"SMSG_PARTY_COMMAND_RESULT", LogicalOpcode::SMSG_PARTY_COMMAND_RESULT},
{"MSG_RAID_TARGET_UPDATE", LogicalOpcode::MSG_RAID_TARGET_UPDATE},
{"CMSG_REQUEST_RAID_INFO", LogicalOpcode::CMSG_REQUEST_RAID_INFO},
{"SMSG_RAID_INSTANCE_INFO", LogicalOpcode::SMSG_RAID_INSTANCE_INFO},
{"CMSG_AUTOSTORE_LOOT_ITEM", LogicalOpcode::CMSG_AUTOSTORE_LOOT_ITEM},
{"CMSG_LOOT", LogicalOpcode::CMSG_LOOT},
{"CMSG_LOOT_MONEY", LogicalOpcode::CMSG_LOOT_MONEY},
{"CMSG_LOOT_RELEASE", LogicalOpcode::CMSG_LOOT_RELEASE},
{"SMSG_LOOT_RESPONSE", LogicalOpcode::SMSG_LOOT_RESPONSE},
{"SMSG_LOOT_RELEASE_RESPONSE", LogicalOpcode::SMSG_LOOT_RELEASE_RESPONSE},
{"SMSG_LOOT_REMOVED", LogicalOpcode::SMSG_LOOT_REMOVED},
{"SMSG_LOOT_MONEY_NOTIFY", LogicalOpcode::SMSG_LOOT_MONEY_NOTIFY},
{"SMSG_LOOT_CLEAR_MONEY", LogicalOpcode::SMSG_LOOT_CLEAR_MONEY},
{"CMSG_ACTIVATETAXI", LogicalOpcode::CMSG_ACTIVATETAXI},
{"CMSG_GOSSIP_HELLO", LogicalOpcode::CMSG_GOSSIP_HELLO},
{"CMSG_GOSSIP_SELECT_OPTION", LogicalOpcode::CMSG_GOSSIP_SELECT_OPTION},
{"SMSG_GOSSIP_MESSAGE", LogicalOpcode::SMSG_GOSSIP_MESSAGE},
{"SMSG_GOSSIP_COMPLETE", LogicalOpcode::SMSG_GOSSIP_COMPLETE},
{"SMSG_NPC_TEXT_UPDATE", LogicalOpcode::SMSG_NPC_TEXT_UPDATE},
{"CMSG_GAMEOBJECT_USE", LogicalOpcode::CMSG_GAMEOBJECT_USE},
{"CMSG_QUESTGIVER_STATUS_QUERY", LogicalOpcode::CMSG_QUESTGIVER_STATUS_QUERY},
{"SMSG_QUESTGIVER_STATUS", LogicalOpcode::SMSG_QUESTGIVER_STATUS},
{"SMSG_QUESTGIVER_STATUS_MULTIPLE", LogicalOpcode::SMSG_QUESTGIVER_STATUS_MULTIPLE},
{"CMSG_QUESTGIVER_HELLO", LogicalOpcode::CMSG_QUESTGIVER_HELLO},
{"CMSG_QUESTGIVER_QUERY_QUEST", LogicalOpcode::CMSG_QUESTGIVER_QUERY_QUEST},
{"SMSG_QUESTGIVER_QUEST_DETAILS", LogicalOpcode::SMSG_QUESTGIVER_QUEST_DETAILS},
{"CMSG_QUESTGIVER_ACCEPT_QUEST", LogicalOpcode::CMSG_QUESTGIVER_ACCEPT_QUEST},
{"CMSG_QUESTGIVER_COMPLETE_QUEST", LogicalOpcode::CMSG_QUESTGIVER_COMPLETE_QUEST},
{"SMSG_QUESTGIVER_REQUEST_ITEMS", LogicalOpcode::SMSG_QUESTGIVER_REQUEST_ITEMS},
{"CMSG_QUESTGIVER_REQUEST_REWARD", LogicalOpcode::CMSG_QUESTGIVER_REQUEST_REWARD},
{"SMSG_QUESTGIVER_OFFER_REWARD", LogicalOpcode::SMSG_QUESTGIVER_OFFER_REWARD},
{"CMSG_QUESTGIVER_CHOOSE_REWARD", LogicalOpcode::CMSG_QUESTGIVER_CHOOSE_REWARD},
{"SMSG_QUESTGIVER_QUEST_INVALID", LogicalOpcode::SMSG_QUESTGIVER_QUEST_INVALID},
{"SMSG_QUESTGIVER_QUEST_COMPLETE", LogicalOpcode::SMSG_QUESTGIVER_QUEST_COMPLETE},
{"CMSG_QUESTLOG_REMOVE_QUEST", LogicalOpcode::CMSG_QUESTLOG_REMOVE_QUEST},
{"SMSG_QUESTUPDATE_ADD_KILL", LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL},
{"SMSG_QUESTUPDATE_COMPLETE", LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE},
{"CMSG_QUEST_QUERY", LogicalOpcode::CMSG_QUEST_QUERY},
{"SMSG_QUEST_QUERY_RESPONSE", LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE},
{"SMSG_QUESTLOG_FULL", LogicalOpcode::SMSG_QUESTLOG_FULL},
{"CMSG_LIST_INVENTORY", LogicalOpcode::CMSG_LIST_INVENTORY},
{"SMSG_LIST_INVENTORY", LogicalOpcode::SMSG_LIST_INVENTORY},
{"CMSG_SELL_ITEM", LogicalOpcode::CMSG_SELL_ITEM},
{"SMSG_SELL_ITEM", LogicalOpcode::SMSG_SELL_ITEM},
{"CMSG_BUY_ITEM", LogicalOpcode::CMSG_BUY_ITEM},
{"SMSG_BUY_FAILED", LogicalOpcode::SMSG_BUY_FAILED},
{"CMSG_TRAINER_LIST", LogicalOpcode::CMSG_TRAINER_LIST},
{"SMSG_TRAINER_LIST", LogicalOpcode::SMSG_TRAINER_LIST},
{"CMSG_TRAINER_BUY_SPELL", LogicalOpcode::CMSG_TRAINER_BUY_SPELL},
{"SMSG_TRAINER_BUY_FAILED", LogicalOpcode::SMSG_TRAINER_BUY_FAILED},
{"CMSG_ITEM_QUERY_SINGLE", LogicalOpcode::CMSG_ITEM_QUERY_SINGLE},
{"SMSG_ITEM_QUERY_SINGLE_RESPONSE", LogicalOpcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE},
{"CMSG_USE_ITEM", LogicalOpcode::CMSG_USE_ITEM},
{"CMSG_AUTOEQUIP_ITEM", LogicalOpcode::CMSG_AUTOEQUIP_ITEM},
{"CMSG_SWAP_ITEM", LogicalOpcode::CMSG_SWAP_ITEM},
{"CMSG_SWAP_INV_ITEM", LogicalOpcode::CMSG_SWAP_INV_ITEM},
{"SMSG_INVENTORY_CHANGE_FAILURE", LogicalOpcode::SMSG_INVENTORY_CHANGE_FAILURE},
{"CMSG_INSPECT", LogicalOpcode::CMSG_INSPECT},
{"SMSG_INSPECT_RESULTS", LogicalOpcode::SMSG_INSPECT_RESULTS},
{"CMSG_REPOP_REQUEST", LogicalOpcode::CMSG_REPOP_REQUEST},
{"SMSG_RESURRECT_REQUEST", LogicalOpcode::SMSG_RESURRECT_REQUEST},
{"CMSG_RESURRECT_RESPONSE", LogicalOpcode::CMSG_RESURRECT_RESPONSE},
{"CMSG_SPIRIT_HEALER_ACTIVATE", LogicalOpcode::CMSG_SPIRIT_HEALER_ACTIVATE},
{"SMSG_SPIRIT_HEALER_CONFIRM", LogicalOpcode::SMSG_SPIRIT_HEALER_CONFIRM},
{"SMSG_RESURRECT_CANCEL", LogicalOpcode::SMSG_RESURRECT_CANCEL},
{"MSG_MOVE_TELEPORT_ACK", LogicalOpcode::MSG_MOVE_TELEPORT_ACK},
{"SMSG_TRANSFER_PENDING", LogicalOpcode::SMSG_TRANSFER_PENDING},
{"SMSG_NEW_WORLD", LogicalOpcode::SMSG_NEW_WORLD},
{"MSG_MOVE_WORLDPORT_ACK", LogicalOpcode::MSG_MOVE_WORLDPORT_ACK},
{"SMSG_TRANSFER_ABORTED", LogicalOpcode::SMSG_TRANSFER_ABORTED},
{"SMSG_FORCE_RUN_SPEED_CHANGE", LogicalOpcode::SMSG_FORCE_RUN_SPEED_CHANGE},
{"CMSG_FORCE_RUN_SPEED_CHANGE_ACK", LogicalOpcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK},
{"CMSG_CANCEL_MOUNT_AURA", LogicalOpcode::CMSG_CANCEL_MOUNT_AURA},
{"SMSG_SHOWTAXINODES", LogicalOpcode::SMSG_SHOWTAXINODES},
{"SMSG_ACTIVATETAXIREPLY", LogicalOpcode::SMSG_ACTIVATETAXIREPLY},
{"SMSG_ACTIVATETAXIREPLY_ALT", LogicalOpcode::SMSG_ACTIVATETAXIREPLY_ALT},
{"SMSG_NEW_TAXI_PATH", LogicalOpcode::SMSG_NEW_TAXI_PATH},
{"CMSG_ACTIVATETAXIEXPRESS", LogicalOpcode::CMSG_ACTIVATETAXIEXPRESS},
{"SMSG_BATTLEFIELD_PORT_DENIED", LogicalOpcode::SMSG_BATTLEFIELD_PORT_DENIED},
{"SMSG_REMOVED_FROM_PVP_QUEUE", LogicalOpcode::SMSG_REMOVED_FROM_PVP_QUEUE},
{"SMSG_TRAINER_BUY_SUCCEEDED", LogicalOpcode::SMSG_TRAINER_BUY_SUCCEEDED},
{"SMSG_BINDPOINTUPDATE", LogicalOpcode::SMSG_BINDPOINTUPDATE},
{"CMSG_BATTLEFIELD_LIST", LogicalOpcode::CMSG_BATTLEFIELD_LIST},
{"SMSG_BATTLEFIELD_LIST", LogicalOpcode::SMSG_BATTLEFIELD_LIST},
{"CMSG_BATTLEFIELD_JOIN", LogicalOpcode::CMSG_BATTLEFIELD_JOIN},
{"CMSG_BATTLEFIELD_STATUS", LogicalOpcode::CMSG_BATTLEFIELD_STATUS},
{"SMSG_BATTLEFIELD_STATUS", LogicalOpcode::SMSG_BATTLEFIELD_STATUS},
{"CMSG_BATTLEFIELD_PORT", LogicalOpcode::CMSG_BATTLEFIELD_PORT},
{"CMSG_BATTLEMASTER_HELLO", LogicalOpcode::CMSG_BATTLEMASTER_HELLO},
{"MSG_PVP_LOG_DATA", LogicalOpcode::MSG_PVP_LOG_DATA},
{"CMSG_LEAVE_BATTLEFIELD", LogicalOpcode::CMSG_LEAVE_BATTLEFIELD},
{"SMSG_GROUP_JOINED_BATTLEGROUND", LogicalOpcode::SMSG_GROUP_JOINED_BATTLEGROUND},
{"MSG_BATTLEGROUND_PLAYER_POSITIONS", LogicalOpcode::MSG_BATTLEGROUND_PLAYER_POSITIONS},
{"SMSG_BATTLEGROUND_PLAYER_JOINED", LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_JOINED},
{"SMSG_BATTLEGROUND_PLAYER_LEFT", LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_LEFT},
{"CMSG_BATTLEMASTER_JOIN", LogicalOpcode::CMSG_BATTLEMASTER_JOIN},
{"SMSG_JOINED_BATTLEGROUND_QUEUE", LogicalOpcode::SMSG_JOINED_BATTLEGROUND_QUEUE},
{"CMSG_ARENA_TEAM_CREATE", LogicalOpcode::CMSG_ARENA_TEAM_CREATE},
{"SMSG_ARENA_TEAM_COMMAND_RESULT", LogicalOpcode::SMSG_ARENA_TEAM_COMMAND_RESULT},
{"CMSG_ARENA_TEAM_QUERY", LogicalOpcode::CMSG_ARENA_TEAM_QUERY},
{"SMSG_ARENA_TEAM_QUERY_RESPONSE", LogicalOpcode::SMSG_ARENA_TEAM_QUERY_RESPONSE},
{"CMSG_ARENA_TEAM_ROSTER", LogicalOpcode::CMSG_ARENA_TEAM_ROSTER},
{"SMSG_ARENA_TEAM_ROSTER", LogicalOpcode::SMSG_ARENA_TEAM_ROSTER},
{"CMSG_ARENA_TEAM_INVITE", LogicalOpcode::CMSG_ARENA_TEAM_INVITE},
{"SMSG_ARENA_TEAM_INVITE", LogicalOpcode::SMSG_ARENA_TEAM_INVITE},
{"CMSG_ARENA_TEAM_ACCEPT", LogicalOpcode::CMSG_ARENA_TEAM_ACCEPT},
{"CMSG_ARENA_TEAM_DECLINE", LogicalOpcode::CMSG_ARENA_TEAM_DECLINE},
{"CMSG_ARENA_TEAM_LEAVE", LogicalOpcode::CMSG_ARENA_TEAM_LEAVE},
{"CMSG_ARENA_TEAM_REMOVE", LogicalOpcode::CMSG_ARENA_TEAM_REMOVE},
{"CMSG_ARENA_TEAM_DISBAND", LogicalOpcode::CMSG_ARENA_TEAM_DISBAND},
{"CMSG_ARENA_TEAM_LEADER", LogicalOpcode::CMSG_ARENA_TEAM_LEADER},
{"SMSG_ARENA_TEAM_EVENT", LogicalOpcode::SMSG_ARENA_TEAM_EVENT},
{"CMSG_BATTLEMASTER_JOIN_ARENA", LogicalOpcode::CMSG_BATTLEMASTER_JOIN_ARENA},
{"SMSG_ARENA_TEAM_STATS", LogicalOpcode::SMSG_ARENA_TEAM_STATS},
{"SMSG_ARENA_ERROR", LogicalOpcode::SMSG_ARENA_ERROR},
{"MSG_INSPECT_ARENA_TEAMS", LogicalOpcode::MSG_INSPECT_ARENA_TEAMS},
};
// clang-format on
static constexpr size_t kOpcodeNameCount = sizeof(kOpcodeNames) / sizeof(kOpcodeNames[0]);
std::optional<LogicalOpcode> OpcodeTable::nameToLogical(const std::string& name) {
for (size_t i = 0; i < kOpcodeNameCount; ++i) {
if (name == kOpcodeNames[i].name) return kOpcodeNames[i].op;
}
return std::nullopt;
}
const char* OpcodeTable::logicalToName(LogicalOpcode op) {
uint16_t val = static_cast<uint16_t>(op);
for (size_t i = 0; i < kOpcodeNameCount; ++i) {
if (static_cast<uint16_t>(kOpcodeNames[i].op) == val) return kOpcodeNames[i].name;
}
return "UNKNOWN";
}
void OpcodeTable::loadWotlkDefaults() {
// WotLK 3.3.5a wire values — matches the original hardcoded Opcode enum
struct { LogicalOpcode op; uint16_t wire; } defaults[] = {
{LogicalOpcode::CMSG_PING, 0x1DC},
{LogicalOpcode::CMSG_AUTH_SESSION, 0x1ED},
{LogicalOpcode::CMSG_CHAR_CREATE, 0x036},
{LogicalOpcode::CMSG_CHAR_ENUM, 0x037},
{LogicalOpcode::CMSG_CHAR_DELETE, 0x038},
{LogicalOpcode::CMSG_PLAYER_LOGIN, 0x03D},
{LogicalOpcode::CMSG_MOVE_START_FORWARD, 0x0B5},
{LogicalOpcode::CMSG_MOVE_START_BACKWARD, 0x0B6},
{LogicalOpcode::CMSG_MOVE_STOP, 0x0B7},
{LogicalOpcode::CMSG_MOVE_START_STRAFE_LEFT, 0x0B8},
{LogicalOpcode::CMSG_MOVE_START_STRAFE_RIGHT, 0x0B9},
{LogicalOpcode::CMSG_MOVE_STOP_STRAFE, 0x0BA},
{LogicalOpcode::CMSG_MOVE_JUMP, 0x0BB},
{LogicalOpcode::CMSG_MOVE_START_TURN_LEFT, 0x0BC},
{LogicalOpcode::CMSG_MOVE_START_TURN_RIGHT, 0x0BD},
{LogicalOpcode::CMSG_MOVE_STOP_TURN, 0x0BE},
{LogicalOpcode::CMSG_MOVE_SET_FACING, 0x0DA},
{LogicalOpcode::CMSG_MOVE_FALL_LAND, 0x0C9},
{LogicalOpcode::CMSG_MOVE_START_SWIM, 0x0CA},
{LogicalOpcode::CMSG_MOVE_STOP_SWIM, 0x0CB},
{LogicalOpcode::CMSG_MOVE_HEARTBEAT, 0x0EE},
{LogicalOpcode::SMSG_AUTH_CHALLENGE, 0x1EC},
{LogicalOpcode::SMSG_AUTH_RESPONSE, 0x1EE},
{LogicalOpcode::SMSG_CHAR_CREATE, 0x03A},
{LogicalOpcode::SMSG_CHAR_ENUM, 0x03B},
{LogicalOpcode::SMSG_CHAR_DELETE, 0x03C},
{LogicalOpcode::SMSG_PONG, 0x1DD},
{LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD, 0x236},
{LogicalOpcode::SMSG_LOGIN_SETTIMESPEED, 0x042},
{LogicalOpcode::SMSG_TUTORIAL_FLAGS, 0x0FD},
{LogicalOpcode::SMSG_WARDEN_DATA, 0x2E6},
{LogicalOpcode::CMSG_WARDEN_DATA, 0x2E7},
{LogicalOpcode::SMSG_ACCOUNT_DATA_TIMES, 0x209},
{LogicalOpcode::SMSG_CLIENTCACHE_VERSION, 0x4AB},
{LogicalOpcode::SMSG_FEATURE_SYSTEM_STATUS, 0x3ED},
{LogicalOpcode::SMSG_MOTD, 0x33D},
{LogicalOpcode::SMSG_UPDATE_OBJECT, 0x0A9},
{LogicalOpcode::SMSG_COMPRESSED_UPDATE_OBJECT, 0x1F6},
{LogicalOpcode::SMSG_MONSTER_MOVE_TRANSPORT, 0x2AE},
{LogicalOpcode::SMSG_DESTROY_OBJECT, 0x0AA},
{LogicalOpcode::CMSG_MESSAGECHAT, 0x095},
{LogicalOpcode::SMSG_MESSAGECHAT, 0x096},
{LogicalOpcode::CMSG_WHO, 0x062},
{LogicalOpcode::SMSG_WHO, 0x063},
{LogicalOpcode::CMSG_REQUEST_PLAYED_TIME, 0x1CC},
{LogicalOpcode::SMSG_PLAYED_TIME, 0x1CD},
{LogicalOpcode::CMSG_QUERY_TIME, 0x1CE},
{LogicalOpcode::SMSG_QUERY_TIME_RESPONSE, 0x1CF},
{LogicalOpcode::SMSG_FRIEND_STATUS, 0x068},
{LogicalOpcode::CMSG_ADD_FRIEND, 0x069},
{LogicalOpcode::CMSG_DEL_FRIEND, 0x06A},
{LogicalOpcode::CMSG_SET_CONTACT_NOTES, 0x06B},
{LogicalOpcode::CMSG_ADD_IGNORE, 0x06C},
{LogicalOpcode::CMSG_DEL_IGNORE, 0x06D},
{LogicalOpcode::CMSG_PLAYER_LOGOUT, 0x04A},
{LogicalOpcode::CMSG_LOGOUT_REQUEST, 0x04B},
{LogicalOpcode::CMSG_LOGOUT_CANCEL, 0x04E},
{LogicalOpcode::SMSG_LOGOUT_RESPONSE, 0x04C},
{LogicalOpcode::SMSG_LOGOUT_COMPLETE, 0x04D},
{LogicalOpcode::CMSG_STAND_STATE_CHANGE, 0x101},
{LogicalOpcode::CMSG_SHOWING_HELM, 0x2B9},
{LogicalOpcode::CMSG_SHOWING_CLOAK, 0x2BA},
{LogicalOpcode::CMSG_TOGGLE_PVP, 0x253},
{LogicalOpcode::CMSG_GUILD_INVITE, 0x082},
{LogicalOpcode::CMSG_GUILD_ACCEPT, 0x084},
{LogicalOpcode::CMSG_GUILD_DECLINE_INVITATION, 0x085},
{LogicalOpcode::CMSG_GUILD_INFO, 0x087},
{LogicalOpcode::CMSG_GUILD_GET_ROSTER, 0x089},
{LogicalOpcode::CMSG_GUILD_PROMOTE_MEMBER, 0x08B},
{LogicalOpcode::CMSG_GUILD_DEMOTE_MEMBER, 0x08C},
{LogicalOpcode::CMSG_GUILD_LEAVE, 0x08D},
{LogicalOpcode::CMSG_GUILD_MOTD, 0x091},
{LogicalOpcode::SMSG_GUILD_INFO, 0x088},
{LogicalOpcode::SMSG_GUILD_ROSTER, 0x08A},
{LogicalOpcode::MSG_RAID_READY_CHECK, 0x322},
{LogicalOpcode::MSG_RAID_READY_CHECK_CONFIRM, 0x3AE},
{LogicalOpcode::CMSG_DUEL_PROPOSED, 0x166},
{LogicalOpcode::CMSG_DUEL_ACCEPTED, 0x16C},
{LogicalOpcode::CMSG_DUEL_CANCELLED, 0x16D},
{LogicalOpcode::SMSG_DUEL_REQUESTED, 0x167},
{LogicalOpcode::CMSG_INITIATE_TRADE, 0x116},
{LogicalOpcode::MSG_RANDOM_ROLL, 0x1FB},
{LogicalOpcode::CMSG_SET_SELECTION, 0x13D},
{LogicalOpcode::CMSG_NAME_QUERY, 0x050},
{LogicalOpcode::SMSG_NAME_QUERY_RESPONSE, 0x051},
{LogicalOpcode::CMSG_CREATURE_QUERY, 0x060},
{LogicalOpcode::SMSG_CREATURE_QUERY_RESPONSE, 0x061},
{LogicalOpcode::CMSG_GAMEOBJECT_QUERY, 0x05E},
{LogicalOpcode::SMSG_GAMEOBJECT_QUERY_RESPONSE, 0x05F},
{LogicalOpcode::CMSG_SET_ACTIVE_MOVER, 0x26A},
{LogicalOpcode::CMSG_BINDER_ACTIVATE, 0x1B5},
{LogicalOpcode::SMSG_LOG_XPGAIN, 0x1D0},
{LogicalOpcode::SMSG_MONSTER_MOVE, 0x0DD},
{LogicalOpcode::CMSG_ATTACKSWING, 0x141},
{LogicalOpcode::CMSG_ATTACKSTOP, 0x142},
{LogicalOpcode::SMSG_ATTACKSTART, 0x143},
{LogicalOpcode::SMSG_ATTACKSTOP, 0x144},
{LogicalOpcode::SMSG_ATTACKERSTATEUPDATE, 0x14A},
{LogicalOpcode::SMSG_SPELLNONMELEEDAMAGELOG, 0x250},
{LogicalOpcode::SMSG_SPELLHEALLOG, 0x150},
{LogicalOpcode::SMSG_SPELLENERGIZELOG, 0x25B},
{LogicalOpcode::SMSG_PERIODICAURALOG, 0x24E},
{LogicalOpcode::SMSG_ENVIRONMENTALDAMAGELOG, 0x1FC},
{LogicalOpcode::CMSG_CAST_SPELL, 0x12E},
{LogicalOpcode::CMSG_CANCEL_CAST, 0x12F},
{LogicalOpcode::CMSG_CANCEL_AURA, 0x033},
{LogicalOpcode::SMSG_CAST_FAILED, 0x130},
{LogicalOpcode::SMSG_SPELL_START, 0x131},
{LogicalOpcode::SMSG_SPELL_GO, 0x132},
{LogicalOpcode::SMSG_SPELL_FAILURE, 0x133},
{LogicalOpcode::SMSG_SPELL_COOLDOWN, 0x134},
{LogicalOpcode::SMSG_COOLDOWN_EVENT, 0x135},
{LogicalOpcode::SMSG_UPDATE_AURA_DURATION, 0x137},
{LogicalOpcode::SMSG_INITIAL_SPELLS, 0x12A},
{LogicalOpcode::SMSG_LEARNED_SPELL, 0x12B},
{LogicalOpcode::SMSG_SUPERCEDED_SPELL, 0x12C},
{LogicalOpcode::SMSG_REMOVED_SPELL, 0x203},
{LogicalOpcode::SMSG_SEND_UNLEARN_SPELLS, 0x41F},
{LogicalOpcode::SMSG_SPELL_DELAYED, 0x1E2},
{LogicalOpcode::SMSG_AURA_UPDATE, 0x3FA},
{LogicalOpcode::SMSG_AURA_UPDATE_ALL, 0x495},
{LogicalOpcode::SMSG_SET_FLAT_SPELL_MODIFIER, 0x266},
{LogicalOpcode::SMSG_SET_PCT_SPELL_MODIFIER, 0x267},
{LogicalOpcode::SMSG_TALENTS_INFO, 0x4C0},
{LogicalOpcode::CMSG_LEARN_TALENT, 0x251},
{LogicalOpcode::MSG_TALENT_WIPE_CONFIRM, 0x2AB},
{LogicalOpcode::CMSG_GROUP_INVITE, 0x06E},
{LogicalOpcode::SMSG_GROUP_INVITE, 0x06F},
{LogicalOpcode::CMSG_GROUP_ACCEPT, 0x072},
{LogicalOpcode::CMSG_GROUP_DECLINE, 0x073},
{LogicalOpcode::SMSG_GROUP_DECLINE, 0x074},
{LogicalOpcode::CMSG_GROUP_UNINVITE_GUID, 0x076},
{LogicalOpcode::SMSG_GROUP_UNINVITE, 0x077},
{LogicalOpcode::CMSG_GROUP_SET_LEADER, 0x078},
{LogicalOpcode::SMSG_GROUP_SET_LEADER, 0x079},
{LogicalOpcode::CMSG_GROUP_DISBAND, 0x07B},
{LogicalOpcode::SMSG_GROUP_LIST, 0x07D},
{LogicalOpcode::SMSG_PARTY_COMMAND_RESULT, 0x07E},
{LogicalOpcode::MSG_RAID_TARGET_UPDATE, 0x321},
{LogicalOpcode::CMSG_REQUEST_RAID_INFO, 0x2CD},
{LogicalOpcode::SMSG_RAID_INSTANCE_INFO, 0x2CC},
{LogicalOpcode::CMSG_AUTOSTORE_LOOT_ITEM, 0x108},
{LogicalOpcode::CMSG_LOOT, 0x15D},
{LogicalOpcode::CMSG_LOOT_MONEY, 0x15E},
{LogicalOpcode::CMSG_LOOT_RELEASE, 0x15F},
{LogicalOpcode::SMSG_LOOT_RESPONSE, 0x160},
{LogicalOpcode::SMSG_LOOT_RELEASE_RESPONSE, 0x161},
{LogicalOpcode::SMSG_LOOT_REMOVED, 0x162},
{LogicalOpcode::SMSG_LOOT_MONEY_NOTIFY, 0x163},
{LogicalOpcode::SMSG_LOOT_CLEAR_MONEY, 0x165},
{LogicalOpcode::CMSG_ACTIVATETAXI, 0x19D},
{LogicalOpcode::CMSG_GOSSIP_HELLO, 0x17B},
{LogicalOpcode::CMSG_GOSSIP_SELECT_OPTION, 0x17C},
{LogicalOpcode::SMSG_GOSSIP_MESSAGE, 0x17D},
{LogicalOpcode::SMSG_GOSSIP_COMPLETE, 0x17E},
{LogicalOpcode::SMSG_NPC_TEXT_UPDATE, 0x180},
{LogicalOpcode::CMSG_GAMEOBJECT_USE, 0x01B},
{LogicalOpcode::CMSG_QUESTGIVER_STATUS_QUERY, 0x182},
{LogicalOpcode::SMSG_QUESTGIVER_STATUS, 0x183},
{LogicalOpcode::SMSG_QUESTGIVER_STATUS_MULTIPLE, 0x198},
{LogicalOpcode::CMSG_QUESTGIVER_HELLO, 0x184},
{LogicalOpcode::CMSG_QUESTGIVER_QUERY_QUEST, 0x186},
{LogicalOpcode::SMSG_QUESTGIVER_QUEST_DETAILS, 0x188},
{LogicalOpcode::CMSG_QUESTGIVER_ACCEPT_QUEST, 0x189},
{LogicalOpcode::CMSG_QUESTGIVER_COMPLETE_QUEST, 0x18A},
{LogicalOpcode::SMSG_QUESTGIVER_REQUEST_ITEMS, 0x18B},
{LogicalOpcode::CMSG_QUESTGIVER_REQUEST_REWARD, 0x18C},
{LogicalOpcode::SMSG_QUESTGIVER_OFFER_REWARD, 0x18D},
{LogicalOpcode::CMSG_QUESTGIVER_CHOOSE_REWARD, 0x18E},
{LogicalOpcode::SMSG_QUESTGIVER_QUEST_INVALID, 0x18F},
{LogicalOpcode::SMSG_QUESTGIVER_QUEST_COMPLETE, 0x191},
{LogicalOpcode::CMSG_QUESTLOG_REMOVE_QUEST, 0x194},
{LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL, 0x196},
{LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE, 0x195},
{LogicalOpcode::CMSG_QUEST_QUERY, 0x05C},
{LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE, 0x05D},
{LogicalOpcode::SMSG_QUESTLOG_FULL, 0x1A3},
{LogicalOpcode::CMSG_LIST_INVENTORY, 0x19E},
{LogicalOpcode::SMSG_LIST_INVENTORY, 0x19F},
{LogicalOpcode::CMSG_SELL_ITEM, 0x1A0},
{LogicalOpcode::SMSG_SELL_ITEM, 0x1A1},
{LogicalOpcode::CMSG_BUY_ITEM, 0x1A2},
{LogicalOpcode::SMSG_BUY_FAILED, 0x1A5},
{LogicalOpcode::CMSG_TRAINER_LIST, 0x01B0},
{LogicalOpcode::SMSG_TRAINER_LIST, 0x01B1},
{LogicalOpcode::CMSG_TRAINER_BUY_SPELL, 0x01B2},
{LogicalOpcode::SMSG_TRAINER_BUY_FAILED, 0x01B4},
{LogicalOpcode::CMSG_ITEM_QUERY_SINGLE, 0x056},
{LogicalOpcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE, 0x058},
{LogicalOpcode::CMSG_USE_ITEM, 0x00AB},
{LogicalOpcode::CMSG_AUTOEQUIP_ITEM, 0x10A},
{LogicalOpcode::CMSG_SWAP_ITEM, 0x10C},
{LogicalOpcode::CMSG_SWAP_INV_ITEM, 0x10D},
{LogicalOpcode::SMSG_INVENTORY_CHANGE_FAILURE, 0x112},
{LogicalOpcode::CMSG_INSPECT, 0x114},
{LogicalOpcode::SMSG_INSPECT_RESULTS, 0x115},
{LogicalOpcode::CMSG_REPOP_REQUEST, 0x015A},
{LogicalOpcode::SMSG_RESURRECT_REQUEST, 0x015B},
{LogicalOpcode::CMSG_RESURRECT_RESPONSE, 0x015C},
{LogicalOpcode::CMSG_SPIRIT_HEALER_ACTIVATE, 0x021C},
{LogicalOpcode::SMSG_SPIRIT_HEALER_CONFIRM, 0x0222},
{LogicalOpcode::SMSG_RESURRECT_CANCEL, 0x0390},
{LogicalOpcode::MSG_MOVE_TELEPORT_ACK, 0x0C7},
{LogicalOpcode::SMSG_TRANSFER_PENDING, 0x003F},
{LogicalOpcode::SMSG_NEW_WORLD, 0x003E},
{LogicalOpcode::MSG_MOVE_WORLDPORT_ACK, 0x00DC},
{LogicalOpcode::SMSG_TRANSFER_ABORTED, 0x0040},
{LogicalOpcode::SMSG_FORCE_RUN_SPEED_CHANGE, 0x00E2},
{LogicalOpcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, 0x00E3},
{LogicalOpcode::CMSG_CANCEL_MOUNT_AURA, 0x0375},
{LogicalOpcode::SMSG_SHOWTAXINODES, 0x01A9},
{LogicalOpcode::SMSG_ACTIVATETAXIREPLY, 0x01AE},
{LogicalOpcode::SMSG_ACTIVATETAXIREPLY_ALT, 0x029D},
{LogicalOpcode::SMSG_NEW_TAXI_PATH, 0x01AF},
{LogicalOpcode::CMSG_ACTIVATETAXIEXPRESS, 0x0312},
{LogicalOpcode::SMSG_BATTLEFIELD_PORT_DENIED, 0x014B},
{LogicalOpcode::SMSG_REMOVED_FROM_PVP_QUEUE, 0x0170},
{LogicalOpcode::SMSG_TRAINER_BUY_SUCCEEDED, 0x01B3},
{LogicalOpcode::SMSG_BINDPOINTUPDATE, 0x0155},
{LogicalOpcode::CMSG_BATTLEFIELD_LIST, 0x023C},
{LogicalOpcode::SMSG_BATTLEFIELD_LIST, 0x023D},
{LogicalOpcode::CMSG_BATTLEFIELD_JOIN, 0x023E},
{LogicalOpcode::CMSG_BATTLEFIELD_STATUS, 0x02D3},
{LogicalOpcode::SMSG_BATTLEFIELD_STATUS, 0x02D4},
{LogicalOpcode::CMSG_BATTLEFIELD_PORT, 0x02D5},
{LogicalOpcode::CMSG_BATTLEMASTER_HELLO, 0x02D7},
{LogicalOpcode::MSG_PVP_LOG_DATA, 0x02E0},
{LogicalOpcode::CMSG_LEAVE_BATTLEFIELD, 0x02E1},
{LogicalOpcode::SMSG_GROUP_JOINED_BATTLEGROUND, 0x02E8},
{LogicalOpcode::MSG_BATTLEGROUND_PLAYER_POSITIONS, 0x02E9},
{LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_JOINED, 0x02EC},
{LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_LEFT, 0x02ED},
{LogicalOpcode::CMSG_BATTLEMASTER_JOIN, 0x02EE},
{LogicalOpcode::SMSG_JOINED_BATTLEGROUND_QUEUE, 0x038A},
{LogicalOpcode::CMSG_ARENA_TEAM_CREATE, 0x0348},
{LogicalOpcode::SMSG_ARENA_TEAM_COMMAND_RESULT, 0x0349},
{LogicalOpcode::CMSG_ARENA_TEAM_QUERY, 0x034B},
{LogicalOpcode::SMSG_ARENA_TEAM_QUERY_RESPONSE, 0x034C},
{LogicalOpcode::CMSG_ARENA_TEAM_ROSTER, 0x034D},
{LogicalOpcode::SMSG_ARENA_TEAM_ROSTER, 0x034E},
{LogicalOpcode::CMSG_ARENA_TEAM_INVITE, 0x034F},
{LogicalOpcode::SMSG_ARENA_TEAM_INVITE, 0x0350},
{LogicalOpcode::CMSG_ARENA_TEAM_ACCEPT, 0x0351},
{LogicalOpcode::CMSG_ARENA_TEAM_DECLINE, 0x0352},
{LogicalOpcode::CMSG_ARENA_TEAM_LEAVE, 0x0353},
{LogicalOpcode::CMSG_ARENA_TEAM_REMOVE, 0x0354},
{LogicalOpcode::CMSG_ARENA_TEAM_DISBAND, 0x0355},
{LogicalOpcode::CMSG_ARENA_TEAM_LEADER, 0x0356},
{LogicalOpcode::SMSG_ARENA_TEAM_EVENT, 0x0357},
{LogicalOpcode::CMSG_BATTLEMASTER_JOIN_ARENA, 0x0358},
{LogicalOpcode::SMSG_ARENA_TEAM_STATS, 0x035B},
{LogicalOpcode::SMSG_ARENA_ERROR, 0x0376},
{LogicalOpcode::MSG_INSPECT_ARENA_TEAMS, 0x0377},
};
logicalToWire_.clear();
wireToLogical_.clear();
for (auto& d : defaults) {
uint16_t logIdx = static_cast<uint16_t>(d.op);
logicalToWire_[logIdx] = d.wire;
wireToLogical_[d.wire] = logIdx;
}
LOG_INFO("OpcodeTable: loaded ", logicalToWire_.size(), " WotLK default opcodes");
}
bool OpcodeTable::loadFromJson(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) {
LOG_WARNING("OpcodeTable: cannot open ", path, ", using defaults");
return false;
}
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
logicalToWire_.clear();
wireToLogical_.clear();
// Parse simple JSON: { "NAME": "0xHEX", ... } or { "NAME": 123, ... }
size_t pos = 0;
size_t loaded = 0;
while (pos < json.size()) {
// Find next quoted key
size_t keyStart = json.find('"', pos);
if (keyStart == std::string::npos) break;
size_t keyEnd = json.find('"', keyStart + 1);
if (keyEnd == std::string::npos) break;
std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1);
// Find colon then value
size_t colon = json.find(':', keyEnd);
if (colon == std::string::npos) break;
// Skip whitespace
size_t valStart = colon + 1;
while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' ||
json[valStart] == '\r' || json[valStart] == '\n' || json[valStart] == '"'))
++valStart;
size_t valEnd = json.find_first_of(",}\"\r\n", valStart);
if (valEnd == std::string::npos) valEnd = json.size();
std::string valStr = json.substr(valStart, valEnd - valStart);
// Parse hex or decimal value
uint16_t wire = 0;
try {
if (valStr.size() > 2 && (valStr[0] == '0' && (valStr[1] == 'x' || valStr[1] == 'X'))) {
wire = static_cast<uint16_t>(std::stoul(valStr, nullptr, 16));
} else {
wire = static_cast<uint16_t>(std::stoul(valStr));
}
} catch (...) {
pos = valEnd + 1;
continue;
}
auto logOp = nameToLogical(key);
if (logOp) {
uint16_t logIdx = static_cast<uint16_t>(*logOp);
logicalToWire_[logIdx] = wire;
wireToLogical_[wire] = logIdx;
++loaded;
}
pos = valEnd + 1;
}
LOG_INFO("OpcodeTable: loaded ", loaded, " opcodes from ", path);
return loaded > 0;
}
uint16_t OpcodeTable::toWire(LogicalOpcode op) const {
auto it = logicalToWire_.find(static_cast<uint16_t>(op));
return (it != logicalToWire_.end()) ? it->second : 0xFFFF;
}
std::optional<LogicalOpcode> OpcodeTable::fromWire(uint16_t wireValue) const {
auto it = wireToLogical_.find(wireValue);
if (it != wireToLogical_.end()) {
return static_cast<LogicalOpcode>(it->second);
}
return std::nullopt;
}
bool OpcodeTable::hasOpcode(LogicalOpcode op) const {
return logicalToWire_.count(static_cast<uint16_t>(op)) > 0;
}
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,344 @@
#include "game/packet_parsers.hpp"
#include "core/logger.hpp"
namespace wowee {
namespace game {
// ============================================================================
// Classic 1.12.1 movement flag constants
// Key differences from TBC:
// - SPLINE_ENABLED at 0x00400000 (TBC/WotLK: 0x08000000)
// - No FLYING flag (flight was added in TBC)
// - ONTRANSPORT at 0x02000000 (not used for pitch in Classic)
// Same as TBC: ON_TRANSPORT=0x200, JUMPING=0x2000, SWIMMING=0x200000,
// SPLINE_ELEVATION=0x04000000
// ============================================================================
namespace ClassicMoveFlags {
constexpr uint32_t ONTRANSPORT = 0x02000000; // Gates transport data (vmangos authoritative)
constexpr uint32_t JUMPING = 0x00002000; // Gates jump data
constexpr uint32_t SWIMMING = 0x00200000; // Gates pitch
constexpr uint32_t SPLINE_ENABLED = 0x00400000; // TBC/WotLK: 0x08000000
constexpr uint32_t SPLINE_ELEVATION = 0x04000000; // Same as TBC
}
// ============================================================================
// Classic parseMovementBlock
// Key differences from TBC:
// - NO moveFlags2 (TBC reads u8, WotLK reads u16)
// - SPLINE_ENABLED at 0x00400000 (not 0x08000000)
// - Transport data: NO timestamp (TBC adds u32 timestamp)
// - Pitch: only SWIMMING (no ONTRANSPORT secondary pitch, no FLYING)
// Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate
// ============================================================================
bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
// Classic: UpdateFlags is uint8 (same as TBC)
uint8_t updateFlags = packet.readUInt8();
block.updateFlags = static_cast<uint16_t>(updateFlags);
LOG_DEBUG(" [Classic] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec);
const uint8_t UPDATEFLAG_LIVING = 0x20;
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
const uint8_t UPDATEFLAG_HAS_TARGET = 0x04;
const uint8_t UPDATEFLAG_TRANSPORT = 0x02;
const uint8_t UPDATEFLAG_LOWGUID = 0x08;
const uint8_t UPDATEFLAG_HIGHGUID = 0x10;
if (updateFlags & UPDATEFLAG_LIVING) {
// Movement flags (u32 only — NO extra flags byte in Classic)
uint32_t moveFlags = packet.readUInt32();
/*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(" [Classic] LIVING: (", block.x, ", ", block.y, ", ", block.z,
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
// Transport data (Classic: ONTRANSPORT=0x02000000, no timestamp)
if (moveFlags & ClassicMoveFlags::ONTRANSPORT) {
block.onTransport = true;
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
block.transportX = packet.readFloat();
block.transportY = packet.readFloat();
block.transportZ = packet.readFloat();
block.transportO = packet.readFloat();
// Classic: NO transport timestamp (TBC adds u32 timestamp)
// Classic: NO transport seat byte
}
// Pitch (Classic: only SWIMMING, no FLYING or ONTRANSPORT pitch)
if (moveFlags & ClassicMoveFlags::SWIMMING) {
/*float pitch =*/ packet.readFloat();
}
// Fall time (always present)
/*uint32_t fallTime =*/ packet.readUInt32();
// Jumping (Classic: JUMPING=0x2000, same as TBC)
if (moveFlags & ClassicMoveFlags::JUMPING) {
/*float jumpVelocity =*/ packet.readFloat();
/*float jumpSinAngle =*/ packet.readFloat();
/*float jumpCosAngle =*/ packet.readFloat();
/*float jumpXYSpeed =*/ packet.readFloat();
}
// Spline elevation
if (moveFlags & ClassicMoveFlags::SPLINE_ELEVATION) {
/*float splineElevation =*/ packet.readFloat();
}
// Speeds (Classic: 6 values — no flight speeds, no pitchRate)
// TBC added flying_speed + backwards_flying_speed (8 total)
// WotLK added pitchRate (9 total)
/*float walkSpeed =*/ packet.readFloat();
float runSpeed = packet.readFloat();
/*float runBackSpeed =*/ packet.readFloat();
/*float swimSpeed =*/ packet.readFloat();
/*float swimBackSpeed =*/ packet.readFloat();
/*float turnRate =*/ packet.readFloat();
block.runSpeed = runSpeed;
// Spline data (Classic: SPLINE_ENABLED=0x00400000)
if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) {
uint32_t splineFlags = packet.readUInt32();
LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec);
if (splineFlags & 0x00010000) { // FINAL_POINT
/*float finalX =*/ packet.readFloat();
/*float finalY =*/ packet.readFloat();
/*float finalZ =*/ packet.readFloat();
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
/*uint64_t finalTarget =*/ packet.readUInt64();
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
/*float finalAngle =*/ packet.readFloat();
}
// Classic spline: timePassed, duration, id, nodes, finalNode (same as TBC)
/*uint32_t timePassed =*/ packet.readUInt32();
/*uint32_t duration =*/ packet.readUInt32();
/*uint32_t splineId =*/ packet.readUInt32();
uint32_t pointCount = packet.readUInt32();
if (pointCount > 256) {
LOG_WARNING(" [Classic] Spline pointCount=", pointCount, " exceeds max, capping");
pointCount = 0;
}
for (uint32_t i = 0; i < pointCount; i++) {
/*float px =*/ packet.readFloat();
/*float py =*/ packet.readFloat();
/*float pz =*/ packet.readFloat();
}
// Classic: NO splineMode byte
/*float endPointX =*/ packet.readFloat();
/*float endPointY =*/ packet.readFloat();
/*float endPointZ =*/ packet.readFloat();
}
}
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
block.x = packet.readFloat();
block.y = packet.readFloat();
block.z = packet.readFloat();
block.orientation = packet.readFloat();
block.hasMovement = true;
LOG_DEBUG(" [Classic] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
}
// Target GUID
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
/*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
}
// Transport time
if (updateFlags & UPDATEFLAG_TRANSPORT) {
/*uint32_t transportTime =*/ packet.readUInt32();
}
// Low GUID
if (updateFlags & UPDATEFLAG_LOWGUID) {
/*uint32_t lowGuid =*/ packet.readUInt32();
}
// High GUID
if (updateFlags & UPDATEFLAG_HIGHGUID) {
/*uint32_t highGuid =*/ packet.readUInt32();
}
return true;
}
// ============================================================================
// Classic writeMovementPayload
// Key differences from TBC:
// - NO flags2 byte (TBC writes u8)
// - Transport data: NO timestamp
// - Pitch: only SWIMMING (no ONTRANSPORT pitch)
// ============================================================================
void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
// Movement flags (uint32)
packet.writeUInt32(info.flags);
// Classic: NO flags2 byte (TBC has u8, WotLK has u16)
// Timestamp
packet.writeUInt32(info.time);
// 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));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
// Transport data (Classic ONTRANSPORT = 0x02000000, no timestamp)
if (info.flags & ClassicMoveFlags::ONTRANSPORT) {
// 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]);
}
// 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));
// Classic: NO transport timestamp
// Classic: NO transport seat byte
}
// Pitch (Classic: only SWIMMING)
if (info.flags & ClassicMoveFlags::SWIMMING) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
}
// Fall time (always present)
packet.writeUInt32(info.fallTime);
// Jump data (Classic JUMPING = 0x2000)
if (info.flags & ClassicMoveFlags::JUMPING) {
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));
}
}
// ============================================================================
// Classic buildMovementPacket
// Classic/TBC: client movement packets do NOT include PackedGuid prefix
// (WotLK added PackedGuid to client packets)
// ============================================================================
network::Packet ClassicPacketParsers::buildMovementPacket(LogicalOpcode opcode,
const MovementInfo& info,
uint64_t /*playerGuid*/) {
network::Packet packet(wireOpcode(opcode));
// Classic: NO PackedGuid prefix for client packets
writeMovementPayload(packet, info);
return packet;
}
// ============================================================================
// Classic 1.12.1 parseCharEnum
// Differences from TBC:
// - Equipment: 20 items, but NO enchantment field per slot
// Classic: displayId(u32) + inventoryType(u8) = 5 bytes/slot
// TBC/WotLK: displayId(u32) + inventoryType(u8) + enchant(u32) = 9 bytes/slot
// - After flags: uint8 firstLogin (same as TBC)
// ============================================================================
bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
uint8_t count = packet.readUInt8();
LOG_INFO("[Classic] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
response.characters.clear();
response.characters.reserve(count);
for (uint8_t i = 0; i < count; ++i) {
Character character;
// GUID (8 bytes)
character.guid = packet.readUInt64();
// Name (null-terminated string)
character.name = packet.readString();
// Race, class, gender
character.race = static_cast<Race>(packet.readUInt8());
character.characterClass = static_cast<Class>(packet.readUInt8());
character.gender = static_cast<Gender>(packet.readUInt8());
// Appearance (5 bytes: skin, face, hairStyle, hairColor packed + facialFeatures)
character.appearanceBytes = packet.readUInt32();
character.facialFeatures = packet.readUInt8();
// Level
character.level = packet.readUInt8();
// Location
character.zoneId = packet.readUInt32();
character.mapId = packet.readUInt32();
character.x = packet.readFloat();
character.y = packet.readFloat();
character.z = packet.readFloat();
// Guild ID
character.guildId = packet.readUInt32();
// Flags
character.flags = packet.readUInt32();
// Classic: uint8 firstLogin (same as TBC)
/*uint8_t firstLogin =*/ packet.readUInt8();
// Pet data (always present)
character.pet.displayModel = packet.readUInt32();
character.pet.level = packet.readUInt32();
character.pet.family = packet.readUInt32();
// Equipment (Classic: 20 items, NO enchantment field)
character.equipment.reserve(20);
for (int j = 0; j < 20; ++j) {
EquipmentItem item;
item.displayModel = packet.readUInt32();
item.inventoryType = packet.readUInt8();
item.enchantment = 0; // Classic has no enchant field in char enum
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);
response.characters.push_back(character);
}
LOG_INFO("[Classic] Parsed ", response.characters.size(), " characters");
return true;
}
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,424 @@
#include "game/packet_parsers.hpp"
#include "core/logger.hpp"
namespace wowee {
namespace game {
// ============================================================================
// TBC 2.4.3 movement flag constants (shifted relative to WotLK 3.3.5a)
// ============================================================================
namespace TbcMoveFlags {
constexpr uint32_t ON_TRANSPORT = 0x00000200; // Gates transport data (same as WotLK)
constexpr uint32_t JUMPING = 0x00002000; // Gates jump data (WotLK: FALLING=0x1000)
constexpr uint32_t SWIMMING = 0x00200000; // Same as WotLK
constexpr uint32_t FLYING = 0x01000000; // WotLK: 0x02000000
constexpr uint32_t ONTRANSPORT = 0x02000000; // Secondary pitch check
constexpr uint32_t SPLINE_ELEVATION = 0x04000000; // Same as WotLK
constexpr uint32_t SPLINE_ENABLED = 0x08000000; // Same as WotLK
}
// ============================================================================
// TBC parseMovementBlock
// Key differences from WotLK:
// - UpdateFlags is uint8 (not uint16)
// - No VEHICLE (0x0080), POSITION (0x0100), ROTATION (0x0200) flags
// - moveFlags2 is uint8 (not uint16)
// - No transport seat byte
// - No interpolated movement (flags2 & 0x0200) check
// - Pitch check: SWIMMING, else ONTRANSPORT(0x02000000)
// - Spline data: has splineId, no durationMod/durationModNext/verticalAccel/effectStartTime/splineMode
// - Flag 0x08 (HIGH_GUID) reads 2 u32s (Classic: 1 u32)
// ============================================================================
bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
// TBC 2.4.3: UpdateFlags is uint8 (1 byte)
uint8_t updateFlags = packet.readUInt8();
block.updateFlags = static_cast<uint16_t>(updateFlags);
LOG_DEBUG(" [TBC] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec);
// TBC UpdateFlag bit values (same as lower byte of WotLK):
// 0x01 = SELF
// 0x02 = TRANSPORT
// 0x04 = HAS_TARGET
// 0x08 = LOWGUID
// 0x10 = HIGHGUID
// 0x20 = LIVING
// 0x40 = HAS_POSITION (stationary)
const uint8_t UPDATEFLAG_LIVING = 0x20;
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
const uint8_t UPDATEFLAG_HAS_TARGET = 0x04;
const uint8_t UPDATEFLAG_TRANSPORT = 0x02;
const uint8_t UPDATEFLAG_LOWGUID = 0x08;
const uint8_t UPDATEFLAG_HIGHGUID = 0x10;
if (updateFlags & UPDATEFLAG_LIVING) {
// Full movement block for living units
uint32_t moveFlags = packet.readUInt32();
uint8_t moveFlags2 = packet.readUInt8(); // TBC: uint8, not uint16
(void)moveFlags2;
/*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(" [TBC] LIVING: (", block.x, ", ", block.y, ", ", block.z,
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
// Transport data
if (moveFlags & TbcMoveFlags::ON_TRANSPORT) {
block.onTransport = true;
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
block.transportX = packet.readFloat();
block.transportY = packet.readFloat();
block.transportZ = packet.readFloat();
block.transportO = packet.readFloat();
/*uint32_t tTime =*/ packet.readUInt32();
// TBC: NO transport seat byte
// TBC: NO interpolated movement check
}
// Pitch: SWIMMING, or else ONTRANSPORT (TBC-specific secondary pitch)
if (moveFlags & TbcMoveFlags::SWIMMING) {
/*float pitch =*/ packet.readFloat();
} else if (moveFlags & TbcMoveFlags::ONTRANSPORT) {
/*float pitch =*/ packet.readFloat();
}
// Fall time (always present)
/*uint32_t fallTime =*/ packet.readUInt32();
// Jumping (TBC: JUMPING=0x2000, WotLK: FALLING=0x1000)
if (moveFlags & TbcMoveFlags::JUMPING) {
/*float jumpVelocity =*/ packet.readFloat();
/*float jumpSinAngle =*/ packet.readFloat();
/*float jumpCosAngle =*/ packet.readFloat();
/*float jumpXYSpeed =*/ packet.readFloat();
}
// Spline elevation (TBC: 0x02000000, WotLK: 0x04000000)
if (moveFlags & TbcMoveFlags::SPLINE_ELEVATION) {
/*float splineElevation =*/ packet.readFloat();
}
// Speeds (TBC: 8 values — walk, run, runBack, swim, fly, flyBack, swimBack, turn)
// WotLK adds pitchRate (9 total)
/*float walkSpeed =*/ packet.readFloat();
float runSpeed = packet.readFloat();
/*float runBackSpeed =*/ packet.readFloat();
/*float swimSpeed =*/ packet.readFloat();
/*float flySpeed =*/ packet.readFloat();
/*float flyBackSpeed =*/ packet.readFloat();
/*float swimBackSpeed =*/ packet.readFloat();
/*float turnRate =*/ packet.readFloat();
block.runSpeed = runSpeed;
// Spline data (TBC/WotLK: SPLINE_ENABLED = 0x08000000)
if (moveFlags & TbcMoveFlags::SPLINE_ENABLED) {
uint32_t splineFlags = packet.readUInt32();
LOG_DEBUG(" [TBC] Spline: flags=0x", std::hex, splineFlags, std::dec);
if (splineFlags & 0x00010000) { // FINAL_POINT
/*float finalX =*/ packet.readFloat();
/*float finalY =*/ packet.readFloat();
/*float finalZ =*/ packet.readFloat();
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
/*uint64_t finalTarget =*/ packet.readUInt64();
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
/*float finalAngle =*/ packet.readFloat();
}
// TBC spline: timePassed, duration, id, nodes, finalNode
// (no durationMod, durationModNext, verticalAccel, effectStartTime, splineMode)
/*uint32_t timePassed =*/ packet.readUInt32();
/*uint32_t duration =*/ packet.readUInt32();
/*uint32_t splineId =*/ packet.readUInt32();
uint32_t pointCount = packet.readUInt32();
if (pointCount > 256) {
LOG_WARNING(" [TBC] Spline pointCount=", pointCount, " exceeds max, capping");
pointCount = 0;
}
for (uint32_t i = 0; i < pointCount; i++) {
/*float px =*/ packet.readFloat();
/*float py =*/ packet.readFloat();
/*float pz =*/ packet.readFloat();
}
// TBC: NO splineMode byte (WotLK adds it)
/*float endPointX =*/ packet.readFloat();
/*float endPointY =*/ packet.readFloat();
/*float endPointZ =*/ packet.readFloat();
}
}
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
// TBC: Simple stationary position (same as WotLK STATIONARY)
block.x = packet.readFloat();
block.y = packet.readFloat();
block.z = packet.readFloat();
block.orientation = packet.readFloat();
block.hasMovement = true;
LOG_DEBUG(" [TBC] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
}
// TBC: No UPDATEFLAG_POSITION (0x0100) code path
// Target GUID
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
/*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
}
// Transport time
if (updateFlags & UPDATEFLAG_TRANSPORT) {
/*uint32_t transportTime =*/ packet.readUInt32();
}
// TBC: No VEHICLE flag (WotLK 0x0080)
// TBC: No ROTATION flag (WotLK 0x0200)
// HIGH_GUID (0x08) — TBC has 2 u32s, Classic has 1 u32
if (updateFlags & UPDATEFLAG_LOWGUID) {
/*uint32_t unknown0 =*/ packet.readUInt32();
/*uint32_t unknown1 =*/ packet.readUInt32();
}
// ALL (0x10)
if (updateFlags & UPDATEFLAG_HIGHGUID) {
/*uint32_t unknown2 =*/ packet.readUInt32();
}
return true;
}
// ============================================================================
// TBC writeMovementPayload
// Key differences from WotLK:
// - flags2 is uint8 (not uint16)
// - No transport seat byte
// - No interpolated movement (flags2 & 0x0200) write
// - Pitch check uses TBC flag positions
// ============================================================================
void TbcPacketParsers::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
// Movement flags (uint32, same as WotLK)
packet.writeUInt32(info.flags);
// TBC: flags2 is uint8 (WotLK: uint16)
packet.writeUInt8(static_cast<uint8_t>(info.flags2 & 0xFF));
// Timestamp
packet.writeUInt32(info.time);
// 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));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
// Transport data (TBC ON_TRANSPORT = 0x200, same bit as WotLK)
if (info.flags & TbcMoveFlags::ON_TRANSPORT) {
// 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]);
}
// 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));
// Transport time
packet.writeUInt32(info.transportTime);
// TBC: NO transport seat byte
// TBC: NO interpolated movement time
}
// Pitch: SWIMMING or else ONTRANSPORT (TBC flag positions)
if (info.flags & TbcMoveFlags::SWIMMING) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
} else if (info.flags & TbcMoveFlags::ONTRANSPORT) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
}
// Fall time (always present)
packet.writeUInt32(info.fallTime);
// Jump data (TBC JUMPING = 0x2000, WotLK FALLING = 0x1000)
if (info.flags & TbcMoveFlags::JUMPING) {
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));
}
}
// ============================================================================
// TBC buildMovementPacket
// Classic/TBC: client movement packets do NOT include PackedGuid prefix
// (WotLK added PackedGuid to client packets)
// ============================================================================
network::Packet TbcPacketParsers::buildMovementPacket(LogicalOpcode opcode,
const MovementInfo& info,
uint64_t /*playerGuid*/) {
network::Packet packet(wireOpcode(opcode));
// TBC: NO PackedGuid prefix for client packets
writeMovementPayload(packet, info);
return packet;
}
// ============================================================================
// TBC parseCharEnum
// Differences from WotLK:
// - After flags: uint8 firstLogin (not uint32 customization + uint8 unknown)
// - Equipment: 20 items (not 23)
// ============================================================================
bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
uint8_t count = packet.readUInt8();
LOG_INFO("[TBC] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
response.characters.clear();
response.characters.reserve(count);
for (uint8_t i = 0; i < count; ++i) {
Character character;
// GUID (8 bytes)
character.guid = packet.readUInt64();
// Name (null-terminated string)
character.name = packet.readString();
// Race, class, gender
character.race = static_cast<Race>(packet.readUInt8());
character.characterClass = static_cast<Class>(packet.readUInt8());
character.gender = static_cast<Gender>(packet.readUInt8());
// Appearance (5 bytes: skin, face, hairStyle, hairColor packed + facialFeatures)
character.appearanceBytes = packet.readUInt32();
character.facialFeatures = packet.readUInt8();
// Level
character.level = packet.readUInt8();
// Location
character.zoneId = packet.readUInt32();
character.mapId = packet.readUInt32();
character.x = packet.readFloat();
character.y = packet.readFloat();
character.z = packet.readFloat();
// Guild ID
character.guildId = packet.readUInt32();
// Flags
character.flags = packet.readUInt32();
// TBC: uint8 firstLogin (WotLK: uint32 customization + uint8 unknown)
/*uint8_t firstLogin =*/ packet.readUInt8();
// Pet data (always present)
character.pet.displayModel = packet.readUInt32();
character.pet.level = packet.readUInt32();
character.pet.family = packet.readUInt32();
// Equipment (TBC: 20 items, WotLK: 23 items)
character.equipment.reserve(20);
for (int j = 0; j < 20; ++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);
response.characters.push_back(character);
}
LOG_INFO("[TBC] Parsed ", response.characters.size(), " characters");
return true;
}
// ============================================================================
// TBC parseUpdateObject
// Key difference from WotLK: u8 has_transport byte after blockCount
// (WotLK removed this field)
// ============================================================================
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
// Read block count
data.blockCount = packet.readUInt32();
// TBC/Classic: has_transport byte (WotLK removed this)
/*uint8_t hasTransport =*/ packet.readUInt8();
LOG_DEBUG("[TBC] SMSG_UPDATE_OBJECT: objectCount=", 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)) {
uint32_t count = packet.readUInt32();
for (uint32_t i = 0; i < count; ++i) {
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
data.outOfRangeGuids.push_back(guid);
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
}
} else {
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 (!UpdateObjectParser::parseUpdateBlock(packet, block)) {
LOG_ERROR("Failed to parse update block ", i + 1);
return false;
}
data.blocks.push_back(block);
}
return true;
}
// ============================================================================
// TBC parseAuraUpdate - SMSG_AURA_UPDATE doesn't exist in TBC
// TBC uses inline aura update fields + SMSG_INIT_EXTRA_AURA_INFO (0x3A3) /
// SMSG_SET_EXTRA_AURA_INFO (0x3A4) instead
// ============================================================================
bool TbcPacketParsers::parseAuraUpdate(network::Packet& /*packet*/, AuraUpdateData& /*data*/, bool /*isAll*/) {
LOG_WARNING("[TBC] parseAuraUpdate called but SMSG_AURA_UPDATE does not exist in TBC 2.4.3");
return false;
}
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,156 @@
#include "game/update_field_table.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
#include <algorithm>
namespace wowee {
namespace game {
static const UpdateFieldTable* g_activeUpdateFieldTable = nullptr;
void setActiveUpdateFieldTable(const UpdateFieldTable* table) { g_activeUpdateFieldTable = table; }
const UpdateFieldTable* getActiveUpdateFieldTable() { return g_activeUpdateFieldTable; }
struct UFNameEntry {
const char* name;
UF field;
};
static const UFNameEntry kUFNames[] = {
{"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY},
{"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO},
{"UNIT_FIELD_TARGET_HI", UF::UNIT_FIELD_TARGET_HI},
{"UNIT_FIELD_HEALTH", UF::UNIT_FIELD_HEALTH},
{"UNIT_FIELD_POWER1", UF::UNIT_FIELD_POWER1},
{"UNIT_FIELD_MAXHEALTH", UF::UNIT_FIELD_MAXHEALTH},
{"UNIT_FIELD_MAXPOWER1", UF::UNIT_FIELD_MAXPOWER1},
{"UNIT_FIELD_LEVEL", UF::UNIT_FIELD_LEVEL},
{"UNIT_FIELD_FACTIONTEMPLATE", UF::UNIT_FIELD_FACTIONTEMPLATE},
{"UNIT_FIELD_FLAGS", UF::UNIT_FIELD_FLAGS},
{"UNIT_FIELD_FLAGS_2", UF::UNIT_FIELD_FLAGS_2},
{"UNIT_FIELD_DISPLAYID", UF::UNIT_FIELD_DISPLAYID},
{"UNIT_FIELD_MOUNTDISPLAYID", UF::UNIT_FIELD_MOUNTDISPLAYID},
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
{"UNIT_END", UF::UNIT_END},
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
{"PLAYER_XP", UF::PLAYER_XP},
{"PLAYER_NEXT_LEVEL_XP", UF::PLAYER_NEXT_LEVEL_XP},
{"PLAYER_FIELD_COINAGE", UF::PLAYER_FIELD_COINAGE},
{"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START},
{"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD},
{"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1},
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
{"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START},
{"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID},
{"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT},
};
static constexpr size_t kUFNameCount = sizeof(kUFNames) / sizeof(kUFNames[0]);
void UpdateFieldTable::loadWotlkDefaults() {
fieldMap_.clear();
struct { UF field; uint16_t idx; } defaults[] = {
{UF::OBJECT_FIELD_ENTRY, 3},
{UF::UNIT_FIELD_TARGET_LO, 6},
{UF::UNIT_FIELD_TARGET_HI, 7},
{UF::UNIT_FIELD_HEALTH, 24},
{UF::UNIT_FIELD_POWER1, 25},
{UF::UNIT_FIELD_MAXHEALTH, 32},
{UF::UNIT_FIELD_MAXPOWER1, 33},
{UF::UNIT_FIELD_LEVEL, 54},
{UF::UNIT_FIELD_FACTIONTEMPLATE, 55},
{UF::UNIT_FIELD_FLAGS, 59},
{UF::UNIT_FIELD_FLAGS_2, 60},
{UF::UNIT_FIELD_DISPLAYID, 67},
{UF::UNIT_FIELD_MOUNTDISPLAYID, 69},
{UF::UNIT_NPC_FLAGS, 82},
{UF::UNIT_DYNAMIC_FLAGS, 147},
{UF::UNIT_END, 148},
{UF::PLAYER_FLAGS, 150},
{UF::PLAYER_XP, 634},
{UF::PLAYER_NEXT_LEVEL_XP, 635},
{UF::PLAYER_FIELD_COINAGE, 1170},
{UF::PLAYER_QUEST_LOG_START, 158},
{UF::PLAYER_FIELD_INV_SLOT_HEAD, 324},
{UF::PLAYER_FIELD_PACK_SLOT_1, 370},
{UF::PLAYER_SKILL_INFO_START, 636},
{UF::PLAYER_EXPLORED_ZONES_START, 1041},
{UF::GAMEOBJECT_DISPLAYID, 8},
{UF::ITEM_FIELD_STACK_COUNT, 14},
};
for (auto& d : defaults) {
fieldMap_[static_cast<uint16_t>(d.field)] = d.idx;
}
LOG_INFO("UpdateFieldTable: loaded ", fieldMap_.size(), " WotLK default fields");
}
bool UpdateFieldTable::loadFromJson(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) {
LOG_WARNING("UpdateFieldTable: cannot open ", path);
return false;
}
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
fieldMap_.clear();
size_t loaded = 0;
size_t pos = 0;
while (pos < json.size()) {
size_t keyStart = json.find('"', pos);
if (keyStart == std::string::npos) break;
size_t keyEnd = json.find('"', keyStart + 1);
if (keyEnd == std::string::npos) break;
std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1);
size_t colon = json.find(':', keyEnd);
if (colon == std::string::npos) break;
size_t valStart = colon + 1;
while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' ||
json[valStart] == '\r' || json[valStart] == '\n'))
++valStart;
size_t valEnd = json.find_first_of(",}\r\n", valStart);
if (valEnd == std::string::npos) valEnd = json.size();
std::string valStr = json.substr(valStart, valEnd - valStart);
// Trim whitespace
while (!valStr.empty() && (valStr.back() == ' ' || valStr.back() == '\t'))
valStr.pop_back();
uint16_t idx = 0;
try { idx = static_cast<uint16_t>(std::stoul(valStr)); } catch (...) {
pos = valEnd + 1;
continue;
}
// Find matching UF enum
for (size_t i = 0; i < kUFNameCount; ++i) {
if (key == kUFNames[i].name) {
fieldMap_[static_cast<uint16_t>(kUFNames[i].field)] = idx;
++loaded;
break;
}
}
pos = valEnd + 1;
}
LOG_INFO("UpdateFieldTable: loaded ", loaded, " fields from ", path);
return loaded > 0;
}
uint16_t UpdateFieldTable::index(UF field) const {
auto it = fieldMap_.find(static_cast<uint16_t>(field));
return (it != fieldMap_.end()) ? it->second : 0xFFFF;
}
bool UpdateFieldTable::hasField(UF field) const {
return fieldMap_.count(static_cast<uint16_t>(field)) > 0;
}
} // namespace game
} // namespace wowee

View file

@ -39,40 +39,33 @@ network::Packet AuthSessionPacket::build(uint32_t build,
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));
network::Packet packet(wireOpcode(Opcode::CMSG_AUTH_SESSION));
// AzerothCore 3.3.5a expects this exact field order:
// Build, LoginServerID, Account, LoginServerType, LocalChallenge,
// RegionID, BattlegroupID, RealmID, DosResponse, Digest, AddonInfo
bool isTbc = (build <= 8606); // TBC 2.4.3 = 8606, WotLK starts at 11159+
// Build number (uint32, little-endian)
packet.writeUInt32(build);
// Login server ID (uint32, always 0)
packet.writeUInt32(0);
// Account name (null-terminated string)
packet.writeString(upperAccount);
// Login server type (uint32, always 0)
packet.writeUInt32(0);
// LocalChallenge / Client seed (uint32, little-endian)
packet.writeUInt32(clientSeed);
// Region ID (uint32)
packet.writeUInt32(0);
// Battlegroup ID (uint32)
packet.writeUInt32(0);
// Realm ID (uint32)
packet.writeUInt32(realmId);
LOG_DEBUG(" Realm ID: ", realmId);
// DOS response (uint64) - required for 3.x
packet.writeUInt32(0);
packet.writeUInt32(0);
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);
packet.writeUInt32(0); // RegionID
packet.writeUInt32(0); // BattlegroupID
packet.writeUInt32(realmId); // RealmID
LOG_DEBUG(" Realm ID: ", realmId);
packet.writeUInt32(0); // DOS response (uint64)
packet.writeUInt32(0);
}
// Authentication hash/digest (20 bytes)
packet.writeBytes(authHash.data(), authHash.size());
@ -160,25 +153,30 @@ std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
}
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
// SMSG_AUTH_CHALLENGE format (WoW 3.3.5a):
// uint32 unknown1 (always 1?)
// uint32 serverSeed
// 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)
if (packet.getSize() < 8) {
if (packet.getSize() < 4) {
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
return false;
}
data.unknown1 = packet.readUInt32();
data.serverSeed = packet.readUInt32();
if (packet.getSize() < 8) {
// TBC format: just the server seed (4 bytes)
data.unknown1 = 0;
data.serverSeed = packet.readUInt32();
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (TBC format):");
} else {
// WotLK format: unknown1 + serverSeed + encryption seeds
data.unknown1 = packet.readUInt32();
data.serverSeed = packet.readUInt32();
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (WotLK format):");
LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
}
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;
}
@ -257,7 +255,7 @@ const char* getAuthResultString(AuthResult result) {
// ============================================================
network::Packet CharCreatePacket::build(const CharCreateData& data) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_CREATE));
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_CREATE));
// Convert nonbinary gender to server-compatible value (servers only support male/female)
Gender serverGender = toServerGender(data.gender);
@ -305,7 +303,7 @@ bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponse
network::Packet CharEnumPacket::build() {
// CMSG_CHAR_ENUM has no body - just the opcode
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_ENUM));
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
@ -399,7 +397,7 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response)
}
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PLAYER_LOGIN));
network::Packet packet(wireOpcode(Opcode::CMSG_PLAYER_LOGIN));
// Write character GUID (8 bytes, little-endian)
packet.writeUInt64(characterGuid);
@ -491,7 +489,7 @@ bool MotdParser::parse(network::Packet& packet, MotdData& data) {
}
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PING));
network::Packet packet(wireOpcode(Opcode::CMSG_PING));
// Write sequence number (uint32, little-endian)
packet.writeUInt32(sequence);
@ -618,7 +616,7 @@ void MovementPacket::writeMovementPayload(network::Packet& packet, const Movemen
}
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) {
network::Packet packet(static_cast<uint16_t>(opcode));
network::Packet packet(wireOpcode(opcode));
// Movement packet format (WoW 3.3.5a):
// packed GUID + movement payload
@ -634,7 +632,7 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
char b[4]; snprintf(b, sizeof(b), "%02x ", raw[i]);
hex += b;
}
LOG_INFO("MOVEPKT opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec,
LOG_INFO("MOVEPKT opcode=0x", std::hex, wireOpcode(opcode), std::dec,
" guid=0x", std::hex, playerGuid, std::dec,
" payload=", raw.size(), " bytes",
" flags=0x", std::hex, info.flags, std::dec,
@ -1096,7 +1094,7 @@ 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));
network::Packet packet(wireOpcode(Opcode::CMSG_MESSAGECHAT));
// Write chat type
packet.writeUInt32(static_cast<uint32_t>(type));
@ -1267,21 +1265,21 @@ const char* getChatTypeString(ChatType type) {
// ============================================================
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_SELECTION));
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(static_cast<uint16_t>(Opcode::CMSG_SET_ACTIVE_MOVER));
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(static_cast<uint16_t>(Opcode::CMSG_INSPECT));
network::Packet packet(wireOpcode(Opcode::CMSG_INSPECT));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_INSPECT: target=0x", std::hex, targetGuid, std::dec);
return packet;
@ -1292,7 +1290,7 @@ network::Packet InspectPacket::build(uint64_t targetGuid) {
// ============================================================
network::Packet QueryTimePacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUERY_TIME));
network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_TIME));
LOG_DEBUG("Built CMSG_QUERY_TIME");
return packet;
}
@ -1305,7 +1303,7 @@ bool QueryTimeResponseParser::parse(network::Packet& packet, QueryTimeResponseDa
}
network::Packet RequestPlayedTimePacket::build(bool sendToChat) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_REQUEST_PLAYED_TIME));
network::Packet packet(wireOpcode(Opcode::CMSG_REQUEST_PLAYED_TIME));
packet.writeUInt8(sendToChat ? 1 : 0);
LOG_DEBUG("Built CMSG_REQUEST_PLAYED_TIME: sendToChat=", sendToChat);
return packet;
@ -1324,7 +1322,7 @@ network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel,
const std::string& guildName,
uint32_t raceMask, uint32_t classMask,
uint32_t zones) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_WHO));
network::Packet packet(wireOpcode(Opcode::CMSG_WHO));
packet.writeUInt32(minLevel);
packet.writeUInt32(maxLevel);
packet.writeString(playerName);
@ -1341,7 +1339,7 @@ network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel,
// ============================================================
network::Packet AddFriendPacket::build(const std::string& playerName, const std::string& note) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ADD_FRIEND));
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_FRIEND));
packet.writeString(playerName);
packet.writeString(note);
LOG_DEBUG("Built CMSG_ADD_FRIEND: player=", playerName);
@ -1349,14 +1347,14 @@ network::Packet AddFriendPacket::build(const std::string& playerName, const std:
}
network::Packet DelFriendPacket::build(uint64_t friendGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DEL_FRIEND));
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(static_cast<uint16_t>(Opcode::CMSG_SET_CONTACT_NOTES));
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);
@ -1375,14 +1373,14 @@ bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data)
}
network::Packet AddIgnorePacket::build(const std::string& playerName) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ADD_IGNORE));
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(static_cast<uint16_t>(Opcode::CMSG_DEL_IGNORE));
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;
@ -1393,13 +1391,13 @@ network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) {
// ============================================================
network::Packet LogoutRequestPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOGOUT_REQUEST));
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_REQUEST));
LOG_DEBUG("Built CMSG_LOGOUT_REQUEST");
return packet;
}
network::Packet LogoutCancelPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOGOUT_CANCEL));
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_CANCEL));
LOG_DEBUG("Built CMSG_LOGOUT_CANCEL");
return packet;
}
@ -1416,7 +1414,7 @@ bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& da
// ============================================================
network::Packet StandStateChangePacket::build(uint8_t state) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_STAND_STATE_CHANGE));
network::Packet packet(wireOpcode(Opcode::CMSG_STAND_STATE_CHANGE));
packet.writeUInt32(state);
LOG_DEBUG("Built CMSG_STAND_STATE_CHANGE: state=", (int)state);
return packet;
@ -1427,14 +1425,14 @@ network::Packet StandStateChangePacket::build(uint8_t state) {
// ============================================================
network::Packet ShowingHelmPacket::build(bool show) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SHOWING_HELM));
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(static_cast<uint16_t>(Opcode::CMSG_SHOWING_CLOAK));
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_CLOAK));
packet.writeUInt8(show ? 1 : 0);
LOG_DEBUG("Built CMSG_SHOWING_CLOAK: show=", show);
return packet;
@ -1445,7 +1443,7 @@ network::Packet ShowingCloakPacket::build(bool show) {
// ============================================================
network::Packet TogglePvpPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_TOGGLE_PVP));
network::Packet packet(wireOpcode(Opcode::CMSG_TOGGLE_PVP));
LOG_DEBUG("Built CMSG_TOGGLE_PVP");
return packet;
}
@ -1455,46 +1453,46 @@ network::Packet TogglePvpPacket::build() {
// ============================================================
network::Packet GuildInfoPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_INFO));
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INFO));
LOG_DEBUG("Built CMSG_GUILD_INFO");
return packet;
}
network::Packet GuildRosterPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_GET_ROSTER));
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_GET_ROSTER));
LOG_DEBUG("Built CMSG_GUILD_GET_ROSTER");
return packet;
}
network::Packet GuildMotdPacket::build(const std::string& motd) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_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(static_cast<uint16_t>(Opcode::CMSG_GUILD_PROMOTE_MEMBER));
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_PROMOTE_MEMBER));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_PROMOTE_MEMBER: ", playerName);
return packet;
}
network::Packet GuildDemotePacket::build(const std::string& playerName) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_DEMOTE_MEMBER));
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DEMOTE_MEMBER));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_DEMOTE_MEMBER: ", playerName);
return packet;
}
network::Packet GuildLeavePacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_LEAVE));
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(static_cast<uint16_t>(Opcode::CMSG_GUILD_INVITE));
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INVITE));
packet.writeString(playerName);
LOG_DEBUG("Built CMSG_GUILD_INVITE: ", playerName);
return packet;
@ -1505,13 +1503,13 @@ network::Packet GuildInvitePacket::build(const std::string& playerName) {
// ============================================================
network::Packet ReadyCheckPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RAID_READY_CHECK));
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(static_cast<uint16_t>(Opcode::MSG_RAID_READY_CHECK_CONFIRM));
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;
@ -1522,7 +1520,7 @@ network::Packet ReadyCheckConfirmPacket::build(bool ready) {
// ============================================================
network::Packet DuelCancelPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DUEL_CANCELLED));
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED));
LOG_DEBUG("Built CMSG_DUEL_CANCELLED");
return packet;
}
@ -1532,20 +1530,20 @@ network::Packet DuelCancelPacket::build() {
// ============================================================
network::Packet GroupUninvitePacket::build(const std::string& playerName) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_UNINVITE_GUID));
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(static_cast<uint16_t>(Opcode::CMSG_GROUP_DISBAND));
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DISBAND));
LOG_DEBUG("Built CMSG_GROUP_DISBAND");
return packet;
}
network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RAID_TARGET_UPDATE));
network::Packet packet(wireOpcode(Opcode::MSG_RAID_TARGET_UPDATE));
packet.writeUInt8(targetIndex);
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", (uint32_t)targetIndex, ", guid: 0x", std::hex, targetGuid, std::dec);
@ -1553,7 +1551,7 @@ network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targ
}
network::Packet RequestRaidInfoPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_REQUEST_RAID_INFO));
network::Packet packet(wireOpcode(Opcode::CMSG_REQUEST_RAID_INFO));
LOG_DEBUG("Built CMSG_REQUEST_RAID_INFO");
return packet;
}
@ -1563,34 +1561,34 @@ network::Packet RequestRaidInfoPacket::build() {
// ============================================================
network::Packet DuelProposedPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DUEL_PROPOSED));
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_PROPOSED));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_DUEL_PROPOSED for target: 0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_INITIATE_TRADE));
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;
}
network::Packet AttackSwingPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSWING));
network::Packet packet(wireOpcode(Opcode::CMSG_ATTACKSWING));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_ATTACKSWING for target: 0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet AttackStopPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSTOP));
network::Packet packet(wireOpcode(Opcode::CMSG_ATTACKSTOP));
LOG_DEBUG("Built CMSG_ATTACKSTOP");
return packet;
}
network::Packet CancelCastPacket::build(uint32_t spellId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_CAST));
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_CAST));
packet.writeUInt32(0); // cast count/sequence
packet.writeUInt32(spellId);
LOG_DEBUG("Built CMSG_CANCEL_CAST for spell: ", spellId);
@ -1602,7 +1600,7 @@ network::Packet CancelCastPacket::build(uint32_t spellId) {
// ============================================================
network::Packet RandomRollPacket::build(uint32_t minRoll, uint32_t maxRoll) {
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RANDOM_ROLL));
network::Packet packet(wireOpcode(Opcode::MSG_RANDOM_ROLL));
packet.writeUInt32(minRoll);
packet.writeUInt32(maxRoll);
LOG_DEBUG("Built MSG_RANDOM_ROLL: ", minRoll, "-", maxRoll);
@ -1621,7 +1619,7 @@ bool RandomRollParser::parse(network::Packet& packet, RandomRollData& data) {
}
network::Packet NameQueryPacket::build(uint64_t playerGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_NAME_QUERY));
network::Packet packet(wireOpcode(Opcode::CMSG_NAME_QUERY));
packet.writeUInt64(playerGuid);
LOG_DEBUG("Built CMSG_NAME_QUERY: guid=0x", std::hex, playerGuid, std::dec);
return packet;
@ -1650,7 +1648,7 @@ bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseDa
}
network::Packet CreatureQueryPacket::build(uint32_t entry, uint64_t guid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CREATURE_QUERY));
network::Packet packet(wireOpcode(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);
@ -1691,7 +1689,7 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
// ---- GameObject Query ----
network::Packet GameObjectQueryPacket::build(uint32_t entry, uint64_t guid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GAMEOBJECT_QUERY));
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJECT_QUERY));
packet.writeUInt32(entry);
packet.writeUInt64(guid);
LOG_DEBUG("Built CMSG_GAMEOBJECT_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
@ -1725,7 +1723,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue
// ---- Item Query ----
network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ITEM_QUERY_SINGLE));
network::Packet packet(wireOpcode(Opcode::CMSG_ITEM_QUERY_SINGLE));
packet.writeUInt32(entry);
packet.writeUInt64(guid);
LOG_DEBUG("Built CMSG_ITEM_QUERY_SINGLE: entry=", entry, " guid=0x", std::hex, guid, std::dec);
@ -2116,7 +2114,7 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data
}
network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CAST_SPELL));
network::Packet packet(wireOpcode(Opcode::CMSG_CAST_SPELL));
packet.writeUInt8(castCount);
packet.writeUInt32(spellId);
packet.writeUInt8(0x00); // castFlags = 0 for normal cast
@ -2152,7 +2150,7 @@ network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, ui
}
network::Packet CancelAuraPacket::build(uint32_t spellId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_AURA));
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_AURA));
packet.writeUInt32(spellId);
return packet;
}
@ -2273,7 +2271,7 @@ bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data
// ============================================================
network::Packet GroupInvitePacket::build(const std::string& playerName) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_INVITE));
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_INVITE));
packet.writeString(playerName);
packet.writeUInt32(0); // unused
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
@ -2288,13 +2286,13 @@ bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteRespon
}
network::Packet GroupAcceptPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_ACCEPT));
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_ACCEPT));
packet.writeUInt32(0); // unused in 3.3.5a
return packet;
}
network::Packet GroupDeclinePacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DECLINE));
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DECLINE));
return packet;
}
@ -2365,20 +2363,20 @@ bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData
// ============================================================
network::Packet LootPacket::build(uint64_t targetGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT));
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT));
packet.writeUInt64(targetGuid);
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
return packet;
}
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
packet.writeUInt8(slotIndex);
return packet;
}
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_USE_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM));
packet.writeUInt8(bagIndex);
packet.writeUInt8(slotIndex);
packet.writeUInt8(0); // cast count
@ -2392,14 +2390,14 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64
}
network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOEQUIP_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOEQUIP_ITEM));
packet.writeUInt8(srcBag);
packet.writeUInt8(srcSlot);
return packet;
}
network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SWAP_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_ITEM));
packet.writeUInt8(dstBag);
packet.writeUInt8(dstSlot);
packet.writeUInt8(srcBag);
@ -2408,19 +2406,19 @@ network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t s
}
network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SWAP_INV_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM));
packet.writeUInt8(srcSlot);
packet.writeUInt8(dstSlot);
return packet;
}
network::Packet LootMoneyPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_MONEY));
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_MONEY));
return packet;
}
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_RELEASE));
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_RELEASE));
packet.writeUInt64(lootGuid);
return packet;
}
@ -2453,19 +2451,19 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data)
// ============================================================
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_HELLO));
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_HELLO));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet QuestgiverHelloPacket::build(uint64_t npcGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_HELLO));
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_HELLO));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_SELECT_OPTION));
packet.writeUInt64(npcGuid);
packet.writeUInt32(menuId);
packet.writeUInt32(optionId);
@ -2476,7 +2474,7 @@ network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuI
}
network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
packet.writeUInt8(1); // isDialogContinued = 1 (from gossip)
@ -2484,7 +2482,7 @@ network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t que
}
network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
packet.writeUInt32(0); // unused
@ -2584,7 +2582,7 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data
// ============================================================
network::Packet BinderActivatePacket::build(uint64_t npcGuid) {
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_BINDER_ACTIVATE));
network::Packet pkt(wireOpcode(Opcode::CMSG_BINDER_ACTIVATE));
pkt.writeUInt64(npcGuid);
return pkt;
}
@ -2703,14 +2701,14 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
}
network::Packet QuestgiverCompleteQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_COMPLETE_QUEST));
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_COMPLETE_QUEST));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
return packet;
}
network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t questId, uint32_t rewardIndex) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
packet.writeUInt32(rewardIndex);
@ -2722,13 +2720,13 @@ network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t q
// ============================================================
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LIST_INVENTORY));
network::Packet packet(wireOpcode(Opcode::CMSG_LIST_INVENTORY));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_BUY_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_BUY_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt32(itemId);
packet.writeUInt32(slot);
@ -2738,7 +2736,7 @@ network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint3
}
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SELL_ITEM));
network::Packet packet(wireOpcode(Opcode::CMSG_SELL_ITEM));
packet.writeUInt64(vendorGuid);
packet.writeUInt64(itemGuid);
packet.writeUInt32(count);
@ -2812,7 +2810,7 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) {
}
network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spellId) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_TRAINER_BUY_SPELL));
network::Packet packet(wireOpcode(Opcode::CMSG_TRAINER_BUY_SPELL));
packet.writeUInt64(trainerGuid);
packet.writeUInt32(spellId);
return packet;
@ -2919,14 +2917,14 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
}
network::Packet LearnTalentPacket::build(uint32_t talentId, uint32_t requestedRank) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LEARN_TALENT));
network::Packet packet(wireOpcode(Opcode::CMSG_LEARN_TALENT));
packet.writeUInt32(talentId);
packet.writeUInt32(requestedRank);
return packet;
}
network::Packet TalentWipeConfirmPacket::build(bool accept) {
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_TALENT_WIPE_CONFIRM));
network::Packet packet(wireOpcode(Opcode::MSG_TALENT_WIPE_CONFIRM));
packet.writeUInt32(accept ? 1 : 0);
return packet;
}
@ -2936,19 +2934,19 @@ network::Packet TalentWipeConfirmPacket::build(bool accept) {
// ============================================================
network::Packet RepopRequestPacket::build() {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_REPOP_REQUEST));
network::Packet packet(wireOpcode(Opcode::CMSG_REPOP_REQUEST));
packet.writeUInt8(1); // request release (1 = manual)
return packet;
}
network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
packet.writeUInt64(npcGuid);
return packet;
}
network::Packet ResurrectResponsePacket::build(uint64_t casterGuid, bool accept) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_RESURRECT_RESPONSE));
network::Packet packet(wireOpcode(Opcode::CMSG_RESURRECT_RESPONSE));
packet.writeUInt64(casterGuid);
packet.writeUInt8(accept ? 1 : 0);
return packet;
@ -2989,7 +2987,7 @@ bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyDa
}
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ACTIVATETAXIEXPRESS));
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXIEXPRESS));
packet.writeUInt64(npcGuid);
packet.writeUInt32(totalCost);
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
@ -3002,7 +3000,7 @@ network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t tota
}
network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ACTIVATETAXI));
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXI));
packet.writeUInt64(npcGuid);
packet.writeUInt32(srcNode);
packet.writeUInt32(destNode);
@ -3010,7 +3008,7 @@ network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, ui
}
network::Packet GameObjectUsePacket::build(uint64_t guid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GAMEOBJECT_USE));
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJECT_USE));
packet.writeUInt64(guid);
return packet;
}