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

7
.gitignore vendored
View file

@ -55,13 +55,6 @@ imgui.ini
config.ini
config.json
# WoW data (users must supply their own)
Data/
*.mpq
# Texture assets (not distributed - see README)
assets/textures/
# Runtime cache (floor heights, etc.)
cache/

View file

@ -95,6 +95,9 @@ set(WOWEE_SOURCES
src/auth/rc4.cpp
# Game
src/game/expansion_profile.cpp
src/game/opcode_table.cpp
src/game/update_field_table.cpp
src/game/game_handler.cpp
src/game/warden_crypto.cpp
src/game/warden_module.cpp
@ -105,6 +108,8 @@ set(WOWEE_SOURCES
src/game/entity.cpp
src/game/opcodes.cpp
src/game/world_packets.cpp
src/game/packet_parsers_tbc.cpp
src/game/packet_parsers_classic.cpp
src/game/character.cpp
src/game/zone_manager.cpp
src/game/inventory.cpp
@ -131,6 +136,8 @@ set(WOWEE_SOURCES
src/pipeline/m2_loader.cpp
src/pipeline/wmo_loader.cpp
src/pipeline/adt_loader.cpp
src/pipeline/dbc_layout.cpp
src/pipeline/hd_pack_manager.cpp
src/pipeline/terrain_mesh.cpp
# Rendering

View file

@ -0,0 +1,92 @@
{
"Spell": {
"ID": 0, "Attributes": 5, "IconID": 124,
"Name": 127, "Tooltip": 154, "Rank": 136
},
"ItemDisplayInfo": {
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9
},
"CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6,
"VariationIndex": 8, "ColorIndex": 9
},
"SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": {
"ID": 0, "Faction": 1, "FactionGroup": 3,
"FriendGroup": 4, "EnemyGroup": 5,
"Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9
},
"Faction": {
"ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3,
"ReputationRaceMask2": 4, "ReputationRaceMask3": 5,
"ReputationBase0": 10, "ReputationBase1": 11,
"ReputationBase2": 12, "ReputationBase3": 13
},
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
"CreatureDisplayInfoExtra": {
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
},
"CreatureDisplayInfo": {
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
"Skin1": 6, "Skin2": 7, "Skin3": 8
},
"TaxiNodes": {
"ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5,
"MountDisplayIdAlliance": 12, "MountDisplayIdHorde": 13
},
"TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 },
"TaxiPathNode": {
"ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3,
"X": 4, "Y": 5, "Z": 6
},
"TalentTab": {
"ID": 0, "Name": 1, "ClassMask": 12,
"OrderIndex": 14, "BackgroundFile": 15
},
"Talent": {
"ID": 0, "TabID": 1, "Row": 2, "Column": 3,
"RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12
},
"SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 },
"SkillLine": { "ID": 0, "Category": 1, "Name": 3 },
"Map": { "ID": 0, "InternalName": 1 },
"CreatureModelData": { "ID": 0, "ModelPath": 2 },
"CharHairGeosets": {
"RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0, "SexID": 1, "Variation": 2,
"Geoset100": 3, "Geoset300": 4, "Geoset200": 5
},
"GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 },
"Emotes": { "ID": 0, "AnimID": 2 },
"EmotesText": {
"Command": 1, "EmoteRef": 2,
"SenderTargetTextID": 5, "SenderNoTargetTextID": 9
},
"EmotesTextData": { "ID": 0, "Text": 1 },
"Light": {
"ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4,
"InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7,
"LightParamsIDRain": 8, "LightParamsIDUnderwater": 9
},
"LightParams": { "LightParamsID": 0 },
"LightIntBand": {
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
},
"LightFloatBand": {
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
},
"WorldMapArea": {
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
"DisplayMapID": 8, "ParentWorldMapID": 10
}
}

View file

@ -0,0 +1,11 @@
{
"id": "classic",
"name": "Classic",
"shortName": "Classic",
"version": { "major": 1, "minor": 12, "patch": 1 },
"build": 5875,
"protocolVersion": 8,
"maxLevel": 60,
"races": [1, 2, 3, 4, 5, 6, 7, 8],
"classes": [1, 2, 3, 4, 5, 7, 8, 9, 11]
}

View file

@ -0,0 +1,226 @@
{
"CMSG_PING": "0x1DC",
"CMSG_AUTH_SESSION": "0x1ED",
"CMSG_CHAR_CREATE": "0x036",
"CMSG_CHAR_ENUM": "0x037",
"CMSG_CHAR_DELETE": "0x038",
"CMSG_PLAYER_LOGIN": "0x03D",
"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",
"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_UPDATE_OBJECT": "0x0A9",
"SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6",
"SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE",
"SMSG_DESTROY_OBJECT": "0x0AA",
"CMSG_MESSAGECHAT": "0x095",
"SMSG_MESSAGECHAT": "0x096",
"CMSG_WHO": "0x062",
"SMSG_WHO": "0x063",
"CMSG_REQUEST_PLAYED_TIME": "0x1CC",
"SMSG_PLAYED_TIME": "0x1CD",
"CMSG_QUERY_TIME": "0x1CE",
"SMSG_QUERY_TIME_RESPONSE": "0x1CF",
"SMSG_FRIEND_STATUS": "0x068",
"CMSG_ADD_FRIEND": "0x069",
"CMSG_DEL_FRIEND": "0x06A",
"CMSG_ADD_IGNORE": "0x06C",
"CMSG_DEL_IGNORE": "0x06D",
"CMSG_PLAYER_LOGOUT": "0x04A",
"CMSG_LOGOUT_REQUEST": "0x04B",
"CMSG_LOGOUT_CANCEL": "0x04E",
"SMSG_LOGOUT_RESPONSE": "0x04C",
"SMSG_LOGOUT_COMPLETE": "0x04D",
"CMSG_STAND_STATE_CHANGE": "0x101",
"CMSG_SHOWING_HELM": "0x2B9",
"CMSG_SHOWING_CLOAK": "0x2BA",
"CMSG_TOGGLE_PVP": "0x253",
"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",
"MSG_RAID_READY_CHECK": "0x322",
"CMSG_DUEL_PROPOSED": "0x166",
"CMSG_DUEL_ACCEPTED": "0x16C",
"CMSG_DUEL_CANCELLED": "0x16D",
"SMSG_DUEL_REQUESTED": "0x167",
"CMSG_INITIATE_TRADE": "0x116",
"MSG_RANDOM_ROLL": "0x1FB",
"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",
"SMSG_LOG_XPGAIN": "0x1D0",
"SMSG_MONSTER_MOVE": "0x0DD",
"CMSG_ATTACKSWING": "0x141",
"CMSG_ATTACKSTOP": "0x142",
"SMSG_ATTACKSTART": "0x143",
"SMSG_ATTACKSTOP": "0x144",
"SMSG_ATTACKERSTATEUPDATE": "0x14A",
"SMSG_SPELLNONMELEEDAMAGELOG": "0x250",
"SMSG_SPELLHEALLOG": "0x150",
"SMSG_SPELLENERGIZELOG": "0x151",
"SMSG_PERIODICAURALOG": "0x24E",
"SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC",
"CMSG_CAST_SPELL": "0x12E",
"CMSG_CANCEL_CAST": "0x12F",
"CMSG_CANCEL_AURA": "0x136",
"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_SPELL_DELAYED": "0x1E2",
"SMSG_SET_FLAT_SPELL_MODIFIER": "0x266",
"SMSG_SET_PCT_SPELL_MODIFIER": "0x267",
"CMSG_LEARN_TALENT": "0x251",
"MSG_TALENT_WIPE_CONFIRM": "0x2AA",
"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": "0x07F",
"MSG_RAID_TARGET_UPDATE": "0x321",
"CMSG_REQUEST_RAID_INFO": "0x2CD",
"SMSG_RAID_INSTANCE_INFO": "0x2CC",
"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",
"CMSG_ACTIVATETAXI": "0x1AD",
"CMSG_GOSSIP_HELLO": "0x17B",
"CMSG_GOSSIP_SELECT_OPTION": "0x17C",
"SMSG_GOSSIP_MESSAGE": "0x17D",
"SMSG_GOSSIP_COMPLETE": "0x17E",
"SMSG_NPC_TEXT_UPDATE": "0x180",
"CMSG_GAMEOBJECT_USE": "0x0B1",
"CMSG_QUESTGIVER_STATUS_QUERY": "0x182",
"SMSG_QUESTGIVER_STATUS": "0x183",
"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": "0x199",
"SMSG_QUESTUPDATE_COMPLETE": "0x198",
"CMSG_QUEST_QUERY": "0x05C",
"SMSG_QUEST_QUERY_RESPONSE": "0x05D",
"SMSG_QUESTLOG_FULL": "0x195",
"CMSG_LIST_INVENTORY": "0x19E",
"SMSG_LIST_INVENTORY": "0x19F",
"CMSG_SELL_ITEM": "0x1A0",
"SMSG_SELL_ITEM": "0x1A1",
"CMSG_BUY_ITEM": "0x1A2",
"SMSG_BUY_FAILED": "0x1A5",
"CMSG_TRAINER_LIST": "0x1B0",
"SMSG_TRAINER_LIST": "0x1B1",
"CMSG_TRAINER_BUY_SPELL": "0x1B2",
"SMSG_TRAINER_BUY_FAILED": "0x1B4",
"CMSG_ITEM_QUERY_SINGLE": "0x056",
"SMSG_ITEM_QUERY_SINGLE_RESPONSE": "0x058",
"CMSG_USE_ITEM": "0x0AB",
"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",
"CMSG_REPOP_REQUEST": "0x15A",
"SMSG_RESURRECT_REQUEST": "0x15B",
"CMSG_RESURRECT_RESPONSE": "0x15C",
"CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C",
"SMSG_SPIRIT_HEALER_CONFIRM": "0x222",
"MSG_MOVE_TELEPORT_ACK": "0x0C7",
"SMSG_TRANSFER_PENDING": "0x03F",
"SMSG_NEW_WORLD": "0x03E",
"MSG_MOVE_WORLDPORT_ACK": "0x0DC",
"SMSG_TRANSFER_ABORTED": "0x040",
"SMSG_FORCE_RUN_SPEED_CHANGE": "0x0E2",
"CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x0E3",
"SMSG_SHOWTAXINODES": "0x1A9",
"SMSG_ACTIVATETAXIREPLY": "0x1AE",
"SMSG_NEW_TAXI_PATH": "0x1AF",
"CMSG_ACTIVATETAXIEXPRESS": "0x312",
"CMSG_TAXINODE_STATUS_QUERY": "0x1AA",
"SMSG_TAXINODE_STATUS": "0x1AB",
"SMSG_TRAINER_BUY_SUCCEEDED": "0x1B3",
"SMSG_BINDPOINTUPDATE": "0x155",
"SMSG_SET_PROFICIENCY": "0x127",
"SMSG_ACTION_BUTTONS": "0x129",
"SMSG_LEVELUP_INFO": "0x1D4",
"CMSG_UPDATE_ACCOUNT_DATA": "0x20B",
"CMSG_BATTLEFIELD_LIST": "0x23C",
"SMSG_BATTLEFIELD_LIST": "0x23D",
"CMSG_BATTLEFIELD_JOIN": "0x23E",
"CMSG_BATTLEFIELD_STATUS": "0x2D3",
"SMSG_BATTLEFIELD_STATUS": "0x2D4",
"CMSG_BATTLEFIELD_PORT": "0x2D5",
"CMSG_BATTLEMASTER_HELLO": "0x2D7",
"MSG_PVP_LOG_DATA": "0x2E0",
"CMSG_LEAVE_BATTLEFIELD": "0x2E1",
"SMSG_GROUP_JOINED_BATTLEGROUND": "0x2E8",
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9",
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
"CMSG_BATTLEMASTER_JOIN": "0x2EE"
}

View file

@ -0,0 +1,29 @@
{
"OBJECT_FIELD_ENTRY": 3,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_FIELD_TARGET_HI": 17,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_FACTIONTEMPLATE": 35,
"UNIT_FIELD_FLAGS": 46,
"UNIT_FIELD_DISPLAYID": 131,
"UNIT_FIELD_MOUNTDISPLAYID": 133,
"UNIT_NPC_FLAGS": 147,
"UNIT_DYNAMIC_FLAGS": 143,
"UNIT_END": 188,
"PLAYER_FLAGS": 190,
"PLAYER_XP": 716,
"PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_FIELD_COINAGE": 1176,
"PLAYER_QUEST_LOG_START": 198,
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
"PLAYER_FIELD_PACK_SLOT_1": 532,
"PLAYER_SKILL_INFO_START": 718,
"PLAYER_EXPLORED_ZONES_START": 1111,
"PLAYER_END": 1282,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14
}

View file

@ -0,0 +1,93 @@
{
"Spell": {
"ID": 0, "Attributes": 5, "IconID": 124,
"Name": 127, "Tooltip": 154, "Rank": 136
},
"ItemDisplayInfo": {
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9
},
"CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6,
"VariationIndex": 8, "ColorIndex": 9
},
"SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": {
"ID": 0, "Faction": 1, "FactionGroup": 3,
"FriendGroup": 4, "EnemyGroup": 5,
"Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9
},
"Faction": {
"ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3,
"ReputationRaceMask2": 4, "ReputationRaceMask3": 5,
"ReputationBase0": 10, "ReputationBase1": 11,
"ReputationBase2": 12, "ReputationBase3": 13
},
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
"CreatureDisplayInfoExtra": {
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
},
"CreatureDisplayInfo": {
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
"Skin1": 6, "Skin2": 7, "Skin3": 8
},
"TaxiNodes": {
"ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5,
"MountDisplayIdAllianceFallback": 12, "MountDisplayIdHordeFallback": 13,
"MountDisplayIdAlliance": 14, "MountDisplayIdHorde": 15
},
"TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 },
"TaxiPathNode": {
"ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3,
"X": 4, "Y": 5, "Z": 6
},
"TalentTab": {
"ID": 0, "Name": 1, "ClassMask": 12,
"OrderIndex": 14, "BackgroundFile": 15
},
"Talent": {
"ID": 0, "TabID": 1, "Row": 2, "Column": 3,
"RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12
},
"SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 },
"SkillLine": { "ID": 0, "Category": 1, "Name": 3 },
"Map": { "ID": 0, "InternalName": 1 },
"CreatureModelData": { "ID": 0, "ModelPath": 2 },
"CharHairGeosets": {
"RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0, "SexID": 1, "Variation": 2,
"Geoset100": 3, "Geoset300": 4, "Geoset200": 5
},
"GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 },
"Emotes": { "ID": 0, "AnimID": 2 },
"EmotesText": {
"Command": 1, "EmoteRef": 2,
"SenderTargetTextID": 5, "SenderNoTargetTextID": 9
},
"EmotesTextData": { "ID": 0, "Text": 1 },
"Light": {
"ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4,
"InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7,
"LightParamsIDRain": 8, "LightParamsIDUnderwater": 9
},
"LightParams": { "LightParamsID": 0 },
"LightIntBand": {
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
},
"LightFloatBand": {
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
},
"WorldMapArea": {
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
"DisplayMapID": 8, "ParentWorldMapID": 10
}
}

View file

@ -0,0 +1,11 @@
{
"id": "tbc",
"name": "The Burning Crusade",
"shortName": "TBC",
"version": { "major": 2, "minor": 4, "patch": 3 },
"build": 8606,
"protocolVersion": 8,
"maxLevel": 70,
"races": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
"classes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
}

View file

@ -0,0 +1,255 @@
{
"CMSG_PING": "0x1DC",
"CMSG_AUTH_SESSION": "0x1ED",
"CMSG_CHAR_CREATE": "0x036",
"CMSG_CHAR_ENUM": "0x037",
"CMSG_CHAR_DELETE": "0x038",
"CMSG_PLAYER_LOGIN": "0x03D",
"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",
"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_MOTD": "0x33D",
"SMSG_UPDATE_OBJECT": "0x0A9",
"SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6",
"SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE",
"SMSG_DESTROY_OBJECT": "0x0AA",
"CMSG_MESSAGECHAT": "0x095",
"SMSG_MESSAGECHAT": "0x096",
"CMSG_WHO": "0x062",
"SMSG_WHO": "0x063",
"CMSG_REQUEST_PLAYED_TIME": "0x1CC",
"SMSG_PLAYED_TIME": "0x1CD",
"CMSG_QUERY_TIME": "0x1CE",
"SMSG_QUERY_TIME_RESPONSE": "0x1CF",
"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",
"CMSG_PLAYER_LOGOUT": "0x04A",
"CMSG_LOGOUT_REQUEST": "0x04B",
"CMSG_LOGOUT_CANCEL": "0x04E",
"SMSG_LOGOUT_RESPONSE": "0x04C",
"SMSG_LOGOUT_COMPLETE": "0x04D",
"CMSG_STAND_STATE_CHANGE": "0x101",
"CMSG_SHOWING_HELM": "0x2B9",
"CMSG_SHOWING_CLOAK": "0x2BA",
"CMSG_TOGGLE_PVP": "0x253",
"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",
"MSG_RAID_READY_CHECK": "0x322",
"MSG_RAID_READY_CHECK_CONFIRM": "0x3AE",
"CMSG_DUEL_PROPOSED": "0x166",
"CMSG_DUEL_ACCEPTED": "0x16C",
"CMSG_DUEL_CANCELLED": "0x16D",
"SMSG_DUEL_REQUESTED": "0x167",
"CMSG_INITIATE_TRADE": "0x116",
"MSG_RANDOM_ROLL": "0x1FB",
"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",
"SMSG_LOG_XPGAIN": "0x1D0",
"SMSG_MONSTER_MOVE": "0x0DD",
"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",
"CMSG_CAST_SPELL": "0x12E",
"CMSG_CANCEL_CAST": "0x12F",
"CMSG_CANCEL_AURA": "0x136",
"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_SPELL_DELAYED": "0x1E2",
"SMSG_SET_FLAT_SPELL_MODIFIER": "0x266",
"SMSG_SET_PCT_SPELL_MODIFIER": "0x267",
"CMSG_LEARN_TALENT": "0x251",
"MSG_TALENT_WIPE_CONFIRM": "0x2AB",
"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": "0x07F",
"MSG_RAID_TARGET_UPDATE": "0x321",
"CMSG_REQUEST_RAID_INFO": "0x2CD",
"SMSG_RAID_INSTANCE_INFO": "0x2CC",
"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",
"CMSG_ACTIVATETAXI": "0x1AD",
"CMSG_GOSSIP_HELLO": "0x17B",
"CMSG_GOSSIP_SELECT_OPTION": "0x17C",
"SMSG_GOSSIP_MESSAGE": "0x17D",
"SMSG_GOSSIP_COMPLETE": "0x17E",
"SMSG_NPC_TEXT_UPDATE": "0x180",
"CMSG_GAMEOBJECT_USE": "0x01B",
"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",
"SMSG_QUESTUPDATE_COMPLETE": "0x195",
"CMSG_QUEST_QUERY": "0x05C",
"SMSG_QUEST_QUERY_RESPONSE": "0x05D",
"SMSG_QUESTLOG_FULL": "0x1A3",
"CMSG_LIST_INVENTORY": "0x19E",
"SMSG_LIST_INVENTORY": "0x19F",
"CMSG_SELL_ITEM": "0x1A0",
"SMSG_SELL_ITEM": "0x1A1",
"CMSG_BUY_ITEM": "0x1A2",
"SMSG_BUY_FAILED": "0x1A5",
"CMSG_TRAINER_LIST": "0x1B0",
"SMSG_TRAINER_LIST": "0x1B1",
"CMSG_TRAINER_BUY_SPELL": "0x1B2",
"SMSG_TRAINER_BUY_FAILED": "0x1B4",
"CMSG_ITEM_QUERY_SINGLE": "0x056",
"SMSG_ITEM_QUERY_SINGLE_RESPONSE": "0x058",
"CMSG_USE_ITEM": "0x0AB",
"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",
"CMSG_REPOP_REQUEST": "0x15A",
"SMSG_RESURRECT_REQUEST": "0x15B",
"CMSG_RESURRECT_RESPONSE": "0x15C",
"CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C",
"SMSG_SPIRIT_HEALER_CONFIRM": "0x222",
"SMSG_RESURRECT_CANCEL": "0x390",
"MSG_MOVE_TELEPORT_ACK": "0x0C7",
"SMSG_TRANSFER_PENDING": "0x03F",
"SMSG_NEW_WORLD": "0x03E",
"MSG_MOVE_WORLDPORT_ACK": "0x0DC",
"SMSG_TRANSFER_ABORTED": "0x040",
"SMSG_FORCE_RUN_SPEED_CHANGE": "0x0E2",
"CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x0E3",
"CMSG_CANCEL_MOUNT_AURA": "0x375",
"SMSG_SHOWTAXINODES": "0x1A9",
"SMSG_ACTIVATETAXIREPLY": "0x1AE",
"SMSG_NEW_TAXI_PATH": "0x1AF",
"CMSG_ACTIVATETAXIEXPRESS": "0x312",
"SMSG_BATTLEFIELD_PORT_DENIED": "0x14B",
"SMSG_REMOVED_FROM_PVP_QUEUE": "0x170",
"SMSG_TRAINER_BUY_SUCCEEDED": "0x1B3",
"SMSG_BINDPOINTUPDATE": "0x155",
"CMSG_BATTLEFIELD_LIST": "0x23C",
"SMSG_BATTLEFIELD_LIST": "0x23D",
"CMSG_BATTLEFIELD_JOIN": "0x23E",
"CMSG_BATTLEFIELD_STATUS": "0x2D3",
"SMSG_BATTLEFIELD_STATUS": "0x2D4",
"CMSG_BATTLEFIELD_PORT": "0x2D5",
"CMSG_BATTLEMASTER_HELLO": "0x2D7",
"MSG_PVP_LOG_DATA": "0x2E0",
"CMSG_LEAVE_BATTLEFIELD": "0x2E1",
"SMSG_GROUP_JOINED_BATTLEGROUND": "0x2E8",
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9",
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
"SMSG_JOINED_BATTLEGROUND_QUEUE": "0x38A",
"CMSG_ARENA_TEAM_CREATE": "0x348",
"SMSG_ARENA_TEAM_COMMAND_RESULT": "0x349",
"CMSG_ARENA_TEAM_QUERY": "0x34B",
"SMSG_ARENA_TEAM_QUERY_RESPONSE": "0x34C",
"CMSG_ARENA_TEAM_ROSTER": "0x34D",
"SMSG_ARENA_TEAM_ROSTER": "0x34E",
"CMSG_ARENA_TEAM_INVITE": "0x34F",
"SMSG_ARENA_TEAM_INVITE": "0x350",
"CMSG_ARENA_TEAM_ACCEPT": "0x351",
"CMSG_ARENA_TEAM_DECLINE": "0x352",
"CMSG_ARENA_TEAM_LEAVE": "0x353",
"CMSG_ARENA_TEAM_REMOVE": "0x354",
"CMSG_ARENA_TEAM_DISBAND": "0x355",
"CMSG_ARENA_TEAM_LEADER": "0x356",
"SMSG_ARENA_TEAM_EVENT": "0x357",
"CMSG_BATTLEMASTER_JOIN_ARENA": "0x358",
"SMSG_ARENA_TEAM_STATS": "0x35B",
"SMSG_ARENA_ERROR": "0x376",
"MSG_INSPECT_ARENA_TEAMS": "0x377",
"SMSG_LEVELUP_INFO": "0x1D4",
"SMSG_SET_PROFICIENCY": "0x127",
"SMSG_ACTION_BUTTONS": "0x129",
"CMSG_TAXINODE_STATUS_QUERY": "0x1AA",
"SMSG_TAXINODE_STATUS": "0x1AB",
"SMSG_INIT_EXTRA_AURA_INFO": "0x3A3",
"SMSG_SET_EXTRA_AURA_INFO": "0x3A4"
}

View file

@ -0,0 +1,29 @@
{
"OBJECT_FIELD_ENTRY": 3,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_FIELD_TARGET_HI": 17,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_FACTIONTEMPLATE": 35,
"UNIT_FIELD_FLAGS": 46,
"UNIT_FIELD_FLAGS_2": 47,
"UNIT_FIELD_DISPLAYID": 152,
"UNIT_FIELD_MOUNTDISPLAYID": 154,
"UNIT_NPC_FLAGS": 168,
"UNIT_DYNAMIC_FLAGS": 164,
"UNIT_END": 234,
"PLAYER_FLAGS": 236,
"PLAYER_XP": 926,
"PLAYER_NEXT_LEVEL_XP": 927,
"PLAYER_FIELD_COINAGE": 1441,
"PLAYER_QUEST_LOG_START": 244,
"PLAYER_FIELD_INV_SLOT_HEAD": 650,
"PLAYER_FIELD_PACK_SLOT_1": 696,
"PLAYER_SKILL_INFO_START": 928,
"PLAYER_EXPLORED_ZONES_START": 1312,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14
}

View file

@ -0,0 +1,93 @@
{
"Spell": {
"ID": 0, "Attributes": 4, "IconID": 133,
"Name": 136, "Tooltip": 139, "Rank": 153
},
"ItemDisplayInfo": {
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9
},
"CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6,
"VariationIndex": 8, "ColorIndex": 9
},
"SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": {
"ID": 0, "Faction": 1, "FactionGroup": 3,
"FriendGroup": 4, "EnemyGroup": 5,
"Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9
},
"Faction": {
"ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3,
"ReputationRaceMask2": 4, "ReputationRaceMask3": 5,
"ReputationBase0": 10, "ReputationBase1": 11,
"ReputationBase2": 12, "ReputationBase3": 13
},
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
"CreatureDisplayInfoExtra": {
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
},
"CreatureDisplayInfo": {
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
"Skin1": 6, "Skin2": 7, "Skin3": 8
},
"TaxiNodes": {
"ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5,
"MountDisplayIdAllianceFallback": 20, "MountDisplayIdHordeFallback": 21,
"MountDisplayIdAlliance": 22, "MountDisplayIdHorde": 23
},
"TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 },
"TaxiPathNode": {
"ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3,
"X": 4, "Y": 5, "Z": 6
},
"TalentTab": {
"ID": 0, "Name": 1, "ClassMask": 20,
"OrderIndex": 22, "BackgroundFile": 23
},
"Talent": {
"ID": 0, "TabID": 1, "Row": 2, "Column": 3,
"RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12
},
"SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 },
"SkillLine": { "ID": 0, "Category": 1, "Name": 3 },
"Map": { "ID": 0, "InternalName": 1 },
"CreatureModelData": { "ID": 0, "ModelPath": 2 },
"CharHairGeosets": {
"RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0, "SexID": 1, "Variation": 2,
"Geoset100": 3, "Geoset300": 4, "Geoset200": 5
},
"GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 },
"Emotes": { "ID": 0, "AnimID": 2 },
"EmotesText": {
"Command": 1, "EmoteRef": 2,
"SenderTargetTextID": 5, "SenderNoTargetTextID": 9
},
"EmotesTextData": { "ID": 0, "Text": 1 },
"Light": {
"ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4,
"InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7,
"LightParamsIDRain": 8, "LightParamsIDUnderwater": 9
},
"LightParams": { "LightParamsID": 0 },
"LightIntBand": {
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
},
"LightFloatBand": {
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
},
"WorldMapArea": {
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
"DisplayMapID": 8, "ParentWorldMapID": 10
}
}

