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

@ -45,6 +45,10 @@ public:
void authenticate(const std::string& username, const std::string& password);
void authenticateWithHash(const std::string& username, const std::vector<uint8_t>& authHash);
// Set client version info (call before authenticate)
void setClientInfo(const ClientInfo& info) { clientInfo = info; }
const ClientInfo& getClientInfo() const { return clientInfo; }
// Realm list
void requestRealmList();
const std::vector<Realm>& getRealms() const { return realms; }

View file

@ -15,6 +15,7 @@ struct ClientInfo {
uint8_t minorVersion = 3;
uint8_t patchVersion = 5;
uint16_t build = 12340; // 3.3.5a
uint8_t protocolVersion = 8; // SRP auth protocol version
std::string game = "WoW";
std::string platform = "x86";
std::string os = "Win";

View file

@ -15,8 +15,8 @@ namespace wowee {
namespace rendering { class Renderer; }
namespace ui { class UIManager; }
namespace auth { class AuthHandler; }
namespace game { class GameHandler; class World; }
namespace pipeline { class AssetManager; }
namespace game { class GameHandler; class World; class ExpansionRegistry; }
namespace pipeline { class AssetManager; class DBCLayout; class HDPackManager; }
namespace audio { enum class VoiceType; }
namespace core {
@ -54,6 +54,9 @@ public:
game::GameHandler* getGameHandler() { return gameHandler.get(); }
game::World* getWorld() { return world.get(); }
pipeline::AssetManager* getAssetManager() { return assetManager.get(); }
game::ExpansionRegistry* getExpansionRegistry() { return expansionRegistry_.get(); }
pipeline::DBCLayout* getDBCLayout() { return dbcLayout_.get(); }
pipeline::HDPackManager* getHDPackManager() { return hdPackManager_.get(); }
// Singleton access
static Application& getInstance() { return *instance; }
@ -104,6 +107,9 @@ private:
std::unique_ptr<game::GameHandler> gameHandler;
std::unique_ptr<game::World> world;
std::unique_ptr<pipeline::AssetManager> assetManager;
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
std::unique_ptr<pipeline::HDPackManager> hdPackManager_;
AppState state = AppState::AUTHENTICATION;
bool running = false;

View file

@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <optional>
namespace wowee {
namespace game {
/**
* Identifies a WoW expansion for protocol/asset selection.
*/
struct ExpansionProfile {
std::string id; // "classic", "tbc", "wotlk", "cata"
std::string name; // "Wrath of the Lich King"
std::string shortName; // "WotLK"
uint8_t majorVersion = 0;
uint8_t minorVersion = 0;
uint8_t patchVersion = 0;
uint16_t build = 0;
uint8_t protocolVersion = 0; // SRP auth protocol version byte
std::string dataPath; // Absolute path to expansion data dir
uint32_t maxLevel = 60;
std::vector<uint32_t> races;
std::vector<uint32_t> classes;
std::string versionString() const; // e.g. "3.3.5a"
};
/**
* Scans Data/expansions/ for available expansion profiles and manages the active selection.
*/
class ExpansionRegistry {
public:
/**
* Scan dataRoot/expansions/ for expansion.json files.
* @param dataRoot Path to Data/ directory (e.g. "./Data")
* @return Number of profiles discovered
*/
size_t initialize(const std::string& dataRoot);
/** All discovered profiles. */
const std::vector<ExpansionProfile>& getAllProfiles() const { return profiles_; }
/** Lookup by id (e.g. "wotlk"). Returns nullptr if not found. */
const ExpansionProfile* getProfile(const std::string& id) const;
/** Set the active expansion. Returns false if id not found. */
bool setActive(const std::string& id);
/** Get the active expansion profile. Never null after successful initialize(). */
const ExpansionProfile* getActive() const;
/** Convenience: active expansion id. Empty if none. */
const std::string& getActiveId() const { return activeId_; }
private:
std::vector<ExpansionProfile> profiles_;
std::string activeId_;
bool loadProfile(const std::string& jsonPath, const std::string& dirPath);
};
} // namespace game
} // namespace wowee

View file

@ -2,6 +2,8 @@
#include "game/world_packets.hpp"
#include "game/character.hpp"
#include "game/opcode_table.hpp"
#include "game/update_field_table.hpp"
#include "game/inventory.hpp"
#include "game/spell_defines.hpp"
#include "game/group_defines.hpp"
@ -23,6 +25,7 @@ namespace wowee::game {
class TransportManager;
class WardenCrypto;
class WardenModuleManager;
class PacketParsers;
}
namespace wowee {
@ -109,6 +112,14 @@ public:
GameHandler();
~GameHandler();
/** Access the active opcode table (wire ↔ logical mapping). */
const OpcodeTable& getOpcodeTable() const { return opcodeTable_; }
OpcodeTable& getOpcodeTable() { return opcodeTable_; }
const UpdateFieldTable& getUpdateFieldTable() const { return updateFieldTable_; }
UpdateFieldTable& getUpdateFieldTable() { return updateFieldTable_; }
PacketParsers* getPacketParsers() { return packetParsers_.get(); }
void setPacketParsers(std::unique_ptr<PacketParsers> parsers);
/**
* Connect to world server
*
@ -927,6 +938,15 @@ private:
float localOrientation);
void clearTransportAttachment(uint64_t childGuid);
// Opcode translation table (expansion-specific wire ↔ logical mapping)
OpcodeTable opcodeTable_;
// Update field table (expansion-specific field index mapping)
UpdateFieldTable updateFieldTable_;
// Packet parsers (expansion-specific binary format handling)
std::unique_ptr<PacketParsers> packetParsers_;
// Network
std::unique_ptr<network::WorldSocket> socket;
@ -1210,7 +1230,6 @@ private:
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
bool skillLineDbcLoaded_ = false;
bool skillLineAbilityLoaded_ = false;
static constexpr uint16_t PLAYER_EXPLORED_ZONES_START = 1041; // 3.3.5a UpdateFields
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;
std::vector<uint32_t> playerExploredZones_ =
std::vector<uint32_t>(PLAYER_EXPLORED_ZONES_COUNT, 0u);

View file

@ -0,0 +1,407 @@
#pragma once
#include <cstdint>
#include <string>
#include <unordered_map>
#include <optional>
namespace wowee {
namespace game {
/**
* Logical opcode identifiers (expansion-agnostic).
*
* These are compile-time enum values used in switch statements.
* The actual wire values depend on the active expansion and are
* loaded from JSON at runtime via OpcodeTable.
*/
enum class LogicalOpcode : uint16_t {
// ---- Client to Server (Core) ----
CMSG_PING,
CMSG_AUTH_SESSION,
CMSG_CHAR_CREATE,
CMSG_CHAR_ENUM,
CMSG_CHAR_DELETE,
CMSG_PLAYER_LOGIN,
// ---- Movement ----
CMSG_MOVE_START_FORWARD,
CMSG_MOVE_START_BACKWARD,
CMSG_MOVE_STOP,
CMSG_MOVE_START_STRAFE_LEFT,
CMSG_MOVE_START_STRAFE_RIGHT,
CMSG_MOVE_STOP_STRAFE,
CMSG_MOVE_JUMP,
CMSG_MOVE_START_TURN_LEFT,
CMSG_MOVE_START_TURN_RIGHT,
CMSG_MOVE_STOP_TURN,
CMSG_MOVE_SET_FACING,
CMSG_MOVE_FALL_LAND,
CMSG_MOVE_START_SWIM,
CMSG_MOVE_STOP_SWIM,
CMSG_MOVE_HEARTBEAT,
// ---- Server to Client (Core) ----
SMSG_AUTH_CHALLENGE,
SMSG_AUTH_RESPONSE,
SMSG_CHAR_CREATE,
SMSG_CHAR_ENUM,
SMSG_CHAR_DELETE,
SMSG_PONG,
SMSG_LOGIN_VERIFY_WORLD,
SMSG_LOGIN_SETTIMESPEED,
SMSG_TUTORIAL_FLAGS,
SMSG_WARDEN_DATA,
CMSG_WARDEN_DATA,
SMSG_ACCOUNT_DATA_TIMES,
SMSG_CLIENTCACHE_VERSION,
SMSG_FEATURE_SYSTEM_STATUS,
SMSG_MOTD,
// ---- Entity/Object updates ----
SMSG_UPDATE_OBJECT,
SMSG_COMPRESSED_UPDATE_OBJECT,
SMSG_MONSTER_MOVE_TRANSPORT,
SMSG_DESTROY_OBJECT,
// ---- Chat ----
CMSG_MESSAGECHAT,
SMSG_MESSAGECHAT,
// ---- Server Info Commands ----
CMSG_WHO,
SMSG_WHO,
CMSG_REQUEST_PLAYED_TIME,
SMSG_PLAYED_TIME,
CMSG_QUERY_TIME,
SMSG_QUERY_TIME_RESPONSE,
// ---- Social Commands ----
SMSG_FRIEND_STATUS,
CMSG_ADD_FRIEND,
CMSG_DEL_FRIEND,
CMSG_SET_CONTACT_NOTES,
CMSG_ADD_IGNORE,
CMSG_DEL_IGNORE,
// ---- Logout Commands ----
CMSG_PLAYER_LOGOUT,
CMSG_LOGOUT_REQUEST,
CMSG_LOGOUT_CANCEL,
SMSG_LOGOUT_RESPONSE,
SMSG_LOGOUT_COMPLETE,
// ---- Stand State ----
CMSG_STAND_STATE_CHANGE,
// ---- Display Toggles ----
CMSG_SHOWING_HELM,
CMSG_SHOWING_CLOAK,
// ---- PvP ----
CMSG_TOGGLE_PVP,
// ---- Guild ----
CMSG_GUILD_INVITE,
CMSG_GUILD_ACCEPT,
CMSG_GUILD_DECLINE_INVITATION,
CMSG_GUILD_INFO,
CMSG_GUILD_GET_ROSTER,
CMSG_GUILD_PROMOTE_MEMBER,
CMSG_GUILD_DEMOTE_MEMBER,
CMSG_GUILD_LEAVE,
CMSG_GUILD_MOTD,
SMSG_GUILD_INFO,
SMSG_GUILD_ROSTER,
// ---- Ready Check ----
MSG_RAID_READY_CHECK,
MSG_RAID_READY_CHECK_CONFIRM,
// ---- Duel ----
CMSG_DUEL_PROPOSED,
CMSG_DUEL_ACCEPTED,
CMSG_DUEL_CANCELLED,
SMSG_DUEL_REQUESTED,
// ---- Trade ----
CMSG_INITIATE_TRADE,
// ---- Random Roll ----
MSG_RANDOM_ROLL,
// ---- Phase 1: Foundation (Targeting, Queries) ----
CMSG_SET_SELECTION,
CMSG_NAME_QUERY,
SMSG_NAME_QUERY_RESPONSE,
CMSG_CREATURE_QUERY,
SMSG_CREATURE_QUERY_RESPONSE,
CMSG_GAMEOBJECT_QUERY,
SMSG_GAMEOBJECT_QUERY_RESPONSE,
CMSG_SET_ACTIVE_MOVER,
CMSG_BINDER_ACTIVATE,
// ---- XP ----
SMSG_LOG_XPGAIN,
// ---- Creature Movement ----
SMSG_MONSTER_MOVE,
// ---- Phase 2: Combat Core ----
CMSG_ATTACKSWING,
CMSG_ATTACKSTOP,
SMSG_ATTACKSTART,
SMSG_ATTACKSTOP,
SMSG_ATTACKERSTATEUPDATE,
SMSG_SPELLNONMELEEDAMAGELOG,
SMSG_SPELLHEALLOG,
SMSG_SPELLENERGIZELOG,
SMSG_PERIODICAURALOG,
SMSG_ENVIRONMENTALDAMAGELOG,
// ---- Phase 3: Spells, Action Bar, Auras ----
CMSG_CAST_SPELL,
CMSG_CANCEL_CAST,
CMSG_CANCEL_AURA,
SMSG_CAST_FAILED,
SMSG_SPELL_START,
SMSG_SPELL_GO,
SMSG_SPELL_FAILURE,
SMSG_SPELL_COOLDOWN,
SMSG_COOLDOWN_EVENT,
SMSG_UPDATE_AURA_DURATION,
SMSG_INITIAL_SPELLS,
SMSG_LEARNED_SPELL,
SMSG_SUPERCEDED_SPELL,
SMSG_REMOVED_SPELL,
SMSG_SEND_UNLEARN_SPELLS,
SMSG_SPELL_DELAYED,
SMSG_AURA_UPDATE,
SMSG_AURA_UPDATE_ALL,
SMSG_SET_FLAT_SPELL_MODIFIER,
SMSG_SET_PCT_SPELL_MODIFIER,
// ---- Talents ----
SMSG_TALENTS_INFO,
CMSG_LEARN_TALENT,
MSG_TALENT_WIPE_CONFIRM,
// ---- Phase 4: Group/Party ----
CMSG_GROUP_INVITE,
SMSG_GROUP_INVITE,
CMSG_GROUP_ACCEPT,
CMSG_GROUP_DECLINE,
SMSG_GROUP_DECLINE,
CMSG_GROUP_UNINVITE_GUID,
SMSG_GROUP_UNINVITE,
CMSG_GROUP_SET_LEADER,
SMSG_GROUP_SET_LEADER,
CMSG_GROUP_DISBAND,
SMSG_GROUP_LIST,
SMSG_PARTY_COMMAND_RESULT,
MSG_RAID_TARGET_UPDATE,
CMSG_REQUEST_RAID_INFO,
SMSG_RAID_INSTANCE_INFO,
// ---- Phase 5: Loot ----
CMSG_AUTOSTORE_LOOT_ITEM,
CMSG_LOOT,
CMSG_LOOT_MONEY,
CMSG_LOOT_RELEASE,
SMSG_LOOT_RESPONSE,
SMSG_LOOT_RELEASE_RESPONSE,
SMSG_LOOT_REMOVED,
SMSG_LOOT_MONEY_NOTIFY,
SMSG_LOOT_CLEAR_MONEY,
// ---- Phase 5: Taxi / Flight Paths ----
CMSG_ACTIVATETAXI,
// ---- Phase 5: NPC Gossip ----
CMSG_GOSSIP_HELLO,
CMSG_GOSSIP_SELECT_OPTION,
SMSG_GOSSIP_MESSAGE,
SMSG_GOSSIP_COMPLETE,
SMSG_NPC_TEXT_UPDATE,
// ---- Phase 5: GameObject ----
CMSG_GAMEOBJECT_USE,
// ---- Phase 5: Quests ----
CMSG_QUESTGIVER_STATUS_QUERY,
SMSG_QUESTGIVER_STATUS,
SMSG_QUESTGIVER_STATUS_MULTIPLE,
CMSG_QUESTGIVER_HELLO,
CMSG_QUESTGIVER_QUERY_QUEST,
SMSG_QUESTGIVER_QUEST_DETAILS,
CMSG_QUESTGIVER_ACCEPT_QUEST,
CMSG_QUESTGIVER_COMPLETE_QUEST,
SMSG_QUESTGIVER_REQUEST_ITEMS,
CMSG_QUESTGIVER_REQUEST_REWARD,
SMSG_QUESTGIVER_OFFER_REWARD,
CMSG_QUESTGIVER_CHOOSE_REWARD,
SMSG_QUESTGIVER_QUEST_INVALID,
SMSG_QUESTGIVER_QUEST_COMPLETE,
CMSG_QUESTLOG_REMOVE_QUEST,
SMSG_QUESTUPDATE_ADD_KILL,
SMSG_QUESTUPDATE_COMPLETE,
CMSG_QUEST_QUERY,
SMSG_QUEST_QUERY_RESPONSE,
SMSG_QUESTLOG_FULL,
// ---- Phase 5: Vendor ----
CMSG_LIST_INVENTORY,
SMSG_LIST_INVENTORY,
CMSG_SELL_ITEM,
SMSG_SELL_ITEM,
CMSG_BUY_ITEM,
SMSG_BUY_FAILED,
// ---- Trainer ----
CMSG_TRAINER_LIST,
SMSG_TRAINER_LIST,
CMSG_TRAINER_BUY_SPELL,
SMSG_TRAINER_BUY_FAILED,
// ---- Phase 5: Item/Equip ----
CMSG_ITEM_QUERY_SINGLE,
SMSG_ITEM_QUERY_SINGLE_RESPONSE,
CMSG_USE_ITEM,
CMSG_AUTOEQUIP_ITEM,
CMSG_SWAP_ITEM,
CMSG_SWAP_INV_ITEM,
SMSG_INVENTORY_CHANGE_FAILURE,
CMSG_INSPECT,
SMSG_INSPECT_RESULTS,
// ---- Death/Respawn ----
CMSG_REPOP_REQUEST,
SMSG_RESURRECT_REQUEST,
CMSG_RESURRECT_RESPONSE,
CMSG_SPIRIT_HEALER_ACTIVATE,
SMSG_SPIRIT_HEALER_CONFIRM,
SMSG_RESURRECT_CANCEL,
// ---- Teleport / Transfer ----
MSG_MOVE_TELEPORT_ACK,
SMSG_TRANSFER_PENDING,
SMSG_NEW_WORLD,
MSG_MOVE_WORLDPORT_ACK,
SMSG_TRANSFER_ABORTED,
// ---- Speed Changes ----
SMSG_FORCE_RUN_SPEED_CHANGE,
CMSG_FORCE_RUN_SPEED_CHANGE_ACK,
// ---- Mount ----
CMSG_CANCEL_MOUNT_AURA,
// ---- Taxi / Flight Paths ----
SMSG_SHOWTAXINODES,
SMSG_ACTIVATETAXIREPLY,
SMSG_ACTIVATETAXIREPLY_ALT,
SMSG_NEW_TAXI_PATH,
CMSG_ACTIVATETAXIEXPRESS,
// ---- Battleground ----
SMSG_BATTLEFIELD_PORT_DENIED,
SMSG_REMOVED_FROM_PVP_QUEUE,
SMSG_TRAINER_BUY_SUCCEEDED,
SMSG_BINDPOINTUPDATE,
CMSG_BATTLEFIELD_LIST,
SMSG_BATTLEFIELD_LIST,
CMSG_BATTLEFIELD_JOIN,
CMSG_BATTLEFIELD_STATUS,
SMSG_BATTLEFIELD_STATUS,
CMSG_BATTLEFIELD_PORT,
CMSG_BATTLEMASTER_HELLO,
MSG_PVP_LOG_DATA,
CMSG_LEAVE_BATTLEFIELD,
SMSG_GROUP_JOINED_BATTLEGROUND,
MSG_BATTLEGROUND_PLAYER_POSITIONS,
SMSG_BATTLEGROUND_PLAYER_JOINED,
SMSG_BATTLEGROUND_PLAYER_LEFT,
CMSG_BATTLEMASTER_JOIN,
SMSG_JOINED_BATTLEGROUND_QUEUE,
// ---- Arena Team ----
CMSG_ARENA_TEAM_CREATE,
SMSG_ARENA_TEAM_COMMAND_RESULT,
CMSG_ARENA_TEAM_QUERY,
SMSG_ARENA_TEAM_QUERY_RESPONSE,
CMSG_ARENA_TEAM_ROSTER,
SMSG_ARENA_TEAM_ROSTER,
CMSG_ARENA_TEAM_INVITE,
SMSG_ARENA_TEAM_INVITE,
CMSG_ARENA_TEAM_ACCEPT,
CMSG_ARENA_TEAM_DECLINE,
CMSG_ARENA_TEAM_LEAVE,
CMSG_ARENA_TEAM_REMOVE,
CMSG_ARENA_TEAM_DISBAND,
CMSG_ARENA_TEAM_LEADER,
SMSG_ARENA_TEAM_EVENT,
CMSG_BATTLEMASTER_JOIN_ARENA,
SMSG_ARENA_TEAM_STATS,
SMSG_ARENA_ERROR,
MSG_INSPECT_ARENA_TEAMS,
// Sentinel
COUNT
};
/**
* Maps LogicalOpcode expansion-specific wire values.
*
* Loaded from JSON (e.g. Data/expansions/wotlk/opcodes.json).
* Used for sending packets (toWire) and receiving them (fromWire).
*/
class OpcodeTable {
public:
/**
* Load opcode mappings from a JSON file.
* Format: { "CMSG_PING": "0x1DC", "SMSG_AUTH_CHALLENGE": "0x1EC", ... }
*/
bool loadFromJson(const std::string& path);
/** Load built-in WotLK defaults (hardcoded fallback). */
void loadWotlkDefaults();
/** LogicalOpcode → wire value for sending packets. Returns 0xFFFF if unknown. */
uint16_t toWire(LogicalOpcode op) const;
/** Wire value → LogicalOpcode for receiving packets. Returns nullopt if unknown. */
std::optional<LogicalOpcode> fromWire(uint16_t wireValue) const;
/** Check if a logical opcode has a wire mapping. */
bool hasOpcode(LogicalOpcode op) const;
/** Number of mapped opcodes. */
size_t size() const { return logicalToWire_.size(); }
private:
std::unordered_map<uint16_t, uint16_t> logicalToWire_; // LogicalOpcode → wire
std::unordered_map<uint16_t, uint16_t> wireToLogical_; // wire → LogicalOpcode
static std::optional<LogicalOpcode> nameToLogical(const std::string& name);
static const char* logicalToName(LogicalOpcode op);
};
/**
* Global active opcode table pointer (set by GameHandler at startup).
* Used by world_packets.cpp and other code that needs to send packets
* without direct access to a GameHandler instance.
*/
void setActiveOpcodeTable(const OpcodeTable* table);
const OpcodeTable* getActiveOpcodeTable();
/**
* Get the wire value for a logical opcode using the active table.
* Convenience helper for packet construction code.
*/
inline uint16_t wireOpcode(LogicalOpcode op) {
const auto* table = getActiveOpcodeTable();
return table ? table->toWire(op) : 0xFFFF;
}
} // namespace game
} // namespace wowee

View file

@ -1,344 +1,14 @@
#pragma once
#include <cstdint>
#include "game/opcode_table.hpp"
namespace wowee {
namespace game {
// World of Warcraft 3.3.5a opcodes
// Values derived from community reverse-engineering efforts
// Reference: https://wowdev.wiki/World_Packet
enum class Opcode : uint16_t {
// ---- Client to Server (Core) ----
CMSG_PING = 0x1DC,
CMSG_AUTH_SESSION = 0x1ED,
CMSG_CHAR_CREATE = 0x036,
CMSG_CHAR_ENUM = 0x037,
CMSG_CHAR_DELETE = 0x038,
CMSG_PLAYER_LOGIN = 0x03D,
// ---- Movement ----
CMSG_MOVE_START_FORWARD = 0x0B5,
CMSG_MOVE_START_BACKWARD = 0x0B6,
CMSG_MOVE_STOP = 0x0B7,
CMSG_MOVE_START_STRAFE_LEFT = 0x0B8,
CMSG_MOVE_START_STRAFE_RIGHT = 0x0B9,
CMSG_MOVE_STOP_STRAFE = 0x0BA,
CMSG_MOVE_JUMP = 0x0BB,
CMSG_MOVE_START_TURN_LEFT = 0x0BC,
CMSG_MOVE_START_TURN_RIGHT = 0x0BD,
CMSG_MOVE_STOP_TURN = 0x0BE,
CMSG_MOVE_SET_FACING = 0x0DA,
CMSG_MOVE_FALL_LAND = 0x0C9,
CMSG_MOVE_START_SWIM = 0x0CA,
CMSG_MOVE_STOP_SWIM = 0x0CB,
CMSG_MOVE_HEARTBEAT = 0x0EE,
// ---- Server to Client (Core) ----
SMSG_AUTH_CHALLENGE = 0x1EC,
SMSG_AUTH_RESPONSE = 0x1EE,
SMSG_CHAR_CREATE = 0x03A,
SMSG_CHAR_ENUM = 0x03B,
SMSG_CHAR_DELETE = 0x03C,
SMSG_PONG = 0x1DD,
SMSG_LOGIN_VERIFY_WORLD = 0x236,
SMSG_LOGIN_SETTIMESPEED = 0x042,
SMSG_TUTORIAL_FLAGS = 0x0FD,
SMSG_WARDEN_DATA = 0x2E6,
CMSG_WARDEN_DATA = 0x2E7,
SMSG_ACCOUNT_DATA_TIMES = 0x209,
SMSG_CLIENTCACHE_VERSION = 0x4AB,
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
SMSG_MOTD = 0x33D,
// ---- Entity/Object updates ----
SMSG_UPDATE_OBJECT = 0x0A9,
SMSG_COMPRESSED_UPDATE_OBJECT = 0x1F6,
SMSG_MONSTER_MOVE_TRANSPORT = 0x2AE,
SMSG_DESTROY_OBJECT = 0x0AA,
// ---- Chat ----
CMSG_MESSAGECHAT = 0x095,
SMSG_MESSAGECHAT = 0x096,
// ---- Server Info Commands ----
CMSG_WHO = 0x062,
SMSG_WHO = 0x063,
CMSG_REQUEST_PLAYED_TIME = 0x1CC,
SMSG_PLAYED_TIME = 0x1CD,
CMSG_QUERY_TIME = 0x1CE,
SMSG_QUERY_TIME_RESPONSE = 0x1CF,
// ---- Social Commands ----
SMSG_FRIEND_STATUS = 0x068,
CMSG_ADD_FRIEND = 0x069,
CMSG_DEL_FRIEND = 0x06A,
CMSG_SET_CONTACT_NOTES = 0x06B,
CMSG_ADD_IGNORE = 0x06C,
CMSG_DEL_IGNORE = 0x06D,
// ---- Logout Commands ----
CMSG_PLAYER_LOGOUT = 0x04A,
CMSG_LOGOUT_REQUEST = 0x04B,
CMSG_LOGOUT_CANCEL = 0x04E,
SMSG_LOGOUT_RESPONSE = 0x04C,
SMSG_LOGOUT_COMPLETE = 0x04D,
// ---- Stand State ----
CMSG_STAND_STATE_CHANGE = 0x101,
// ---- Display Toggles ----
CMSG_SHOWING_HELM = 0x2B9,
CMSG_SHOWING_CLOAK = 0x2BA,
// ---- PvP ----
CMSG_TOGGLE_PVP = 0x253,
// ---- Guild ----
CMSG_GUILD_INVITE = 0x082,
CMSG_GUILD_ACCEPT = 0x084,
CMSG_GUILD_DECLINE_INVITATION = 0x085,
CMSG_GUILD_INFO = 0x087,
CMSG_GUILD_GET_ROSTER = 0x089,
CMSG_GUILD_PROMOTE_MEMBER = 0x08B,
CMSG_GUILD_DEMOTE_MEMBER = 0x08C,
CMSG_GUILD_LEAVE = 0x08D,
CMSG_GUILD_MOTD = 0x091,
SMSG_GUILD_INFO = 0x088,
SMSG_GUILD_ROSTER = 0x08A,
// ---- Ready Check ----
MSG_RAID_READY_CHECK = 0x322,
MSG_RAID_READY_CHECK_CONFIRM = 0x3AE,
// ---- Duel ----
CMSG_DUEL_PROPOSED = 0x166,
CMSG_DUEL_ACCEPTED = 0x16C,
CMSG_DUEL_CANCELLED = 0x16D,
SMSG_DUEL_REQUESTED = 0x167,
// ---- Trade ----
CMSG_INITIATE_TRADE = 0x116,
// ---- Random Roll ----
MSG_RANDOM_ROLL = 0x1FB,
// ---- Phase 1: Foundation (Targeting, Queries) ----
CMSG_SET_SELECTION = 0x13D,
CMSG_NAME_QUERY = 0x050,
SMSG_NAME_QUERY_RESPONSE = 0x051,
CMSG_CREATURE_QUERY = 0x060,
SMSG_CREATURE_QUERY_RESPONSE = 0x061,
CMSG_GAMEOBJECT_QUERY = 0x05E,
SMSG_GAMEOBJECT_QUERY_RESPONSE = 0x05F,
CMSG_SET_ACTIVE_MOVER = 0x26A,
CMSG_BINDER_ACTIVATE = 0x1B5,
// ---- XP ----
SMSG_LOG_XPGAIN = 0x1D0,
// ---- Creature Movement ----
SMSG_MONSTER_MOVE = 0x0DD,
// ---- Phase 2: Combat Core ----
CMSG_ATTACKSWING = 0x141,
CMSG_ATTACKSTOP = 0x142,
SMSG_ATTACKSTART = 0x143,
SMSG_ATTACKSTOP = 0x144,
SMSG_ATTACKERSTATEUPDATE = 0x14A,
SMSG_SPELLNONMELEEDAMAGELOG = 0x250,
SMSG_SPELLHEALLOG = 0x150,
SMSG_SPELLENERGIZELOG = 0x25B,
SMSG_PERIODICAURALOG = 0x24E,
SMSG_ENVIRONMENTALDAMAGELOG = 0x1FC,
// ---- Phase 3: Spells, Action Bar, Auras ----
CMSG_CAST_SPELL = 0x12E,
CMSG_CANCEL_CAST = 0x12F,
CMSG_CANCEL_AURA = 0x033,
SMSG_CAST_FAILED = 0x130,
SMSG_SPELL_START = 0x131,
SMSG_SPELL_GO = 0x132,
SMSG_SPELL_FAILURE = 0x133,
SMSG_SPELL_COOLDOWN = 0x134,
SMSG_COOLDOWN_EVENT = 0x135,
SMSG_UPDATE_AURA_DURATION = 0x137,
SMSG_INITIAL_SPELLS = 0x12A,
SMSG_LEARNED_SPELL = 0x12B,
SMSG_SUPERCEDED_SPELL = 0x12C,
SMSG_REMOVED_SPELL = 0x203,
SMSG_SEND_UNLEARN_SPELLS = 0x41F,
SMSG_SPELL_DELAYED = 0x1E2,
SMSG_AURA_UPDATE = 0x3FA,
SMSG_AURA_UPDATE_ALL = 0x495,
SMSG_SET_FLAT_SPELL_MODIFIER = 0x266,
SMSG_SET_PCT_SPELL_MODIFIER = 0x267,
// ---- Talents ----
SMSG_TALENTS_INFO = 0x4C0,
CMSG_LEARN_TALENT = 0x251,
MSG_TALENT_WIPE_CONFIRM = 0x2AB,
// ---- Phase 4: Group/Party ----
CMSG_GROUP_INVITE = 0x06E,
SMSG_GROUP_INVITE = 0x06F,
CMSG_GROUP_ACCEPT = 0x072,
CMSG_GROUP_DECLINE = 0x073,
SMSG_GROUP_DECLINE = 0x074,
CMSG_GROUP_UNINVITE_GUID = 0x076,
SMSG_GROUP_UNINVITE = 0x077,
CMSG_GROUP_SET_LEADER = 0x078,
SMSG_GROUP_SET_LEADER = 0x079,
CMSG_GROUP_DISBAND = 0x07B,
SMSG_GROUP_LIST = 0x07D,
SMSG_PARTY_COMMAND_RESULT = 0x07E,
MSG_RAID_TARGET_UPDATE = 0x321,
CMSG_REQUEST_RAID_INFO = 0x2CD,
SMSG_RAID_INSTANCE_INFO = 0x2CC,
// ---- Phase 5: Loot ----
CMSG_AUTOSTORE_LOOT_ITEM = 0x108,
CMSG_LOOT = 0x15D,
CMSG_LOOT_MONEY = 0x15E,
CMSG_LOOT_RELEASE = 0x15F,
SMSG_LOOT_RESPONSE = 0x160,
SMSG_LOOT_RELEASE_RESPONSE = 0x161,
SMSG_LOOT_REMOVED = 0x162,
SMSG_LOOT_MONEY_NOTIFY = 0x163,
SMSG_LOOT_CLEAR_MONEY = 0x165,
// ---- Phase 5: Taxi / Flight Paths ----
CMSG_ACTIVATETAXI = 0x19D,
// ---- Phase 5: NPC Gossip ----
CMSG_GOSSIP_HELLO = 0x17B,
CMSG_GOSSIP_SELECT_OPTION = 0x17C,
SMSG_GOSSIP_MESSAGE = 0x17D,
SMSG_GOSSIP_COMPLETE = 0x17E,
SMSG_NPC_TEXT_UPDATE = 0x180,
// ---- Phase 5: GameObject ----
CMSG_GAMEOBJECT_USE = 0x01B,
// ---- Phase 5: Quests ----
CMSG_QUESTGIVER_STATUS_QUERY = 0x182,
SMSG_QUESTGIVER_STATUS = 0x183,
SMSG_QUESTGIVER_STATUS_MULTIPLE = 0x198,
CMSG_QUESTGIVER_HELLO = 0x184,
CMSG_QUESTGIVER_QUERY_QUEST = 0x186,
SMSG_QUESTGIVER_QUEST_DETAILS = 0x188,
CMSG_QUESTGIVER_ACCEPT_QUEST = 0x189,
CMSG_QUESTGIVER_COMPLETE_QUEST = 0x18A,
SMSG_QUESTGIVER_REQUEST_ITEMS = 0x18B,
CMSG_QUESTGIVER_REQUEST_REWARD = 0x18C,
SMSG_QUESTGIVER_OFFER_REWARD = 0x18D,
CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E,
SMSG_QUESTGIVER_QUEST_INVALID = 0x18F,
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
CMSG_QUESTLOG_REMOVE_QUEST = 0x194,
SMSG_QUESTUPDATE_ADD_KILL = 0x196, // Quest kill count update
SMSG_QUESTUPDATE_COMPLETE = 0x195, // Quest objectives completed
CMSG_QUEST_QUERY = 0x05C, // Client requests quest data
SMSG_QUEST_QUERY_RESPONSE = 0x05D, // Server sends quest data
SMSG_QUESTLOG_FULL = 0x1A3, // Full quest log on login
// ---- Phase 5: Vendor ----
CMSG_LIST_INVENTORY = 0x19E,
SMSG_LIST_INVENTORY = 0x19F,
CMSG_SELL_ITEM = 0x1A0,
SMSG_SELL_ITEM = 0x1A1,
CMSG_BUY_ITEM = 0x1A2,
SMSG_BUY_FAILED = 0x1A5,
// ---- Trainer ----
CMSG_TRAINER_LIST = 0x01B0,
SMSG_TRAINER_LIST = 0x01B1,
CMSG_TRAINER_BUY_SPELL = 0x01B2,
SMSG_TRAINER_BUY_FAILED = 0x01B4,
// ---- Phase 5: Item/Equip ----
CMSG_ITEM_QUERY_SINGLE = 0x056,
SMSG_ITEM_QUERY_SINGLE_RESPONSE = 0x058,
CMSG_USE_ITEM = 0x00AB,
CMSG_AUTOEQUIP_ITEM = 0x10A,
CMSG_SWAP_ITEM = 0x10C,
CMSG_SWAP_INV_ITEM = 0x10D,
SMSG_INVENTORY_CHANGE_FAILURE = 0x112,
CMSG_INSPECT = 0x114,
SMSG_INSPECT_RESULTS = 0x115,
// ---- Death/Respawn ----
CMSG_REPOP_REQUEST = 0x015A,
SMSG_RESURRECT_REQUEST = 0x015B,
CMSG_RESURRECT_RESPONSE = 0x015C,
CMSG_SPIRIT_HEALER_ACTIVATE = 0x021C,
SMSG_SPIRIT_HEALER_CONFIRM = 0x0222,
SMSG_RESURRECT_CANCEL = 0x0390,
// ---- Teleport / Transfer ----
MSG_MOVE_TELEPORT_ACK = 0x0C7,
SMSG_TRANSFER_PENDING = 0x003F,
SMSG_NEW_WORLD = 0x003E,
MSG_MOVE_WORLDPORT_ACK = 0x00DC,
SMSG_TRANSFER_ABORTED = 0x0040,
// ---- Speed Changes ----
SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2,
CMSG_FORCE_RUN_SPEED_CHANGE_ACK = 0x00E3,
// ---- Mount ----
CMSG_CANCEL_MOUNT_AURA = 0x0375,
// ---- Taxi / Flight Paths ----
SMSG_SHOWTAXINODES = 0x01A9,
SMSG_ACTIVATETAXIREPLY = 0x01AE,
// Some cores send activate taxi reply on 0x029D (observed in logs)
SMSG_ACTIVATETAXIREPLY_ALT = 0x029D,
SMSG_NEW_TAXI_PATH = 0x01AF,
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
// ---- Battleground ----
SMSG_BATTLEFIELD_PORT_DENIED = 0x014B,
SMSG_REMOVED_FROM_PVP_QUEUE = 0x0170,
SMSG_TRAINER_BUY_SUCCEEDED = 0x01B3,
SMSG_BINDPOINTUPDATE = 0x0155,
CMSG_BATTLEFIELD_LIST = 0x023C,
SMSG_BATTLEFIELD_LIST = 0x023D,
CMSG_BATTLEFIELD_JOIN = 0x023E,
CMSG_BATTLEFIELD_STATUS = 0x02D3,
SMSG_BATTLEFIELD_STATUS = 0x02D4,
CMSG_BATTLEFIELD_PORT = 0x02D5,
CMSG_BATTLEMASTER_HELLO = 0x02D7,
MSG_PVP_LOG_DATA = 0x02E0,
CMSG_LEAVE_BATTLEFIELD = 0x02E1,
SMSG_GROUP_JOINED_BATTLEGROUND = 0x02E8,
MSG_BATTLEGROUND_PLAYER_POSITIONS = 0x02E9,
SMSG_BATTLEGROUND_PLAYER_JOINED = 0x02EC,
SMSG_BATTLEGROUND_PLAYER_LEFT = 0x02ED,
CMSG_BATTLEMASTER_JOIN = 0x02EE,
SMSG_JOINED_BATTLEGROUND_QUEUE = 0x038A,
// ---- Arena Team ----
CMSG_ARENA_TEAM_CREATE = 0x0348,
SMSG_ARENA_TEAM_COMMAND_RESULT = 0x0349,
CMSG_ARENA_TEAM_QUERY = 0x034B,
SMSG_ARENA_TEAM_QUERY_RESPONSE = 0x034C,
CMSG_ARENA_TEAM_ROSTER = 0x034D,
SMSG_ARENA_TEAM_ROSTER = 0x034E,
CMSG_ARENA_TEAM_INVITE = 0x034F,
SMSG_ARENA_TEAM_INVITE = 0x0350,
CMSG_ARENA_TEAM_ACCEPT = 0x0351,
CMSG_ARENA_TEAM_DECLINE = 0x0352,
CMSG_ARENA_TEAM_LEAVE = 0x0353,
CMSG_ARENA_TEAM_REMOVE = 0x0354,
CMSG_ARENA_TEAM_DISBAND = 0x0355,
CMSG_ARENA_TEAM_LEADER = 0x0356,
SMSG_ARENA_TEAM_EVENT = 0x0357,
CMSG_BATTLEMASTER_JOIN_ARENA = 0x0358,
SMSG_ARENA_TEAM_STATS = 0x035B,
SMSG_ARENA_ERROR = 0x0376,
MSG_INSPECT_ARENA_TEAMS = 0x0377,
};
// Backwards-compatibility alias: existing code uses Opcode::X which now maps
// to LogicalOpcode::X (the expansion-agnostic logical enum).
// Wire values are resolved at runtime via OpcodeTable.
using Opcode = LogicalOpcode;
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,178 @@
#pragma once
#include "game/world_packets.hpp"
#include <memory>
#include <string>
namespace wowee {
namespace game {
/**
* PacketParsers - Polymorphic interface for expansion-specific packet parsing.
*
* Binary packet formats differ significantly between WoW expansions
* (movement flags, update fields, character enum layout, etc.).
* Each expansion implements this interface with its specific parsing logic.
*
* The base PacketParsers delegates to the existing static parser classes
* in world_packets.hpp. Expansion subclasses override the methods that
* differ from WotLK.
*/
class PacketParsers {
public:
virtual ~PacketParsers() = default;
// --- Movement ---
/** Parse movement block from SMSG_UPDATE_OBJECT */
virtual bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
return UpdateObjectParser::parseMovementBlock(packet, block);
}
/** Write movement payload for CMSG_MOVE_* packets */
virtual void writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
MovementPacket::writeMovementPayload(packet, info);
}
/** Build a complete movement packet with packed GUID + payload */
virtual network::Packet buildMovementPacket(LogicalOpcode opcode,
const MovementInfo& info,
uint64_t playerGuid = 0) {
return MovementPacket::build(opcode, info, playerGuid);
}
// --- Character Enumeration ---
/** Parse SMSG_CHAR_ENUM */
virtual bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
return CharEnumParser::parse(packet, response);
}
// --- Update Object ---
/** Parse a full SMSG_UPDATE_OBJECT packet */
virtual bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
return UpdateObjectParser::parse(packet, data);
}
/** Parse update fields block (value mask + field values) */
virtual bool parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
return UpdateObjectParser::parseUpdateFields(packet, block);
}
// --- Monster Movement ---
/** Parse SMSG_MONSTER_MOVE */
virtual bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
return MonsterMoveParser::parse(packet, data);
}
// --- Combat ---
/** Parse SMSG_ATTACKERSTATEUPDATE */
virtual bool parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
return AttackerStateUpdateParser::parse(packet, data);
}
/** Parse SMSG_SPELLNONMELEEDAMAGELOG */
virtual bool parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
return SpellDamageLogParser::parse(packet, data);
}
// --- Spells ---
/** Parse SMSG_INITIAL_SPELLS */
virtual bool parseInitialSpells(network::Packet& packet, InitialSpellsData& data) {
return InitialSpellsParser::parse(packet, data);
}
/** Parse SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL */
virtual bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) {
return AuraUpdateParser::parse(packet, data, isAll);
}
// --- Utility ---
/** Read a packed GUID from the packet */
virtual uint64_t readPackedGuid(network::Packet& packet) {
return UpdateObjectParser::readPackedGuid(packet);
}
/** Write a packed GUID to the packet */
virtual void writePackedGuid(network::Packet& packet, uint64_t guid) {
MovementPacket::writePackedGuid(packet, guid);
}
};
/**
* WotLK 3.3.5a packet parsers.
*
* Uses the default implementations which delegate to the existing
* static parser classes. All current parsing code is WotLK-specific,
* so no overrides are needed.
*/
class WotlkPacketParsers : public PacketParsers {
// All methods use the defaults from PacketParsers base class,
// which delegate to the existing WotLK static parsers.
};
/**
* TBC 2.4.3 packet parsers.
*
* Overrides methods where TBC binary format differs from WotLK:
* - SMSG_UPDATE_OBJECT: u8 has_transport after blockCount (WotLK removed it)
* - UpdateFlags is u8 (not u16), no VEHICLE/ROTATION/POSITION flags
* - Movement flags2 is u8 (not u16), no transport seat byte
* - Movement flags: JUMPING=0x2000 gates jump data (WotLK: FALLING=0x1000)
* - SPLINE_ENABLED=0x08000000, SPLINE_ELEVATION=0x04000000 (same as WotLK)
* - Pitch: SWIMMING or else ONTRANSPORT(0x02000000)
* - CharEnum: uint8 firstLogin (not uint32+uint8), 20 equipment items (not 23)
* - Aura updates use inline update fields, not SMSG_AURA_UPDATE
*/
class TbcPacketParsers : public PacketParsers {
public:
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
network::Packet buildMovementPacket(LogicalOpcode opcode,
const MovementInfo& info,
uint64_t playerGuid = 0) override;
bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) override;
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) override;
};
/**
* Classic 1.12.1 packet parsers.
*
* Inherits from TBC (shared: u8 UpdateFlags, has_transport byte).
*
* Differences from TBC:
* - No moveFlags2 byte (TBC has u8, Classic has none)
* - Only 6 speed fields (no flight speeds flying added in TBC)
* - SPLINE_ENABLED at 0x00400000 (TBC/WotLK: 0x08000000)
* - Transport data has no timestamp (TBC adds u32 timestamp)
* - Pitch: only SWIMMING (no ONTRANSPORT secondary pitch)
* - CharEnum: no enchantment field per equipment slot
* - No SMSG_AURA_UPDATE (uses update fields, same as TBC)
*/
class ClassicPacketParsers : public TbcPacketParsers {
public:
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
network::Packet buildMovementPacket(LogicalOpcode opcode,
const MovementInfo& info,
uint64_t playerGuid = 0) override;
};
/**
* Factory function to create the right parser set for an expansion.
*/
inline std::unique_ptr<PacketParsers> createPacketParsers(const std::string& expansionId) {
if (expansionId == "classic") return std::make_unique<ClassicPacketParsers>();
if (expansionId == "tbc") return std::make_unique<TbcPacketParsers>();
return std::make_unique<WotlkPacketParsers>();
}
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,93 @@
#pragma once
#include <cstdint>
#include <string>
#include <unordered_map>
namespace wowee {
namespace game {
/**
* Logical update field identifiers (expansion-agnostic).
* Wire indices are loaded at runtime from JSON.
*/
enum class UF : uint16_t {
// Object fields
OBJECT_FIELD_ENTRY,
// Unit fields
UNIT_FIELD_TARGET_LO,
UNIT_FIELD_TARGET_HI,
UNIT_FIELD_HEALTH,
UNIT_FIELD_POWER1,
UNIT_FIELD_MAXHEALTH,
UNIT_FIELD_MAXPOWER1,
UNIT_FIELD_LEVEL,
UNIT_FIELD_FACTIONTEMPLATE,
UNIT_FIELD_FLAGS,
UNIT_FIELD_FLAGS_2,
UNIT_FIELD_DISPLAYID,
UNIT_FIELD_MOUNTDISPLAYID,
UNIT_NPC_FLAGS,
UNIT_DYNAMIC_FLAGS,
UNIT_END,
// Player fields
PLAYER_FLAGS,
PLAYER_XP,
PLAYER_NEXT_LEVEL_XP,
PLAYER_FIELD_COINAGE,
PLAYER_QUEST_LOG_START,
PLAYER_FIELD_INV_SLOT_HEAD,
PLAYER_FIELD_PACK_SLOT_1,
PLAYER_SKILL_INFO_START,
PLAYER_EXPLORED_ZONES_START,
// GameObject fields
GAMEOBJECT_DISPLAYID,
// Item fields
ITEM_FIELD_STACK_COUNT,
COUNT
};
/**
* Maps logical update field names to expansion-specific wire indices.
* Loaded from JSON (e.g. Data/expansions/wotlk/update_fields.json).
*/
class UpdateFieldTable {
public:
/** Load from JSON file. Returns true if successful. */
bool loadFromJson(const std::string& path);
/** Load built-in WotLK 3.3.5a defaults. */
void loadWotlkDefaults();
/** Get the wire index for a logical field. Returns 0xFFFF if unknown. */
uint16_t index(UF field) const;
/** Check if a field is mapped. */
bool hasField(UF field) const;
/** Number of mapped fields. */
size_t size() const { return fieldMap_.size(); }
private:
std::unordered_map<uint16_t, uint16_t> fieldMap_; // UF enum → wire index
};
/**
* Global active update field table (set by Application at startup).
*/
void setActiveUpdateFieldTable(const UpdateFieldTable* table);
const UpdateFieldTable* getActiveUpdateFieldTable();
/** Convenience: get wire index for a logical field. */
inline uint16_t fieldIndex(UF field) {
const auto* t = getActiveUpdateFieldTable();
return t ? t->index(field) : 0xFFFF;
}
} // namespace game
} // namespace wowee

