From 7092844b5e6e7e8b3297f1282758b53d5c852fd2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Feb 2026 22:56:36 -0800 Subject: [PATCH] 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 --- .gitignore | 7 - CMakeLists.txt | 7 + Data/expansions/classic/dbc_layouts.json | 92 +++ Data/expansions/classic/expansion.json | 11 + Data/expansions/classic/opcodes.json | 226 +++++++ Data/expansions/classic/update_fields.json | 29 + Data/expansions/tbc/dbc_layouts.json | 93 +++ Data/expansions/tbc/expansion.json | 11 + Data/expansions/tbc/opcodes.json | 255 ++++++++ Data/expansions/tbc/update_fields.json | 29 + Data/expansions/wotlk/dbc_layouts.json | 93 +++ Data/expansions/wotlk/expansion.json | 11 + Data/expansions/wotlk/opcodes.json | 255 ++++++++ Data/expansions/wotlk/update_fields.json | 29 + include/auth/auth_handler.hpp | 4 + include/auth/auth_packets.hpp | 1 + include/core/application.hpp | 10 +- include/game/expansion_profile.hpp | 67 +++ include/game/game_handler.hpp | 21 +- include/game/opcode_table.hpp | 407 +++++++++++++ include/game/opcodes.hpp | 340 +---------- include/game/packet_parsers.hpp | 178 ++++++ include/game/update_field_table.hpp | 93 +++ include/game/world_packets.hpp | 1 - include/pipeline/asset_manager.hpp | 39 +- include/pipeline/dbc_layout.hpp | 64 ++ include/pipeline/hd_pack_manager.hpp | 97 +++ include/ui/auth_screen.hpp | 3 +- src/auth/auth_packets.cpp | 4 +- src/core/application.cpp | 274 ++++++--- src/game/expansion_profile.cpp | 185 ++++++ src/game/game_handler.cpp | 500 ++++++++-------- src/game/opcode_table.cpp | 649 +++++++++++++++++++++ src/game/packet_parsers_classic.cpp | 344 +++++++++++ src/game/packet_parsers_tbc.cpp | 424 ++++++++++++++ src/game/update_field_table.cpp | 156 +++++ src/game/world_packets.cpp | 246 ++++---- src/pipeline/asset_manager.cpp | 83 ++- src/pipeline/dbc_layout.cpp | 226 +++++++ src/pipeline/hd_pack_manager.cpp | 204 +++++++ src/rendering/character_preview.cpp | 24 +- src/rendering/lighting_manager.cpp | 49 +- src/rendering/renderer.cpp | 22 +- src/rendering/world_map.cpp | 43 +- src/ui/auth_screen.cpp | 57 +- src/ui/character_create_screen.cpp | 29 +- src/ui/game_screen.cpp | 94 ++- src/ui/inventory_screen.cpp | 4 +- src/ui/spellbook_screen.cpp | 29 +- src/ui/talent_screen.cpp | 13 +- tools/asset_extract/main.cpp | 13 + 51 files changed, 5258 insertions(+), 887 deletions(-) create mode 100644 Data/expansions/classic/dbc_layouts.json create mode 100644 Data/expansions/classic/expansion.json create mode 100644 Data/expansions/classic/opcodes.json create mode 100644 Data/expansions/classic/update_fields.json create mode 100644 Data/expansions/tbc/dbc_layouts.json create mode 100644 Data/expansions/tbc/expansion.json create mode 100644 Data/expansions/tbc/opcodes.json create mode 100644 Data/expansions/tbc/update_fields.json create mode 100644 Data/expansions/wotlk/dbc_layouts.json create mode 100644 Data/expansions/wotlk/expansion.json create mode 100644 Data/expansions/wotlk/opcodes.json create mode 100644 Data/expansions/wotlk/update_fields.json create mode 100644 include/game/expansion_profile.hpp create mode 100644 include/game/opcode_table.hpp create mode 100644 include/game/packet_parsers.hpp create mode 100644 include/game/update_field_table.hpp create mode 100644 include/pipeline/dbc_layout.hpp create mode 100644 include/pipeline/hd_pack_manager.hpp create mode 100644 src/game/expansion_profile.cpp create mode 100644 src/game/opcode_table.cpp create mode 100644 src/game/packet_parsers_classic.cpp create mode 100644 src/game/packet_parsers_tbc.cpp create mode 100644 src/game/update_field_table.cpp create mode 100644 src/pipeline/dbc_layout.cpp create mode 100644 src/pipeline/hd_pack_manager.cpp diff --git a/.gitignore b/.gitignore index a1f8a334..59a208b0 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8813334b..96705d88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json new file mode 100644 index 00000000..620dd9c5 --- /dev/null +++ b/Data/expansions/classic/dbc_layouts.json @@ -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 + } +} diff --git a/Data/expansions/classic/expansion.json b/Data/expansions/classic/expansion.json new file mode 100644 index 00000000..b8d3181e --- /dev/null +++ b/Data/expansions/classic/expansion.json @@ -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] +} diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json new file mode 100644 index 00000000..d527f0e0 --- /dev/null +++ b/Data/expansions/classic/opcodes.json @@ -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" +} diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json new file mode 100644 index 00000000..cf7fe18e --- /dev/null +++ b/Data/expansions/classic/update_fields.json @@ -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 +} diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json new file mode 100644 index 00000000..d31b5d18 --- /dev/null +++ b/Data/expansions/tbc/dbc_layouts.json @@ -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 + } +} diff --git a/Data/expansions/tbc/expansion.json b/Data/expansions/tbc/expansion.json new file mode 100644 index 00000000..c3af88cf --- /dev/null +++ b/Data/expansions/tbc/expansion.json @@ -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] +} diff --git a/Data/expansions/tbc/opcodes.json b/Data/expansions/tbc/opcodes.json new file mode 100644 index 00000000..ab1bb627 --- /dev/null +++ b/Data/expansions/tbc/opcodes.json @@ -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" +} diff --git a/Data/expansions/tbc/update_fields.json b/Data/expansions/tbc/update_fields.json new file mode 100644 index 00000000..43aa7985 --- /dev/null +++ b/Data/expansions/tbc/update_fields.json @@ -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 +} diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json new file mode 100644 index 00000000..8b9c1ff1 --- /dev/null +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -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 + } +} diff --git a/Data/expansions/wotlk/expansion.json b/Data/expansions/wotlk/expansion.json new file mode 100644 index 00000000..ae0fe25e --- /dev/null +++ b/Data/expansions/wotlk/expansion.json @@ -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] +} diff --git a/Data/expansions/wotlk/opcodes.json b/Data/expansions/wotlk/opcodes.json new file mode 100644 index 00000000..f082c82a --- /dev/null +++ b/Data/expansions/wotlk/opcodes.json @@ -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" +} diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json new file mode 100644 index 00000000..c7e1316e --- /dev/null +++ b/Data/expansions/wotlk/update_fields.json @@ -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 +} diff --git a/include/auth/auth_handler.hpp b/include/auth/auth_handler.hpp index 7143d18f..103711e7 100644 --- a/include/auth/auth_handler.hpp +++ b/include/auth/auth_handler.hpp @@ -45,6 +45,10 @@ public: void authenticate(const std::string& username, const std::string& password); void authenticateWithHash(const std::string& username, const std::vector& 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& getRealms() const { return realms; } diff --git a/include/auth/auth_packets.hpp b/include/auth/auth_packets.hpp index d6271a1d..7a86deaa 100644 --- a/include/auth/auth_packets.hpp +++ b/include/auth/auth_packets.hpp @@ -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"; diff --git a/include/core/application.hpp b/include/core/application.hpp index 8467e79c..8f7229bf 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -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 gameHandler; std::unique_ptr world; std::unique_ptr assetManager; + std::unique_ptr expansionRegistry_; + std::unique_ptr dbcLayout_; + std::unique_ptr hdPackManager_; AppState state = AppState::AUTHENTICATION; bool running = false; diff --git a/include/game/expansion_profile.hpp b/include/game/expansion_profile.hpp new file mode 100644 index 00000000..ce21650f --- /dev/null +++ b/include/game/expansion_profile.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include + +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 races; + std::vector 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& 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 profiles_; + std::string activeId_; + + bool loadProfile(const std::string& jsonPath, const std::string& dirPath); +}; + +} // namespace game +} // namespace wowee diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 9918c581..31e839ec 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -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 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_; + // Network std::unique_ptr socket; @@ -1210,7 +1230,6 @@ private: std::unordered_map 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 playerExploredZones_ = std::vector(PLAYER_EXPLORED_ZONES_COUNT, 0u); diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp new file mode 100644 index 00000000..c3856b40 --- /dev/null +++ b/include/game/opcode_table.hpp @@ -0,0 +1,407 @@ +#pragma once + +#include +#include +#include +#include + +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 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 logicalToWire_; // LogicalOpcode → wire + std::unordered_map wireToLogical_; // wire → LogicalOpcode + + static std::optional 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 diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 72ed1ef3..387d96e4 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -1,344 +1,14 @@ #pragma once -#include +#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 diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp new file mode 100644 index 00000000..e65ce068 --- /dev/null +++ b/include/game/packet_parsers.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include "game/world_packets.hpp" +#include +#include + +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 createPacketParsers(const std::string& expansionId) { + if (expansionId == "classic") return std::make_unique(); + if (expansionId == "tbc") return std::make_unique(); + return std::make_unique(); +} + +} // namespace game +} // namespace wowee diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp new file mode 100644 index 00000000..ff8532aa --- /dev/null +++ b/include/game/update_field_table.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +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 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 diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 040395aa..89c19478 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -514,7 +514,6 @@ public: */ static uint64_t readPackedGuid(network::Packet& packet); -private: /** * Parse a single update block * diff --git a/include/pipeline/asset_manager.hpp b/include/pipeline/asset_manager.hpp index b146bd8e..a3b9be6a 100644 --- a/include/pipeline/asset_manager.hpp +++ b/include/pipeline/asset_manager.hpp @@ -6,6 +6,7 @@ #include "pipeline/loose_file_reader.hpp" #include #include +#include #include #include @@ -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 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 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> dbcCache; diff --git a/include/pipeline/dbc_layout.hpp b/include/pipeline/dbc_layout.hpp new file mode 100644 index 00000000..bea88c91 --- /dev/null +++ b/include/pipeline/dbc_layout.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +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 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 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 diff --git a/include/pipeline/hd_pack_manager.hpp b/include/pipeline/hd_pack_manager.hpp new file mode 100644 index 00000000..1eaa6ff2 --- /dev/null +++ b/include/pipeline/hd_pack_manager.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +class AssetManager; + +/** + * Metadata for a single HD texture pack on disk. + * + * Each pack lives in Data/hd// 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 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& getAllPacks() const { return packs_; } + + /** + * Get packs compatible with a specific expansion. + */ + std::vector 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 packs_; + std::unordered_map enabledState_; // packId → enabled + + // Overlay IDs currently applied to AssetManager (for removal on re-apply) + std::vector appliedOverlayIds_; + + static constexpr int HD_OVERLAY_PRIORITY_BASE = 100; // High priority, above expansion base +}; + +} // namespace pipeline +} // namespace wowee diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index 198a8c22..de6dc729 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -3,6 +3,7 @@ #include "auth/auth_handler.hpp" #include "rendering/video_player.hpp" #include +#include #include 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; diff --git a/src/auth/auth_packets.cpp b/src/auth/auth_packets.cpp index 7ce3bab3..6afc8242 100644 --- a/src/auth/auth_packets.cpp +++ b/src/auth/auth_packets.cpp @@ -19,8 +19,8 @@ network::Packet LogonChallengePacket::build(const std::string& account, const Cl network::Packet packet(static_cast(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); diff --git a/src/core/application.cpp b/src/core/application.cpp index e0a3f4df..92ec9ccd 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 #include #include @@ -48,6 +52,7 @@ #include #include #include +#include namespace wowee { namespace core { @@ -118,6 +123,15 @@ bool Application::initialize() { gameHandler = std::make_unique(); world = std::make_unique(); + // Create and initialize expansion registry + expansionRegistry_ = std::make_unique(); + + // Create DBC layout + dbcLayout_ = std::make_unique(); + + // Create HD pack manager + hdPackManager_ = std::make_unique(); + // Create asset manager assetManager = std::make_unique(); @@ -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(recIdx), 1); - // DBC field 3 = modelTexture_1 (e.g. "Sword_1H_Short_A_02Rusty") - std::string textureName = displayInfoDbc->getString(static_cast(recIdx), 3); + const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + std::string modelName = displayInfoDbc->getString(static_cast(recIdx), idiL ? (*idiL)["LeftModel"] : 1); + std::string textureName = displayInfoDbc->getString(static_cast(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 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 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(cdie->getUInt32(i, 1)); - extra.sexId = static_cast(cdie->getUInt32(i, 2)); - extra.skinId = static_cast(cdie->getUInt32(i, 3)); - extra.faceId = static_cast(cdie->getUInt32(i, 4)); - extra.hairStyleId = static_cast(cdie->getUInt32(i, 5)); - extra.hairColorId = static_cast(cdie->getUInt32(i, 6)); - extra.facialHairId = static_cast(cdie->getUInt32(i, 7)); - // Equipment display IDs (columns 8-18) + extra.raceId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["RaceID"] : 1)); + extra.sexId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["SexID"] : 2)); + extra.skinId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["SkinID"] : 3)); + extra.faceId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["FaceID"] : 4)); + extra.hairStyleId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["HairStyleID"] : 5)); + extra.hairColorId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["HairColorID"] : 6)); + extra.facialHairId = static_cast(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(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(cfh->getUInt32(i, 3)); - fhg.geoset300 = static_cast(cfh->getUInt32(i, 4)); - fhg.geoset200 = static_cast(cfh->getUInt32(i, 5)); + fhg.geoset100 = static_cast(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset100"] : 3)); + fhg.geoset300 = static_cast(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset300"] : 4)); + fhg.geoset200 = static_cast(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(extra.raceId); uint32_t targetSex = static_cast(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(extra.hairStyleId)) continue; if (colorIdx != static_cast(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(idx), 7); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); if (gg > 0) geosetChest = static_cast(501 + gg); // Robes: GeosetGroup[2] > 0 shows kilt legs - uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast(idx), 9); + uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast(idx), fGG3); if (gg3 > 0) geosetPants = static_cast(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(idx), 7); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); if (gg > 0) geosetPants = static_cast(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(idx), 7); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); if (gg > 0) geosetBoots = static_cast(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(idx), 7); + uint32_t gg = itemDisplayDbc->getUInt32(static_cast(idx), fGG1); if (gg > 0) geosetGloves = static_cast(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(helmIdx), 1); + // Get helmet model name from ItemDisplayInfo.dbc (LeftModel) + std::string helmModelName = itemDisplayDbc->getString(static_cast(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(helmIdx), 3); + // Get texture from ItemDisplayInfo (LeftModelTexture) + std::string helmTexName = itemDisplayDbc->getString(static_cast(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(helmIdx), 1); + std::string helmModelName = itemDisplayDbc->getString(static_cast(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(helmIdx), 3); + std::string helmTexName = itemDisplayDbc->getString(static_cast(helmIdx), idiL2 ? (*idiL2)["LeftModelTexture"] : 3); std::string helmTexPath; if (!helmTexName.empty()) { if (!raceSuffix.empty()) { diff --git a/src/game/expansion_profile.cpp b/src/game/expansion_profile.cpp new file mode 100644 index 00000000..63b30aeb --- /dev/null +++ b/src/game/expansion_profile.cpp @@ -0,0 +1,185 @@ +#include "game/expansion_profile.hpp" +#include "core/logger.hpp" +#include +#include +#include +#include + +// 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 jsonUintArray(const std::string& json, const std::string& key) { + std::vector 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(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(f)), std::istreambuf_iterator()); + + 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(jsonInt(ver, "major")); + p.minorVersion = static_cast(jsonInt(ver, "minor")); + p.patchVersion = static_cast(jsonInt(ver, "patch")); + } + + p.build = static_cast(jsonInt(json, "build")); + p.protocolVersion = static_cast(jsonInt(json, "protocolVersion")); + p.maxLevel = static_cast(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 diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7eee2924..a56d955f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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(Opcode::SMSG_AUTH_CHALLENGE): - case static_cast(Opcode::SMSG_AUTH_RESPONSE): - case static_cast(Opcode::SMSG_CLIENTCACHE_VERSION): - case static_cast(Opcode::SMSG_TUTORIAL_FLAGS): - case static_cast(Opcode::SMSG_WARDEN_DATA): - case static_cast(Opcode::SMSG_CHAR_ENUM): - case static_cast(Opcode::SMSG_CHAR_CREATE): - case static_cast(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(); + // Initialize transport manager transportManager_ = std::make_unique(); @@ -92,6 +106,10 @@ GameHandler::~GameHandler() { disconnect(); } +void GameHandler::setPacketParsers(std::unique_ptr parsers) { + packetParsers_ = std::move(parsers); +} + bool GameHandler::connect(const std::string& host, uint16_t port, const std::vector& sessionKey, @@ -542,10 +560,11 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint16_t opcode = packet.getOpcode(); - if (wardenGateSeen_ && opcode != static_cast(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); + // 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(entity); if (unit->getNpcFlags() & 0x02) { - network::Packet qsPkt(static_cast(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(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 encryptedResponse = wardenCrypto_->encrypt(hashResponse); // Send HASH_RESULT response - network::Packet response(static_cast(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(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(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(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(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(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(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(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(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(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(it2->second) << 32); } @@ -4581,8 +4608,7 @@ void GameHandler::detectInventorySlotBases(const std::map& 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(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& f bool GameHandler::applyInventoryFields(const std::map& 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(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD)); + int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast(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(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(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(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(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(Opcode::CMSG_QUESTLOG_REMOVE_QUEST)); + network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST)); pkt.writeUInt8(static_cast(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(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(talentDbc->getUInt32(i, 2)); - entry.column = static_cast(talentDbc->getUInt32(i, 3)); + entry.tabId = talentDbc->getUInt32(i, tTabID); + entry.row = static_cast(talentDbc->getUInt32(i, tRow)); + entry.column = static_cast(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(talentDbc->getUInt32(i, 12 + p)); + entry.prereqTalent[p] = talentDbc->getUInt32(i, tPrereq0 + p); + entry.prereqRank[p] = static_cast(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(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(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(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(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& 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 newSkills; @@ -7745,7 +7781,7 @@ void GameHandler::extractExploredZoneFields(const std::map& bool foundAny = false; for (size_t i = 0; i < PLAYER_EXPLORED_ZONES_COUNT; i++) { - const uint16_t fieldIdx = static_cast(PLAYER_EXPLORED_ZONES_START + i); + const uint16_t fieldIdx = static_cast(fieldIndex(UF::PLAYER_EXPLORED_ZONES_START) + i); auto it = fields.find(fieldIdx); if (it == fields.end()) continue; playerExploredZones_[i] = it->second; diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp new file mode 100644 index 00000000..d819a902 --- /dev/null +++ b/src/game/opcode_table.cpp @@ -0,0 +1,649 @@ +#include "game/opcode_table.hpp" +#include "core/logger.hpp" +#include +#include +#include +#include + +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 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(op); + for (size_t i = 0; i < kOpcodeNameCount; ++i) { + if (static_cast(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(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(f)), std::istreambuf_iterator()); + + 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(std::stoul(valStr, nullptr, 16)); + } else { + wire = static_cast(std::stoul(valStr)); + } + } catch (...) { + pos = valEnd + 1; + continue; + } + + auto logOp = nameToLogical(key); + if (logOp) { + uint16_t logIdx = static_cast(*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(op)); + return (it != logicalToWire_.end()) ? it->second : 0xFFFF; +} + +std::optional OpcodeTable::fromWire(uint16_t wireValue) const { + auto it = wireToLogical_.find(wireValue); + if (it != wireToLogical_.end()) { + return static_cast(it->second); + } + return std::nullopt; +} + +bool OpcodeTable::hasOpcode(LogicalOpcode op) const { + return logicalToWire_.count(static_cast(op)) > 0; +} + +} // namespace game +} // namespace wowee diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp new file mode 100644 index 00000000..3aa8c454 --- /dev/null +++ b/src/game/packet_parsers_classic.cpp @@ -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(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(&info.x), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.y), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.z), sizeof(float)); + packet.writeBytes(reinterpret_cast(&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((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(&info.transportX), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.transportY), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.transportZ), sizeof(float)); + packet.writeBytes(reinterpret_cast(&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(&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(&info.jumpVelocity), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.jumpSinAngle), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.jumpCosAngle), sizeof(float)); + packet.writeBytes(reinterpret_cast(&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(packet.readUInt8()); + character.characterClass = static_cast(packet.readUInt8()); + character.gender = static_cast(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 diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp new file mode 100644 index 00000000..1d881806 --- /dev/null +++ b/src/game/packet_parsers_tbc.cpp @@ -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(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(info.flags2 & 0xFF)); + + // Timestamp + packet.writeUInt32(info.time); + + // Position + packet.writeBytes(reinterpret_cast(&info.x), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.y), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.z), sizeof(float)); + packet.writeBytes(reinterpret_cast(&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((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(&info.transportX), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.transportY), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.transportZ), sizeof(float)); + packet.writeBytes(reinterpret_cast(&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(&info.pitch), sizeof(float)); + } else if (info.flags & TbcMoveFlags::ONTRANSPORT) { + packet.writeBytes(reinterpret_cast(&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(&info.jumpVelocity), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.jumpSinAngle), sizeof(float)); + packet.writeBytes(reinterpret_cast(&info.jumpCosAngle), sizeof(float)); + packet.writeBytes(reinterpret_cast(&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(packet.readUInt8()); + character.characterClass = static_cast(packet.readUInt8()); + character.gender = static_cast(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(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 diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp new file mode 100644 index 00000000..a83f7c71 --- /dev/null +++ b/src/game/update_field_table.cpp @@ -0,0 +1,156 @@ +#include "game/update_field_table.hpp" +#include "core/logger.hpp" +#include +#include +#include + +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(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(f)), std::istreambuf_iterator()); + + 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(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(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(field)); + return (it != fieldMap_.end()) ? it->second : 0xFFFF; +} + +bool UpdateFieldTable::hasField(UF field) const { + return fieldMap_.count(static_cast(field)) > 0; +} + +} // namespace game +} // namespace wowee diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 61f8ff98..3fbbc16c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -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(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 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(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(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(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(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(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(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(Opcode::CMSG_MESSAGECHAT)); + network::Packet packet(wireOpcode(Opcode::CMSG_MESSAGECHAT)); // Write chat type packet.writeUInt32(static_cast(type)); @@ -1267,21 +1265,21 @@ const char* getChatTypeString(ChatType type) { // ============================================================ network::Packet SetSelectionPacket::build(uint64_t targetGuid) { - network::Packet packet(static_cast(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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& pathNodes) { - network::Packet packet(static_cast(Opcode::CMSG_ACTIVATETAXIEXPRESS)); + network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXIEXPRESS)); packet.writeUInt64(npcGuid); packet.writeUInt32(totalCost); packet.writeUInt32(static_cast(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(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(Opcode::CMSG_GAMEOBJECT_USE)); + network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJECT_USE)); packet.writeUInt64(guid); return packet; } diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 9a9cc5dc..1f93b464 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -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 lock(cacheMutex); + fileCache.clear(); + fileCacheTotalBytes = 0; + } + LOG_INFO("Removed overlay '", id, "', file cache cleared"); + } +} + +std::vector AssetManager::getOverlayIds() const { + std::vector 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 AssetManager::readFile(const std::string& path) const { @@ -240,8 +315,8 @@ std::vector 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 {}; } diff --git a/src/pipeline/dbc_layout.cpp b/src/pipeline/dbc_layout.cpp new file mode 100644 index 00000000..d001ed37 --- /dev/null +++ b/src/pipeline/dbc_layout.cpp @@ -0,0 +1,226 @@ +#include "pipeline/dbc_layout.hpp" +#include "core/logger.hpp" +#include +#include + +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(f)), std::istreambuf_iterator()); + + 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(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 diff --git a/src/pipeline/hd_pack_manager.cpp b/src/pipeline/hd_pack_manager.cpp new file mode 100644 index 00000000..0e068f87 --- /dev/null +++ b/src/pipeline/hd_pack_manager.cpp @@ -0,0 +1,204 @@ +#include "pipeline/hd_pack_manager.hpp" +#include "pipeline/asset_manager.hpp" +#include "core/logger.hpp" +#include +#include +#include +#include + +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(std::strtoul(json.c_str() + pos, nullptr, 10)); +} + +// Parse a JSON string array value +std::vector jsonStringArray(const std::string& json, const std::string& key) { + std::vector 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(f)), std::istreambuf_iterator()); + + 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 HDPackManager::getPacksForExpansion(const std::string& expansionId) const { + std::vector 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 diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 7cecd5e0..c70e1d82 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -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 #include @@ -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(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(face) && colorIndex == static_cast(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(hairStyle) && colorIndex == static_cast(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(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); diff --git a/src/rendering/lighting_manager.cpp b/src/rendering/lighting_manager.cpp index 5195defb..beaef514 100644 --- a/src/rendering/lighting_manager.cpp +++ b/src/rendering/lighting_manager.cpp @@ -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 #include @@ -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(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(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); } } } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index b7279cde..1e76e15b 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -27,6 +27,7 @@ #include #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 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; diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 288aac49..9afc30e9 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -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(mapDbc->getUInt32(i, 0)); + mapID = static_cast(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 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(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(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, diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 2dc2be78..d5078986 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -5,6 +5,7 @@ #include "rendering/renderer.hpp" #include "pipeline/asset_manager.hpp" #include "audio/music_manager.hpp" +#include "game/expansion_profile.hpp" #include #include #include @@ -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(profiles.size())) { + preview = profiles[expansionIndex].shortName + " (" + profiles[expansionIndex].versionString() + ")"; + } + if (ImGui::BeginCombo("Expansion", preview.c_str())) { + for (int i = 0; i < static_cast(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(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(profiles.size()); ++i) { + if (profiles[i].id == val) { expansionIndex = i; break; } + } + } } } diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index 217b6ef7..b3583350 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -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 #include @@ -169,16 +170,17 @@ void CharacterCreateScreen::updateAppearanceRanges() { uint32_t targetRaceId = static_cast(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(colorIndex)); @@ -199,13 +201,13 @@ void CharacterCreateScreen::updateAppearanceRanges() { int faceMax = -1; std::vector 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(skin)) { faceMax = std::max(faceMax, static_cast(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(variation)); } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index dfc19292..04fc7801 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -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 #include @@ -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(recIdx), 3); + const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + std::string capeName = displayInfoDbc->getString(static_cast(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// 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(); } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 3addb533..95088fab 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -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 #include @@ -60,7 +61,8 @@ GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) { } // Field 5 = inventoryIcon_1 - std::string iconName = displayInfoDbc->getString(static_cast(recIdx), 5); + const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + std::string iconName = displayInfoDbc->getString(static_cast(recIdx), dispL ? (*dispL)["InventoryIcon"] : 5); if (iconName.empty()) { iconCache_[displayInfoId] = 0; return 0; diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index ad1bb84d..3bb9fd25 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -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 #include @@ -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; } diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index 741243f7..9532a388 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -4,6 +4,7 @@ #include "core/logger.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" +#include "pipeline/dbc_layout.hpp" #include #include @@ -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; } diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index c804139a..a63bab01 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -13,6 +13,8 @@ static void printUsage(const char* prog) { << " --output Output directory for extracted assets\n" << "\n" << "Options:\n" + << " --expansion Expansion ID (classic/tbc/wotlk/cata).\n" + << " Output goes to /expansions//\n" << " --verify CRC32 verify all extracted files\n" << " --threads 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// 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";