View file

@ -0,0 +1,11 @@
{
"id": "wotlk",
"name": "Wrath of the Lich King",
"shortName": "WotLK",
"version": { "major": 3, "minor": 3, "patch": 5 },
"build": 12340,
"protocolVersion": 8,
"maxLevel": 80,
"races": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
"classes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
}

View file

@ -0,0 +1,255 @@
{
"CMSG_PING": "0x1DC",
"CMSG_AUTH_SESSION": "0x1ED",
"CMSG_CHAR_CREATE": "0x036",
"CMSG_CHAR_ENUM": "0x037",
"CMSG_CHAR_DELETE": "0x038",
"CMSG_PLAYER_LOGIN": "0x03D",
"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",
"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",
"SMSG_UPDATE_OBJECT": "0x0A9",
"SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6",
"SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE",
"SMSG_DESTROY_OBJECT": "0x0AA",
"CMSG_MESSAGECHAT": "0x095",
"SMSG_MESSAGECHAT": "0x096",
"CMSG_WHO": "0x062",
"SMSG_WHO": "0x063",
"CMSG_REQUEST_PLAYED_TIME": "0x1CC",
"SMSG_PLAYED_TIME": "0x1CD",
"CMSG_QUERY_TIME": "0x1CE",
"SMSG_QUERY_TIME_RESPONSE": "0x1CF",
"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",
"CMSG_PLAYER_LOGOUT": "0x04A",
"CMSG_LOGOUT_REQUEST": "0x04B",
"CMSG_LOGOUT_CANCEL": "0x04E",
"SMSG_LOGOUT_RESPONSE": "0x04C",
"SMSG_LOGOUT_COMPLETE": "0x04D",
"CMSG_STAND_STATE_CHANGE": "0x101",
"CMSG_SHOWING_HELM": "0x2B9",
"CMSG_SHOWING_CLOAK": "0x2BA",
"CMSG_TOGGLE_PVP": "0x253",
"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",
"MSG_RAID_READY_CHECK": "0x322",
"MSG_RAID_READY_CHECK_CONFIRM": "0x3AE",
"CMSG_DUEL_PROPOSED": "0x166",
"CMSG_DUEL_ACCEPTED": "0x16C",
"CMSG_DUEL_CANCELLED": "0x16D",
"SMSG_DUEL_REQUESTED": "0x167",
"CMSG_INITIATE_TRADE": "0x116",
"MSG_RANDOM_ROLL": "0x1FB",
"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",
"SMSG_LOG_XPGAIN": "0x1D0",
"SMSG_MONSTER_MOVE": "0x0DD",
"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",
"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",
"SMSG_TALENTS_INFO": "0x4C0",
"CMSG_LEARN_TALENT": "0x251",
"MSG_TALENT_WIPE_CONFIRM": "0x2AB",
"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",
"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",
"CMSG_ACTIVATETAXI": "0x19D",
"CMSG_GOSSIP_HELLO": "0x17B",
"CMSG_GOSSIP_SELECT_OPTION": "0x17C",
"SMSG_GOSSIP_MESSAGE": "0x17D",
"SMSG_GOSSIP_COMPLETE": "0x17E",
"SMSG_NPC_TEXT_UPDATE": "0x180",
"CMSG_GAMEOBJECT_USE": "0x01B",
"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",
"SMSG_QUESTUPDATE_COMPLETE": "0x195",
"CMSG_QUEST_QUERY": "0x05C",
"SMSG_QUEST_QUERY_RESPONSE": "0x05D",
"SMSG_QUESTLOG_FULL": "0x1A3",
"CMSG_LIST_INVENTORY": "0x19E",
"SMSG_LIST_INVENTORY": "0x19F",
"CMSG_SELL_ITEM": "0x1A0",
"SMSG_SELL_ITEM": "0x1A1",
"CMSG_BUY_ITEM": "0x1A2",
"SMSG_BUY_FAILED": "0x1A5",
"CMSG_TRAINER_LIST": "0x01B0",
"SMSG_TRAINER_LIST": "0x01B1",
"CMSG_TRAINER_BUY_SPELL": "0x01B2",
"SMSG_TRAINER_BUY_FAILED": "0x01B4",
"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",
"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",
"MSG_MOVE_TELEPORT_ACK": "0x0C7",
"SMSG_TRANSFER_PENDING": "0x003F",
"SMSG_NEW_WORLD": "0x003E",
"MSG_MOVE_WORLDPORT_ACK": "0x00DC",
"SMSG_TRANSFER_ABORTED": "0x0040",
"SMSG_FORCE_RUN_SPEED_CHANGE": "0x00E2",
"CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x00E3",
"CMSG_CANCEL_MOUNT_AURA": "0x0375",
"SMSG_SHOWTAXINODES": "0x01A9",
"SMSG_ACTIVATETAXIREPLY": "0x01AE",
"SMSG_ACTIVATETAXIREPLY_ALT": "0x029D",
"SMSG_NEW_TAXI_PATH": "0x01AF",
"CMSG_ACTIVATETAXIEXPRESS": "0x0312",
"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",
"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"
}