View file

@ -514,7 +514,6 @@ public:
*/
static uint64_t readPackedGuid(network::Packet& packet);
private:
/**
* Parse a single update block
*

View file

@ -6,6 +6,7 @@
#include "pipeline/loose_file_reader.hpp"
#include <memory>
#include <string>
#include <vector>
#include <map>
#include <mutex>
@ -16,6 +17,8 @@ namespace pipeline {
* AssetManager - Unified interface for loading WoW assets
*
* Reads pre-extracted loose files indexed by manifest.json.
* Supports layered manifests: overlay manifests (HD packs, mods)
* are checked before the base manifest, with higher priority first.
* Use the asset_extract tool to extract MPQ archives first.
* All reads are fully parallel (no serialization mutex needed).
*/
@ -41,6 +44,26 @@ public:
*/
bool isInitialized() const { return initialized; }
/**
* Add an overlay manifest (HD packs, mods) checked before the base manifest.
* Higher priority overlays are checked first.
* @param manifestPath Full path to the overlay's manifest.json
* @param priority Priority level (higher = checked first)
* @param id Unique identifier for this overlay (e.g. "hd_character")
* @return true if overlay loaded successfully
*/
bool addOverlayManifest(const std::string& manifestPath, int priority, const std::string& id);
/**
* Remove a previously added overlay manifest by id.
*/
void removeOverlay(const std::string& id);
/**
* Get list of active overlay IDs.
*/
std::vector<std::string> getOverlayIds() const;
/**
* Load a BLP texture
* @param path Virtual path to BLP file (e.g., "Textures\\Minimap\\Background.blp")
@ -105,10 +128,24 @@ private:
bool initialized = false;
std::string dataPath;
// Loose file backend
// Base manifest (loaded from dataPath/manifest.json)
AssetManifest manifest_;
LooseFileReader looseReader_;
// Overlay manifests (HD packs, mods) - sorted by priority descending
struct ManifestLayer {
AssetManifest manifest;
int priority;
std::string id;
};
std::vector<ManifestLayer> overlayLayers_; // Sorted by priority desc
/**
* Resolve filesystem path checking overlays first, then base manifest.
* Returns empty string if not found in any layer.
*/
std::string resolveLayeredPath(const std::string& normalizedPath) const;
mutable std::mutex cacheMutex;
std::map<std::string, std::shared_ptr<DBCFile>> dbcCache;