View file

@ -0,0 +1,29 @@
{
"OBJECT_FIELD_ENTRY": 3,
"UNIT_FIELD_TARGET_LO": 6,
"UNIT_FIELD_TARGET_HI": 7,
"UNIT_FIELD_HEALTH": 24,
"UNIT_FIELD_POWER1": 25,
"UNIT_FIELD_MAXHEALTH": 32,
"UNIT_FIELD_MAXPOWER1": 33,
"UNIT_FIELD_LEVEL": 54,
"UNIT_FIELD_FACTIONTEMPLATE": 55,
"UNIT_FIELD_FLAGS": 59,
"UNIT_FIELD_FLAGS_2": 60,
"UNIT_FIELD_DISPLAYID": 67,
"UNIT_FIELD_MOUNTDISPLAYID": 69,
"UNIT_NPC_FLAGS": 82,
"UNIT_DYNAMIC_FLAGS": 147,
"UNIT_END": 148,
"PLAYER_FLAGS": 150,
"PLAYER_XP": 634,
"PLAYER_NEXT_LEVEL_XP": 635,
"PLAYER_FIELD_COINAGE": 1170,
"PLAYER_QUEST_LOG_START": 158,
"PLAYER_FIELD_INV_SLOT_HEAD": 324,
"PLAYER_FIELD_PACK_SLOT_1": 370,
"PLAYER_SKILL_INFO_START": 636,
"PLAYER_EXPLORED_ZONES_START": 1041,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14
}

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;

View file