View file

@ -0,0 +1,64 @@
#pragma once
#include <cstdint>
#include <string>
#include <unordered_map>
namespace wowee {
namespace pipeline {
/**
* Maps DBC field names to column indices for a single DBC file.
* Column indices vary between WoW expansions.
*/
struct DBCFieldMap {
std::unordered_map<std::string, uint32_t> fields;
/** Get column index by field name. Returns 0xFFFFFFFF if unknown. */
uint32_t field(const std::string& name) const {
auto it = fields.find(name);
return (it != fields.end()) ? it->second : 0xFFFFFFFF;
}
/** Convenience operator for shorter syntax: layout["Name"] */
uint32_t operator[](const std::string& name) const { return field(name); }
};
/**
* Maps DBC file names to their field layouts.
* Loaded from JSON (e.g. Data/expansions/wotlk/dbc_layouts.json).
*/
class DBCLayout {
public:
/** Load from JSON file. Returns true if successful. */
bool loadFromJson(const std::string& path);
/** Load built-in WotLK 3.3.5a defaults. */
void loadWotlkDefaults();
/** Get the field map for a DBC file. Returns nullptr if unknown. */
const DBCFieldMap* getLayout(const std::string& dbcName) const;
/** Number of DBC layouts loaded. */
size_t size() const { return layouts_.size(); }
private:
std::unordered_map<std::string, DBCFieldMap> layouts_;
};
/**
* Global active DBC layout (set by Application at startup).
*/
void setActiveDBCLayout(const DBCLayout* layout);
const DBCLayout* getActiveDBCLayout();
/** Convenience: get field index for a DBC field. */
inline uint32_t dbcField(const std::string& dbcName, const std::string& fieldName) {
const auto* l = getActiveDBCLayout();
if (!l) return 0xFFFFFFFF;
const auto* fm = l->getLayout(dbcName);
return fm ? fm->field(fieldName) : 0xFFFFFFFF;
}
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,97 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
namespace wowee {
namespace pipeline {
class AssetManager;
/**
* Metadata for a single HD texture pack on disk.
*
* Each pack lives in Data/hd/<packDir>/ and contains:
* pack.json - metadata (id, name, group, compatible expansions, size)
* manifest.json - standard asset manifest with HD override textures
* assets/ - the actual HD files
*/
struct HDPack {
std::string id; // Unique identifier (e.g. "character_hd")
std::string name; // Human-readable name
std::string group; // Grouping label (e.g. "Character", "Terrain")
std::vector<std::string> expansions; // Compatible expansion IDs
uint32_t totalSizeMB = 0; // Approximate total size on disk
std::string manifestPath; // Full path to manifest.json
std::string packDir; // Full path to pack directory
bool enabled = false; // User-toggled enable state
};
/**
* HDPackManager - discovers, manages, and wires HD texture packs.
*
* Scans Data/hd/ subdirectories for pack.json files. Each pack can be
* enabled/disabled via setPackEnabled(). Enabled packs are wired into
* AssetManager as high-priority overlay manifests so that HD textures
* override the base expansion assets transparently.
*/
class HDPackManager {
public:
HDPackManager() = default;
/**
* Scan the HD root directory for available packs.
* @param hdRootPath Path to Data/hd/ directory
*/
void initialize(const std::string& hdRootPath);
/**
* Get all discovered packs.
*/
const std::vector<HDPack>& getAllPacks() const { return packs_; }
/**
* Get packs compatible with a specific expansion.
*/
std::vector<const HDPack*> getPacksForExpansion(const std::string& expansionId) const;
/**
* Enable or disable a pack. Persists state in enabledPacks_ map.
*/
void setPackEnabled(const std::string& packId, bool enabled);
/**
* Check if a pack is enabled.
*/
bool isPackEnabled(const std::string& packId) const;
/**
* Apply enabled packs as overlays to the asset manager.
* Removes previously applied overlays and re-adds enabled ones.
*/
void applyToAssetManager(AssetManager* assetManager, const std::string& expansionId);
/**
* Save enabled pack state to a settings file.
*/
void saveSettings(const std::string& settingsPath) const;
/**
* Load enabled pack state from a settings file.
*/
void loadSettings(const std::string& settingsPath);
private:
std::vector<HDPack> packs_;
std::unordered_map<std::string, bool> enabledState_; // packId → enabled
// Overlay IDs currently applied to AssetManager (for removal on re-apply)
std::vector<std::string> appliedOverlayIds_;
static constexpr int HD_OVERLAY_PRIORITY_BASE = 100; // High priority, above expansion base
};
} // namespace pipeline
} // namespace wowee

View file

@ -3,6 +3,7 @@
#include "auth/auth_handler.hpp"
#include "rendering/video_player.hpp"
#include <string>
#include <vector>
#include <functional>
namespace wowee { namespace ui {
@ -46,7 +47,7 @@ private:
char username[256] = "";
char password[256] = "";
int port = 3724;
int compatibilityMode = 0; // 0 = 3.3.5a
int expansionIndex = 0; // Index into expansion registry profiles
bool authenticating = false;
bool showPassword = false;