@ -19,8 +19,8 @@ network::Packet LogonChallengePacket::build(const std::string& account, const Cl
network::Packet packet(static_cast<uint16_t>(AuthOpcode::LOGON_CHALLENGE));
// Protocol version (WoW 3.3.5a build 12340 uses protocol version 8)
packet.writeUInt8(0x08);
// Protocol version (e.g. 8 for WoW 3.3.5a build 12340)
packet.writeUInt8(info.protocolVersion);
// Payload size
packet.writeUInt16(payloadSize);

View file

@ -37,7 +37,11 @@
#include "game/game_handler.hpp"
#include "game/transport_manager.hpp"
#include "game/world.hpp"
#include "game/expansion_profile.hpp"
#include "game/packet_parsers.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include "pipeline/hd_pack_manager.hpp"
#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <chrono>
@ -48,6 +52,7 @@
#include <optional>
#include <sstream>
#include <set>
#include <filesystem>
namespace wowee {
namespace core {
@ -118,6 +123,15 @@ bool Application::initialize() {
gameHandler = std::make_unique<game::GameHandler>();
world = std::make_unique<game::World>();
// Create and initialize expansion registry
expansionRegistry_ = std::make_unique<game::ExpansionRegistry>();
// Create DBC layout
dbcLayout_ = std::make_unique<pipeline::DBCLayout>();
// Create HD pack manager
hdPackManager_ = std::make_unique<pipeline::HDPackManager>();
// Create asset manager
assetManager = std::make_unique<pipeline::AssetManager>();
@ -125,8 +139,56 @@ bool Application::initialize() {
const char* dataPathEnv = std::getenv("WOW_DATA_PATH");
std::string dataPath = dataPathEnv ? dataPathEnv : "./Data";
LOG_INFO("Attempting to load WoW assets from: ", dataPath);
if (assetManager->initialize(dataPath)) {
// Scan for available expansion profiles
expansionRegistry_->initialize(dataPath);
// Load expansion-specific opcode table
if (gameHandler && expansionRegistry_) {
auto* profile = expansionRegistry_->getActive();
if (profile) {
std::string opcodesPath = profile->dataPath + "/opcodes.json";
if (!gameHandler->getOpcodeTable().loadFromJson(opcodesPath)) {
LOG_INFO("Using built-in WotLK opcode defaults");
}
game::setActiveOpcodeTable(&gameHandler->getOpcodeTable());
// Load expansion-specific update field table
std::string updateFieldsPath = profile->dataPath + "/update_fields.json";
if (!gameHandler->getUpdateFieldTable().loadFromJson(updateFieldsPath)) {
LOG_INFO("Using built-in WotLK update field defaults");
}
game::setActiveUpdateFieldTable(&gameHandler->getUpdateFieldTable());
// Create expansion-specific packet parsers
gameHandler->setPacketParsers(game::createPacketParsers(profile->id));
// Load expansion-specific DBC layouts
if (dbcLayout_) {
std::string dbcLayoutsPath = profile->dataPath + "/dbc_layouts.json";
if (!dbcLayout_->loadFromJson(dbcLayoutsPath)) {
dbcLayout_->loadWotlkDefaults();
LOG_INFO("Using built-in WotLK DBC layout defaults");
}
pipeline::setActiveDBCLayout(dbcLayout_.get());
}
}
}
// Try expansion-specific asset path first, fall back to base Data/
std::string assetPath = dataPath;
if (expansionRegistry_) {
auto* profile = expansionRegistry_->getActive();
if (profile && !profile->dataPath.empty()) {
std::string expansionManifest = profile->dataPath + "/manifest.json";
if (std::filesystem::exists(expansionManifest)) {
assetPath = profile->dataPath;
LOG_INFO("Using expansion-specific asset path: ", assetPath);
}
}
}
LOG_INFO("Attempting to load WoW assets from: ", assetPath);
if (assetManager->initialize(assetPath)) {
LOG_INFO("Asset manager initialized successfully");
// Eagerly load creature display DBC lookups so first spawn doesn't stall
buildCreatureDisplayLookups();
@ -142,6 +204,28 @@ bool Application::initialize() {
if (gameHandler && gameHandler->getTransportManager()) {
gameHandler->getTransportManager()->loadTransportAnimationDBC(assetManager.get());
}
// Initialize HD texture packs
if (hdPackManager_) {
std::string hdPath = dataPath + "/hd";
std::string settingsDir;
const char* xdg = std::getenv("XDG_DATA_HOME");
if (xdg && *xdg) {
settingsDir = std::string(xdg) + "/wowee";
} else {
const char* home = std::getenv("HOME");
settingsDir = std::string(home ? home : ".") + "/.local/share/wowee";
}
hdPackManager_->loadSettings(settingsDir + "/settings.cfg");
hdPackManager_->initialize(hdPath);
// Apply enabled packs as overlays
std::string expansionId = "wotlk";
if (expansionRegistry_ && expansionRegistry_->getActive()) {
expansionId = expansionRegistry_->getActive()->id;
}
hdPackManager_->applyToAssetManager(assetManager.get(), expansionId);
}
} else {
LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable");
LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory");
@ -805,7 +889,12 @@ void Application::setupUICallbacks() {
accountName = "TESTACCOUNT";
}
if (gameHandler->connect(host, port, sessionKey, accountName)) {
uint32_t clientBuild = 12340; // default WotLK
if (expansionRegistry_) {
auto* profile = expansionRegistry_->getActive();
if (profile) clientBuild = profile->build;
}
if (gameHandler->connect(host, port, sessionKey, accountName, clientBuild)) {
LOG_INFO("Connected to world server, transitioning to character selection");
setState(AppState::CHARACTER_SELECTION);
} else {
@ -1643,22 +1732,24 @@ void Application::spawnPlayerCharacter() {
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
if (charSectionsDbc) {
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
bool foundSkin = false;
bool foundUnderwear = false;
bool foundFaceLower = false;
bool foundHair = false;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0 = skin: match by colorIndex = skin byte
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4;
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex1 = charSectionsDbc->getString(r, csTex1);
if (!tex1.empty()) {
bodySkinPath = tex1;
foundSkin = true;
@ -1668,7 +1759,7 @@ void Application::spawnPlayerCharacter() {
// Section 3 = hair: match variation=hairStyle, color=hairColor
else if (baseSection == 3 && !foundHair &&
variationIndex == charHairStyleId && colorIndex == charHairColorId) {
hairTexturePath = charSectionsDbc->getString(r, 4);
hairTexturePath = charSectionsDbc->getString(r, csTex1);
if (!hairTexturePath.empty()) {
foundHair = true;
LOG_INFO(" DBC hair texture: ", hairTexturePath,
@ -1678,7 +1769,7 @@ void Application::spawnPlayerCharacter() {
// Section 1 = face lower: match variation=faceId
else if (baseSection == 1 && !foundFaceLower &&
variationIndex == charFaceId && colorIndex == charSkinId) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex1 = charSectionsDbc->getString(r, csTex1);
if (!tex1.empty()) {
faceLowerTexturePath = tex1;
foundFaceLower = true;
@ -1687,7 +1778,7 @@ void Application::spawnPlayerCharacter() {
}
// Section 4 = underwear
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
for (int f = 4; f <= 6; f++) {
for (uint32_t f = csTex1; f <= csTex1 + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) {
underwearPaths.push_back(tex);
@ -1988,10 +2079,9 @@ void Application::loadEquippedWeapons() {
continue;
}
// DBC field 1 = modelName_1 (e.g. "Sword_1H_Short_A_02.mdx")
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 1);
// DBC field 3 = modelTexture_1 (e.g. "Sword_1H_Short_A_02Rusty")
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModel"] : 1);
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
if (modelName.empty()) {
LOG_WARNING("loadEquippedWeapons: empty model name for displayInfoId ", displayInfoId);
@ -2099,14 +2189,19 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
}
// Build set of hostile parent faction IDs from Faction.dbc base reputation
const auto* facL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
const auto* ftL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("FactionTemplate") : nullptr;
std::unordered_set<uint32_t> hostileParentFactions;
if (fDbc && fDbc->isLoaded()) {
const uint32_t facID = facL ? (*facL)["ID"] : 0;
const uint32_t facRaceMask0 = facL ? (*facL)["ReputationRaceMask0"] : 2;
const uint32_t facBase0 = facL ? (*facL)["ReputationBase0"] : 10;
for (uint32_t i = 0; i < fDbc->getRecordCount(); i++) {
uint32_t factionId = fDbc->getUInt32(i, 0);
uint32_t factionId = fDbc->getUInt32(i, facID);
for (int slot = 0; slot < 4; slot++) {
uint32_t raceMask = fDbc->getUInt32(i, 2 + slot); // ReputationRaceMask[4] at fields 2-5
uint32_t raceMask = fDbc->getUInt32(i, facRaceMask0 + slot);
if (raceMask & playerRaceMask) {
int32_t baseRep = fDbc->getInt32(i, 10 + slot); // ReputationBase[4] at fields 10-13
int32_t baseRep = fDbc->getInt32(i, facBase0 + slot);
if (baseRep < 0) {
hostileParentFactions.insert(factionId);
}
@ -2118,14 +2213,20 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
}
// Get player faction template data
const uint32_t ftID = ftL ? (*ftL)["ID"] : 0;
const uint32_t ftFaction = ftL ? (*ftL)["Faction"] : 1;
const uint32_t ftFG = ftL ? (*ftL)["FactionGroup"] : 3;
const uint32_t ftFriend = ftL ? (*ftL)["FriendGroup"] : 4;
const uint32_t ftEnemy = ftL ? (*ftL)["EnemyGroup"] : 5;
const uint32_t ftEnemy0 = ftL ? (*ftL)["Enemy0"] : 6;
uint32_t playerFriendGroup = 0;
uint32_t playerEnemyGroup = 0;
uint32_t playerFactionId = 0;
for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) {
if (ftDbc->getUInt32(i, 0) == playerFtId) {
playerFriendGroup = ftDbc->getUInt32(i, 4) | ftDbc->getUInt32(i, 3);
playerEnemyGroup = ftDbc->getUInt32(i, 5);
playerFactionId = ftDbc->getUInt32(i, 1);
if (ftDbc->getUInt32(i, ftID) == playerFtId) {
playerFriendGroup = ftDbc->getUInt32(i, ftFriend) | ftDbc->getUInt32(i, ftFG);
playerEnemyGroup = ftDbc->getUInt32(i, ftEnemy);
playerFactionId = ftDbc->getUInt32(i, ftFaction);
break;
}
}
@ -2133,11 +2234,11 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
// Build hostility map for each faction template
std::unordered_map<uint32_t, bool> factionMap;
for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) {
uint32_t id = ftDbc->getUInt32(i, 0);
uint32_t parentFaction = ftDbc->getUInt32(i, 1);
uint32_t factionGroup = ftDbc->getUInt32(i, 3);
uint32_t friendGroup = ftDbc->getUInt32(i, 4);
uint32_t enemyGroup = ftDbc->getUInt32(i, 5);
uint32_t id = ftDbc->getUInt32(i, ftID);
uint32_t parentFaction = ftDbc->getUInt32(i, ftFaction);
uint32_t factionGroup = ftDbc->getUInt32(i, ftFG);
uint32_t friendGroup = ftDbc->getUInt32(i, ftFriend);
uint32_t enemyGroup = ftDbc->getUInt32(i, ftEnemy);
// 1. Symmetric group check
bool hostile = (enemyGroup & playerFriendGroup) != 0
@ -2148,9 +2249,9 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
hostile = true;
}
// 3. Individual enemy faction IDs (fields 6-9)
// 3. Individual enemy faction IDs
if (!hostile && playerFactionId > 0) {
for (int e = 6; e <= 9; e++) {
for (uint32_t e = ftEnemy0; e <= ftEnemy0 + 3; e++) {
if (ftDbc->getUInt32(i, e) == playerFactionId) {
hostile = true;
break;
@ -2227,9 +2328,10 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
mapNameCacheLoaded = true;
if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) {
mapNameById.reserve(mapDbc->getRecordCount());
const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr;
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
uint32_t id = mapDbc->getUInt32(i, 0);
std::string internalName = mapDbc->getString(i, 1);
uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0);
std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
if (!internalName.empty()) {
mapNameById[id] = std::move(internalName);
}
@ -2509,14 +2611,15 @@ void Application::buildCreatureDisplayLookups() {
// Col 7: Skin2
// Col 8: Skin3
if (auto cdi = assetManager->loadDBC("CreatureDisplayInfo.dbc"); cdi && cdi->isLoaded()) {
const auto* cdiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfo") : nullptr;
for (uint32_t i = 0; i < cdi->getRecordCount(); i++) {
CreatureDisplayData data;
data.modelId = cdi->getUInt32(i, 1);
data.extraDisplayId = cdi->getUInt32(i, 3);
data.skin1 = cdi->getString(i, 6);
data.skin2 = cdi->getString(i, 7);
data.skin3 = cdi->getString(i, 8);
displayDataMap_[cdi->getUInt32(i, 0)] = data;
data.modelId = cdi->getUInt32(i, cdiL ? (*cdiL)["ModelID"] : 1);
data.extraDisplayId = cdi->getUInt32(i, cdiL ? (*cdiL)["ExtraDisplayId"] : 3);
data.skin1 = cdi->getString(i, cdiL ? (*cdiL)["Skin1"] : 6);
data.skin2 = cdi->getString(i, cdiL ? (*cdiL)["Skin2"] : 7);
data.skin3 = cdi->getString(i, cdiL ? (*cdiL)["Skin3"] : 8);
displayDataMap_[cdi->getUInt32(i, cdiL ? (*cdiL)["ID"] : 0)] = data;
}
LOG_INFO("Loaded ", displayDataMap_.size(), " display→model mappings");
}
@ -2534,37 +2637,38 @@ void Application::buildCreatureDisplayLookups() {
// Col 19: Flags
// Col 20: BakeName (pre-baked texture path)
if (auto cdie = assetManager->loadDBC("CreatureDisplayInfoExtra.dbc"); cdie && cdie->isLoaded()) {
const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr;
const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8;
uint32_t withBakeName = 0;
for (uint32_t i = 0; i < cdie->getRecordCount(); i++) {
HumanoidDisplayExtra extra;
extra.raceId = static_cast<uint8_t>(cdie->getUInt32(i, 1));
extra.sexId = static_cast<uint8_t>(cdie->getUInt32(i, 2));
extra.skinId = static_cast<uint8_t>(cdie->getUInt32(i, 3));
extra.faceId = static_cast<uint8_t>(cdie->getUInt32(i, 4));
extra.hairStyleId = static_cast<uint8_t>(cdie->getUInt32(i, 5));
extra.hairColorId = static_cast<uint8_t>(cdie->getUInt32(i, 6));
extra.facialHairId = static_cast<uint8_t>(cdie->getUInt32(i, 7));
// Equipment display IDs (columns 8-18)
extra.raceId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["RaceID"] : 1));
extra.sexId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["SexID"] : 2));
extra.skinId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["SkinID"] : 3));
extra.faceId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["FaceID"] : 4));
extra.hairStyleId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["HairStyleID"] : 5));
extra.hairColorId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["HairColorID"] : 6));
extra.facialHairId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["FacialHairID"] : 7));
for (int eq = 0; eq < 11; eq++) {
extra.equipDisplayId[eq] = cdie->getUInt32(i, 8 + eq);
extra.equipDisplayId[eq] = cdie->getUInt32(i, cdieEquip0 + eq);
}
extra.bakeName = cdie->getString(i, 20);
extra.bakeName = cdie->getString(i, cdieL ? (*cdieL)["BakeName"] : 20);
if (!extra.bakeName.empty()) withBakeName++;
humanoidExtraMap_[cdie->getUInt32(i, 0)] = extra;
humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra;
}
LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (", withBakeName, " with baked textures)");
}
// CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2)
if (auto cmd = assetManager->loadDBC("CreatureModelData.dbc"); cmd && cmd->isLoaded()) {
const auto* cmdL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureModelData") : nullptr;
for (uint32_t i = 0; i < cmd->getRecordCount(); i++) {
std::string mdx = cmd->getString(i, 2);
std::string mdx = cmd->getString(i, cmdL ? (*cmdL)["ModelPath"] : 2);
if (mdx.empty()) continue;
// Convert .mdx to .m2
if (mdx.size() >= 4) {
mdx = mdx.substr(0, mdx.size() - 4) + ".m2";
}
modelIdToPath_[cmd->getUInt32(i, 0)] = mdx;
modelIdToPath_[cmd->getUInt32(i, cmdL ? (*cmdL)["ID"] : 0)] = mdx;
}
LOG_INFO("Loaded ", modelIdToPath_.size(), " model→path mappings");
}
@ -2612,11 +2716,12 @@ void Application::buildCreatureDisplayLookups() {
// CharHairGeosets.dbc: maps (race, sex, hairStyleId) → skinSectionId for hair mesh
// Col 0: ID, Col 1: RaceID, Col 2: SexID, Col 3: VariationID, Col 4: GeosetID, Col 5: Showscalp
if (auto chg = assetManager->loadDBC("CharHairGeosets.dbc"); chg && chg->isLoaded()) {
const auto* chgL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharHairGeosets") : nullptr;
for (uint32_t i = 0; i < chg->getRecordCount(); i++) {
uint32_t raceId = chg->getUInt32(i, 1);
uint32_t sexId = chg->getUInt32(i, 2);
uint32_t variation = chg->getUInt32(i, 3);
uint32_t geosetId = chg->getUInt32(i, 4);
uint32_t raceId = chg->getUInt32(i, chgL ? (*chgL)["RaceID"] : 1);
uint32_t sexId = chg->getUInt32(i, chgL ? (*chgL)["SexID"] : 2);
uint32_t variation = chg->getUInt32(i, chgL ? (*chgL)["Variation"] : 3);
uint32_t geosetId = chg->getUInt32(i, chgL ? (*chgL)["GeosetID"] : 4);
uint32_t key = (raceId << 16) | (sexId << 8) | variation;
hairGeosetMap_[key] = static_cast<uint16_t>(geosetId);
}
@ -2634,15 +2739,16 @@ void Application::buildCreatureDisplayLookups() {
// No ID column: Col 0: RaceID, Col 1: SexID, Col 2: VariationID
// Col 3: Geoset100, Col 4: Geoset300, Col 5: Geoset200
if (auto cfh = assetManager->loadDBC("CharacterFacialHairStyles.dbc"); cfh && cfh->isLoaded()) {
const auto* cfhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
for (uint32_t i = 0; i < cfh->getRecordCount(); i++) {
uint32_t raceId = cfh->getUInt32(i, 0);
uint32_t sexId = cfh->getUInt32(i, 1);
uint32_t variation = cfh->getUInt32(i, 2);
uint32_t raceId = cfh->getUInt32(i, cfhL ? (*cfhL)["RaceID"] : 0);
uint32_t sexId = cfh->getUInt32(i, cfhL ? (*cfhL)["SexID"] : 1);
uint32_t variation = cfh->getUInt32(i, cfhL ? (*cfhL)["Variation"] : 2);
uint32_t key = (raceId << 16) | (sexId << 8) | variation;
FacialHairGeosets fhg;
fhg.geoset100 = static_cast<uint16_t>(cfh->getUInt32(i, 3));
fhg.geoset300 = static_cast<uint16_t>(cfh->getUInt32(i, 4));
fhg.geoset200 = static_cast<uint16_t>(cfh->getUInt32(i, 5));
fhg.geoset100 = static_cast<uint16_t>(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset100"] : 3));
fhg.geoset300 = static_cast<uint16_t>(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset300"] : 4));
fhg.geoset200 = static_cast<uint16_t>(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset200"] : 5));
facialHairGeosetMap_[key] = fhg;
}
LOG_INFO("Loaded ", facialHairGeosetMap_.size(), " facial hair geoset mappings from CharacterFacialHairStyles.dbc");
@ -2722,9 +2828,10 @@ void Application::buildGameObjectDisplayLookups() {
// Col 0: ID (displayId)
// Col 1: ModelName
if (auto godi = assetManager->loadDBC("GameObjectDisplayInfo.dbc"); godi && godi->isLoaded()) {
const auto* godiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("GameObjectDisplayInfo") : nullptr;
for (uint32_t i = 0; i < godi->getRecordCount(); i++) {
uint32_t displayId = godi->getUInt32(i, 0);
std::string modelName = godi->getString(i, 1);
uint32_t displayId = godi->getUInt32(i, godiL ? (*godiL)["ID"] : 0);
std::string modelName = godi->getString(i, godiL ? (*godiL)["ModelName"] : 1);
if (modelName.empty()) continue;
if (modelName.size() >= 4) {
std::string ext = modelName.substr(modelName.size() - 4);
@ -2913,23 +3020,24 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Load hair texture from CharSections.dbc (section 3)
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
if (charSectionsDbc) {
const auto* csL2 = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
uint32_t targetRace = static_cast<uint32_t>(extra.raceId);
uint32_t targetSex = static_cast<uint32_t>(extra.sexId);
std::string hairTexPath;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
uint32_t section = charSectionsDbc->getUInt32(r, 3);
uint32_t variation = charSectionsDbc->getUInt32(r, 8);
uint32_t colorIdx = charSectionsDbc->getUInt32(r, 9);
uint32_t raceId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2);
uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3);
uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 8);
uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 9);
if (raceId != targetRace || sexId != targetSex) continue;
if (section != 3) continue; // Section 3 = hair
if (variation != static_cast<uint32_t>(extra.hairStyleId)) continue;
if (colorIdx != static_cast<uint32_t>(extra.hairColorId)) continue;
hairTexPath = charSectionsDbc->getString(r, 4);
hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 4);
break;
}
@ -3058,8 +3166,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Load equipment geosets from ItemDisplayInfo.dbc
// DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2]
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
if (itemDisplayDbc) {
// Equipment slots: 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
const uint32_t fGG3 = idiL ? (*idiL)["GeosetGroup3"] : 9;
// Helm (slot 0) - noted for helmet model attachment below
@ -3067,10 +3178,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (extra.equipDisplayId[3] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetChest = static_cast<uint16_t>(501 + gg);
// Robes: GeosetGroup[2] > 0 shows kilt legs
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 9);
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG3);
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
}
}
@ -3079,7 +3190,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (extra.equipDisplayId[5] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
}
}
@ -3088,7 +3199,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (extra.equipDisplayId[6] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetBoots = static_cast<uint16_t>(401 + gg);
}
}
@ -3097,7 +3208,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (extra.equipDisplayId[8] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetGloves = static_cast<uint16_t>(301 + gg);
}
}
@ -3162,8 +3273,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
if (helmIdx >= 0) {
// Get helmet model name from ItemDisplayInfo.dbc (col 1 = LeftModel)
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 1);
// Get helmet model name from ItemDisplayInfo.dbc (LeftModel)
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL ? (*idiL)["LeftModel"] : 1);
if (!helmModelName.empty()) {
// Convert .mdx to .m2
size_t dotPos = helmModelName.rfind('.');
@ -3209,8 +3320,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (helmModel.isValid()) {
// Attachment point 11 = Head
uint32_t helmModelId = nextCreatureModelId_++;
// Get texture from ItemDisplayInfo (col 3 = LeftModelTexture)
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 3);
// Get texture from ItemDisplayInfo (LeftModelTexture)
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
std::string helmTexPath;
if (!helmTexName.empty()) {
// Try race/gender suffixed texture first
@ -3245,10 +3356,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
const auto& extra = itExtra->second;
if (extra.equipDisplayId[0] != 0) { // Helm slot
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
const auto* idiL2 = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
if (itemDisplayDbc) {
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
if (helmIdx >= 0) {
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 1);
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL2 ? (*idiL2)["LeftModel"] : 1);
if (!helmModelName.empty()) {
size_t dotPos = helmModelName.rfind('.');
if (dotPos != std::string::npos) {
@ -3287,7 +3399,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (helmModel.isValid()) {
uint32_t helmModelId = nextCreatureModelId_++;
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 3);
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL2 ? (*idiL2)["LeftModelTexture"] : 3);
std::string helmTexPath;
if (!helmTexName.empty()) {
if (!raceSuffix.empty()) {

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;
}

View file

@ -103,9 +103,77 @@ void AssetManager::shutdown() {
}
clearCache();
overlayLayers_.clear();
initialized = false;
}
bool AssetManager::addOverlayManifest(const std::string& manifestPath, int priority, const std::string& id) {
// Check for duplicate
for (const auto& layer : overlayLayers_) {
if (layer.id == id) {
LOG_WARNING("Overlay '", id, "' already loaded, skipping");
return false;
}
}
ManifestLayer layer;
layer.priority = priority;
layer.id = id;
if (!layer.manifest.load(manifestPath)) {
LOG_ERROR("Failed to load overlay manifest: ", manifestPath);
return false;
}
overlayLayers_.push_back(std::move(layer));
// Sort by priority descending (highest priority first)
std::sort(overlayLayers_.begin(), overlayLayers_.end(),
[](const ManifestLayer& a, const ManifestLayer& b) {
return a.priority > b.priority;
});
LOG_INFO("Added overlay '", id, "' (priority ", priority, ", ",
overlayLayers_.back().manifest.getEntryCount(), " files) from ", manifestPath);
return true;
}
void AssetManager::removeOverlay(const std::string& id) {
auto it = std::remove_if(overlayLayers_.begin(), overlayLayers_.end(),
[&id](const ManifestLayer& layer) { return layer.id == id; });
if (it != overlayLayers_.end()) {
overlayLayers_.erase(it, overlayLayers_.end());
// Clear file cache since overlay removal changes file resolution
{
std::lock_guard<std::mutex> lock(cacheMutex);
fileCache.clear();
fileCacheTotalBytes = 0;
}
LOG_INFO("Removed overlay '", id, "', file cache cleared");
}
}
std::vector<std::string> AssetManager::getOverlayIds() const {
std::vector<std::string> ids;
ids.reserve(overlayLayers_.size());
for (const auto& layer : overlayLayers_) {
ids.push_back(layer.id);
}
return ids;
}
std::string AssetManager::resolveLayeredPath(const std::string& normalizedPath) const {
// Check overlay manifests first (sorted by priority desc)
for (const auto& layer : overlayLayers_) {
std::string fsPath = layer.manifest.resolveFilesystemPath(normalizedPath);
if (!fsPath.empty()) {
return fsPath;
}
}
// Fall back to base manifest
return manifest_.resolveFilesystemPath(normalizedPath);
}
BLPImage AssetManager::loadTexture(const std::string& path) {
if (!initialized) {
LOG_ERROR("AssetManager not initialized");
@ -144,7 +212,7 @@ BLPImage AssetManager::tryLoadPngOverride(const std::string& normalizedPath) con
std::string ext = normalizedPath.substr(normalizedPath.size() - 4);
if (ext != ".blp") return BLPImage();
std::string fsPath = manifest_.resolveFilesystemPath(normalizedPath);
std::string fsPath = resolveLayeredPath(normalizedPath);
if (fsPath.empty()) return BLPImage();
// Replace .blp/.BLP extension with .png
@ -219,7 +287,14 @@ bool AssetManager::fileExists(const std::string& path) const {
if (!initialized) {
return false;
}
return manifest_.hasEntry(normalizePath(path));
std::string normalized = normalizePath(path);
// Check overlay manifests first
for (const auto& layer : overlayLayers_) {
if (layer.manifest.hasEntry(normalized)) {
return true;
}
}
return manifest_.hasEntry(normalized);
}
std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
@ -240,8 +315,8 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
}
}
// Read from filesystem (fully parallel, no serialization needed)
std::string fsPath = manifest_.resolveFilesystemPath(normalized);
// Read from filesystem using layered resolution (overlays first, then base)
std::string fsPath = resolveLayeredPath(normalized);
if (fsPath.empty()) {
return {};
}

226
src/pipeline/dbc_layout.cpp Normal file
View file

@ -0,0 +1,226 @@
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
namespace wowee {
namespace pipeline {
static const DBCLayout* g_activeDBCLayout = nullptr;
void setActiveDBCLayout(const DBCLayout* layout) { g_activeDBCLayout = layout; }
const DBCLayout* getActiveDBCLayout() { return g_activeDBCLayout; }
void DBCLayout::loadWotlkDefaults() {
layouts_.clear();
// Spell.dbc
layouts_["Spell"] = {{{ "ID", 0 }, { "Attributes", 4 }, { "IconID", 133 },
{ "Name", 136 }, { "Tooltip", 139 }, { "Rank", 153 }}};
// ItemDisplayInfo.dbc
layouts_["ItemDisplayInfo"] = {{{ "ID", 0 }, { "LeftModel", 1 }, { "LeftModelTexture", 3 },
{ "InventoryIcon", 5 }, { "GeosetGroup1", 7 }, { "GeosetGroup3", 9 }}};
// CharSections.dbc
layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 },
{ "Texture1", 4 }, { "Texture2", 5 }, { "Texture3", 6 },
{ "VariationIndex", 8 }, { "ColorIndex", 9 }}};
// SpellIcon.dbc (Icon.dbc in code but actually SpellIcon)
layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}};
// FactionTemplate.dbc
layouts_["FactionTemplate"] = {{{ "ID", 0 }, { "Faction", 1 }, { "FactionGroup", 3 },
{ "FriendGroup", 4 }, { "EnemyGroup", 5 },
{ "Enemy0", 6 }, { "Enemy1", 7 }, { "Enemy2", 8 }, { "Enemy3", 9 }}};
// Faction.dbc
layouts_["Faction"] = {{{ "ID", 0 }, { "ReputationRaceMask0", 2 }, { "ReputationRaceMask1", 3 },
{ "ReputationRaceMask2", 4 }, { "ReputationRaceMask3", 5 },
{ "ReputationBase0", 10 }, { "ReputationBase1", 11 },
{ "ReputationBase2", 12 }, { "ReputationBase3", 13 }}};
// AreaTable.dbc
layouts_["AreaTable"] = {{{ "ID", 0 }, { "ExploreFlag", 3 }}};
// CreatureDisplayInfoExtra.dbc
layouts_["CreatureDisplayInfoExtra"] = {{{ "ID", 0 }, { "RaceID", 1 }, { "SexID", 2 },
{ "SkinID", 3 }, { "FaceID", 4 }, { "HairStyleID", 5 }, { "HairColorID", 6 },
{ "FacialHairID", 7 }, { "EquipDisplay0", 8 }, { "EquipDisplay1", 9 },
{ "EquipDisplay2", 10 }, { "EquipDisplay3", 11 }, { "EquipDisplay4", 12 },
{ "EquipDisplay5", 13 }, { "EquipDisplay6", 14 }, { "EquipDisplay7", 15 },
{ "EquipDisplay8", 16 }, { "EquipDisplay9", 17 }, { "EquipDisplay10", 18 },
{ "BakeName", 20 }}};
// CreatureDisplayInfo.dbc
layouts_["CreatureDisplayInfo"] = {{{ "ID", 0 }, { "ModelID", 1 }, { "ExtraDisplayId", 3 },
{ "Skin1", 6 }, { "Skin2", 7 }, { "Skin3", 8 }}};
// TaxiNodes.dbc
layouts_["TaxiNodes"] = {{{ "ID", 0 }, { "MapID", 1 }, { "X", 2 }, { "Y", 3 }, { "Z", 4 },
{ "Name", 5 }, { "MountDisplayIdAllianceFallback", 20 },
{ "MountDisplayIdHordeFallback", 21 },
{ "MountDisplayIdAlliance", 22 }, { "MountDisplayIdHorde", 23 }}};
// TaxiPath.dbc
layouts_["TaxiPath"] = {{{ "ID", 0 }, { "FromNode", 1 }, { "ToNode", 2 }, { "Cost", 3 }}};
// TaxiPathNode.dbc
layouts_["TaxiPathNode"] = {{{ "ID", 0 }, { "PathID", 1 }, { "NodeIndex", 2 },
{ "MapID", 3 }, { "X", 4 }, { "Y", 5 }, { "Z", 6 }}};
// TalentTab.dbc
layouts_["TalentTab"] = {{{ "ID", 0 }, { "Name", 1 }, { "ClassMask", 20 },
{ "OrderIndex", 22 }, { "BackgroundFile", 23 }}};
// Talent.dbc
layouts_["Talent"] = {{{ "ID", 0 }, { "TabID", 1 }, { "Row", 2 }, { "Column", 3 },
{ "RankSpell0", 4 }, { "PrereqTalent0", 9 }, { "PrereqRank0", 12 }}};
// SkillLineAbility.dbc
layouts_["SkillLineAbility"] = {{{ "SkillLineID", 1 }, { "SpellID", 2 }}};
// SkillLine.dbc
layouts_["SkillLine"] = {{{ "ID", 0 }, { "Category", 1 }, { "Name", 3 }}};
// Map.dbc
layouts_["Map"] = {{{ "ID", 0 }, { "InternalName", 1 }}};
// CreatureModelData.dbc
layouts_["CreatureModelData"] = {{{ "ID", 0 }, { "ModelPath", 2 }}};
// CharHairGeosets.dbc
layouts_["CharHairGeosets"] = {{{ "RaceID", 1 }, { "SexID", 2 },
{ "Variation", 3 }, { "GeosetID", 4 }}};
// CharacterFacialHairStyles.dbc
layouts_["CharacterFacialHairStyles"] = {{{ "RaceID", 0 }, { "SexID", 1 },
{ "Variation", 2 }, { "Geoset100", 3 }, { "Geoset300", 4 }, { "Geoset200", 5 }}};
// GameObjectDisplayInfo.dbc
layouts_["GameObjectDisplayInfo"] = {{{ "ID", 0 }, { "ModelName", 1 }}};
// Emotes.dbc
layouts_["Emotes"] = {{{ "ID", 0 }, { "AnimID", 2 }}};
// EmotesText.dbc
layouts_["EmotesText"] = {{{ "Command", 1 }, { "EmoteRef", 2 },
{ "SenderTargetTextID", 5 }, { "SenderNoTargetTextID", 9 }}};
// EmotesTextData.dbc
layouts_["EmotesTextData"] = {{{ "ID", 0 }, { "Text", 1 }}};
// Light.dbc
layouts_["Light"] = {{{ "ID", 0 }, { "MapID", 1 }, { "X", 2 }, { "Z", 3 }, { "Y", 4 },
{ "InnerRadius", 5 }, { "OuterRadius", 6 }, { "LightParamsID", 7 },
{ "LightParamsIDRain", 8 }, { "LightParamsIDUnderwater", 9 }}};
// LightParams.dbc
layouts_["LightParams"] = {{{ "LightParamsID", 0 }}};
// LightParamsBands.dbc (custom split from LightIntBand/LightFloatBand)
layouts_["LightParamsBands"] = {{{ "BlockIndex", 1 }, { "NumKeyframes", 2 },
{ "TimeKey0", 3 }, { "Value0", 19 }}};
// LightIntBand.dbc (same structure as LightParamsBands)
layouts_["LightIntBand"] = {{{ "BlockIndex", 1 }, { "NumKeyframes", 2 },
{ "TimeKey0", 3 }, { "Value0", 19 }}};
// LightFloatBand.dbc
layouts_["LightFloatBand"] = {{{ "BlockIndex", 1 }, { "NumKeyframes", 2 },
{ "TimeKey0", 3 }, { "Value0", 19 }}};
// WorldMapArea.dbc
layouts_["WorldMapArea"] = {{{ "ID", 0 }, { "MapID", 1 }, { "AreaID", 2 },
{ "AreaName", 3 }, { "LocLeft", 4 }, { "LocRight", 5 }, { "LocTop", 6 },
{ "LocBottom", 7 }, { "DisplayMapID", 8 }, { "ParentWorldMapID", 10 }}};
LOG_INFO("DBCLayout: loaded ", layouts_.size(), " WotLK default layouts");
}
bool DBCLayout::loadFromJson(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) {
LOG_WARNING("DBCLayout: cannot open ", path);
return false;
}
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
layouts_.clear();
size_t loaded = 0;
size_t pos = 0;
// Parse top-level object: { "DbcName": { "FieldName": index, ... }, ... }
// Find the first '{'
pos = json.find('{', pos);
if (pos == std::string::npos) return false;
++pos;
while (pos < json.size()) {
// Find DBC name key
size_t dbcKeyStart = json.find('"', pos);
if (dbcKeyStart == std::string::npos) break;
size_t dbcKeyEnd = json.find('"', dbcKeyStart + 1);
if (dbcKeyEnd == std::string::npos) break;
std::string dbcName = json.substr(dbcKeyStart + 1, dbcKeyEnd - dbcKeyStart - 1);
// Find the nested object '{'
size_t objStart = json.find('{', dbcKeyEnd);
if (objStart == std::string::npos) break;
// Find the matching '}'
size_t objEnd = json.find('}', objStart);
if (objEnd == std::string::npos) break;
// Parse the inner object
std::string inner = json.substr(objStart + 1, objEnd - objStart - 1);
DBCFieldMap fieldMap;
size_t ipos = 0;
while (ipos < inner.size()) {
size_t fkStart = inner.find('"', ipos);
if (fkStart == std::string::npos) break;
size_t fkEnd = inner.find('"', fkStart + 1);
if (fkEnd == std::string::npos) break;
std::string fieldName = inner.substr(fkStart + 1, fkEnd - fkStart - 1);
size_t colon = inner.find(':', fkEnd);
if (colon == std::string::npos) break;
size_t valStart = colon + 1;
while (valStart < inner.size() && (inner[valStart] == ' ' || inner[valStart] == '\t' ||
inner[valStart] == '\r' || inner[valStart] == '\n'))
++valStart;
size_t valEnd = inner.find_first_of(",}\r\n", valStart);
if (valEnd == std::string::npos) valEnd = inner.size();
std::string valStr = inner.substr(valStart, valEnd - valStart);
while (!valStr.empty() && (valStr.back() == ' ' || valStr.back() == '\t'))
valStr.pop_back();
try {
uint32_t idx = static_cast<uint32_t>(std::stoul(valStr));
fieldMap.fields[fieldName] = idx;
} catch (...) {}
ipos = valEnd + 1;
}
if (!fieldMap.fields.empty()) {
layouts_[dbcName] = std::move(fieldMap);
++loaded;
}
pos = objEnd + 1;
}
LOG_INFO("DBCLayout: loaded ", loaded, " layouts from ", path);
return loaded > 0;
}
const DBCFieldMap* DBCLayout::getLayout(const std::string& dbcName) const {
auto it = layouts_.find(dbcName);
return (it != layouts_.end()) ? &it->second : nullptr;
}
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,204 @@
#include "pipeline/hd_pack_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <algorithm>
namespace wowee {
namespace pipeline {
namespace {
// Minimal JSON string value parser (key must be unique in the flat object)
std::string jsonStringValue(const std::string& json, const std::string& key) {
std::string needle = "\"" + key + "\"";
size_t pos = json.find(needle);
if (pos == std::string::npos) return "";
pos = json.find(':', pos + needle.size());
if (pos == std::string::npos) return "";
pos = json.find('"', pos + 1);
if (pos == std::string::npos) return "";
size_t end = json.find('"', pos + 1);
if (end == std::string::npos) return "";
return json.substr(pos + 1, end - pos - 1);
}
// Parse a JSON number value
uint32_t jsonUintValue(const std::string& json, const std::string& key) {
std::string needle = "\"" + key + "\"";
size_t pos = json.find(needle);
if (pos == std::string::npos) return 0;
pos = json.find(':', pos + needle.size());
if (pos == std::string::npos) return 0;
++pos;
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) ++pos;
return static_cast<uint32_t>(std::strtoul(json.c_str() + pos, nullptr, 10));
}
// Parse a JSON string array value
std::vector<std::string> jsonStringArray(const std::string& json, const std::string& key) {
std::vector<std::string> result;
std::string needle = "\"" + key + "\"";
size_t pos = json.find(needle);
if (pos == std::string::npos) return result;
pos = json.find('[', pos + needle.size());
if (pos == std::string::npos) return result;
size_t end = json.find(']', pos);
if (end == std::string::npos) return result;
std::string arr = json.substr(pos + 1, end - pos - 1);
size_t p = 0;
while (p < arr.size()) {
size_t qs = arr.find('"', p);
if (qs == std::string::npos) break;
size_t qe = arr.find('"', qs + 1);
if (qe == std::string::npos) break;
result.push_back(arr.substr(qs + 1, qe - qs - 1));
p = qe + 1;
}
return result;
}
} // namespace
void HDPackManager::initialize(const std::string& hdRootPath) {
packs_.clear();
if (!std::filesystem::exists(hdRootPath) || !std::filesystem::is_directory(hdRootPath)) {
LOG_DEBUG("HD pack directory not found: ", hdRootPath);
return;
}
for (const auto& entry : std::filesystem::directory_iterator(hdRootPath)) {
if (!entry.is_directory()) continue;
std::string packJsonPath = entry.path().string() + "/pack.json";
if (!std::filesystem::exists(packJsonPath)) continue;
std::ifstream f(packJsonPath);
if (!f.is_open()) continue;
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
HDPack pack;
pack.id = jsonStringValue(json, "id");
pack.name = jsonStringValue(json, "name");
pack.group = jsonStringValue(json, "group");
pack.totalSizeMB = jsonUintValue(json, "totalSizeMB");
pack.expansions = jsonStringArray(json, "expansions");
pack.packDir = entry.path().string();
pack.manifestPath = entry.path().string() + "/manifest.json";
if (pack.id.empty()) {
LOG_WARNING("HD pack in ", entry.path().string(), " has no id, skipping");
continue;
}
if (!std::filesystem::exists(pack.manifestPath)) {
LOG_WARNING("HD pack '", pack.id, "' missing manifest.json, skipping");
continue;
}
// Apply saved enabled state if available
auto it = enabledState_.find(pack.id);
if (it != enabledState_.end()) {
pack.enabled = it->second;
}
LOG_INFO("Discovered HD pack: '", pack.id, "' (", pack.name, ") ",
pack.totalSizeMB, " MB, ", pack.expansions.size(), " expansions");
packs_.push_back(std::move(pack));
}
LOG_INFO("HDPackManager: found ", packs_.size(), " packs in ", hdRootPath);
}
std::vector<const HDPack*> HDPackManager::getPacksForExpansion(const std::string& expansionId) const {
std::vector<const HDPack*> result;
for (const auto& pack : packs_) {
if (pack.expansions.empty()) {
// No expansion filter = compatible with all
result.push_back(&pack);
} else {
for (const auto& exp : pack.expansions) {
if (exp == expansionId) {
result.push_back(&pack);
break;
}
}
}
}
return result;
}
void HDPackManager::setPackEnabled(const std::string& packId, bool enabled) {
enabledState_[packId] = enabled;
for (auto& pack : packs_) {
if (pack.id == packId) {
pack.enabled = enabled;
break;
}
}
}
bool HDPackManager::isPackEnabled(const std::string& packId) const {
auto it = enabledState_.find(packId);
return it != enabledState_.end() && it->second;
}
void HDPackManager::applyToAssetManager(AssetManager* assetManager, const std::string& expansionId) {
if (!assetManager) return;
// Remove previously applied overlays
for (const auto& overlayId : appliedOverlayIds_) {
assetManager->removeOverlay(overlayId);
}
appliedOverlayIds_.clear();
// Get packs compatible with current expansion
auto compatiblePacks = getPacksForExpansion(expansionId);
int priorityOffset = 0;
for (const auto* pack : compatiblePacks) {
if (!pack->enabled) continue;
std::string overlayId = "hd_" + pack->id;
int priority = HD_OVERLAY_PRIORITY_BASE + priorityOffset;
if (assetManager->addOverlayManifest(pack->manifestPath, priority, overlayId)) {
appliedOverlayIds_.push_back(overlayId);
LOG_INFO("Applied HD pack '", pack->id, "' as overlay (priority ", priority, ")");
}
++priorityOffset;
}
if (!appliedOverlayIds_.empty()) {
LOG_INFO("Applied ", appliedOverlayIds_.size(), " HD pack overlays");
}
}
void HDPackManager::saveSettings(const std::string& settingsPath) const {
std::ofstream f(settingsPath, std::ios::app);
if (!f.is_open()) return;
for (const auto& [packId, enabled] : enabledState_) {
f << "hd_pack_" << packId << "=" << (enabled ? "1" : "0") << "\n";
}
}
void HDPackManager::loadSettings(const std::string& settingsPath) {
std::ifstream f(settingsPath);
if (!f.is_open()) return;
std::string line;
while (std::getline(f, line)) {
if (line.substr(0, 8) != "hd_pack_") continue;
size_t eq = line.find('=');
if (eq == std::string::npos) continue;
std::string packId = line.substr(8, eq - 8);
bool enabled = (line.substr(eq + 1) == "1");
enabledState_[packId] = enabled;
}
}
} // namespace pipeline
} // namespace wowee

View file

@ -4,6 +4,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
@ -164,19 +165,21 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
bool foundHair = false;
bool foundUnderwear = false;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0: Body skin (variation=0, colorIndex = skin color)
if (baseSection == 0 && !foundSkin &&
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
if (!tex1.empty()) {
bodySkinPath_ = tex1;
foundSkin = true;
@ -186,8 +189,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
else if (baseSection == 1 && !foundFace &&
variationIndex == static_cast<uint32_t>(face) &&
colorIndex == static_cast<uint32_t>(skin)) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex2 = charSectionsDbc->getString(r, 5);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5);
if (!tex1.empty()) faceLowerPath = tex1;
if (!tex2.empty()) faceUpperPath = tex2;
foundFace = true;
@ -196,7 +199,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
else if (baseSection == 3 && !foundHair &&
variationIndex == static_cast<uint32_t>(hairStyle) &&
colorIndex == static_cast<uint32_t>(hairColor)) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
if (!tex1.empty()) {
hairScalpPath = tex1;
foundHair = true;
@ -205,7 +208,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
// Section 4: Underwear (variation=0, colorIndex = skin color)
else if (baseSection == 4 && !foundUnderwear &&
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
for (int f = 4; f <= 6; f++) {
uint32_t texBase = csL ? (*csL)["Texture1"] : 4;
for (uint32_t f = texBase; f <= texBase + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) {
underwearPaths.push_back(tex);

View file

@ -1,6 +1,7 @@
#include "rendering/lighting_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cmath>
@ -78,27 +79,30 @@ bool LightingManager::loadLightDbc(pipeline::AssetManager* assetManager) {
// 9: uint32 LightParamsID (underwater)
// ... more params for death, phases, etc.
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* lL = activeLayout ? activeLayout->getLayout("Light") : nullptr;
for (uint32_t i = 0; i < recordCount; ++i) {
LightVolume volume;
volume.lightId = dbc->getUInt32(i, 0);
volume.mapId = dbc->getUInt32(i, 1);
volume.lightId = dbc->getUInt32(i, lL ? (*lL)["ID"] : 0);
volume.mapId = dbc->getUInt32(i, lL ? (*lL)["MapID"] : 1);
// Position (note: DBC stores as x,z,y - need to swap!)
float x = dbc->getFloat(i, 2);
float z = dbc->getFloat(i, 3);
float y = dbc->getFloat(i, 4);
float x = dbc->getFloat(i, lL ? (*lL)["X"] : 2);
float z = dbc->getFloat(i, lL ? (*lL)["Z"] : 3);
float y = dbc->getFloat(i, lL ? (*lL)["Y"] : 4);
volume.position = glm::vec3(x, y, z); // Convert to x,y,z
volume.innerRadius = dbc->getFloat(i, 5);
volume.outerRadius = dbc->getFloat(i, 6);
volume.innerRadius = dbc->getFloat(i, lL ? (*lL)["InnerRadius"] : 5);
volume.outerRadius = dbc->getFloat(i, lL ? (*lL)["OuterRadius"] : 6);
// LightParams IDs for different conditions
volume.lightParamsId = dbc->getUInt32(i, 7);
volume.lightParamsId = dbc->getUInt32(i, lL ? (*lL)["LightParamsID"] : 7);
if (dbc->getFieldCount() > 8) {
volume.lightParamsIdRain = dbc->getUInt32(i, 8);
volume.lightParamsIdRain = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDRain"] : 8);
}
if (dbc->getFieldCount() > 9) {
volume.lightParamsIdUnderwater = dbc->getUInt32(i, 9);
volume.lightParamsIdUnderwater = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDUnderwater"] : 9);
}
// Add to map-specific list
@ -126,8 +130,9 @@ bool LightingManager::loadLightParamsDbc(pipeline::AssetManager* assetManager) {
LOG_INFO("Loaded LightParams.dbc: ", recordCount, " profiles");
// Create profile entries (will be populated by band loading)
const auto* lpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightParams") : nullptr;
for (uint32_t i = 0; i < recordCount; ++i) {
uint32_t paramId = dbc->getUInt32(i, 0);
uint32_t paramId = dbc->getUInt32(i, lpL ? (*lpL)["LightParamsID"] : 0);
LightParamsProfile profile;
profile.lightParamsId = paramId;
lightParamsProfiles_[paramId] = profile;
@ -147,8 +152,9 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
// Parse int bands
// Structure: ID, Entry (block index), NumValues, Time[16], Color[16]
// Block index = LightParamsID * 18 + channel
const auto* libL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightIntBand") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t blockIndex = dbc->getUInt32(i, 1);
uint32_t blockIndex = dbc->getUInt32(i, libL ? (*libL)["BlockIndex"] : 1);
uint32_t lightParamsId = blockIndex / 18;
uint32_t channelIndex = blockIndex % 18;
@ -158,18 +164,20 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
if (channelIndex >= LightParamsProfile::COLOR_CHANNEL_COUNT) continue;
ColorBand& band = it->second.colorBands[channelIndex];
band.numKeyframes = dbc->getUInt32(i, 2);
band.numKeyframes = dbc->getUInt32(i, libL ? (*libL)["NumKeyframes"] : 2);
if (band.numKeyframes > 16) band.numKeyframes = 16;
// Read time keys (field 3-18) - stored as uint16 half-minutes
uint32_t timeKeyBase = libL ? (*libL)["TimeKey0"] : 3;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
uint32_t timeValue = dbc->getUInt32(i, 3 + k);
uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k);
band.times[k] = static_cast<uint16_t>(timeValue % 2880); // Clamp to valid range
}
// Read color values (field 19-34) - stored as BGRA packed uint32
uint32_t valueBase = libL ? (*libL)["Value0"] : 19;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
uint32_t colorBGRA = dbc->getUInt32(i, 19 + k);
uint32_t colorBGRA = dbc->getUInt32(i, valueBase + k);
band.colors[k] = dbcColorToVec3(colorBGRA);
}
}
@ -186,8 +194,9 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
// Parse float bands
// Structure: ID, Entry (block index), NumValues, Time[16], Value[16]
// Block index = LightParamsID * 6 + channel
const auto* lfbL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightFloatBand") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t blockIndex = dbc->getUInt32(i, 1);
uint32_t blockIndex = dbc->getUInt32(i, lfbL ? (*lfbL)["BlockIndex"] : 1);
uint32_t lightParamsId = blockIndex / 6;
uint32_t channelIndex = blockIndex % 6;
@ -197,18 +206,20 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
if (channelIndex >= LightParamsProfile::FLOAT_CHANNEL_COUNT) continue;
FloatBand& band = it->second.floatBands[channelIndex];
band.numKeyframes = dbc->getUInt32(i, 2);
band.numKeyframes = dbc->getUInt32(i, lfbL ? (*lfbL)["NumKeyframes"] : 2);
if (band.numKeyframes > 16) band.numKeyframes = 16;
// Read time keys (field 3-18)
uint32_t timeKeyBase = lfbL ? (*lfbL)["TimeKey0"] : 3;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
uint32_t timeValue = dbc->getUInt32(i, 3 + k);
uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k);
band.times[k] = static_cast<uint16_t>(timeValue % 2880); // Clamp to valid range
}
// Read float values (field 19-34)
uint32_t valueBase = lfbL ? (*lfbL)["Value0"] : 19;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
band.values[k] = dbc->getFloat(i, 19 + k);
band.values[k] = dbc->getFloat(i, valueBase + k);
}
}
}

View file

@ -27,6 +27,7 @@
#include <algorithm>
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/adt_loader.hpp"
@ -156,11 +157,16 @@ static void loadEmotesFromDbc() {
return;
}
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr;
const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr;
const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr;
std::unordered_map<uint32_t, std::string> textData;
textData.reserve(emotesTextDataDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
uint32_t id = emotesTextDataDbc->getUInt32(r, 0);
std::string text = emotesTextDataDbc->getString(r, 1);
uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0);
std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1);
if (!text.empty()) textData.emplace(id, std::move(text));
}
@ -168,8 +174,8 @@ static void loadEmotesFromDbc() {
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
uint32_t emoteId = emotesDbc->getUInt32(r, 0);
uint32_t animId = emotesDbc->getUInt32(r, 2);
uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0);
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
if (animId != 0) emoteIdToAnim[emoteId] = animId;
}
}
@ -177,10 +183,10 @@ static void loadEmotesFromDbc() {
EMOTE_TABLE.clear();
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
std::string cmdRaw = emotesTextDbc->getString(r, 1);
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
if (cmdRaw.empty()) continue;
uint32_t emoteRef = emotesTextDbc->getUInt32(r, 2);
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
uint32_t animId = 0;
auto animIt = emoteIdToAnim.find(emoteRef);
if (animIt != emoteIdToAnim.end()) {
@ -189,8 +195,8 @@ static void loadEmotesFromDbc() {
animId = emoteRef; // fallback if EmotesText stores animation id directly
}
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, 5); // unisex, target, sender
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, 9); // unisex, no target, sender
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); // unisex, target, sender
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); // unisex, no target, sender
std::string textTarget;
std::string textNoTarget;

View file

@ -1,6 +1,7 @@
#include "rendering/world_map.hpp"
#include "rendering/shader.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/coordinates.hpp"
#include "core/input.hpp"
#include "core/logger.hpp"
@ -182,13 +183,16 @@ void WorldMap::loadZonesFromDBC() {
if (!zones.empty() || !assetManager) return;
// Step 1: Resolve mapID from Map.dbc
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr;
int mapID = -1;
auto mapDbc = assetManager->loadDBC("Map.dbc");
if (mapDbc && mapDbc->isLoaded()) {
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
std::string dir = mapDbc->getString(i, 1);
std::string dir = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
if (dir == mapName) {
mapID = static_cast<int>(mapDbc->getUInt32(i, 0));
mapID = static_cast<int>(mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0));
LOG_INFO("WorldMap: Map.dbc '", mapName, "' -> mapID=", mapID);
break;
}
@ -207,12 +211,13 @@ void WorldMap::loadZonesFromDBC() {
}
// Step 2: Load AreaTable explore flags by areaID.
const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr;
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
auto areaDbc = assetManager->loadDBC("AreaTable.dbc");
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) {
const uint32_t areaId = areaDbc->getUInt32(i, 0);
const uint32_t exploreFlag = areaDbc->getUInt32(i, 3);
const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0);
const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3);
if (areaId != 0) {
exploreFlagByAreaId[areaId] = exploreFlag;
}
@ -236,20 +241,22 @@ void WorldMap::loadZonesFromDBC() {
// 4: locLeft, 5: locRight, 6: locTop, 7: locBottom
// 8: displayMapID, 9: defaultDungeonFloor, 10: parentWorldMapID
const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr;
for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) {
uint32_t recMapID = wmaDbc->getUInt32(i, 1);
uint32_t recMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["MapID"] : 1);
if (static_cast<int>(recMapID) != mapID) continue;
WorldMapZone zone;
zone.wmaID = wmaDbc->getUInt32(i, 0);
zone.areaID = wmaDbc->getUInt32(i, 2);
zone.areaName = wmaDbc->getString(i, 3);
zone.locLeft = wmaDbc->getFloat(i, 4);
zone.locRight = wmaDbc->getFloat(i, 5);
zone.locTop = wmaDbc->getFloat(i, 6);
zone.locBottom = wmaDbc->getFloat(i, 7);
zone.displayMapID = wmaDbc->getUInt32(i, 8);
zone.parentWorldMapID = wmaDbc->getUInt32(i, 10);
zone.wmaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ID"] : 0);
zone.areaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["AreaID"] : 2);
zone.areaName = wmaDbc->getString(i, wmaL ? (*wmaL)["AreaName"] : 3);
zone.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4);
zone.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5);
zone.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6);
zone.locBottom = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocBottom"] : 7);
zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8);
zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10);
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
if (exploreIt != exploreFlagByAreaId.end()) {
zone.exploreFlag = exploreIt->second;
@ -258,10 +265,10 @@ void WorldMap::loadZonesFromDBC() {
int idx = static_cast<int>(zones.size());
// Debug: also log raw uint32 values for bounds fields
uint32_t raw4 = wmaDbc->getUInt32(i, 4);
uint32_t raw5 = wmaDbc->getUInt32(i, 5);
uint32_t raw6 = wmaDbc->getUInt32(i, 6);
uint32_t raw7 = wmaDbc->getUInt32(i, 7);
uint32_t raw4 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocLeft"] : 4);
uint32_t raw5 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocRight"] : 5);
uint32_t raw6 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocTop"] : 6);
uint32_t raw7 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocBottom"] : 7);
LOG_INFO("WorldMap: zone[", idx, "] areaID=", zone.areaID,
" '", zone.areaName, "' L=", zone.locLeft,

View file

@ -5,6 +5,7 @@
#include "rendering/renderer.hpp"
#include "pipeline/asset_manager.hpp"
#include "audio/music_manager.hpp"
#include "game/expansion_profile.hpp"
#include <imgui.h>
#include <filesystem>
#include <sstream>
@ -148,9 +149,30 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
if (port < 1) port = 1;
if (port > 65535) port = 65535;
// Compatibility mode dropdown
const char* compatModes[] = { "3.3.5a" };
ImGui::Combo("Compatibility Mode", &compatibilityMode, compatModes, IM_ARRAYSIZE(compatModes));
// Expansion selector (populated from ExpansionRegistry)
auto* registry = core::Application::getInstance().getExpansionRegistry();
if (registry && !registry->getAllProfiles().empty()) {
auto& profiles = registry->getAllProfiles();
// Build combo items: "WotLK (3.3.5a)"
std::string preview;
if (expansionIndex >= 0 && expansionIndex < static_cast<int>(profiles.size())) {
preview = profiles[expansionIndex].shortName + " (" + profiles[expansionIndex].versionString() + ")";
}
if (ImGui::BeginCombo("Expansion", preview.c_str())) {
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
std::string label = profiles[i].shortName + " (" + profiles[i].versionString() + ")";
bool selected = (expansionIndex == i);
if (ImGui::Selectable(label.c_str(), selected)) {
expansionIndex = i;
registry->setActive(profiles[i].id);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
} else {
ImGui::Text("Expansion: WotLK 3.3.5a (default)");
}
ImGui::Spacing();
ImGui::Separator();
@ -291,6 +313,21 @@ void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) {
failureReason = reason;
});
// Configure client version from active expansion profile
auto* reg = core::Application::getInstance().getExpansionRegistry();
if (reg) {
auto* profile = reg->getActive();
if (profile) {
auth::ClientInfo info;
info.majorVersion = profile->majorVersion;
info.minorVersion = profile->minorVersion;
info.patchVersion = profile->patchVersion;
info.build = profile->build;
info.protocolVersion = profile->protocolVersion;
authHandler.setClientInfo(info);
}
}
if (authHandler.connect(hostname, static_cast<uint16_t>(port))) {
authenticating = true;
authTimer = 0.0f;
@ -350,6 +387,11 @@ void AuthScreen::saveLoginInfo() {
if (!savedPasswordHash.empty()) {
out << "password_hash=" << savedPasswordHash << "\n";
}
// Save active expansion id
auto* expReg = core::Application::getInstance().getExpansionRegistry();
if (expReg && !expReg->getActiveId().empty()) {
out << "expansion=" << expReg->getActiveId() << "\n";
}
LOG_INFO("Login info saved to ", path);
}
@ -376,6 +418,15 @@ void AuthScreen::loadLoginInfo() {
username[sizeof(username) - 1] = '\0';
} else if (key == "password_hash" && !val.empty()) {
savedPasswordHash = val;
} else if (key == "expansion" && !val.empty()) {
auto* expReg = core::Application::getInstance().getExpansionRegistry();
if (expReg && expReg->setActive(val)) {
// Find matching index
auto& profiles = expReg->getAllProfiles();
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
if (profiles[i].id == val) { expansionIndex = i; break; }
}
}
}
}

View file

@ -2,6 +2,7 @@
#include "rendering/character_preview.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include <imgui.h>
#include <cstring>
@ -169,16 +170,17 @@ void CharacterCreateScreen::updateAppearanceRanges() {
uint32_t targetRaceId = static_cast<uint32_t>(allRaces[raceIndex]);
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
int skinMax = -1;
int hairStyleMax = -1;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, 1);
uint32_t sexId = dbc->getUInt32(r, 2);
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, 3);
uint32_t variationIndex = dbc->getUInt32(r, 8);
uint32_t colorIndex = dbc->getUInt32(r, 9);
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (baseSection == 0 && variationIndex == 0) {
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
@ -199,13 +201,13 @@ void CharacterCreateScreen::updateAppearanceRanges() {
int faceMax = -1;
std::vector<uint8_t> hairColorIds;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, 1);
uint32_t sexId = dbc->getUInt32(r, 2);
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, 3);
uint32_t variationIndex = dbc->getUInt32(r, 8);
uint32_t colorIndex = dbc->getUInt32(r, 9);
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
faceMax = std::max(faceMax, static_cast<int>(variationIndex));
@ -232,12 +234,13 @@ void CharacterCreateScreen::updateAppearanceRanges() {
}
int facialMax = -1;
auto facialDbc = assetManager_->loadDBC("CharacterFacialHairStyles.dbc");
const auto* fhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
if (facialDbc) {
for (uint32_t r = 0; r < facialDbc->getRecordCount(); r++) {
uint32_t raceId = facialDbc->getUInt32(r, 0);
uint32_t sexId = facialDbc->getUInt32(r, 1);
uint32_t raceId = facialDbc->getUInt32(r, fhL ? (*fhL)["RaceID"] : 0);
uint32_t sexId = facialDbc->getUInt32(r, fhL ? (*fhL)["SexID"] : 1);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t variation = facialDbc->getUInt32(r, 2);
uint32_t variation = facialDbc->getUInt32(r, fhL ? (*fhL)["Variation"] : 2);
facialMax = std::max(facialMax, static_cast<int>(variation));
}
}

View file

@ -22,6 +22,9 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "pipeline/hd_pack_manager.hpp"
#include "game/expansion_profile.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <algorithm>
@ -2360,7 +2363,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId);
if (recIdx >= 0) {
// DBC field 3 = modelTexture_1 (cape texture name)
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["LeftModelTexture"] : 3);
if (!capeName.empty()) {
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
GLuint capeTex = charRenderer->loadTexture(capePath);
@ -2422,10 +2426,11 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
// Load SpellIcon.dbc: field 0 = ID, field 1 = icon path
auto iconDbc = am->loadDBC("SpellIcon.dbc");
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
if (iconDbc && iconDbc->isLoaded()) {
for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) {
uint32_t id = iconDbc->getUInt32(i, 0);
std::string path = iconDbc->getString(i, 1);
uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths_[id] = path;
}
@ -2434,10 +2439,11 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
// Load Spell.dbc: field 133 = SpellIconID
auto spellDbc = am->loadDBC("Spell.dbc");
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
if (spellDbc && spellDbc->isLoaded() && spellDbc->getFieldCount() > 133) {
for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) {
uint32_t id = spellDbc->getUInt32(i, 0);
uint32_t iconId = spellDbc->getUInt32(i, 133);
uint32_t id = spellDbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
uint32_t iconId = spellDbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
if (id > 0 && iconId > 0) {
spellIconIds_[id] = iconId;
}
@ -2530,8 +2536,9 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
if (assetMgr && assetMgr->isInitialized()) {
auto dbc = assetMgr->loadDBC("Spell.dbc");
if (dbc && dbc->isLoaded()) {
const auto* actionSpellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t fieldCount = dbc->getFieldCount();
uint32_t nameField = 136;
uint32_t nameField = actionSpellL ? (*actionSpellL)["Name"] : 136;
if (fieldCount < 137) {
if (fieldCount > 10) {
nameField = fieldCount > 140 ? 136 : 1;
@ -2542,7 +2549,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
uint32_t count = dbc->getRecordCount();
actionSpellNames.reserve(count);
for (uint32_t r = 0; r < count; ++r) {
uint32_t id = dbc->getUInt32(r, 0);
uint32_t id = dbc->getUInt32(r, actionSpellL ? (*actionSpellL)["ID"] : 0);
std::string name = dbc->getString(r, nameField);
if (!name.empty() && id > 0) {
actionSpellNames[id] = name;
@ -4835,6 +4842,79 @@ void GameScreen::renderSettingsWindow() {
ImGui::EndTabItem();
}
// ============================================================
// HD TEXTURES TAB
// ============================================================
if (ImGui::BeginTabItem("HD Textures")) {
ImGui::Spacing();
auto& app = core::Application::getInstance();
auto* hdMgr = app.getHDPackManager();
if (hdMgr) {
const auto& packs = hdMgr->getAllPacks();
if (packs.empty()) {
ImGui::TextWrapped("No HD texture packs found.");
ImGui::Spacing();
ImGui::TextWrapped("Place packs in Data/hd/<pack_name>/ with a pack.json and manifest.json.");
} else {
ImGui::Text("Available HD Texture Packs:");
ImGui::Spacing();
bool changed = false;
for (const auto& pack : packs) {
bool enabled = pack.enabled;
if (ImGui::Checkbox(pack.name.c_str(), &enabled)) {
hdMgr->setPackEnabled(pack.id, enabled);
changed = true;
}
ImGui::SameLine(0, 10);
ImGui::TextDisabled("(%u MB)", pack.totalSizeMB);
if (!pack.group.empty()) {
ImGui::SameLine(0, 10);
ImGui::TextDisabled("[%s]", pack.group.c_str());
}
if (!pack.expansions.empty()) {
std::string expList;
for (const auto& e : pack.expansions) {
if (!expList.empty()) expList += ", ";
expList += e;
}
ImGui::TextDisabled(" Compatible: %s", expList.c_str());
}
}
if (changed) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Apply HD Packs", ImVec2(-1, 0))) {
std::string expansionId = "wotlk";
if (app.getExpansionRegistry() && app.getExpansionRegistry()->getActive()) {
expansionId = app.getExpansionRegistry()->getActive()->id;
}
hdMgr->applyToAssetManager(app.getAssetManager(), expansionId);
// Save settings
std::string settingsDir;
const char* xdg = std::getenv("XDG_DATA_HOME");
if (xdg && *xdg) {
settingsDir = std::string(xdg) + "/wowee";
} else {
const char* home = std::getenv("HOME");
settingsDir = std::string(home ? home : ".") + "/.local/share/wowee";
}
hdMgr->saveSettings(settingsDir + "/settings.cfg");
}
}
}
} else {
ImGui::Text("HD Pack Manager not available.");
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}

View file

@ -7,6 +7,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <SDL2/SDL.h>
@ -60,7 +61,8 @@ GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
}
// Field 5 = inventoryIcon_1
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 5);
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["InventoryIcon"] : 5);
if (iconName.empty()) {
iconCache_[displayInfoId] = 0;
return 0;

View file

@ -4,6 +4,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <map>
@ -30,17 +31,18 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
// WoW 3.3.5a Spell.dbc fields (0-based):
// 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 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 spellId = dbc->getUInt32(i, 0);
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
if (spellId == 0) continue;
SpellInfo info;
info.spellId = spellId;
info.attributes = dbc->getUInt32(i, 4);
info.iconId = dbc->getUInt32(i, 133);
info.name = dbc->getString(i, 136);
info.rank = dbc->getString(i, 153);
info.attributes = dbc->getUInt32(i, spellL ? (*spellL)["Attributes"] : 4);
info.iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
info.name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
info.rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
if (!info.name.empty()) {
spellData[spellId] = std::move(info);
@ -63,9 +65,10 @@ void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
return;
}
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, 0);
std::string path = dbc->getString(i, 1);
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths[id] = path;
}
@ -82,11 +85,12 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
// Load SkillLine.dbc: field 0 = ID, field 1 = categoryID, field 3 = name_enUS
auto skillLineDbc = assetManager->loadDBC("SkillLine.dbc");
const auto* slL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
if (skillLineDbc && skillLineDbc->isLoaded()) {
for (uint32_t i = 0; i < skillLineDbc->getRecordCount(); i++) {
uint32_t id = skillLineDbc->getUInt32(i, 0);
uint32_t category = skillLineDbc->getUInt32(i, 1);
std::string name = skillLineDbc->getString(i, 3);
uint32_t id = skillLineDbc->getUInt32(i, slL ? (*slL)["ID"] : 0);
uint32_t category = skillLineDbc->getUInt32(i, slL ? (*slL)["Category"] : 1);
std::string name = skillLineDbc->getString(i, slL ? (*slL)["Name"] : 3);
if (id > 0 && !name.empty()) {
skillLineNames[id] = name;
skillLineCategories[id] = category;
@ -99,10 +103,11 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
// Load SkillLineAbility.dbc: field 0 = ID, field 1 = skillLineID, field 2 = spellID
auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc");
const auto* slaL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLineAbility") : nullptr;
if (slaDbc && slaDbc->isLoaded()) {
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;
}

View file

@ -4,6 +4,7 @@
#include "core/logger.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include <algorithm>
#include <GL/glew.h>
@ -448,15 +449,16 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
}
// WoW 3.3.5a Spell.dbc fields: 0=SpellID, 133=SpellIconID, 136=SpellName_enUS, 139=Tooltip_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 spellId = dbc->getUInt32(i, 0);
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
if (spellId == 0) continue;
uint32_t iconId = dbc->getUInt32(i, 133);
uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
spellIconIds[spellId] = iconId;
std::string tooltip = dbc->getString(i, 139);
std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139);
if (!tooltip.empty()) {
spellTooltips[spellId] = tooltip;
}
@ -477,9 +479,10 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
return;
}
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, 0);
std::string path = dbc->getString(i, 1);
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths[id] = path;
}

View file

@ -13,6 +13,8 @@ static void printUsage(const char* prog) {
<< " --output <path> Output directory for extracted assets\n"
<< "\n"
<< "Options:\n"
<< " --expansion <id> Expansion ID (classic/tbc/wotlk/cata).\n"
<< " Output goes to <output>/expansions/<id>/\n"
<< " --verify CRC32 verify all extracted files\n"
<< " --threads <N> Number of extraction threads (default: auto)\n"
<< " --verbose Verbose output\n"
@ -21,12 +23,15 @@ static void printUsage(const char* prog) {
int main(int argc, char** argv) {
wowee::tools::Extractor::Options opts;
std::string expansion;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--mpq-dir") == 0 && i + 1 < argc) {
opts.mpqDir = argv[++i];
} else if (std::strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
opts.outputDir = argv[++i];
} else if (std::strcmp(argv[i], "--expansion") == 0 && i + 1 < argc) {
expansion = argv[++i];
} else if (std::strcmp(argv[i], "--threads") == 0 && i + 1 < argc) {
opts.threads = std::atoi(argv[++i]);
} else if (std::strcmp(argv[i], "--verify") == 0) {
@ -49,9 +54,17 @@ int main(int argc, char** argv) {
return 1;
}
// If --expansion given, redirect output into expansions/<id>/ subdirectory
if (!expansion.empty()) {
opts.outputDir += "/expansions/" + expansion;
}
std::cout << "=== Wowee Asset Extractor ===\n";
std::cout << "MPQ directory: " << opts.mpqDir << "\n";
std::cout << "Output: " << opts.outputDir << "\n";
if (!expansion.empty()) {
std::cout << "Expansion: " << expansion << "\n";
}
if (!wowee::tools::Extractor::run(opts)) {
std::cerr << "Extraction failed!\n";