mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add multi-expansion support with data-driven protocol layer
Replace hardcoded WotLK protocol constants with a data-driven architecture supporting Classic 1.12.1, TBC 2.4.3, and WotLK 3.3.5a. Each expansion has JSON profiles for opcodes, update fields, and DBC layouts, plus C++ polymorphic packet parsers for binary format differences (movement flags, speed fields, transport data, spline format, char enum layout). Key components: - ExpansionRegistry: scans Data/expansions/*/expansion.json at startup - OpcodeTable: logical enum <-> wire values loaded from JSON - UpdateFieldTable: field indices loaded from JSON per expansion - DBCLayout: schema-driven DBC field lookups replacing magic numbers - PacketParsers: WotLK/TBC/Classic parsers with correct flag positions - Multi-manifest AssetManager: layered manifests with priority ordering - HDPackManager: overlay texture packs with expansion compatibility - Auth screen expansion picker replacing hardcoded version dropdown
This commit is contained in:
parent
aa16a687c2
commit
7092844b5e
51 changed files with 5258 additions and 887 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -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/
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
92
Data/expansions/classic/dbc_layouts.json
Normal file
92
Data/expansions/classic/dbc_layouts.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||
"Name": 127, "Tooltip": 154, "Rank": 136
|
||||
},
|
||||
"ItemDisplayInfo": {
|
||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9
|
||||
},
|
||||
"CharSections": {
|
||||
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
||||
"Texture1": 4, "Texture2": 5, "Texture3": 6,
|
||||
"VariationIndex": 8, "ColorIndex": 9
|
||||
},
|
||||
"SpellIcon": { "ID": 0, "Path": 1 },
|
||||
"FactionTemplate": {
|
||||
"ID": 0, "Faction": 1, "FactionGroup": 3,
|
||||
"FriendGroup": 4, "EnemyGroup": 5,
|
||||
"Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9
|
||||
},
|
||||
"Faction": {
|
||||
"ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3,
|
||||
"ReputationRaceMask2": 4, "ReputationRaceMask3": 5,
|
||||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
|
||||
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
|
||||
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
|
||||
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
|
||||
},
|
||||
"CreatureDisplayInfo": {
|
||||
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
|
||||
"Skin1": 6, "Skin2": 7, "Skin3": 8
|
||||
},
|
||||
"TaxiNodes": {
|
||||
"ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5,
|
||||
"MountDisplayIdAlliance": 12, "MountDisplayIdHorde": 13
|
||||
},
|
||||
"TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 },
|
||||
"TaxiPathNode": {
|
||||
"ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3,
|
||||
"X": 4, "Y": 5, "Z": 6
|
||||
},
|
||||
"TalentTab": {
|
||||
"ID": 0, "Name": 1, "ClassMask": 12,
|
||||
"OrderIndex": 14, "BackgroundFile": 15
|
||||
},
|
||||
"Talent": {
|
||||
"ID": 0, "TabID": 1, "Row": 2, "Column": 3,
|
||||
"RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12
|
||||
},
|
||||
"SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 },
|
||||
"SkillLine": { "ID": 0, "Category": 1, "Name": 3 },
|
||||
"Map": { "ID": 0, "InternalName": 1 },
|
||||
"CreatureModelData": { "ID": 0, "ModelPath": 2 },
|
||||
"CharHairGeosets": {
|
||||
"RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4
|
||||
},
|
||||
"CharacterFacialHairStyles": {
|
||||
"RaceID": 0, "SexID": 1, "Variation": 2,
|
||||
"Geoset100": 3, "Geoset300": 4, "Geoset200": 5
|
||||
},
|
||||
"GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 },
|
||||
"Emotes": { "ID": 0, "AnimID": 2 },
|
||||
"EmotesText": {
|
||||
"Command": 1, "EmoteRef": 2,
|
||||
"SenderTargetTextID": 5, "SenderNoTargetTextID": 9
|
||||
},
|
||||
"EmotesTextData": { "ID": 0, "Text": 1 },
|
||||
"Light": {
|
||||
"ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4,
|
||||
"InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7,
|
||||
"LightParamsIDRain": 8, "LightParamsIDUnderwater": 9
|
||||
},
|
||||
"LightParams": { "LightParamsID": 0 },
|
||||
"LightIntBand": {
|
||||
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
|
||||
},
|
||||
"LightFloatBand": {
|
||||
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
|
||||
},
|
||||
"WorldMapArea": {
|
||||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
}
|
||||
}
|
||||
11
Data/expansions/classic/expansion.json
Normal file
11
Data/expansions/classic/expansion.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "classic",
|
||||
"name": "Classic",
|
||||
"shortName": "Classic",
|
||||
"version": { "major": 1, "minor": 12, "patch": 1 },
|
||||
"build": 5875,
|
||||
"protocolVersion": 8,
|
||||
"maxLevel": 60,
|
||||
"races": [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
"classes": [1, 2, 3, 4, 5, 7, 8, 9, 11]
|
||||
}
|
||||
226
Data/expansions/classic/opcodes.json
Normal file
226
Data/expansions/classic/opcodes.json
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
{
|
||||
"CMSG_PING": "0x1DC",
|
||||
"CMSG_AUTH_SESSION": "0x1ED",
|
||||
"CMSG_CHAR_CREATE": "0x036",
|
||||
"CMSG_CHAR_ENUM": "0x037",
|
||||
"CMSG_CHAR_DELETE": "0x038",
|
||||
"CMSG_PLAYER_LOGIN": "0x03D",
|
||||
"CMSG_MOVE_START_FORWARD": "0x0B5",
|
||||
"CMSG_MOVE_START_BACKWARD": "0x0B6",
|
||||
"CMSG_MOVE_STOP": "0x0B7",
|
||||
"CMSG_MOVE_START_STRAFE_LEFT": "0x0B8",
|
||||
"CMSG_MOVE_START_STRAFE_RIGHT": "0x0B9",
|
||||
"CMSG_MOVE_STOP_STRAFE": "0x0BA",
|
||||
"CMSG_MOVE_JUMP": "0x0BB",
|
||||
"CMSG_MOVE_START_TURN_LEFT": "0x0BC",
|
||||
"CMSG_MOVE_START_TURN_RIGHT": "0x0BD",
|
||||
"CMSG_MOVE_STOP_TURN": "0x0BE",
|
||||
"CMSG_MOVE_SET_FACING": "0x0DA",
|
||||
"CMSG_MOVE_FALL_LAND": "0x0C9",
|
||||
"CMSG_MOVE_START_SWIM": "0x0CA",
|
||||
"CMSG_MOVE_STOP_SWIM": "0x0CB",
|
||||
"CMSG_MOVE_HEARTBEAT": "0x0EE",
|
||||
"SMSG_AUTH_CHALLENGE": "0x1EC",
|
||||
"SMSG_AUTH_RESPONSE": "0x1EE",
|
||||
"SMSG_CHAR_CREATE": "0x03A",
|
||||
"SMSG_CHAR_ENUM": "0x03B",
|
||||
"SMSG_CHAR_DELETE": "0x03C",
|
||||
"SMSG_PONG": "0x1DD",
|
||||
"SMSG_LOGIN_VERIFY_WORLD": "0x236",
|
||||
"SMSG_LOGIN_SETTIMESPEED": "0x042",
|
||||
"SMSG_TUTORIAL_FLAGS": "0x0FD",
|
||||
"SMSG_WARDEN_DATA": "0x2E6",
|
||||
"CMSG_WARDEN_DATA": "0x2E7",
|
||||
"SMSG_ACCOUNT_DATA_TIMES": "0x209",
|
||||
"SMSG_UPDATE_OBJECT": "0x0A9",
|
||||
"SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6",
|
||||
"SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE",
|
||||
"SMSG_DESTROY_OBJECT": "0x0AA",
|
||||
"CMSG_MESSAGECHAT": "0x095",
|
||||
"SMSG_MESSAGECHAT": "0x096",
|
||||
"CMSG_WHO": "0x062",
|
||||
"SMSG_WHO": "0x063",
|
||||
"CMSG_REQUEST_PLAYED_TIME": "0x1CC",
|
||||
"SMSG_PLAYED_TIME": "0x1CD",
|
||||
"CMSG_QUERY_TIME": "0x1CE",
|
||||
"SMSG_QUERY_TIME_RESPONSE": "0x1CF",
|
||||
"SMSG_FRIEND_STATUS": "0x068",
|
||||
"CMSG_ADD_FRIEND": "0x069",
|
||||
"CMSG_DEL_FRIEND": "0x06A",
|
||||
"CMSG_ADD_IGNORE": "0x06C",
|
||||
"CMSG_DEL_IGNORE": "0x06D",
|
||||
"CMSG_PLAYER_LOGOUT": "0x04A",
|
||||
"CMSG_LOGOUT_REQUEST": "0x04B",
|
||||
"CMSG_LOGOUT_CANCEL": "0x04E",
|
||||
"SMSG_LOGOUT_RESPONSE": "0x04C",
|
||||
"SMSG_LOGOUT_COMPLETE": "0x04D",
|
||||
"CMSG_STAND_STATE_CHANGE": "0x101",
|
||||
"CMSG_SHOWING_HELM": "0x2B9",
|
||||
"CMSG_SHOWING_CLOAK": "0x2BA",
|
||||
"CMSG_TOGGLE_PVP": "0x253",
|
||||
"CMSG_GUILD_INVITE": "0x082",
|
||||
"CMSG_GUILD_ACCEPT": "0x084",
|
||||
"CMSG_GUILD_DECLINE_INVITATION": "0x085",
|
||||
"CMSG_GUILD_INFO": "0x087",
|
||||
"CMSG_GUILD_GET_ROSTER": "0x089",
|
||||
"CMSG_GUILD_PROMOTE_MEMBER": "0x08B",
|
||||
"CMSG_GUILD_DEMOTE_MEMBER": "0x08C",
|
||||
"CMSG_GUILD_LEAVE": "0x08D",
|
||||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
"CMSG_DUEL_ACCEPTED": "0x16C",
|
||||
"CMSG_DUEL_CANCELLED": "0x16D",
|
||||
"SMSG_DUEL_REQUESTED": "0x167",
|
||||
"CMSG_INITIATE_TRADE": "0x116",
|
||||
"MSG_RANDOM_ROLL": "0x1FB",
|
||||
"CMSG_SET_SELECTION": "0x13D",
|
||||
"CMSG_NAME_QUERY": "0x050",
|
||||
"SMSG_NAME_QUERY_RESPONSE": "0x051",
|
||||
"CMSG_CREATURE_QUERY": "0x060",
|
||||
"SMSG_CREATURE_QUERY_RESPONSE": "0x061",
|
||||
"CMSG_GAMEOBJECT_QUERY": "0x05E",
|
||||
"SMSG_GAMEOBJECT_QUERY_RESPONSE": "0x05F",
|
||||
"CMSG_SET_ACTIVE_MOVER": "0x26A",
|
||||
"CMSG_BINDER_ACTIVATE": "0x1B5",
|
||||
"SMSG_LOG_XPGAIN": "0x1D0",
|
||||
"SMSG_MONSTER_MOVE": "0x0DD",
|
||||
"CMSG_ATTACKSWING": "0x141",
|
||||
"CMSG_ATTACKSTOP": "0x142",
|
||||
"SMSG_ATTACKSTART": "0x143",
|
||||
"SMSG_ATTACKSTOP": "0x144",
|
||||
"SMSG_ATTACKERSTATEUPDATE": "0x14A",
|
||||
"SMSG_SPELLNONMELEEDAMAGELOG": "0x250",
|
||||
"SMSG_SPELLHEALLOG": "0x150",
|
||||
"SMSG_SPELLENERGIZELOG": "0x151",
|
||||
"SMSG_PERIODICAURALOG": "0x24E",
|
||||
"SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC",
|
||||
"CMSG_CAST_SPELL": "0x12E",
|
||||
"CMSG_CANCEL_CAST": "0x12F",
|
||||
"CMSG_CANCEL_AURA": "0x136",
|
||||
"SMSG_CAST_FAILED": "0x130",
|
||||
"SMSG_SPELL_START": "0x131",
|
||||
"SMSG_SPELL_GO": "0x132",
|
||||
"SMSG_SPELL_FAILURE": "0x133",
|
||||
"SMSG_SPELL_COOLDOWN": "0x134",
|
||||
"SMSG_COOLDOWN_EVENT": "0x135",
|
||||
"SMSG_UPDATE_AURA_DURATION": "0x137",
|
||||
"SMSG_INITIAL_SPELLS": "0x12A",
|
||||
"SMSG_LEARNED_SPELL": "0x12B",
|
||||
"SMSG_SUPERCEDED_SPELL": "0x12C",
|
||||
"SMSG_REMOVED_SPELL": "0x203",
|
||||
"SMSG_SPELL_DELAYED": "0x1E2",
|
||||
"SMSG_SET_FLAT_SPELL_MODIFIER": "0x266",
|
||||
"SMSG_SET_PCT_SPELL_MODIFIER": "0x267",
|
||||
"CMSG_LEARN_TALENT": "0x251",
|
||||
"MSG_TALENT_WIPE_CONFIRM": "0x2AA",
|
||||
"CMSG_GROUP_INVITE": "0x06E",
|
||||
"SMSG_GROUP_INVITE": "0x06F",
|
||||
"CMSG_GROUP_ACCEPT": "0x072",
|
||||
"CMSG_GROUP_DECLINE": "0x073",
|
||||
"SMSG_GROUP_DECLINE": "0x074",
|
||||
"CMSG_GROUP_UNINVITE_GUID": "0x076",
|
||||
"SMSG_GROUP_UNINVITE": "0x077",
|
||||
"CMSG_GROUP_SET_LEADER": "0x078",
|
||||
"SMSG_GROUP_SET_LEADER": "0x079",
|
||||
"CMSG_GROUP_DISBAND": "0x07B",
|
||||
"SMSG_GROUP_LIST": "0x07D",
|
||||
"SMSG_PARTY_COMMAND_RESULT": "0x07F",
|
||||
"MSG_RAID_TARGET_UPDATE": "0x321",
|
||||
"CMSG_REQUEST_RAID_INFO": "0x2CD",
|
||||
"SMSG_RAID_INSTANCE_INFO": "0x2CC",
|
||||
"CMSG_AUTOSTORE_LOOT_ITEM": "0x108",
|
||||
"CMSG_LOOT": "0x15D",
|
||||
"CMSG_LOOT_MONEY": "0x15E",
|
||||
"CMSG_LOOT_RELEASE": "0x15F",
|
||||
"SMSG_LOOT_RESPONSE": "0x160",
|
||||
"SMSG_LOOT_RELEASE_RESPONSE": "0x161",
|
||||
"SMSG_LOOT_REMOVED": "0x162",
|
||||
"SMSG_LOOT_MONEY_NOTIFY": "0x163",
|
||||
"SMSG_LOOT_CLEAR_MONEY": "0x165",
|
||||
"CMSG_ACTIVATETAXI": "0x1AD",
|
||||
"CMSG_GOSSIP_HELLO": "0x17B",
|
||||
"CMSG_GOSSIP_SELECT_OPTION": "0x17C",
|
||||
"SMSG_GOSSIP_MESSAGE": "0x17D",
|
||||
"SMSG_GOSSIP_COMPLETE": "0x17E",
|
||||
"SMSG_NPC_TEXT_UPDATE": "0x180",
|
||||
"CMSG_GAMEOBJECT_USE": "0x0B1",
|
||||
"CMSG_QUESTGIVER_STATUS_QUERY": "0x182",
|
||||
"SMSG_QUESTGIVER_STATUS": "0x183",
|
||||
"CMSG_QUESTGIVER_HELLO": "0x184",
|
||||
"CMSG_QUESTGIVER_QUERY_QUEST": "0x186",
|
||||
"SMSG_QUESTGIVER_QUEST_DETAILS": "0x188",
|
||||
"CMSG_QUESTGIVER_ACCEPT_QUEST": "0x189",
|
||||
"CMSG_QUESTGIVER_COMPLETE_QUEST": "0x18A",
|
||||
"SMSG_QUESTGIVER_REQUEST_ITEMS": "0x18B",
|
||||
"CMSG_QUESTGIVER_REQUEST_REWARD": "0x18C",
|
||||
"SMSG_QUESTGIVER_OFFER_REWARD": "0x18D",
|
||||
"CMSG_QUESTGIVER_CHOOSE_REWARD": "0x18E",
|
||||
"SMSG_QUESTGIVER_QUEST_INVALID": "0x18F",
|
||||
"SMSG_QUESTGIVER_QUEST_COMPLETE": "0x191",
|
||||
"CMSG_QUESTLOG_REMOVE_QUEST": "0x194",
|
||||
"SMSG_QUESTUPDATE_ADD_KILL": "0x199",
|
||||
"SMSG_QUESTUPDATE_COMPLETE": "0x198",
|
||||
"CMSG_QUEST_QUERY": "0x05C",
|
||||
"SMSG_QUEST_QUERY_RESPONSE": "0x05D",
|
||||
"SMSG_QUESTLOG_FULL": "0x195",
|
||||
"CMSG_LIST_INVENTORY": "0x19E",
|
||||
"SMSG_LIST_INVENTORY": "0x19F",
|
||||
"CMSG_SELL_ITEM": "0x1A0",
|
||||
"SMSG_SELL_ITEM": "0x1A1",
|
||||
"CMSG_BUY_ITEM": "0x1A2",
|
||||
"SMSG_BUY_FAILED": "0x1A5",
|
||||
"CMSG_TRAINER_LIST": "0x1B0",
|
||||
"SMSG_TRAINER_LIST": "0x1B1",
|
||||
"CMSG_TRAINER_BUY_SPELL": "0x1B2",
|
||||
"SMSG_TRAINER_BUY_FAILED": "0x1B4",
|
||||
"CMSG_ITEM_QUERY_SINGLE": "0x056",
|
||||
"SMSG_ITEM_QUERY_SINGLE_RESPONSE": "0x058",
|
||||
"CMSG_USE_ITEM": "0x0AB",
|
||||
"CMSG_AUTOEQUIP_ITEM": "0x10A",
|
||||
"CMSG_SWAP_ITEM": "0x10C",
|
||||
"CMSG_SWAP_INV_ITEM": "0x10D",
|
||||
"SMSG_INVENTORY_CHANGE_FAILURE": "0x112",
|
||||
"CMSG_INSPECT": "0x114",
|
||||
"SMSG_INSPECT_RESULTS": "0x115",
|
||||
"CMSG_REPOP_REQUEST": "0x15A",
|
||||
"SMSG_RESURRECT_REQUEST": "0x15B",
|
||||
"CMSG_RESURRECT_RESPONSE": "0x15C",
|
||||
"CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C",
|
||||
"SMSG_SPIRIT_HEALER_CONFIRM": "0x222",
|
||||
"MSG_MOVE_TELEPORT_ACK": "0x0C7",
|
||||
"SMSG_TRANSFER_PENDING": "0x03F",
|
||||
"SMSG_NEW_WORLD": "0x03E",
|
||||
"MSG_MOVE_WORLDPORT_ACK": "0x0DC",
|
||||
"SMSG_TRANSFER_ABORTED": "0x040",
|
||||
"SMSG_FORCE_RUN_SPEED_CHANGE": "0x0E2",
|
||||
"CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x0E3",
|
||||
"SMSG_SHOWTAXINODES": "0x1A9",
|
||||
"SMSG_ACTIVATETAXIREPLY": "0x1AE",
|
||||
"SMSG_NEW_TAXI_PATH": "0x1AF",
|
||||
"CMSG_ACTIVATETAXIEXPRESS": "0x312",
|
||||
"CMSG_TAXINODE_STATUS_QUERY": "0x1AA",
|
||||
"SMSG_TAXINODE_STATUS": "0x1AB",
|
||||
"SMSG_TRAINER_BUY_SUCCEEDED": "0x1B3",
|
||||
"SMSG_BINDPOINTUPDATE": "0x155",
|
||||
"SMSG_SET_PROFICIENCY": "0x127",
|
||||
"SMSG_ACTION_BUTTONS": "0x129",
|
||||
"SMSG_LEVELUP_INFO": "0x1D4",
|
||||
"CMSG_UPDATE_ACCOUNT_DATA": "0x20B",
|
||||
"CMSG_BATTLEFIELD_LIST": "0x23C",
|
||||
"SMSG_BATTLEFIELD_LIST": "0x23D",
|
||||
"CMSG_BATTLEFIELD_JOIN": "0x23E",
|
||||
"CMSG_BATTLEFIELD_STATUS": "0x2D3",
|
||||
"SMSG_BATTLEFIELD_STATUS": "0x2D4",
|
||||
"CMSG_BATTLEFIELD_PORT": "0x2D5",
|
||||
"CMSG_BATTLEMASTER_HELLO": "0x2D7",
|
||||
"MSG_PVP_LOG_DATA": "0x2E0",
|
||||
"CMSG_LEAVE_BATTLEFIELD": "0x2E1",
|
||||
"SMSG_GROUP_JOINED_BATTLEGROUND": "0x2E8",
|
||||
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9",
|
||||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE"
|
||||
}
|
||||
29
Data/expansions/classic/update_fields.json
Normal file
29
Data/expansions/classic/update_fields.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_HEALTH": 22,
|
||||
"UNIT_FIELD_POWER1": 23,
|
||||
"UNIT_FIELD_MAXHEALTH": 28,
|
||||
"UNIT_FIELD_MAXPOWER1": 29,
|
||||
"UNIT_FIELD_LEVEL": 34,
|
||||
"UNIT_FIELD_FACTIONTEMPLATE": 35,
|
||||
"UNIT_FIELD_FLAGS": 46,
|
||||
"UNIT_FIELD_DISPLAYID": 131,
|
||||
"UNIT_FIELD_MOUNTDISPLAYID": 133,
|
||||
"UNIT_NPC_FLAGS": 147,
|
||||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_END": 188,
|
||||
"PLAYER_FLAGS": 190,
|
||||
"PLAYER_XP": 716,
|
||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||
"PLAYER_FIELD_COINAGE": 1176,
|
||||
"PLAYER_QUEST_LOG_START": 198,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
|
||||
"PLAYER_FIELD_PACK_SLOT_1": 532,
|
||||
"PLAYER_SKILL_INFO_START": 718,
|
||||
"PLAYER_EXPLORED_ZONES_START": 1111,
|
||||
"PLAYER_END": 1282,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14
|
||||
}
|
||||
93
Data/expansions/tbc/dbc_layouts.json
Normal file
93
Data/expansions/tbc/dbc_layouts.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"Spell": {
|
||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
||||
"Name": 127, "Tooltip": 154, "Rank": 136
|
||||
},
|
||||
"ItemDisplayInfo": {
|
||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9
|
||||
},
|
||||
"CharSections": {
|
||||
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
||||
"Texture1": 4, "Texture2": 5, "Texture3": 6,
|
||||
"VariationIndex": 8, "ColorIndex": 9
|
||||
},
|
||||
"SpellIcon": { "ID": 0, "Path": 1 },
|
||||
"FactionTemplate": {
|
||||
"ID": 0, "Faction": 1, "FactionGroup": 3,
|
||||
"FriendGroup": 4, "EnemyGroup": 5,
|
||||
"Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9
|
||||
},
|
||||
"Faction": {
|
||||
"ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3,
|
||||
"ReputationRaceMask2": 4, "ReputationRaceMask3": 5,
|
||||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
|
||||
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
|
||||
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
|
||||
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
|
||||
},
|
||||
"CreatureDisplayInfo": {
|
||||
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
|
||||
"Skin1": 6, "Skin2": 7, "Skin3": 8
|
||||
},
|
||||
"TaxiNodes": {
|
||||
"ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5,
|
||||
"MountDisplayIdAllianceFallback": 12, "MountDisplayIdHordeFallback": 13,
|
||||
"MountDisplayIdAlliance": 14, "MountDisplayIdHorde": 15
|
||||
},
|
||||
"TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 },
|
||||
"TaxiPathNode": {
|
||||
"ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3,
|
||||
"X": 4, "Y": 5, "Z": 6
|
||||
},
|
||||
"TalentTab": {
|
||||
"ID": 0, "Name": 1, "ClassMask": 12,
|
||||
"OrderIndex": 14, "BackgroundFile": 15
|
||||
},
|
||||
"Talent": {
|
||||
"ID": 0, "TabID": 1, "Row": 2, "Column": 3,
|
||||
"RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12
|
||||
},
|
||||
"SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 },
|
||||
"SkillLine": { "ID": 0, "Category": 1, "Name": 3 },
|
||||
"Map": { "ID": 0, "InternalName": 1 },
|
||||
"CreatureModelData": { "ID": 0, "ModelPath": 2 },
|
||||
"CharHairGeosets": {
|
||||
"RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4
|
||||
},
|
||||
"CharacterFacialHairStyles": {
|
||||
"RaceID": 0, "SexID": 1, "Variation": 2,
|
||||
"Geoset100": 3, "Geoset300": 4, "Geoset200": 5
|
||||
},
|
||||
"GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 },
|
||||
"Emotes": { "ID": 0, "AnimID": 2 },
|
||||
"EmotesText": {
|
||||
"Command": 1, "EmoteRef": 2,
|
||||
"SenderTargetTextID": 5, "SenderNoTargetTextID": 9
|
||||
},
|
||||
"EmotesTextData": { "ID": 0, "Text": 1 },
|
||||
"Light": {
|
||||
"ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4,
|
||||
"InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7,
|
||||
"LightParamsIDRain": 8, "LightParamsIDUnderwater": 9
|
||||
},
|
||||
"LightParams": { "LightParamsID": 0 },
|
||||
"LightIntBand": {
|
||||
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
|
||||
},
|
||||
"LightFloatBand": {
|
||||
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
|
||||
},
|
||||
"WorldMapArea": {
|
||||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
}
|
||||
}
|
||||
11
Data/expansions/tbc/expansion.json
Normal file
11
Data/expansions/tbc/expansion.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "tbc",
|
||||
"name": "The Burning Crusade",
|
||||
"shortName": "TBC",
|
||||
"version": { "major": 2, "minor": 4, "patch": 3 },
|
||||
"build": 8606,
|
||||
"protocolVersion": 8,
|
||||
"maxLevel": 70,
|
||||
"races": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
|
||||
"classes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
|
||||
}
|
||||
255
Data/expansions/tbc/opcodes.json
Normal file
255
Data/expansions/tbc/opcodes.json
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
{
|
||||
"CMSG_PING": "0x1DC",
|
||||
"CMSG_AUTH_SESSION": "0x1ED",
|
||||
"CMSG_CHAR_CREATE": "0x036",
|
||||
"CMSG_CHAR_ENUM": "0x037",
|
||||
"CMSG_CHAR_DELETE": "0x038",
|
||||
"CMSG_PLAYER_LOGIN": "0x03D",
|
||||
"CMSG_MOVE_START_FORWARD": "0x0B5",
|
||||
"CMSG_MOVE_START_BACKWARD": "0x0B6",
|
||||
"CMSG_MOVE_STOP": "0x0B7",
|
||||
"CMSG_MOVE_START_STRAFE_LEFT": "0x0B8",
|
||||
"CMSG_MOVE_START_STRAFE_RIGHT": "0x0B9",
|
||||
"CMSG_MOVE_STOP_STRAFE": "0x0BA",
|
||||
"CMSG_MOVE_JUMP": "0x0BB",
|
||||
"CMSG_MOVE_START_TURN_LEFT": "0x0BC",
|
||||
"CMSG_MOVE_START_TURN_RIGHT": "0x0BD",
|
||||
"CMSG_MOVE_STOP_TURN": "0x0BE",
|
||||
"CMSG_MOVE_SET_FACING": "0x0DA",
|
||||
"CMSG_MOVE_FALL_LAND": "0x0C9",
|
||||
"CMSG_MOVE_START_SWIM": "0x0CA",
|
||||
"CMSG_MOVE_STOP_SWIM": "0x0CB",
|
||||
"CMSG_MOVE_HEARTBEAT": "0x0EE",
|
||||
"SMSG_AUTH_CHALLENGE": "0x1EC",
|
||||
"SMSG_AUTH_RESPONSE": "0x1EE",
|
||||
"SMSG_CHAR_CREATE": "0x03A",
|
||||
"SMSG_CHAR_ENUM": "0x03B",
|
||||
"SMSG_CHAR_DELETE": "0x03C",
|
||||
"SMSG_PONG": "0x1DD",
|
||||
"SMSG_LOGIN_VERIFY_WORLD": "0x236",
|
||||
"SMSG_LOGIN_SETTIMESPEED": "0x042",
|
||||
"SMSG_TUTORIAL_FLAGS": "0x0FD",
|
||||
"SMSG_WARDEN_DATA": "0x2E6",
|
||||
"CMSG_WARDEN_DATA": "0x2E7",
|
||||
"SMSG_ACCOUNT_DATA_TIMES": "0x209",
|
||||
"SMSG_MOTD": "0x33D",
|
||||
"SMSG_UPDATE_OBJECT": "0x0A9",
|
||||
"SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6",
|
||||
"SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE",
|
||||
"SMSG_DESTROY_OBJECT": "0x0AA",
|
||||
"CMSG_MESSAGECHAT": "0x095",
|
||||
"SMSG_MESSAGECHAT": "0x096",
|
||||
"CMSG_WHO": "0x062",
|
||||
"SMSG_WHO": "0x063",
|
||||
"CMSG_REQUEST_PLAYED_TIME": "0x1CC",
|
||||
"SMSG_PLAYED_TIME": "0x1CD",
|
||||
"CMSG_QUERY_TIME": "0x1CE",
|
||||
"SMSG_QUERY_TIME_RESPONSE": "0x1CF",
|
||||
"SMSG_FRIEND_STATUS": "0x068",
|
||||
"CMSG_ADD_FRIEND": "0x069",
|
||||
"CMSG_DEL_FRIEND": "0x06A",
|
||||
"CMSG_SET_CONTACT_NOTES": "0x06B",
|
||||
"CMSG_ADD_IGNORE": "0x06C",
|
||||
"CMSG_DEL_IGNORE": "0x06D",
|
||||
"CMSG_PLAYER_LOGOUT": "0x04A",
|
||||
"CMSG_LOGOUT_REQUEST": "0x04B",
|
||||
"CMSG_LOGOUT_CANCEL": "0x04E",
|
||||
"SMSG_LOGOUT_RESPONSE": "0x04C",
|
||||
"SMSG_LOGOUT_COMPLETE": "0x04D",
|
||||
"CMSG_STAND_STATE_CHANGE": "0x101",
|
||||
"CMSG_SHOWING_HELM": "0x2B9",
|
||||
"CMSG_SHOWING_CLOAK": "0x2BA",
|
||||
"CMSG_TOGGLE_PVP": "0x253",
|
||||
"CMSG_GUILD_INVITE": "0x082",
|
||||
"CMSG_GUILD_ACCEPT": "0x084",
|
||||
"CMSG_GUILD_DECLINE_INVITATION": "0x085",
|
||||
"CMSG_GUILD_INFO": "0x087",
|
||||
"CMSG_GUILD_GET_ROSTER": "0x089",
|
||||
"CMSG_GUILD_PROMOTE_MEMBER": "0x08B",
|
||||
"CMSG_GUILD_DEMOTE_MEMBER": "0x08C",
|
||||
"CMSG_GUILD_LEAVE": "0x08D",
|
||||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"MSG_RAID_READY_CHECK_CONFIRM": "0x3AE",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
"CMSG_DUEL_ACCEPTED": "0x16C",
|
||||
"CMSG_DUEL_CANCELLED": "0x16D",
|
||||
"SMSG_DUEL_REQUESTED": "0x167",
|
||||
"CMSG_INITIATE_TRADE": "0x116",
|
||||
"MSG_RANDOM_ROLL": "0x1FB",
|
||||
"CMSG_SET_SELECTION": "0x13D",
|
||||
"CMSG_NAME_QUERY": "0x050",
|
||||
"SMSG_NAME_QUERY_RESPONSE": "0x051",
|
||||
"CMSG_CREATURE_QUERY": "0x060",
|
||||
"SMSG_CREATURE_QUERY_RESPONSE": "0x061",
|
||||
"CMSG_GAMEOBJECT_QUERY": "0x05E",
|
||||
"SMSG_GAMEOBJECT_QUERY_RESPONSE": "0x05F",
|
||||
"CMSG_SET_ACTIVE_MOVER": "0x26A",
|
||||
"CMSG_BINDER_ACTIVATE": "0x1B5",
|
||||
"SMSG_LOG_XPGAIN": "0x1D0",
|
||||
"SMSG_MONSTER_MOVE": "0x0DD",
|
||||
"CMSG_ATTACKSWING": "0x141",
|
||||
"CMSG_ATTACKSTOP": "0x142",
|
||||
"SMSG_ATTACKSTART": "0x143",
|
||||
"SMSG_ATTACKSTOP": "0x144",
|
||||
"SMSG_ATTACKERSTATEUPDATE": "0x14A",
|
||||
"SMSG_SPELLNONMELEEDAMAGELOG": "0x250",
|
||||
"SMSG_SPELLHEALLOG": "0x150",
|
||||
"SMSG_SPELLENERGIZELOG": "0x25B",
|
||||
"SMSG_PERIODICAURALOG": "0x24E",
|
||||
"SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC",
|
||||
"CMSG_CAST_SPELL": "0x12E",
|
||||
"CMSG_CANCEL_CAST": "0x12F",
|
||||
"CMSG_CANCEL_AURA": "0x136",
|
||||
"SMSG_CAST_FAILED": "0x130",
|
||||
"SMSG_SPELL_START": "0x131",
|
||||
"SMSG_SPELL_GO": "0x132",
|
||||
"SMSG_SPELL_FAILURE": "0x133",
|
||||
"SMSG_SPELL_COOLDOWN": "0x134",
|
||||
"SMSG_COOLDOWN_EVENT": "0x135",
|
||||
"SMSG_UPDATE_AURA_DURATION": "0x137",
|
||||
"SMSG_INITIAL_SPELLS": "0x12A",
|
||||
"SMSG_LEARNED_SPELL": "0x12B",
|
||||
"SMSG_SUPERCEDED_SPELL": "0x12C",
|
||||
"SMSG_REMOVED_SPELL": "0x203",
|
||||
"SMSG_SPELL_DELAYED": "0x1E2",
|
||||
"SMSG_SET_FLAT_SPELL_MODIFIER": "0x266",
|
||||
"SMSG_SET_PCT_SPELL_MODIFIER": "0x267",
|
||||
"CMSG_LEARN_TALENT": "0x251",
|
||||
"MSG_TALENT_WIPE_CONFIRM": "0x2AB",
|
||||
"CMSG_GROUP_INVITE": "0x06E",
|
||||
"SMSG_GROUP_INVITE": "0x06F",
|
||||
"CMSG_GROUP_ACCEPT": "0x072",
|
||||
"CMSG_GROUP_DECLINE": "0x073",
|
||||
"SMSG_GROUP_DECLINE": "0x074",
|
||||
"CMSG_GROUP_UNINVITE_GUID": "0x076",
|
||||
"SMSG_GROUP_UNINVITE": "0x077",
|
||||
"CMSG_GROUP_SET_LEADER": "0x078",
|
||||
"SMSG_GROUP_SET_LEADER": "0x079",
|
||||
"CMSG_GROUP_DISBAND": "0x07B",
|
||||
"SMSG_GROUP_LIST": "0x07D",
|
||||
"SMSG_PARTY_COMMAND_RESULT": "0x07F",
|
||||
"MSG_RAID_TARGET_UPDATE": "0x321",
|
||||
"CMSG_REQUEST_RAID_INFO": "0x2CD",
|
||||
"SMSG_RAID_INSTANCE_INFO": "0x2CC",
|
||||
"CMSG_AUTOSTORE_LOOT_ITEM": "0x108",
|
||||
"CMSG_LOOT": "0x15D",
|
||||
"CMSG_LOOT_MONEY": "0x15E",
|
||||
"CMSG_LOOT_RELEASE": "0x15F",
|
||||
"SMSG_LOOT_RESPONSE": "0x160",
|
||||
"SMSG_LOOT_RELEASE_RESPONSE": "0x161",
|
||||
"SMSG_LOOT_REMOVED": "0x162",
|
||||
"SMSG_LOOT_MONEY_NOTIFY": "0x163",
|
||||
"SMSG_LOOT_CLEAR_MONEY": "0x165",
|
||||
"CMSG_ACTIVATETAXI": "0x1AD",
|
||||
"CMSG_GOSSIP_HELLO": "0x17B",
|
||||
"CMSG_GOSSIP_SELECT_OPTION": "0x17C",
|
||||
"SMSG_GOSSIP_MESSAGE": "0x17D",
|
||||
"SMSG_GOSSIP_COMPLETE": "0x17E",
|
||||
"SMSG_NPC_TEXT_UPDATE": "0x180",
|
||||
"CMSG_GAMEOBJECT_USE": "0x01B",
|
||||
"CMSG_QUESTGIVER_STATUS_QUERY": "0x182",
|
||||
"SMSG_QUESTGIVER_STATUS": "0x183",
|
||||
"SMSG_QUESTGIVER_STATUS_MULTIPLE": "0x198",
|
||||
"CMSG_QUESTGIVER_HELLO": "0x184",
|
||||
"CMSG_QUESTGIVER_QUERY_QUEST": "0x186",
|
||||
"SMSG_QUESTGIVER_QUEST_DETAILS": "0x188",
|
||||
"CMSG_QUESTGIVER_ACCEPT_QUEST": "0x189",
|
||||
"CMSG_QUESTGIVER_COMPLETE_QUEST": "0x18A",
|
||||
"SMSG_QUESTGIVER_REQUEST_ITEMS": "0x18B",
|
||||
"CMSG_QUESTGIVER_REQUEST_REWARD": "0x18C",
|
||||
"SMSG_QUESTGIVER_OFFER_REWARD": "0x18D",
|
||||
"CMSG_QUESTGIVER_CHOOSE_REWARD": "0x18E",
|
||||
"SMSG_QUESTGIVER_QUEST_INVALID": "0x18F",
|
||||
"SMSG_QUESTGIVER_QUEST_COMPLETE": "0x191",
|
||||
"CMSG_QUESTLOG_REMOVE_QUEST": "0x194",
|
||||
"SMSG_QUESTUPDATE_ADD_KILL": "0x196",
|
||||
"SMSG_QUESTUPDATE_COMPLETE": "0x195",
|
||||
"CMSG_QUEST_QUERY": "0x05C",
|
||||
"SMSG_QUEST_QUERY_RESPONSE": "0x05D",
|
||||
"SMSG_QUESTLOG_FULL": "0x1A3",
|
||||
"CMSG_LIST_INVENTORY": "0x19E",
|
||||
"SMSG_LIST_INVENTORY": "0x19F",
|
||||
"CMSG_SELL_ITEM": "0x1A0",
|
||||
"SMSG_SELL_ITEM": "0x1A1",
|
||||
"CMSG_BUY_ITEM": "0x1A2",
|
||||
"SMSG_BUY_FAILED": "0x1A5",
|
||||
"CMSG_TRAINER_LIST": "0x1B0",
|
||||
"SMSG_TRAINER_LIST": "0x1B1",
|
||||
"CMSG_TRAINER_BUY_SPELL": "0x1B2",
|
||||
"SMSG_TRAINER_BUY_FAILED": "0x1B4",
|
||||
"CMSG_ITEM_QUERY_SINGLE": "0x056",
|
||||
"SMSG_ITEM_QUERY_SINGLE_RESPONSE": "0x058",
|
||||
"CMSG_USE_ITEM": "0x0AB",
|
||||
"CMSG_AUTOEQUIP_ITEM": "0x10A",
|
||||
"CMSG_SWAP_ITEM": "0x10C",
|
||||
"CMSG_SWAP_INV_ITEM": "0x10D",
|
||||
"SMSG_INVENTORY_CHANGE_FAILURE": "0x112",
|
||||
"CMSG_INSPECT": "0x114",
|
||||
"SMSG_INSPECT_RESULTS": "0x115",
|
||||
"CMSG_REPOP_REQUEST": "0x15A",
|
||||
"SMSG_RESURRECT_REQUEST": "0x15B",
|
||||
"CMSG_RESURRECT_RESPONSE": "0x15C",
|
||||
"CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C",
|
||||
"SMSG_SPIRIT_HEALER_CONFIRM": "0x222",
|
||||
"SMSG_RESURRECT_CANCEL": "0x390",
|
||||
"MSG_MOVE_TELEPORT_ACK": "0x0C7",
|
||||
"SMSG_TRANSFER_PENDING": "0x03F",
|
||||
"SMSG_NEW_WORLD": "0x03E",
|
||||
"MSG_MOVE_WORLDPORT_ACK": "0x0DC",
|
||||
"SMSG_TRANSFER_ABORTED": "0x040",
|
||||
"SMSG_FORCE_RUN_SPEED_CHANGE": "0x0E2",
|
||||
"CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x0E3",
|
||||
"CMSG_CANCEL_MOUNT_AURA": "0x375",
|
||||
"SMSG_SHOWTAXINODES": "0x1A9",
|
||||
"SMSG_ACTIVATETAXIREPLY": "0x1AE",
|
||||
"SMSG_NEW_TAXI_PATH": "0x1AF",
|
||||
"CMSG_ACTIVATETAXIEXPRESS": "0x312",
|
||||
"SMSG_BATTLEFIELD_PORT_DENIED": "0x14B",
|
||||
"SMSG_REMOVED_FROM_PVP_QUEUE": "0x170",
|
||||
"SMSG_TRAINER_BUY_SUCCEEDED": "0x1B3",
|
||||
"SMSG_BINDPOINTUPDATE": "0x155",
|
||||
"CMSG_BATTLEFIELD_LIST": "0x23C",
|
||||
"SMSG_BATTLEFIELD_LIST": "0x23D",
|
||||
"CMSG_BATTLEFIELD_JOIN": "0x23E",
|
||||
"CMSG_BATTLEFIELD_STATUS": "0x2D3",
|
||||
"SMSG_BATTLEFIELD_STATUS": "0x2D4",
|
||||
"CMSG_BATTLEFIELD_PORT": "0x2D5",
|
||||
"CMSG_BATTLEMASTER_HELLO": "0x2D7",
|
||||
"MSG_PVP_LOG_DATA": "0x2E0",
|
||||
"CMSG_LEAVE_BATTLEFIELD": "0x2E1",
|
||||
"SMSG_GROUP_JOINED_BATTLEGROUND": "0x2E8",
|
||||
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9",
|
||||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
|
||||
"SMSG_JOINED_BATTLEGROUND_QUEUE": "0x38A",
|
||||
"CMSG_ARENA_TEAM_CREATE": "0x348",
|
||||
"SMSG_ARENA_TEAM_COMMAND_RESULT": "0x349",
|
||||
"CMSG_ARENA_TEAM_QUERY": "0x34B",
|
||||
"SMSG_ARENA_TEAM_QUERY_RESPONSE": "0x34C",
|
||||
"CMSG_ARENA_TEAM_ROSTER": "0x34D",
|
||||
"SMSG_ARENA_TEAM_ROSTER": "0x34E",
|
||||
"CMSG_ARENA_TEAM_INVITE": "0x34F",
|
||||
"SMSG_ARENA_TEAM_INVITE": "0x350",
|
||||
"CMSG_ARENA_TEAM_ACCEPT": "0x351",
|
||||
"CMSG_ARENA_TEAM_DECLINE": "0x352",
|
||||
"CMSG_ARENA_TEAM_LEAVE": "0x353",
|
||||
"CMSG_ARENA_TEAM_REMOVE": "0x354",
|
||||
"CMSG_ARENA_TEAM_DISBAND": "0x355",
|
||||
"CMSG_ARENA_TEAM_LEADER": "0x356",
|
||||
"SMSG_ARENA_TEAM_EVENT": "0x357",
|
||||
"CMSG_BATTLEMASTER_JOIN_ARENA": "0x358",
|
||||
"SMSG_ARENA_TEAM_STATS": "0x35B",
|
||||
"SMSG_ARENA_ERROR": "0x376",
|
||||
"MSG_INSPECT_ARENA_TEAMS": "0x377",
|
||||
"SMSG_LEVELUP_INFO": "0x1D4",
|
||||
"SMSG_SET_PROFICIENCY": "0x127",
|
||||
"SMSG_ACTION_BUTTONS": "0x129",
|
||||
"CMSG_TAXINODE_STATUS_QUERY": "0x1AA",
|
||||
"SMSG_TAXINODE_STATUS": "0x1AB",
|
||||
"SMSG_INIT_EXTRA_AURA_INFO": "0x3A3",
|
||||
"SMSG_SET_EXTRA_AURA_INFO": "0x3A4"
|
||||
}
|
||||
29
Data/expansions/tbc/update_fields.json
Normal file
29
Data/expansions/tbc/update_fields.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_HEALTH": 22,
|
||||
"UNIT_FIELD_POWER1": 23,
|
||||
"UNIT_FIELD_MAXHEALTH": 28,
|
||||
"UNIT_FIELD_MAXPOWER1": 29,
|
||||
"UNIT_FIELD_LEVEL": 34,
|
||||
"UNIT_FIELD_FACTIONTEMPLATE": 35,
|
||||
"UNIT_FIELD_FLAGS": 46,
|
||||
"UNIT_FIELD_FLAGS_2": 47,
|
||||
"UNIT_FIELD_DISPLAYID": 152,
|
||||
"UNIT_FIELD_MOUNTDISPLAYID": 154,
|
||||
"UNIT_NPC_FLAGS": 168,
|
||||
"UNIT_DYNAMIC_FLAGS": 164,
|
||||
"UNIT_END": 234,
|
||||
"PLAYER_FLAGS": 236,
|
||||
"PLAYER_XP": 926,
|
||||
"PLAYER_NEXT_LEVEL_XP": 927,
|
||||
"PLAYER_FIELD_COINAGE": 1441,
|
||||
"PLAYER_QUEST_LOG_START": 244,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 650,
|
||||
"PLAYER_FIELD_PACK_SLOT_1": 696,
|
||||
"PLAYER_SKILL_INFO_START": 928,
|
||||
"PLAYER_EXPLORED_ZONES_START": 1312,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14
|
||||
}
|
||||
93
Data/expansions/wotlk/dbc_layouts.json
Normal file
93
Data/expansions/wotlk/dbc_layouts.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"Spell": {
|
||||
"ID": 0, "Attributes": 4, "IconID": 133,
|
||||
"Name": 136, "Tooltip": 139, "Rank": 153
|
||||
},
|
||||
"ItemDisplayInfo": {
|
||||
"ID": 0, "LeftModel": 1, "LeftModelTexture": 3,
|
||||
"InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9
|
||||
},
|
||||
"CharSections": {
|
||||
"RaceID": 1, "SexID": 2, "BaseSection": 3,
|
||||
"Texture1": 4, "Texture2": 5, "Texture3": 6,
|
||||
"VariationIndex": 8, "ColorIndex": 9
|
||||
},
|
||||
"SpellIcon": { "ID": 0, "Path": 1 },
|
||||
"FactionTemplate": {
|
||||
"ID": 0, "Faction": 1, "FactionGroup": 3,
|
||||
"FriendGroup": 4, "EnemyGroup": 5,
|
||||
"Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9
|
||||
},
|
||||
"Faction": {
|
||||
"ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3,
|
||||
"ReputationRaceMask2": 4, "ReputationRaceMask3": 5,
|
||||
"ReputationBase0": 10, "ReputationBase1": 11,
|
||||
"ReputationBase2": 12, "ReputationBase3": 13
|
||||
},
|
||||
"AreaTable": { "ID": 0, "ExploreFlag": 3 },
|
||||
"CreatureDisplayInfoExtra": {
|
||||
"ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4,
|
||||
"HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7,
|
||||
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
|
||||
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
|
||||
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
|
||||
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
|
||||
},
|
||||
"CreatureDisplayInfo": {
|
||||
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
|
||||
"Skin1": 6, "Skin2": 7, "Skin3": 8
|
||||
},
|
||||
"TaxiNodes": {
|
||||
"ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5,
|
||||
"MountDisplayIdAllianceFallback": 20, "MountDisplayIdHordeFallback": 21,
|
||||
"MountDisplayIdAlliance": 22, "MountDisplayIdHorde": 23
|
||||
},
|
||||
"TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 },
|
||||
"TaxiPathNode": {
|
||||
"ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3,
|
||||
"X": 4, "Y": 5, "Z": 6
|
||||
},
|
||||
"TalentTab": {
|
||||
"ID": 0, "Name": 1, "ClassMask": 20,
|
||||
"OrderIndex": 22, "BackgroundFile": 23
|
||||
},
|
||||
"Talent": {
|
||||
"ID": 0, "TabID": 1, "Row": 2, "Column": 3,
|
||||
"RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12
|
||||
},
|
||||
"SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 },
|
||||
"SkillLine": { "ID": 0, "Category": 1, "Name": 3 },
|
||||
"Map": { "ID": 0, "InternalName": 1 },
|
||||
"CreatureModelData": { "ID": 0, "ModelPath": 2 },
|
||||
"CharHairGeosets": {
|
||||
"RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4
|
||||
},
|
||||
"CharacterFacialHairStyles": {
|
||||
"RaceID": 0, "SexID": 1, "Variation": 2,
|
||||
"Geoset100": 3, "Geoset300": 4, "Geoset200": 5
|
||||
},
|
||||
"GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 },
|
||||
"Emotes": { "ID": 0, "AnimID": 2 },
|
||||
"EmotesText": {
|
||||
"Command": 1, "EmoteRef": 2,
|
||||
"SenderTargetTextID": 5, "SenderNoTargetTextID": 9
|
||||
},
|
||||
"EmotesTextData": { "ID": 0, "Text": 1 },
|
||||
"Light": {
|
||||
"ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4,
|
||||
"InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7,
|
||||
"LightParamsIDRain": 8, "LightParamsIDUnderwater": 9
|
||||
},
|
||||
"LightParams": { "LightParamsID": 0 },
|
||||
"LightIntBand": {
|
||||
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
|
||||
},
|
||||
"LightFloatBand": {
|
||||
"BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19
|
||||
},
|
||||
"WorldMapArea": {
|
||||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
}
|
||||
}
|
||||
11
Data/expansions/wotlk/expansion.json
Normal file
11
Data/expansions/wotlk/expansion.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "wotlk",
|
||||
"name": "Wrath of the Lich King",
|
||||
"shortName": "WotLK",
|
||||
"version": { "major": 3, "minor": 3, "patch": 5 },
|
||||
"build": 12340,
|
||||
"protocolVersion": 8,
|
||||
"maxLevel": 80,
|
||||
"races": [1, 2, 3, 4, 5, 6, 7, 8, 10, 11],
|
||||
"classes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 11]
|
||||
}
|
||||
255
Data/expansions/wotlk/opcodes.json
Normal file
255
Data/expansions/wotlk/opcodes.json
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
{
|
||||
"CMSG_PING": "0x1DC",
|
||||
"CMSG_AUTH_SESSION": "0x1ED",
|
||||
"CMSG_CHAR_CREATE": "0x036",
|
||||
"CMSG_CHAR_ENUM": "0x037",
|
||||
"CMSG_CHAR_DELETE": "0x038",
|
||||
"CMSG_PLAYER_LOGIN": "0x03D",
|
||||
"CMSG_MOVE_START_FORWARD": "0x0B5",
|
||||
"CMSG_MOVE_START_BACKWARD": "0x0B6",
|
||||
"CMSG_MOVE_STOP": "0x0B7",
|
||||
"CMSG_MOVE_START_STRAFE_LEFT": "0x0B8",
|
||||
"CMSG_MOVE_START_STRAFE_RIGHT": "0x0B9",
|
||||
"CMSG_MOVE_STOP_STRAFE": "0x0BA",
|
||||
"CMSG_MOVE_JUMP": "0x0BB",
|
||||
"CMSG_MOVE_START_TURN_LEFT": "0x0BC",
|
||||
"CMSG_MOVE_START_TURN_RIGHT": "0x0BD",
|
||||
"CMSG_MOVE_STOP_TURN": "0x0BE",
|
||||
"CMSG_MOVE_SET_FACING": "0x0DA",
|
||||
"CMSG_MOVE_FALL_LAND": "0x0C9",
|
||||
"CMSG_MOVE_START_SWIM": "0x0CA",
|
||||
"CMSG_MOVE_STOP_SWIM": "0x0CB",
|
||||
"CMSG_MOVE_HEARTBEAT": "0x0EE",
|
||||
"SMSG_AUTH_CHALLENGE": "0x1EC",
|
||||
"SMSG_AUTH_RESPONSE": "0x1EE",
|
||||
"SMSG_CHAR_CREATE": "0x03A",
|
||||
"SMSG_CHAR_ENUM": "0x03B",
|
||||
"SMSG_CHAR_DELETE": "0x03C",
|
||||
"SMSG_PONG": "0x1DD",
|
||||
"SMSG_LOGIN_VERIFY_WORLD": "0x236",
|
||||
"SMSG_LOGIN_SETTIMESPEED": "0x042",
|
||||
"SMSG_TUTORIAL_FLAGS": "0x0FD",
|
||||
"SMSG_WARDEN_DATA": "0x2E6",
|
||||
"CMSG_WARDEN_DATA": "0x2E7",
|
||||
"SMSG_ACCOUNT_DATA_TIMES": "0x209",
|
||||
"SMSG_CLIENTCACHE_VERSION": "0x4AB",
|
||||
"SMSG_FEATURE_SYSTEM_STATUS": "0x3ED",
|
||||
"SMSG_MOTD": "0x33D",
|
||||
"SMSG_UPDATE_OBJECT": "0x0A9",
|
||||
"SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6",
|
||||
"SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE",
|
||||
"SMSG_DESTROY_OBJECT": "0x0AA",
|
||||
"CMSG_MESSAGECHAT": "0x095",
|
||||
"SMSG_MESSAGECHAT": "0x096",
|
||||
"CMSG_WHO": "0x062",
|
||||
"SMSG_WHO": "0x063",
|
||||
"CMSG_REQUEST_PLAYED_TIME": "0x1CC",
|
||||
"SMSG_PLAYED_TIME": "0x1CD",
|
||||
"CMSG_QUERY_TIME": "0x1CE",
|
||||
"SMSG_QUERY_TIME_RESPONSE": "0x1CF",
|
||||
"SMSG_FRIEND_STATUS": "0x068",
|
||||
"CMSG_ADD_FRIEND": "0x069",
|
||||
"CMSG_DEL_FRIEND": "0x06A",
|
||||
"CMSG_SET_CONTACT_NOTES": "0x06B",
|
||||
"CMSG_ADD_IGNORE": "0x06C",
|
||||
"CMSG_DEL_IGNORE": "0x06D",
|
||||
"CMSG_PLAYER_LOGOUT": "0x04A",
|
||||
"CMSG_LOGOUT_REQUEST": "0x04B",
|
||||
"CMSG_LOGOUT_CANCEL": "0x04E",
|
||||
"SMSG_LOGOUT_RESPONSE": "0x04C",
|
||||
"SMSG_LOGOUT_COMPLETE": "0x04D",
|
||||
"CMSG_STAND_STATE_CHANGE": "0x101",
|
||||
"CMSG_SHOWING_HELM": "0x2B9",
|
||||
"CMSG_SHOWING_CLOAK": "0x2BA",
|
||||
"CMSG_TOGGLE_PVP": "0x253",
|
||||
"CMSG_GUILD_INVITE": "0x082",
|
||||
"CMSG_GUILD_ACCEPT": "0x084",
|
||||
"CMSG_GUILD_DECLINE_INVITATION": "0x085",
|
||||
"CMSG_GUILD_INFO": "0x087",
|
||||
"CMSG_GUILD_GET_ROSTER": "0x089",
|
||||
"CMSG_GUILD_PROMOTE_MEMBER": "0x08B",
|
||||
"CMSG_GUILD_DEMOTE_MEMBER": "0x08C",
|
||||
"CMSG_GUILD_LEAVE": "0x08D",
|
||||
"CMSG_GUILD_MOTD": "0x091",
|
||||
"SMSG_GUILD_INFO": "0x088",
|
||||
"SMSG_GUILD_ROSTER": "0x08A",
|
||||
"MSG_RAID_READY_CHECK": "0x322",
|
||||
"MSG_RAID_READY_CHECK_CONFIRM": "0x3AE",
|
||||
"CMSG_DUEL_PROPOSED": "0x166",
|
||||
"CMSG_DUEL_ACCEPTED": "0x16C",
|
||||
"CMSG_DUEL_CANCELLED": "0x16D",
|
||||
"SMSG_DUEL_REQUESTED": "0x167",
|
||||
"CMSG_INITIATE_TRADE": "0x116",
|
||||
"MSG_RANDOM_ROLL": "0x1FB",
|
||||
"CMSG_SET_SELECTION": "0x13D",
|
||||
"CMSG_NAME_QUERY": "0x050",
|
||||
"SMSG_NAME_QUERY_RESPONSE": "0x051",
|
||||
"CMSG_CREATURE_QUERY": "0x060",
|
||||
"SMSG_CREATURE_QUERY_RESPONSE": "0x061",
|
||||
"CMSG_GAMEOBJECT_QUERY": "0x05E",
|
||||
"SMSG_GAMEOBJECT_QUERY_RESPONSE": "0x05F",
|
||||
"CMSG_SET_ACTIVE_MOVER": "0x26A",
|
||||
"CMSG_BINDER_ACTIVATE": "0x1B5",
|
||||
"SMSG_LOG_XPGAIN": "0x1D0",
|
||||
"SMSG_MONSTER_MOVE": "0x0DD",
|
||||
"CMSG_ATTACKSWING": "0x141",
|
||||
"CMSG_ATTACKSTOP": "0x142",
|
||||
"SMSG_ATTACKSTART": "0x143",
|
||||
"SMSG_ATTACKSTOP": "0x144",
|
||||
"SMSG_ATTACKERSTATEUPDATE": "0x14A",
|
||||
"SMSG_SPELLNONMELEEDAMAGELOG": "0x250",
|
||||
"SMSG_SPELLHEALLOG": "0x150",
|
||||
"SMSG_SPELLENERGIZELOG": "0x25B",
|
||||
"SMSG_PERIODICAURALOG": "0x24E",
|
||||
"SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC",
|
||||
"CMSG_CAST_SPELL": "0x12E",
|
||||
"CMSG_CANCEL_CAST": "0x12F",
|
||||
"CMSG_CANCEL_AURA": "0x033",
|
||||
"SMSG_CAST_FAILED": "0x130",
|
||||
"SMSG_SPELL_START": "0x131",
|
||||
"SMSG_SPELL_GO": "0x132",
|
||||
"SMSG_SPELL_FAILURE": "0x133",
|
||||
"SMSG_SPELL_COOLDOWN": "0x134",
|
||||
"SMSG_COOLDOWN_EVENT": "0x135",
|
||||
"SMSG_UPDATE_AURA_DURATION": "0x137",
|
||||
"SMSG_INITIAL_SPELLS": "0x12A",
|
||||
"SMSG_LEARNED_SPELL": "0x12B",
|
||||
"SMSG_SUPERCEDED_SPELL": "0x12C",
|
||||
"SMSG_REMOVED_SPELL": "0x203",
|
||||
"SMSG_SEND_UNLEARN_SPELLS": "0x41F",
|
||||
"SMSG_SPELL_DELAYED": "0x1E2",
|
||||
"SMSG_AURA_UPDATE": "0x3FA",
|
||||
"SMSG_AURA_UPDATE_ALL": "0x495",
|
||||
"SMSG_SET_FLAT_SPELL_MODIFIER": "0x266",
|
||||
"SMSG_SET_PCT_SPELL_MODIFIER": "0x267",
|
||||
"SMSG_TALENTS_INFO": "0x4C0",
|
||||
"CMSG_LEARN_TALENT": "0x251",
|
||||
"MSG_TALENT_WIPE_CONFIRM": "0x2AB",
|
||||
"CMSG_GROUP_INVITE": "0x06E",
|
||||
"SMSG_GROUP_INVITE": "0x06F",
|
||||
"CMSG_GROUP_ACCEPT": "0x072",
|
||||
"CMSG_GROUP_DECLINE": "0x073",
|
||||
"SMSG_GROUP_DECLINE": "0x074",
|
||||
"CMSG_GROUP_UNINVITE_GUID": "0x076",
|
||||
"SMSG_GROUP_UNINVITE": "0x077",
|
||||
"CMSG_GROUP_SET_LEADER": "0x078",
|
||||
"SMSG_GROUP_SET_LEADER": "0x079",
|
||||
"CMSG_GROUP_DISBAND": "0x07B",
|
||||
"SMSG_GROUP_LIST": "0x07D",
|
||||
"SMSG_PARTY_COMMAND_RESULT": "0x07E",
|
||||
"MSG_RAID_TARGET_UPDATE": "0x321",
|
||||
"CMSG_REQUEST_RAID_INFO": "0x2CD",
|
||||
"SMSG_RAID_INSTANCE_INFO": "0x2CC",
|
||||
"CMSG_AUTOSTORE_LOOT_ITEM": "0x108",
|
||||
"CMSG_LOOT": "0x15D",
|
||||
"CMSG_LOOT_MONEY": "0x15E",
|
||||
"CMSG_LOOT_RELEASE": "0x15F",
|
||||
"SMSG_LOOT_RESPONSE": "0x160",
|
||||
"SMSG_LOOT_RELEASE_RESPONSE": "0x161",
|
||||
"SMSG_LOOT_REMOVED": "0x162",
|
||||
"SMSG_LOOT_MONEY_NOTIFY": "0x163",
|
||||
"SMSG_LOOT_CLEAR_MONEY": "0x165",
|
||||
"CMSG_ACTIVATETAXI": "0x19D",
|
||||
"CMSG_GOSSIP_HELLO": "0x17B",
|
||||
"CMSG_GOSSIP_SELECT_OPTION": "0x17C",
|
||||
"SMSG_GOSSIP_MESSAGE": "0x17D",
|
||||
"SMSG_GOSSIP_COMPLETE": "0x17E",
|
||||
"SMSG_NPC_TEXT_UPDATE": "0x180",
|
||||
"CMSG_GAMEOBJECT_USE": "0x01B",
|
||||
"CMSG_QUESTGIVER_STATUS_QUERY": "0x182",
|
||||
"SMSG_QUESTGIVER_STATUS": "0x183",
|
||||
"SMSG_QUESTGIVER_STATUS_MULTIPLE": "0x198",
|
||||
"CMSG_QUESTGIVER_HELLO": "0x184",
|
||||
"CMSG_QUESTGIVER_QUERY_QUEST": "0x186",
|
||||
"SMSG_QUESTGIVER_QUEST_DETAILS": "0x188",
|
||||
"CMSG_QUESTGIVER_ACCEPT_QUEST": "0x189",
|
||||
"CMSG_QUESTGIVER_COMPLETE_QUEST": "0x18A",
|
||||
"SMSG_QUESTGIVER_REQUEST_ITEMS": "0x18B",
|
||||
"CMSG_QUESTGIVER_REQUEST_REWARD": "0x18C",
|
||||
"SMSG_QUESTGIVER_OFFER_REWARD": "0x18D",
|
||||
"CMSG_QUESTGIVER_CHOOSE_REWARD": "0x18E",
|
||||
"SMSG_QUESTGIVER_QUEST_INVALID": "0x18F",
|
||||
"SMSG_QUESTGIVER_QUEST_COMPLETE": "0x191",
|
||||
"CMSG_QUESTLOG_REMOVE_QUEST": "0x194",
|
||||
"SMSG_QUESTUPDATE_ADD_KILL": "0x196",
|
||||
"SMSG_QUESTUPDATE_COMPLETE": "0x195",
|
||||
"CMSG_QUEST_QUERY": "0x05C",
|
||||
"SMSG_QUEST_QUERY_RESPONSE": "0x05D",
|
||||
"SMSG_QUESTLOG_FULL": "0x1A3",
|
||||
"CMSG_LIST_INVENTORY": "0x19E",
|
||||
"SMSG_LIST_INVENTORY": "0x19F",
|
||||
"CMSG_SELL_ITEM": "0x1A0",
|
||||
"SMSG_SELL_ITEM": "0x1A1",
|
||||
"CMSG_BUY_ITEM": "0x1A2",
|
||||
"SMSG_BUY_FAILED": "0x1A5",
|
||||
"CMSG_TRAINER_LIST": "0x01B0",
|
||||
"SMSG_TRAINER_LIST": "0x01B1",
|
||||
"CMSG_TRAINER_BUY_SPELL": "0x01B2",
|
||||
"SMSG_TRAINER_BUY_FAILED": "0x01B4",
|
||||
"CMSG_ITEM_QUERY_SINGLE": "0x056",
|
||||
"SMSG_ITEM_QUERY_SINGLE_RESPONSE": "0x058",
|
||||
"CMSG_USE_ITEM": "0x00AB",
|
||||
"CMSG_AUTOEQUIP_ITEM": "0x10A",
|
||||
"CMSG_SWAP_ITEM": "0x10C",
|
||||
"CMSG_SWAP_INV_ITEM": "0x10D",
|
||||
"SMSG_INVENTORY_CHANGE_FAILURE": "0x112",
|
||||
"CMSG_INSPECT": "0x114",
|
||||
"SMSG_INSPECT_RESULTS": "0x115",
|
||||
"CMSG_REPOP_REQUEST": "0x015A",
|
||||
"SMSG_RESURRECT_REQUEST": "0x015B",
|
||||
"CMSG_RESURRECT_RESPONSE": "0x015C",
|
||||
"CMSG_SPIRIT_HEALER_ACTIVATE": "0x021C",
|
||||
"SMSG_SPIRIT_HEALER_CONFIRM": "0x0222",
|
||||
"SMSG_RESURRECT_CANCEL": "0x0390",
|
||||
"MSG_MOVE_TELEPORT_ACK": "0x0C7",
|
||||
"SMSG_TRANSFER_PENDING": "0x003F",
|
||||
"SMSG_NEW_WORLD": "0x003E",
|
||||
"MSG_MOVE_WORLDPORT_ACK": "0x00DC",
|
||||
"SMSG_TRANSFER_ABORTED": "0x0040",
|
||||
"SMSG_FORCE_RUN_SPEED_CHANGE": "0x00E2",
|
||||
"CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x00E3",
|
||||
"CMSG_CANCEL_MOUNT_AURA": "0x0375",
|
||||
"SMSG_SHOWTAXINODES": "0x01A9",
|
||||
"SMSG_ACTIVATETAXIREPLY": "0x01AE",
|
||||
"SMSG_ACTIVATETAXIREPLY_ALT": "0x029D",
|
||||
"SMSG_NEW_TAXI_PATH": "0x01AF",
|
||||
"CMSG_ACTIVATETAXIEXPRESS": "0x0312",
|
||||
"SMSG_BATTLEFIELD_PORT_DENIED": "0x014B",
|
||||
"SMSG_REMOVED_FROM_PVP_QUEUE": "0x0170",
|
||||
"SMSG_TRAINER_BUY_SUCCEEDED": "0x01B3",
|
||||
"SMSG_BINDPOINTUPDATE": "0x0155",
|
||||
"CMSG_BATTLEFIELD_LIST": "0x023C",
|
||||
"SMSG_BATTLEFIELD_LIST": "0x023D",
|
||||
"CMSG_BATTLEFIELD_JOIN": "0x023E",
|
||||
"CMSG_BATTLEFIELD_STATUS": "0x02D3",
|
||||
"SMSG_BATTLEFIELD_STATUS": "0x02D4",
|
||||
"CMSG_BATTLEFIELD_PORT": "0x02D5",
|
||||
"CMSG_BATTLEMASTER_HELLO": "0x02D7",
|
||||
"MSG_PVP_LOG_DATA": "0x02E0",
|
||||
"CMSG_LEAVE_BATTLEFIELD": "0x02E1",
|
||||
"SMSG_GROUP_JOINED_BATTLEGROUND": "0x02E8",
|
||||
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x02E9",
|
||||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x02EC",
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x02ED",
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x02EE",
|
||||
"SMSG_JOINED_BATTLEGROUND_QUEUE": "0x038A",
|
||||
"CMSG_ARENA_TEAM_CREATE": "0x0348",
|
||||
"SMSG_ARENA_TEAM_COMMAND_RESULT": "0x0349",
|
||||
"CMSG_ARENA_TEAM_QUERY": "0x034B",
|
||||
"SMSG_ARENA_TEAM_QUERY_RESPONSE": "0x034C",
|
||||
"CMSG_ARENA_TEAM_ROSTER": "0x034D",
|
||||
"SMSG_ARENA_TEAM_ROSTER": "0x034E",
|
||||
"CMSG_ARENA_TEAM_INVITE": "0x034F",
|
||||
"SMSG_ARENA_TEAM_INVITE": "0x0350",
|
||||
"CMSG_ARENA_TEAM_ACCEPT": "0x0351",
|
||||
"CMSG_ARENA_TEAM_DECLINE": "0x0352",
|
||||
"CMSG_ARENA_TEAM_LEAVE": "0x0353",
|
||||
"CMSG_ARENA_TEAM_REMOVE": "0x0354",
|
||||
"CMSG_ARENA_TEAM_DISBAND": "0x0355",
|
||||
"CMSG_ARENA_TEAM_LEADER": "0x0356",
|
||||
"SMSG_ARENA_TEAM_EVENT": "0x0357",
|
||||
"CMSG_BATTLEMASTER_JOIN_ARENA": "0x0358",
|
||||
"SMSG_ARENA_TEAM_STATS": "0x035B",
|
||||
"SMSG_ARENA_ERROR": "0x0376",
|
||||
"MSG_INSPECT_ARENA_TEAMS": "0x0377"
|
||||
}
|
||||
29
Data/expansions/wotlk/update_fields.json
Normal file
29
Data/expansions/wotlk/update_fields.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 6,
|
||||
"UNIT_FIELD_TARGET_HI": 7,
|
||||
"UNIT_FIELD_HEALTH": 24,
|
||||
"UNIT_FIELD_POWER1": 25,
|
||||
"UNIT_FIELD_MAXHEALTH": 32,
|
||||
"UNIT_FIELD_MAXPOWER1": 33,
|
||||
"UNIT_FIELD_LEVEL": 54,
|
||||
"UNIT_FIELD_FACTIONTEMPLATE": 55,
|
||||
"UNIT_FIELD_FLAGS": 59,
|
||||
"UNIT_FIELD_FLAGS_2": 60,
|
||||
"UNIT_FIELD_DISPLAYID": 67,
|
||||
"UNIT_FIELD_MOUNTDISPLAYID": 69,
|
||||
"UNIT_NPC_FLAGS": 82,
|
||||
"UNIT_DYNAMIC_FLAGS": 147,
|
||||
"UNIT_END": 148,
|
||||
"PLAYER_FLAGS": 150,
|
||||
"PLAYER_XP": 634,
|
||||
"PLAYER_NEXT_LEVEL_XP": 635,
|
||||
"PLAYER_FIELD_COINAGE": 1170,
|
||||
"PLAYER_QUEST_LOG_START": 158,
|
||||
"PLAYER_FIELD_INV_SLOT_HEAD": 324,
|
||||
"PLAYER_FIELD_PACK_SLOT_1": 370,
|
||||
"PLAYER_SKILL_INFO_START": 636,
|
||||
"PLAYER_EXPLORED_ZONES_START": 1041,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14
|
||||
}
|
||||
|
|
@ -45,6 +45,10 @@ public:
|
|||
void authenticate(const std::string& username, const std::string& password);
|
||||
void authenticateWithHash(const std::string& username, const std::vector<uint8_t>& authHash);
|
||||
|
||||
// Set client version info (call before authenticate)
|
||||
void setClientInfo(const ClientInfo& info) { clientInfo = info; }
|
||||
const ClientInfo& getClientInfo() const { return clientInfo; }
|
||||
|
||||
// Realm list
|
||||
void requestRealmList();
|
||||
const std::vector<Realm>& getRealms() const { return realms; }
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ namespace wowee {
|
|||
namespace rendering { class Renderer; }
|
||||
namespace ui { class UIManager; }
|
||||
namespace auth { class AuthHandler; }
|
||||
namespace game { class GameHandler; class World; }
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace game { class GameHandler; class World; class ExpansionRegistry; }
|
||||
namespace pipeline { class AssetManager; class DBCLayout; class HDPackManager; }
|
||||
namespace audio { enum class VoiceType; }
|
||||
|
||||
namespace core {
|
||||
|
|
@ -54,6 +54,9 @@ public:
|
|||
game::GameHandler* getGameHandler() { return gameHandler.get(); }
|
||||
game::World* getWorld() { return world.get(); }
|
||||
pipeline::AssetManager* getAssetManager() { return assetManager.get(); }
|
||||
game::ExpansionRegistry* getExpansionRegistry() { return expansionRegistry_.get(); }
|
||||
pipeline::DBCLayout* getDBCLayout() { return dbcLayout_.get(); }
|
||||
pipeline::HDPackManager* getHDPackManager() { return hdPackManager_.get(); }
|
||||
|
||||
// Singleton access
|
||||
static Application& getInstance() { return *instance; }
|
||||
|
|
@ -104,6 +107,9 @@ private:
|
|||
std::unique_ptr<game::GameHandler> gameHandler;
|
||||
std::unique_ptr<game::World> world;
|
||||
std::unique_ptr<pipeline::AssetManager> assetManager;
|
||||
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
|
||||
std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
|
||||
std::unique_ptr<pipeline::HDPackManager> hdPackManager_;
|
||||
|
||||
AppState state = AppState::AUTHENTICATION;
|
||||
bool running = false;
|
||||
|
|
|
|||
67
include/game/expansion_profile.hpp
Normal file
67
include/game/expansion_profile.hpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* Identifies a WoW expansion for protocol/asset selection.
|
||||
*/
|
||||
struct ExpansionProfile {
|
||||
std::string id; // "classic", "tbc", "wotlk", "cata"
|
||||
std::string name; // "Wrath of the Lich King"
|
||||
std::string shortName; // "WotLK"
|
||||
uint8_t majorVersion = 0;
|
||||
uint8_t minorVersion = 0;
|
||||
uint8_t patchVersion = 0;
|
||||
uint16_t build = 0;
|
||||
uint8_t protocolVersion = 0; // SRP auth protocol version byte
|
||||
std::string dataPath; // Absolute path to expansion data dir
|
||||
uint32_t maxLevel = 60;
|
||||
std::vector<uint32_t> races;
|
||||
std::vector<uint32_t> classes;
|
||||
|
||||
std::string versionString() const; // e.g. "3.3.5a"
|
||||
};
|
||||
|
||||
/**
|
||||
* Scans Data/expansions/ for available expansion profiles and manages the active selection.
|
||||
*/
|
||||
class ExpansionRegistry {
|
||||
public:
|
||||
/**
|
||||
* Scan dataRoot/expansions/ for expansion.json files.
|
||||
* @param dataRoot Path to Data/ directory (e.g. "./Data")
|
||||
* @return Number of profiles discovered
|
||||
*/
|
||||
size_t initialize(const std::string& dataRoot);
|
||||
|
||||
/** All discovered profiles. */
|
||||
const std::vector<ExpansionProfile>& getAllProfiles() const { return profiles_; }
|
||||
|
||||
/** Lookup by id (e.g. "wotlk"). Returns nullptr if not found. */
|
||||
const ExpansionProfile* getProfile(const std::string& id) const;
|
||||
|
||||
/** Set the active expansion. Returns false if id not found. */
|
||||
bool setActive(const std::string& id);
|
||||
|
||||
/** Get the active expansion profile. Never null after successful initialize(). */
|
||||
const ExpansionProfile* getActive() const;
|
||||
|
||||
/** Convenience: active expansion id. Empty if none. */
|
||||
const std::string& getActiveId() const { return activeId_; }
|
||||
|
||||
private:
|
||||
std::vector<ExpansionProfile> profiles_;
|
||||
std::string activeId_;
|
||||
|
||||
bool loadProfile(const std::string& jsonPath, const std::string& dirPath);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#include "game/world_packets.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "game/opcode_table.hpp"
|
||||
#include "game/update_field_table.hpp"
|
||||
#include "game/inventory.hpp"
|
||||
#include "game/spell_defines.hpp"
|
||||
#include "game/group_defines.hpp"
|
||||
|
|
@ -23,6 +25,7 @@ namespace wowee::game {
|
|||
class TransportManager;
|
||||
class WardenCrypto;
|
||||
class WardenModuleManager;
|
||||
class PacketParsers;
|
||||
}
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -109,6 +112,14 @@ public:
|
|||
GameHandler();
|
||||
~GameHandler();
|
||||
|
||||
/** Access the active opcode table (wire ↔ logical mapping). */
|
||||
const OpcodeTable& getOpcodeTable() const { return opcodeTable_; }
|
||||
OpcodeTable& getOpcodeTable() { return opcodeTable_; }
|
||||
const UpdateFieldTable& getUpdateFieldTable() const { return updateFieldTable_; }
|
||||
UpdateFieldTable& getUpdateFieldTable() { return updateFieldTable_; }
|
||||
PacketParsers* getPacketParsers() { return packetParsers_.get(); }
|
||||
void setPacketParsers(std::unique_ptr<PacketParsers> parsers);
|
||||
|
||||
/**
|
||||
* Connect to world server
|
||||
*
|
||||
|
|
@ -927,6 +938,15 @@ private:
|
|||
float localOrientation);
|
||||
void clearTransportAttachment(uint64_t childGuid);
|
||||
|
||||
// Opcode translation table (expansion-specific wire ↔ logical mapping)
|
||||
OpcodeTable opcodeTable_;
|
||||
|
||||
// Update field table (expansion-specific field index mapping)
|
||||
UpdateFieldTable updateFieldTable_;
|
||||
|
||||
// Packet parsers (expansion-specific binary format handling)
|
||||
std::unique_ptr<PacketParsers> packetParsers_;
|
||||
|
||||
// Network
|
||||
std::unique_ptr<network::WorldSocket> socket;
|
||||
|
||||
|
|
@ -1210,7 +1230,6 @@ private:
|
|||
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
|
||||
bool skillLineDbcLoaded_ = false;
|
||||
bool skillLineAbilityLoaded_ = false;
|
||||
static constexpr uint16_t PLAYER_EXPLORED_ZONES_START = 1041; // 3.3.5a UpdateFields
|
||||
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;
|
||||
std::vector<uint32_t> playerExploredZones_ =
|
||||
std::vector<uint32_t>(PLAYER_EXPLORED_ZONES_COUNT, 0u);
|
||||
|
|
|
|||
407
include/game/opcode_table.hpp
Normal file
407
include/game/opcode_table.hpp
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* Logical opcode identifiers (expansion-agnostic).
|
||||
*
|
||||
* These are compile-time enum values used in switch statements.
|
||||
* The actual wire values depend on the active expansion and are
|
||||
* loaded from JSON at runtime via OpcodeTable.
|
||||
*/
|
||||
enum class LogicalOpcode : uint16_t {
|
||||
// ---- Client to Server (Core) ----
|
||||
CMSG_PING,
|
||||
CMSG_AUTH_SESSION,
|
||||
CMSG_CHAR_CREATE,
|
||||
CMSG_CHAR_ENUM,
|
||||
CMSG_CHAR_DELETE,
|
||||
CMSG_PLAYER_LOGIN,
|
||||
|
||||
// ---- Movement ----
|
||||
CMSG_MOVE_START_FORWARD,
|
||||
CMSG_MOVE_START_BACKWARD,
|
||||
CMSG_MOVE_STOP,
|
||||
CMSG_MOVE_START_STRAFE_LEFT,
|
||||
CMSG_MOVE_START_STRAFE_RIGHT,
|
||||
CMSG_MOVE_STOP_STRAFE,
|
||||
CMSG_MOVE_JUMP,
|
||||
CMSG_MOVE_START_TURN_LEFT,
|
||||
CMSG_MOVE_START_TURN_RIGHT,
|
||||
CMSG_MOVE_STOP_TURN,
|
||||
CMSG_MOVE_SET_FACING,
|
||||
CMSG_MOVE_FALL_LAND,
|
||||
CMSG_MOVE_START_SWIM,
|
||||
CMSG_MOVE_STOP_SWIM,
|
||||
CMSG_MOVE_HEARTBEAT,
|
||||
|
||||
// ---- Server to Client (Core) ----
|
||||
SMSG_AUTH_CHALLENGE,
|
||||
SMSG_AUTH_RESPONSE,
|
||||
SMSG_CHAR_CREATE,
|
||||
SMSG_CHAR_ENUM,
|
||||
SMSG_CHAR_DELETE,
|
||||
SMSG_PONG,
|
||||
SMSG_LOGIN_VERIFY_WORLD,
|
||||
SMSG_LOGIN_SETTIMESPEED,
|
||||
SMSG_TUTORIAL_FLAGS,
|
||||
SMSG_WARDEN_DATA,
|
||||
CMSG_WARDEN_DATA,
|
||||
SMSG_ACCOUNT_DATA_TIMES,
|
||||
SMSG_CLIENTCACHE_VERSION,
|
||||
SMSG_FEATURE_SYSTEM_STATUS,
|
||||
SMSG_MOTD,
|
||||
|
||||
// ---- Entity/Object updates ----
|
||||
SMSG_UPDATE_OBJECT,
|
||||
SMSG_COMPRESSED_UPDATE_OBJECT,
|
||||
SMSG_MONSTER_MOVE_TRANSPORT,
|
||||
SMSG_DESTROY_OBJECT,
|
||||
|
||||
// ---- Chat ----
|
||||
CMSG_MESSAGECHAT,
|
||||
SMSG_MESSAGECHAT,
|
||||
|
||||
// ---- Server Info Commands ----
|
||||
CMSG_WHO,
|
||||
SMSG_WHO,
|
||||
CMSG_REQUEST_PLAYED_TIME,
|
||||
SMSG_PLAYED_TIME,
|
||||
CMSG_QUERY_TIME,
|
||||
SMSG_QUERY_TIME_RESPONSE,
|
||||
|
||||
// ---- Social Commands ----
|
||||
SMSG_FRIEND_STATUS,
|
||||
CMSG_ADD_FRIEND,
|
||||
CMSG_DEL_FRIEND,
|
||||
CMSG_SET_CONTACT_NOTES,
|
||||
CMSG_ADD_IGNORE,
|
||||
CMSG_DEL_IGNORE,
|
||||
|
||||
// ---- Logout Commands ----
|
||||
CMSG_PLAYER_LOGOUT,
|
||||
CMSG_LOGOUT_REQUEST,
|
||||
CMSG_LOGOUT_CANCEL,
|
||||
SMSG_LOGOUT_RESPONSE,
|
||||
SMSG_LOGOUT_COMPLETE,
|
||||
|
||||
// ---- Stand State ----
|
||||
CMSG_STAND_STATE_CHANGE,
|
||||
|
||||
// ---- Display Toggles ----
|
||||
CMSG_SHOWING_HELM,
|
||||
CMSG_SHOWING_CLOAK,
|
||||
|
||||
// ---- PvP ----
|
||||
CMSG_TOGGLE_PVP,
|
||||
|
||||
// ---- Guild ----
|
||||
CMSG_GUILD_INVITE,
|
||||
CMSG_GUILD_ACCEPT,
|
||||
CMSG_GUILD_DECLINE_INVITATION,
|
||||
CMSG_GUILD_INFO,
|
||||
CMSG_GUILD_GET_ROSTER,
|
||||
CMSG_GUILD_PROMOTE_MEMBER,
|
||||
CMSG_GUILD_DEMOTE_MEMBER,
|
||||
CMSG_GUILD_LEAVE,
|
||||
CMSG_GUILD_MOTD,
|
||||
SMSG_GUILD_INFO,
|
||||
SMSG_GUILD_ROSTER,
|
||||
|
||||
// ---- Ready Check ----
|
||||
MSG_RAID_READY_CHECK,
|
||||
MSG_RAID_READY_CHECK_CONFIRM,
|
||||
|
||||
// ---- Duel ----
|
||||
CMSG_DUEL_PROPOSED,
|
||||
CMSG_DUEL_ACCEPTED,
|
||||
CMSG_DUEL_CANCELLED,
|
||||
SMSG_DUEL_REQUESTED,
|
||||
|
||||
// ---- Trade ----
|
||||
CMSG_INITIATE_TRADE,
|
||||
|
||||
// ---- Random Roll ----
|
||||
MSG_RANDOM_ROLL,
|
||||
|
||||
// ---- Phase 1: Foundation (Targeting, Queries) ----
|
||||
CMSG_SET_SELECTION,
|
||||
CMSG_NAME_QUERY,
|
||||
SMSG_NAME_QUERY_RESPONSE,
|
||||
CMSG_CREATURE_QUERY,
|
||||
SMSG_CREATURE_QUERY_RESPONSE,
|
||||
CMSG_GAMEOBJECT_QUERY,
|
||||
SMSG_GAMEOBJECT_QUERY_RESPONSE,
|
||||
CMSG_SET_ACTIVE_MOVER,
|
||||
CMSG_BINDER_ACTIVATE,
|
||||
|
||||
// ---- XP ----
|
||||
SMSG_LOG_XPGAIN,
|
||||
|
||||
// ---- Creature Movement ----
|
||||
SMSG_MONSTER_MOVE,
|
||||
|
||||
// ---- Phase 2: Combat Core ----
|
||||
CMSG_ATTACKSWING,
|
||||
CMSG_ATTACKSTOP,
|
||||
SMSG_ATTACKSTART,
|
||||
SMSG_ATTACKSTOP,
|
||||
SMSG_ATTACKERSTATEUPDATE,
|
||||
SMSG_SPELLNONMELEEDAMAGELOG,
|
||||
SMSG_SPELLHEALLOG,
|
||||
SMSG_SPELLENERGIZELOG,
|
||||
SMSG_PERIODICAURALOG,
|
||||
SMSG_ENVIRONMENTALDAMAGELOG,
|
||||
|
||||
// ---- Phase 3: Spells, Action Bar, Auras ----
|
||||
CMSG_CAST_SPELL,
|
||||
CMSG_CANCEL_CAST,
|
||||
CMSG_CANCEL_AURA,
|
||||
SMSG_CAST_FAILED,
|
||||
SMSG_SPELL_START,
|
||||
SMSG_SPELL_GO,
|
||||
SMSG_SPELL_FAILURE,
|
||||
SMSG_SPELL_COOLDOWN,
|
||||
SMSG_COOLDOWN_EVENT,
|
||||
SMSG_UPDATE_AURA_DURATION,
|
||||
SMSG_INITIAL_SPELLS,
|
||||
SMSG_LEARNED_SPELL,
|
||||
SMSG_SUPERCEDED_SPELL,
|
||||
SMSG_REMOVED_SPELL,
|
||||
SMSG_SEND_UNLEARN_SPELLS,
|
||||
SMSG_SPELL_DELAYED,
|
||||
SMSG_AURA_UPDATE,
|
||||
SMSG_AURA_UPDATE_ALL,
|
||||
SMSG_SET_FLAT_SPELL_MODIFIER,
|
||||
SMSG_SET_PCT_SPELL_MODIFIER,
|
||||
|
||||
// ---- Talents ----
|
||||
SMSG_TALENTS_INFO,
|
||||
CMSG_LEARN_TALENT,
|
||||
MSG_TALENT_WIPE_CONFIRM,
|
||||
|
||||
// ---- Phase 4: Group/Party ----
|
||||
CMSG_GROUP_INVITE,
|
||||
SMSG_GROUP_INVITE,
|
||||
CMSG_GROUP_ACCEPT,
|
||||
CMSG_GROUP_DECLINE,
|
||||
SMSG_GROUP_DECLINE,
|
||||
CMSG_GROUP_UNINVITE_GUID,
|
||||
SMSG_GROUP_UNINVITE,
|
||||
CMSG_GROUP_SET_LEADER,
|
||||
SMSG_GROUP_SET_LEADER,
|
||||
CMSG_GROUP_DISBAND,
|
||||
SMSG_GROUP_LIST,
|
||||
SMSG_PARTY_COMMAND_RESULT,
|
||||
MSG_RAID_TARGET_UPDATE,
|
||||
CMSG_REQUEST_RAID_INFO,
|
||||
SMSG_RAID_INSTANCE_INFO,
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
CMSG_AUTOSTORE_LOOT_ITEM,
|
||||
CMSG_LOOT,
|
||||
CMSG_LOOT_MONEY,
|
||||
CMSG_LOOT_RELEASE,
|
||||
SMSG_LOOT_RESPONSE,
|
||||
SMSG_LOOT_RELEASE_RESPONSE,
|
||||
SMSG_LOOT_REMOVED,
|
||||
SMSG_LOOT_MONEY_NOTIFY,
|
||||
SMSG_LOOT_CLEAR_MONEY,
|
||||
|
||||
// ---- Phase 5: Taxi / Flight Paths ----
|
||||
CMSG_ACTIVATETAXI,
|
||||
|
||||
// ---- Phase 5: NPC Gossip ----
|
||||
CMSG_GOSSIP_HELLO,
|
||||
CMSG_GOSSIP_SELECT_OPTION,
|
||||
SMSG_GOSSIP_MESSAGE,
|
||||
SMSG_GOSSIP_COMPLETE,
|
||||
SMSG_NPC_TEXT_UPDATE,
|
||||
|
||||
// ---- Phase 5: GameObject ----
|
||||
CMSG_GAMEOBJECT_USE,
|
||||
|
||||
// ---- Phase 5: Quests ----
|
||||
CMSG_QUESTGIVER_STATUS_QUERY,
|
||||
SMSG_QUESTGIVER_STATUS,
|
||||
SMSG_QUESTGIVER_STATUS_MULTIPLE,
|
||||
CMSG_QUESTGIVER_HELLO,
|
||||
CMSG_QUESTGIVER_QUERY_QUEST,
|
||||
SMSG_QUESTGIVER_QUEST_DETAILS,
|
||||
CMSG_QUESTGIVER_ACCEPT_QUEST,
|
||||
CMSG_QUESTGIVER_COMPLETE_QUEST,
|
||||
SMSG_QUESTGIVER_REQUEST_ITEMS,
|
||||
CMSG_QUESTGIVER_REQUEST_REWARD,
|
||||
SMSG_QUESTGIVER_OFFER_REWARD,
|
||||
CMSG_QUESTGIVER_CHOOSE_REWARD,
|
||||
SMSG_QUESTGIVER_QUEST_INVALID,
|
||||
SMSG_QUESTGIVER_QUEST_COMPLETE,
|
||||
CMSG_QUESTLOG_REMOVE_QUEST,
|
||||
SMSG_QUESTUPDATE_ADD_KILL,
|
||||
SMSG_QUESTUPDATE_COMPLETE,
|
||||
CMSG_QUEST_QUERY,
|
||||
SMSG_QUEST_QUERY_RESPONSE,
|
||||
SMSG_QUESTLOG_FULL,
|
||||
|
||||
// ---- Phase 5: Vendor ----
|
||||
CMSG_LIST_INVENTORY,
|
||||
SMSG_LIST_INVENTORY,
|
||||
CMSG_SELL_ITEM,
|
||||
SMSG_SELL_ITEM,
|
||||
CMSG_BUY_ITEM,
|
||||
SMSG_BUY_FAILED,
|
||||
|
||||
// ---- Trainer ----
|
||||
CMSG_TRAINER_LIST,
|
||||
SMSG_TRAINER_LIST,
|
||||
CMSG_TRAINER_BUY_SPELL,
|
||||
SMSG_TRAINER_BUY_FAILED,
|
||||
|
||||
// ---- Phase 5: Item/Equip ----
|
||||
CMSG_ITEM_QUERY_SINGLE,
|
||||
SMSG_ITEM_QUERY_SINGLE_RESPONSE,
|
||||
CMSG_USE_ITEM,
|
||||
CMSG_AUTOEQUIP_ITEM,
|
||||
CMSG_SWAP_ITEM,
|
||||
CMSG_SWAP_INV_ITEM,
|
||||
SMSG_INVENTORY_CHANGE_FAILURE,
|
||||
CMSG_INSPECT,
|
||||
SMSG_INSPECT_RESULTS,
|
||||
|
||||
// ---- Death/Respawn ----
|
||||
CMSG_REPOP_REQUEST,
|
||||
SMSG_RESURRECT_REQUEST,
|
||||
CMSG_RESURRECT_RESPONSE,
|
||||
CMSG_SPIRIT_HEALER_ACTIVATE,
|
||||
SMSG_SPIRIT_HEALER_CONFIRM,
|
||||
SMSG_RESURRECT_CANCEL,
|
||||
|
||||
// ---- Teleport / Transfer ----
|
||||
MSG_MOVE_TELEPORT_ACK,
|
||||
SMSG_TRANSFER_PENDING,
|
||||
SMSG_NEW_WORLD,
|
||||
MSG_MOVE_WORLDPORT_ACK,
|
||||
SMSG_TRANSFER_ABORTED,
|
||||
|
||||
// ---- Speed Changes ----
|
||||
SMSG_FORCE_RUN_SPEED_CHANGE,
|
||||
CMSG_FORCE_RUN_SPEED_CHANGE_ACK,
|
||||
|
||||
// ---- Mount ----
|
||||
CMSG_CANCEL_MOUNT_AURA,
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
SMSG_SHOWTAXINODES,
|
||||
SMSG_ACTIVATETAXIREPLY,
|
||||
SMSG_ACTIVATETAXIREPLY_ALT,
|
||||
SMSG_NEW_TAXI_PATH,
|
||||
CMSG_ACTIVATETAXIEXPRESS,
|
||||
|
||||
// ---- Battleground ----
|
||||
SMSG_BATTLEFIELD_PORT_DENIED,
|
||||
SMSG_REMOVED_FROM_PVP_QUEUE,
|
||||
SMSG_TRAINER_BUY_SUCCEEDED,
|
||||
SMSG_BINDPOINTUPDATE,
|
||||
CMSG_BATTLEFIELD_LIST,
|
||||
SMSG_BATTLEFIELD_LIST,
|
||||
CMSG_BATTLEFIELD_JOIN,
|
||||
CMSG_BATTLEFIELD_STATUS,
|
||||
SMSG_BATTLEFIELD_STATUS,
|
||||
CMSG_BATTLEFIELD_PORT,
|
||||
CMSG_BATTLEMASTER_HELLO,
|
||||
MSG_PVP_LOG_DATA,
|
||||
CMSG_LEAVE_BATTLEFIELD,
|
||||
SMSG_GROUP_JOINED_BATTLEGROUND,
|
||||
MSG_BATTLEGROUND_PLAYER_POSITIONS,
|
||||
SMSG_BATTLEGROUND_PLAYER_JOINED,
|
||||
SMSG_BATTLEGROUND_PLAYER_LEFT,
|
||||
CMSG_BATTLEMASTER_JOIN,
|
||||
SMSG_JOINED_BATTLEGROUND_QUEUE,
|
||||
|
||||
// ---- Arena Team ----
|
||||
CMSG_ARENA_TEAM_CREATE,
|
||||
SMSG_ARENA_TEAM_COMMAND_RESULT,
|
||||
CMSG_ARENA_TEAM_QUERY,
|
||||
SMSG_ARENA_TEAM_QUERY_RESPONSE,
|
||||
CMSG_ARENA_TEAM_ROSTER,
|
||||
SMSG_ARENA_TEAM_ROSTER,
|
||||
CMSG_ARENA_TEAM_INVITE,
|
||||
SMSG_ARENA_TEAM_INVITE,
|
||||
CMSG_ARENA_TEAM_ACCEPT,
|
||||
CMSG_ARENA_TEAM_DECLINE,
|
||||
CMSG_ARENA_TEAM_LEAVE,
|
||||
CMSG_ARENA_TEAM_REMOVE,
|
||||
CMSG_ARENA_TEAM_DISBAND,
|
||||
CMSG_ARENA_TEAM_LEADER,
|
||||
SMSG_ARENA_TEAM_EVENT,
|
||||
CMSG_BATTLEMASTER_JOIN_ARENA,
|
||||
SMSG_ARENA_TEAM_STATS,
|
||||
SMSG_ARENA_ERROR,
|
||||
MSG_INSPECT_ARENA_TEAMS,
|
||||
|
||||
// Sentinel
|
||||
COUNT
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps LogicalOpcode ↔ expansion-specific wire values.
|
||||
*
|
||||
* Loaded from JSON (e.g. Data/expansions/wotlk/opcodes.json).
|
||||
* Used for sending packets (toWire) and receiving them (fromWire).
|
||||
*/
|
||||
class OpcodeTable {
|
||||
public:
|
||||
/**
|
||||
* Load opcode mappings from a JSON file.
|
||||
* Format: { "CMSG_PING": "0x1DC", "SMSG_AUTH_CHALLENGE": "0x1EC", ... }
|
||||
*/
|
||||
bool loadFromJson(const std::string& path);
|
||||
|
||||
/** Load built-in WotLK defaults (hardcoded fallback). */
|
||||
void loadWotlkDefaults();
|
||||
|
||||
/** LogicalOpcode → wire value for sending packets. Returns 0xFFFF if unknown. */
|
||||
uint16_t toWire(LogicalOpcode op) const;
|
||||
|
||||
/** Wire value → LogicalOpcode for receiving packets. Returns nullopt if unknown. */
|
||||
std::optional<LogicalOpcode> fromWire(uint16_t wireValue) const;
|
||||
|
||||
/** Check if a logical opcode has a wire mapping. */
|
||||
bool hasOpcode(LogicalOpcode op) const;
|
||||
|
||||
/** Number of mapped opcodes. */
|
||||
size_t size() const { return logicalToWire_.size(); }
|
||||
|
||||
private:
|
||||
std::unordered_map<uint16_t, uint16_t> logicalToWire_; // LogicalOpcode → wire
|
||||
std::unordered_map<uint16_t, uint16_t> wireToLogical_; // wire → LogicalOpcode
|
||||
|
||||
static std::optional<LogicalOpcode> nameToLogical(const std::string& name);
|
||||
static const char* logicalToName(LogicalOpcode op);
|
||||
};
|
||||
|
||||
/**
|
||||
* Global active opcode table pointer (set by GameHandler at startup).
|
||||
* Used by world_packets.cpp and other code that needs to send packets
|
||||
* without direct access to a GameHandler instance.
|
||||
*/
|
||||
void setActiveOpcodeTable(const OpcodeTable* table);
|
||||
const OpcodeTable* getActiveOpcodeTable();
|
||||
|
||||
/**
|
||||
* Get the wire value for a logical opcode using the active table.
|
||||
* Convenience helper for packet construction code.
|
||||
*/
|
||||
inline uint16_t wireOpcode(LogicalOpcode op) {
|
||||
const auto* table = getActiveOpcodeTable();
|
||||
return table ? table->toWire(op) : 0xFFFF;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -1,344 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "game/opcode_table.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
// World of Warcraft 3.3.5a opcodes
|
||||
// Values derived from community reverse-engineering efforts
|
||||
// Reference: https://wowdev.wiki/World_Packet
|
||||
enum class Opcode : uint16_t {
|
||||
// ---- Client to Server (Core) ----
|
||||
CMSG_PING = 0x1DC,
|
||||
CMSG_AUTH_SESSION = 0x1ED,
|
||||
CMSG_CHAR_CREATE = 0x036,
|
||||
CMSG_CHAR_ENUM = 0x037,
|
||||
CMSG_CHAR_DELETE = 0x038,
|
||||
CMSG_PLAYER_LOGIN = 0x03D,
|
||||
|
||||
// ---- Movement ----
|
||||
CMSG_MOVE_START_FORWARD = 0x0B5,
|
||||
CMSG_MOVE_START_BACKWARD = 0x0B6,
|
||||
CMSG_MOVE_STOP = 0x0B7,
|
||||
CMSG_MOVE_START_STRAFE_LEFT = 0x0B8,
|
||||
CMSG_MOVE_START_STRAFE_RIGHT = 0x0B9,
|
||||
CMSG_MOVE_STOP_STRAFE = 0x0BA,
|
||||
CMSG_MOVE_JUMP = 0x0BB,
|
||||
CMSG_MOVE_START_TURN_LEFT = 0x0BC,
|
||||
CMSG_MOVE_START_TURN_RIGHT = 0x0BD,
|
||||
CMSG_MOVE_STOP_TURN = 0x0BE,
|
||||
CMSG_MOVE_SET_FACING = 0x0DA,
|
||||
CMSG_MOVE_FALL_LAND = 0x0C9,
|
||||
CMSG_MOVE_START_SWIM = 0x0CA,
|
||||
CMSG_MOVE_STOP_SWIM = 0x0CB,
|
||||
CMSG_MOVE_HEARTBEAT = 0x0EE,
|
||||
|
||||
// ---- Server to Client (Core) ----
|
||||
SMSG_AUTH_CHALLENGE = 0x1EC,
|
||||
SMSG_AUTH_RESPONSE = 0x1EE,
|
||||
SMSG_CHAR_CREATE = 0x03A,
|
||||
SMSG_CHAR_ENUM = 0x03B,
|
||||
SMSG_CHAR_DELETE = 0x03C,
|
||||
SMSG_PONG = 0x1DD,
|
||||
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
||||
SMSG_LOGIN_SETTIMESPEED = 0x042,
|
||||
SMSG_TUTORIAL_FLAGS = 0x0FD,
|
||||
SMSG_WARDEN_DATA = 0x2E6,
|
||||
CMSG_WARDEN_DATA = 0x2E7,
|
||||
SMSG_ACCOUNT_DATA_TIMES = 0x209,
|
||||
SMSG_CLIENTCACHE_VERSION = 0x4AB,
|
||||
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
|
||||
SMSG_MOTD = 0x33D,
|
||||
|
||||
// ---- Entity/Object updates ----
|
||||
SMSG_UPDATE_OBJECT = 0x0A9,
|
||||
SMSG_COMPRESSED_UPDATE_OBJECT = 0x1F6,
|
||||
SMSG_MONSTER_MOVE_TRANSPORT = 0x2AE,
|
||||
SMSG_DESTROY_OBJECT = 0x0AA,
|
||||
|
||||
// ---- Chat ----
|
||||
CMSG_MESSAGECHAT = 0x095,
|
||||
SMSG_MESSAGECHAT = 0x096,
|
||||
|
||||
// ---- Server Info Commands ----
|
||||
CMSG_WHO = 0x062,
|
||||
SMSG_WHO = 0x063,
|
||||
CMSG_REQUEST_PLAYED_TIME = 0x1CC,
|
||||
SMSG_PLAYED_TIME = 0x1CD,
|
||||
CMSG_QUERY_TIME = 0x1CE,
|
||||
SMSG_QUERY_TIME_RESPONSE = 0x1CF,
|
||||
|
||||
// ---- Social Commands ----
|
||||
SMSG_FRIEND_STATUS = 0x068,
|
||||
CMSG_ADD_FRIEND = 0x069,
|
||||
CMSG_DEL_FRIEND = 0x06A,
|
||||
CMSG_SET_CONTACT_NOTES = 0x06B,
|
||||
CMSG_ADD_IGNORE = 0x06C,
|
||||
CMSG_DEL_IGNORE = 0x06D,
|
||||
|
||||
// ---- Logout Commands ----
|
||||
CMSG_PLAYER_LOGOUT = 0x04A,
|
||||
CMSG_LOGOUT_REQUEST = 0x04B,
|
||||
CMSG_LOGOUT_CANCEL = 0x04E,
|
||||
SMSG_LOGOUT_RESPONSE = 0x04C,
|
||||
SMSG_LOGOUT_COMPLETE = 0x04D,
|
||||
|
||||
// ---- Stand State ----
|
||||
CMSG_STAND_STATE_CHANGE = 0x101,
|
||||
|
||||
// ---- Display Toggles ----
|
||||
CMSG_SHOWING_HELM = 0x2B9,
|
||||
CMSG_SHOWING_CLOAK = 0x2BA,
|
||||
|
||||
// ---- PvP ----
|
||||
CMSG_TOGGLE_PVP = 0x253,
|
||||
|
||||
// ---- Guild ----
|
||||
CMSG_GUILD_INVITE = 0x082,
|
||||
CMSG_GUILD_ACCEPT = 0x084,
|
||||
CMSG_GUILD_DECLINE_INVITATION = 0x085,
|
||||
CMSG_GUILD_INFO = 0x087,
|
||||
CMSG_GUILD_GET_ROSTER = 0x089,
|
||||
CMSG_GUILD_PROMOTE_MEMBER = 0x08B,
|
||||
CMSG_GUILD_DEMOTE_MEMBER = 0x08C,
|
||||
CMSG_GUILD_LEAVE = 0x08D,
|
||||
CMSG_GUILD_MOTD = 0x091,
|
||||
SMSG_GUILD_INFO = 0x088,
|
||||
SMSG_GUILD_ROSTER = 0x08A,
|
||||
|
||||
// ---- Ready Check ----
|
||||
MSG_RAID_READY_CHECK = 0x322,
|
||||
MSG_RAID_READY_CHECK_CONFIRM = 0x3AE,
|
||||
|
||||
// ---- Duel ----
|
||||
CMSG_DUEL_PROPOSED = 0x166,
|
||||
CMSG_DUEL_ACCEPTED = 0x16C,
|
||||
CMSG_DUEL_CANCELLED = 0x16D,
|
||||
SMSG_DUEL_REQUESTED = 0x167,
|
||||
|
||||
// ---- Trade ----
|
||||
CMSG_INITIATE_TRADE = 0x116,
|
||||
|
||||
// ---- Random Roll ----
|
||||
MSG_RANDOM_ROLL = 0x1FB,
|
||||
|
||||
// ---- Phase 1: Foundation (Targeting, Queries) ----
|
||||
CMSG_SET_SELECTION = 0x13D,
|
||||
CMSG_NAME_QUERY = 0x050,
|
||||
SMSG_NAME_QUERY_RESPONSE = 0x051,
|
||||
CMSG_CREATURE_QUERY = 0x060,
|
||||
SMSG_CREATURE_QUERY_RESPONSE = 0x061,
|
||||
CMSG_GAMEOBJECT_QUERY = 0x05E,
|
||||
SMSG_GAMEOBJECT_QUERY_RESPONSE = 0x05F,
|
||||
CMSG_SET_ACTIVE_MOVER = 0x26A,
|
||||
CMSG_BINDER_ACTIVATE = 0x1B5,
|
||||
|
||||
// ---- XP ----
|
||||
SMSG_LOG_XPGAIN = 0x1D0,
|
||||
|
||||
// ---- Creature Movement ----
|
||||
SMSG_MONSTER_MOVE = 0x0DD,
|
||||
|
||||
// ---- Phase 2: Combat Core ----
|
||||
CMSG_ATTACKSWING = 0x141,
|
||||
CMSG_ATTACKSTOP = 0x142,
|
||||
SMSG_ATTACKSTART = 0x143,
|
||||
SMSG_ATTACKSTOP = 0x144,
|
||||
SMSG_ATTACKERSTATEUPDATE = 0x14A,
|
||||
SMSG_SPELLNONMELEEDAMAGELOG = 0x250,
|
||||
SMSG_SPELLHEALLOG = 0x150,
|
||||
SMSG_SPELLENERGIZELOG = 0x25B,
|
||||
SMSG_PERIODICAURALOG = 0x24E,
|
||||
SMSG_ENVIRONMENTALDAMAGELOG = 0x1FC,
|
||||
|
||||
// ---- Phase 3: Spells, Action Bar, Auras ----
|
||||
CMSG_CAST_SPELL = 0x12E,
|
||||
CMSG_CANCEL_CAST = 0x12F,
|
||||
CMSG_CANCEL_AURA = 0x033,
|
||||
SMSG_CAST_FAILED = 0x130,
|
||||
SMSG_SPELL_START = 0x131,
|
||||
SMSG_SPELL_GO = 0x132,
|
||||
SMSG_SPELL_FAILURE = 0x133,
|
||||
SMSG_SPELL_COOLDOWN = 0x134,
|
||||
SMSG_COOLDOWN_EVENT = 0x135,
|
||||
SMSG_UPDATE_AURA_DURATION = 0x137,
|
||||
SMSG_INITIAL_SPELLS = 0x12A,
|
||||
SMSG_LEARNED_SPELL = 0x12B,
|
||||
SMSG_SUPERCEDED_SPELL = 0x12C,
|
||||
SMSG_REMOVED_SPELL = 0x203,
|
||||
SMSG_SEND_UNLEARN_SPELLS = 0x41F,
|
||||
SMSG_SPELL_DELAYED = 0x1E2,
|
||||
SMSG_AURA_UPDATE = 0x3FA,
|
||||
SMSG_AURA_UPDATE_ALL = 0x495,
|
||||
SMSG_SET_FLAT_SPELL_MODIFIER = 0x266,
|
||||
SMSG_SET_PCT_SPELL_MODIFIER = 0x267,
|
||||
|
||||
// ---- Talents ----
|
||||
SMSG_TALENTS_INFO = 0x4C0,
|
||||
CMSG_LEARN_TALENT = 0x251,
|
||||
MSG_TALENT_WIPE_CONFIRM = 0x2AB,
|
||||
|
||||
// ---- Phase 4: Group/Party ----
|
||||
CMSG_GROUP_INVITE = 0x06E,
|
||||
SMSG_GROUP_INVITE = 0x06F,
|
||||
CMSG_GROUP_ACCEPT = 0x072,
|
||||
CMSG_GROUP_DECLINE = 0x073,
|
||||
SMSG_GROUP_DECLINE = 0x074,
|
||||
CMSG_GROUP_UNINVITE_GUID = 0x076,
|
||||
SMSG_GROUP_UNINVITE = 0x077,
|
||||
CMSG_GROUP_SET_LEADER = 0x078,
|
||||
SMSG_GROUP_SET_LEADER = 0x079,
|
||||
CMSG_GROUP_DISBAND = 0x07B,
|
||||
SMSG_GROUP_LIST = 0x07D,
|
||||
SMSG_PARTY_COMMAND_RESULT = 0x07E,
|
||||
MSG_RAID_TARGET_UPDATE = 0x321,
|
||||
CMSG_REQUEST_RAID_INFO = 0x2CD,
|
||||
SMSG_RAID_INSTANCE_INFO = 0x2CC,
|
||||
|
||||
// ---- Phase 5: Loot ----
|
||||
CMSG_AUTOSTORE_LOOT_ITEM = 0x108,
|
||||
CMSG_LOOT = 0x15D,
|
||||
CMSG_LOOT_MONEY = 0x15E,
|
||||
CMSG_LOOT_RELEASE = 0x15F,
|
||||
SMSG_LOOT_RESPONSE = 0x160,
|
||||
SMSG_LOOT_RELEASE_RESPONSE = 0x161,
|
||||
SMSG_LOOT_REMOVED = 0x162,
|
||||
SMSG_LOOT_MONEY_NOTIFY = 0x163,
|
||||
SMSG_LOOT_CLEAR_MONEY = 0x165,
|
||||
|
||||
// ---- Phase 5: Taxi / Flight Paths ----
|
||||
CMSG_ACTIVATETAXI = 0x19D,
|
||||
|
||||
// ---- Phase 5: NPC Gossip ----
|
||||
CMSG_GOSSIP_HELLO = 0x17B,
|
||||
CMSG_GOSSIP_SELECT_OPTION = 0x17C,
|
||||
SMSG_GOSSIP_MESSAGE = 0x17D,
|
||||
SMSG_GOSSIP_COMPLETE = 0x17E,
|
||||
SMSG_NPC_TEXT_UPDATE = 0x180,
|
||||
|
||||
// ---- Phase 5: GameObject ----
|
||||
CMSG_GAMEOBJECT_USE = 0x01B,
|
||||
|
||||
// ---- Phase 5: Quests ----
|
||||
CMSG_QUESTGIVER_STATUS_QUERY = 0x182,
|
||||
SMSG_QUESTGIVER_STATUS = 0x183,
|
||||
SMSG_QUESTGIVER_STATUS_MULTIPLE = 0x198,
|
||||
CMSG_QUESTGIVER_HELLO = 0x184,
|
||||
CMSG_QUESTGIVER_QUERY_QUEST = 0x186,
|
||||
SMSG_QUESTGIVER_QUEST_DETAILS = 0x188,
|
||||
CMSG_QUESTGIVER_ACCEPT_QUEST = 0x189,
|
||||
CMSG_QUESTGIVER_COMPLETE_QUEST = 0x18A,
|
||||
SMSG_QUESTGIVER_REQUEST_ITEMS = 0x18B,
|
||||
CMSG_QUESTGIVER_REQUEST_REWARD = 0x18C,
|
||||
SMSG_QUESTGIVER_OFFER_REWARD = 0x18D,
|
||||
CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E,
|
||||
SMSG_QUESTGIVER_QUEST_INVALID = 0x18F,
|
||||
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
|
||||
CMSG_QUESTLOG_REMOVE_QUEST = 0x194,
|
||||
SMSG_QUESTUPDATE_ADD_KILL = 0x196, // Quest kill count update
|
||||
SMSG_QUESTUPDATE_COMPLETE = 0x195, // Quest objectives completed
|
||||
CMSG_QUEST_QUERY = 0x05C, // Client requests quest data
|
||||
SMSG_QUEST_QUERY_RESPONSE = 0x05D, // Server sends quest data
|
||||
SMSG_QUESTLOG_FULL = 0x1A3, // Full quest log on login
|
||||
|
||||
// ---- Phase 5: Vendor ----
|
||||
CMSG_LIST_INVENTORY = 0x19E,
|
||||
SMSG_LIST_INVENTORY = 0x19F,
|
||||
CMSG_SELL_ITEM = 0x1A0,
|
||||
SMSG_SELL_ITEM = 0x1A1,
|
||||
CMSG_BUY_ITEM = 0x1A2,
|
||||
SMSG_BUY_FAILED = 0x1A5,
|
||||
|
||||
// ---- Trainer ----
|
||||
CMSG_TRAINER_LIST = 0x01B0,
|
||||
SMSG_TRAINER_LIST = 0x01B1,
|
||||
CMSG_TRAINER_BUY_SPELL = 0x01B2,
|
||||
SMSG_TRAINER_BUY_FAILED = 0x01B4,
|
||||
|
||||
// ---- Phase 5: Item/Equip ----
|
||||
CMSG_ITEM_QUERY_SINGLE = 0x056,
|
||||
SMSG_ITEM_QUERY_SINGLE_RESPONSE = 0x058,
|
||||
CMSG_USE_ITEM = 0x00AB,
|
||||
CMSG_AUTOEQUIP_ITEM = 0x10A,
|
||||
CMSG_SWAP_ITEM = 0x10C,
|
||||
CMSG_SWAP_INV_ITEM = 0x10D,
|
||||
SMSG_INVENTORY_CHANGE_FAILURE = 0x112,
|
||||
CMSG_INSPECT = 0x114,
|
||||
SMSG_INSPECT_RESULTS = 0x115,
|
||||
|
||||
// ---- Death/Respawn ----
|
||||
CMSG_REPOP_REQUEST = 0x015A,
|
||||
SMSG_RESURRECT_REQUEST = 0x015B,
|
||||
CMSG_RESURRECT_RESPONSE = 0x015C,
|
||||
CMSG_SPIRIT_HEALER_ACTIVATE = 0x021C,
|
||||
SMSG_SPIRIT_HEALER_CONFIRM = 0x0222,
|
||||
SMSG_RESURRECT_CANCEL = 0x0390,
|
||||
|
||||
// ---- Teleport / Transfer ----
|
||||
MSG_MOVE_TELEPORT_ACK = 0x0C7,
|
||||
SMSG_TRANSFER_PENDING = 0x003F,
|
||||
SMSG_NEW_WORLD = 0x003E,
|
||||
MSG_MOVE_WORLDPORT_ACK = 0x00DC,
|
||||
SMSG_TRANSFER_ABORTED = 0x0040,
|
||||
|
||||
// ---- Speed Changes ----
|
||||
SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2,
|
||||
CMSG_FORCE_RUN_SPEED_CHANGE_ACK = 0x00E3,
|
||||
|
||||
// ---- Mount ----
|
||||
CMSG_CANCEL_MOUNT_AURA = 0x0375,
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
SMSG_SHOWTAXINODES = 0x01A9,
|
||||
SMSG_ACTIVATETAXIREPLY = 0x01AE,
|
||||
// Some cores send activate taxi reply on 0x029D (observed in logs)
|
||||
SMSG_ACTIVATETAXIREPLY_ALT = 0x029D,
|
||||
SMSG_NEW_TAXI_PATH = 0x01AF,
|
||||
CMSG_ACTIVATETAXIEXPRESS = 0x0312,
|
||||
|
||||
// ---- Battleground ----
|
||||
SMSG_BATTLEFIELD_PORT_DENIED = 0x014B,
|
||||
SMSG_REMOVED_FROM_PVP_QUEUE = 0x0170,
|
||||
SMSG_TRAINER_BUY_SUCCEEDED = 0x01B3,
|
||||
SMSG_BINDPOINTUPDATE = 0x0155,
|
||||
CMSG_BATTLEFIELD_LIST = 0x023C,
|
||||
SMSG_BATTLEFIELD_LIST = 0x023D,
|
||||
CMSG_BATTLEFIELD_JOIN = 0x023E,
|
||||
CMSG_BATTLEFIELD_STATUS = 0x02D3,
|
||||
SMSG_BATTLEFIELD_STATUS = 0x02D4,
|
||||
CMSG_BATTLEFIELD_PORT = 0x02D5,
|
||||
CMSG_BATTLEMASTER_HELLO = 0x02D7,
|
||||
MSG_PVP_LOG_DATA = 0x02E0,
|
||||
CMSG_LEAVE_BATTLEFIELD = 0x02E1,
|
||||
SMSG_GROUP_JOINED_BATTLEGROUND = 0x02E8,
|
||||
MSG_BATTLEGROUND_PLAYER_POSITIONS = 0x02E9,
|
||||
SMSG_BATTLEGROUND_PLAYER_JOINED = 0x02EC,
|
||||
SMSG_BATTLEGROUND_PLAYER_LEFT = 0x02ED,
|
||||
CMSG_BATTLEMASTER_JOIN = 0x02EE,
|
||||
SMSG_JOINED_BATTLEGROUND_QUEUE = 0x038A,
|
||||
|
||||
// ---- Arena Team ----
|
||||
CMSG_ARENA_TEAM_CREATE = 0x0348,
|
||||
SMSG_ARENA_TEAM_COMMAND_RESULT = 0x0349,
|
||||
CMSG_ARENA_TEAM_QUERY = 0x034B,
|
||||
SMSG_ARENA_TEAM_QUERY_RESPONSE = 0x034C,
|
||||
CMSG_ARENA_TEAM_ROSTER = 0x034D,
|
||||
SMSG_ARENA_TEAM_ROSTER = 0x034E,
|
||||
CMSG_ARENA_TEAM_INVITE = 0x034F,
|
||||
SMSG_ARENA_TEAM_INVITE = 0x0350,
|
||||
CMSG_ARENA_TEAM_ACCEPT = 0x0351,
|
||||
CMSG_ARENA_TEAM_DECLINE = 0x0352,
|
||||
CMSG_ARENA_TEAM_LEAVE = 0x0353,
|
||||
CMSG_ARENA_TEAM_REMOVE = 0x0354,
|
||||
CMSG_ARENA_TEAM_DISBAND = 0x0355,
|
||||
CMSG_ARENA_TEAM_LEADER = 0x0356,
|
||||
SMSG_ARENA_TEAM_EVENT = 0x0357,
|
||||
CMSG_BATTLEMASTER_JOIN_ARENA = 0x0358,
|
||||
SMSG_ARENA_TEAM_STATS = 0x035B,
|
||||
SMSG_ARENA_ERROR = 0x0376,
|
||||
MSG_INSPECT_ARENA_TEAMS = 0x0377,
|
||||
};
|
||||
// Backwards-compatibility alias: existing code uses Opcode::X which now maps
|
||||
// to LogicalOpcode::X (the expansion-agnostic logical enum).
|
||||
// Wire values are resolved at runtime via OpcodeTable.
|
||||
using Opcode = LogicalOpcode;
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
178
include/game/packet_parsers.hpp
Normal file
178
include/game/packet_parsers.hpp
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#pragma once
|
||||
|
||||
#include "game/world_packets.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* PacketParsers - Polymorphic interface for expansion-specific packet parsing.
|
||||
*
|
||||
* Binary packet formats differ significantly between WoW expansions
|
||||
* (movement flags, update fields, character enum layout, etc.).
|
||||
* Each expansion implements this interface with its specific parsing logic.
|
||||
*
|
||||
* The base PacketParsers delegates to the existing static parser classes
|
||||
* in world_packets.hpp. Expansion subclasses override the methods that
|
||||
* differ from WotLK.
|
||||
*/
|
||||
class PacketParsers {
|
||||
public:
|
||||
virtual ~PacketParsers() = default;
|
||||
|
||||
// --- Movement ---
|
||||
|
||||
/** Parse movement block from SMSG_UPDATE_OBJECT */
|
||||
virtual bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
return UpdateObjectParser::parseMovementBlock(packet, block);
|
||||
}
|
||||
|
||||
/** Write movement payload for CMSG_MOVE_* packets */
|
||||
virtual void writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
|
||||
MovementPacket::writeMovementPayload(packet, info);
|
||||
}
|
||||
|
||||
/** Build a complete movement packet with packed GUID + payload */
|
||||
virtual network::Packet buildMovementPacket(LogicalOpcode opcode,
|
||||
const MovementInfo& info,
|
||||
uint64_t playerGuid = 0) {
|
||||
return MovementPacket::build(opcode, info, playerGuid);
|
||||
}
|
||||
|
||||
// --- Character Enumeration ---
|
||||
|
||||
/** Parse SMSG_CHAR_ENUM */
|
||||
virtual bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
|
||||
return CharEnumParser::parse(packet, response);
|
||||
}
|
||||
|
||||
// --- Update Object ---
|
||||
|
||||
/** Parse a full SMSG_UPDATE_OBJECT packet */
|
||||
virtual bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||||
return UpdateObjectParser::parse(packet, data);
|
||||
}
|
||||
|
||||
/** Parse update fields block (value mask + field values) */
|
||||
virtual bool parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
|
||||
return UpdateObjectParser::parseUpdateFields(packet, block);
|
||||
}
|
||||
|
||||
// --- Monster Movement ---
|
||||
|
||||
/** Parse SMSG_MONSTER_MOVE */
|
||||
virtual bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
||||
return MonsterMoveParser::parse(packet, data);
|
||||
}
|
||||
|
||||
// --- Combat ---
|
||||
|
||||
/** Parse SMSG_ATTACKERSTATEUPDATE */
|
||||
virtual bool parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
|
||||
return AttackerStateUpdateParser::parse(packet, data);
|
||||
}
|
||||
|
||||
/** Parse SMSG_SPELLNONMELEEDAMAGELOG */
|
||||
virtual bool parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
|
||||
return SpellDamageLogParser::parse(packet, data);
|
||||
}
|
||||
|
||||
// --- Spells ---
|
||||
|
||||
/** Parse SMSG_INITIAL_SPELLS */
|
||||
virtual bool parseInitialSpells(network::Packet& packet, InitialSpellsData& data) {
|
||||
return InitialSpellsParser::parse(packet, data);
|
||||
}
|
||||
|
||||
/** Parse SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL */
|
||||
virtual bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) {
|
||||
return AuraUpdateParser::parse(packet, data, isAll);
|
||||
}
|
||||
|
||||
// --- Utility ---
|
||||
|
||||
/** Read a packed GUID from the packet */
|
||||
virtual uint64_t readPackedGuid(network::Packet& packet) {
|
||||
return UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
/** Write a packed GUID to the packet */
|
||||
virtual void writePackedGuid(network::Packet& packet, uint64_t guid) {
|
||||
MovementPacket::writePackedGuid(packet, guid);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* WotLK 3.3.5a packet parsers.
|
||||
*
|
||||
* Uses the default implementations which delegate to the existing
|
||||
* static parser classes. All current parsing code is WotLK-specific,
|
||||
* so no overrides are needed.
|
||||
*/
|
||||
class WotlkPacketParsers : public PacketParsers {
|
||||
// All methods use the defaults from PacketParsers base class,
|
||||
// which delegate to the existing WotLK static parsers.
|
||||
};
|
||||
|
||||
/**
|
||||
* TBC 2.4.3 packet parsers.
|
||||
*
|
||||
* Overrides methods where TBC binary format differs from WotLK:
|
||||
* - SMSG_UPDATE_OBJECT: u8 has_transport after blockCount (WotLK removed it)
|
||||
* - UpdateFlags is u8 (not u16), no VEHICLE/ROTATION/POSITION flags
|
||||
* - Movement flags2 is u8 (not u16), no transport seat byte
|
||||
* - Movement flags: JUMPING=0x2000 gates jump data (WotLK: FALLING=0x1000)
|
||||
* - SPLINE_ENABLED=0x08000000, SPLINE_ELEVATION=0x04000000 (same as WotLK)
|
||||
* - Pitch: SWIMMING or else ONTRANSPORT(0x02000000)
|
||||
* - CharEnum: uint8 firstLogin (not uint32+uint8), 20 equipment items (not 23)
|
||||
* - Aura updates use inline update fields, not SMSG_AURA_UPDATE
|
||||
*/
|
||||
class TbcPacketParsers : public PacketParsers {
|
||||
public:
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||
network::Packet buildMovementPacket(LogicalOpcode opcode,
|
||||
const MovementInfo& info,
|
||||
uint64_t playerGuid = 0) override;
|
||||
bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) override;
|
||||
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
|
||||
bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Classic 1.12.1 packet parsers.
|
||||
*
|
||||
* Inherits from TBC (shared: u8 UpdateFlags, has_transport byte).
|
||||
*
|
||||
* Differences from TBC:
|
||||
* - No moveFlags2 byte (TBC has u8, Classic has none)
|
||||
* - Only 6 speed fields (no flight speeds — flying added in TBC)
|
||||
* - SPLINE_ENABLED at 0x00400000 (TBC/WotLK: 0x08000000)
|
||||
* - Transport data has no timestamp (TBC adds u32 timestamp)
|
||||
* - Pitch: only SWIMMING (no ONTRANSPORT secondary pitch)
|
||||
* - CharEnum: no enchantment field per equipment slot
|
||||
* - No SMSG_AURA_UPDATE (uses update fields, same as TBC)
|
||||
*/
|
||||
class ClassicPacketParsers : public TbcPacketParsers {
|
||||
public:
|
||||
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||
network::Packet buildMovementPacket(LogicalOpcode opcode,
|
||||
const MovementInfo& info,
|
||||
uint64_t playerGuid = 0) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function to create the right parser set for an expansion.
|
||||
*/
|
||||
inline std::unique_ptr<PacketParsers> createPacketParsers(const std::string& expansionId) {
|
||||
if (expansionId == "classic") return std::make_unique<ClassicPacketParsers>();
|
||||
if (expansionId == "tbc") return std::make_unique<TbcPacketParsers>();
|
||||
return std::make_unique<WotlkPacketParsers>();
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
93
include/game/update_field_table.hpp
Normal file
93
include/game/update_field_table.hpp
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* Logical update field identifiers (expansion-agnostic).
|
||||
* Wire indices are loaded at runtime from JSON.
|
||||
*/
|
||||
enum class UF : uint16_t {
|
||||
// Object fields
|
||||
OBJECT_FIELD_ENTRY,
|
||||
|
||||
// Unit fields
|
||||
UNIT_FIELD_TARGET_LO,
|
||||
UNIT_FIELD_TARGET_HI,
|
||||
UNIT_FIELD_HEALTH,
|
||||
UNIT_FIELD_POWER1,
|
||||
UNIT_FIELD_MAXHEALTH,
|
||||
UNIT_FIELD_MAXPOWER1,
|
||||
UNIT_FIELD_LEVEL,
|
||||
UNIT_FIELD_FACTIONTEMPLATE,
|
||||
UNIT_FIELD_FLAGS,
|
||||
UNIT_FIELD_FLAGS_2,
|
||||
UNIT_FIELD_DISPLAYID,
|
||||
UNIT_FIELD_MOUNTDISPLAYID,
|
||||
UNIT_NPC_FLAGS,
|
||||
UNIT_DYNAMIC_FLAGS,
|
||||
UNIT_END,
|
||||
|
||||
// Player fields
|
||||
PLAYER_FLAGS,
|
||||
PLAYER_XP,
|
||||
PLAYER_NEXT_LEVEL_XP,
|
||||
PLAYER_FIELD_COINAGE,
|
||||
PLAYER_QUEST_LOG_START,
|
||||
PLAYER_FIELD_INV_SLOT_HEAD,
|
||||
PLAYER_FIELD_PACK_SLOT_1,
|
||||
PLAYER_SKILL_INFO_START,
|
||||
PLAYER_EXPLORED_ZONES_START,
|
||||
|
||||
// GameObject fields
|
||||
GAMEOBJECT_DISPLAYID,
|
||||
|
||||
// Item fields
|
||||
ITEM_FIELD_STACK_COUNT,
|
||||
|
||||
COUNT
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps logical update field names to expansion-specific wire indices.
|
||||
* Loaded from JSON (e.g. Data/expansions/wotlk/update_fields.json).
|
||||
*/
|
||||
class UpdateFieldTable {
|
||||
public:
|
||||
/** Load from JSON file. Returns true if successful. */
|
||||
bool loadFromJson(const std::string& path);
|
||||
|
||||
/** Load built-in WotLK 3.3.5a defaults. */
|
||||
void loadWotlkDefaults();
|
||||
|
||||
/** Get the wire index for a logical field. Returns 0xFFFF if unknown. */
|
||||
uint16_t index(UF field) const;
|
||||
|
||||
/** Check if a field is mapped. */
|
||||
bool hasField(UF field) const;
|
||||
|
||||
/** Number of mapped fields. */
|
||||
size_t size() const { return fieldMap_.size(); }
|
||||
|
||||
private:
|
||||
std::unordered_map<uint16_t, uint16_t> fieldMap_; // UF enum → wire index
|
||||
};
|
||||
|
||||
/**
|
||||
* Global active update field table (set by Application at startup).
|
||||
*/
|
||||
void setActiveUpdateFieldTable(const UpdateFieldTable* table);
|
||||
const UpdateFieldTable* getActiveUpdateFieldTable();
|
||||
|
||||
/** Convenience: get wire index for a logical field. */
|
||||
inline uint16_t fieldIndex(UF field) {
|
||||
const auto* t = getActiveUpdateFieldTable();
|
||||
return t ? t->index(field) : 0xFFFF;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -514,7 +514,6 @@ public:
|
|||
*/
|
||||
static uint64_t readPackedGuid(network::Packet& packet);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Parse a single update block
|
||||
*
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "pipeline/loose_file_reader.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
|
|
@ -16,6 +17,8 @@ namespace pipeline {
|
|||
* AssetManager - Unified interface for loading WoW assets
|
||||
*
|
||||
* Reads pre-extracted loose files indexed by manifest.json.
|
||||
* Supports layered manifests: overlay manifests (HD packs, mods)
|
||||
* are checked before the base manifest, with higher priority first.
|
||||
* Use the asset_extract tool to extract MPQ archives first.
|
||||
* All reads are fully parallel (no serialization mutex needed).
|
||||
*/
|
||||
|
|
@ -41,6 +44,26 @@ public:
|
|||
*/
|
||||
bool isInitialized() const { return initialized; }
|
||||
|
||||
/**
|
||||
* Add an overlay manifest (HD packs, mods) checked before the base manifest.
|
||||
* Higher priority overlays are checked first.
|
||||
* @param manifestPath Full path to the overlay's manifest.json
|
||||
* @param priority Priority level (higher = checked first)
|
||||
* @param id Unique identifier for this overlay (e.g. "hd_character")
|
||||
* @return true if overlay loaded successfully
|
||||
*/
|
||||
bool addOverlayManifest(const std::string& manifestPath, int priority, const std::string& id);
|
||||
|
||||
/**
|
||||
* Remove a previously added overlay manifest by id.
|
||||
*/
|
||||
void removeOverlay(const std::string& id);
|
||||
|
||||
/**
|
||||
* Get list of active overlay IDs.
|
||||
*/
|
||||
std::vector<std::string> getOverlayIds() const;
|
||||
|
||||
/**
|
||||
* Load a BLP texture
|
||||
* @param path Virtual path to BLP file (e.g., "Textures\\Minimap\\Background.blp")
|
||||
|
|
@ -105,10 +128,24 @@ private:
|
|||
bool initialized = false;
|
||||
std::string dataPath;
|
||||
|
||||
// Loose file backend
|
||||
// Base manifest (loaded from dataPath/manifest.json)
|
||||
AssetManifest manifest_;
|
||||
LooseFileReader looseReader_;
|
||||
|
||||
// Overlay manifests (HD packs, mods) - sorted by priority descending
|
||||
struct ManifestLayer {
|
||||
AssetManifest manifest;
|
||||
int priority;
|
||||
std::string id;
|
||||
};
|
||||
std::vector<ManifestLayer> overlayLayers_; // Sorted by priority desc
|
||||
|
||||
/**
|
||||
* Resolve filesystem path checking overlays first, then base manifest.
|
||||
* Returns empty string if not found in any layer.
|
||||
*/
|
||||
std::string resolveLayeredPath(const std::string& normalizedPath) const;
|
||||
|
||||
mutable std::mutex cacheMutex;
|
||||
std::map<std::string, std::shared_ptr<DBCFile>> dbcCache;
|
||||
|
||||
|
|
|
|||
64
include/pipeline/dbc_layout.hpp
Normal file
64
include/pipeline/dbc_layout.hpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
/**
|
||||
* Maps DBC field names to column indices for a single DBC file.
|
||||
* Column indices vary between WoW expansions.
|
||||
*/
|
||||
struct DBCFieldMap {
|
||||
std::unordered_map<std::string, uint32_t> fields;
|
||||
|
||||
/** Get column index by field name. Returns 0xFFFFFFFF if unknown. */
|
||||
uint32_t field(const std::string& name) const {
|
||||
auto it = fields.find(name);
|
||||
return (it != fields.end()) ? it->second : 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
/** Convenience operator for shorter syntax: layout["Name"] */
|
||||
uint32_t operator[](const std::string& name) const { return field(name); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps DBC file names to their field layouts.
|
||||
* Loaded from JSON (e.g. Data/expansions/wotlk/dbc_layouts.json).
|
||||
*/
|
||||
class DBCLayout {
|
||||
public:
|
||||
/** Load from JSON file. Returns true if successful. */
|
||||
bool loadFromJson(const std::string& path);
|
||||
|
||||
/** Load built-in WotLK 3.3.5a defaults. */
|
||||
void loadWotlkDefaults();
|
||||
|
||||
/** Get the field map for a DBC file. Returns nullptr if unknown. */
|
||||
const DBCFieldMap* getLayout(const std::string& dbcName) const;
|
||||
|
||||
/** Number of DBC layouts loaded. */
|
||||
size_t size() const { return layouts_.size(); }
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, DBCFieldMap> layouts_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global active DBC layout (set by Application at startup).
|
||||
*/
|
||||
void setActiveDBCLayout(const DBCLayout* layout);
|
||||
const DBCLayout* getActiveDBCLayout();
|
||||
|
||||
/** Convenience: get field index for a DBC field. */
|
||||
inline uint32_t dbcField(const std::string& dbcName, const std::string& fieldName) {
|
||||
const auto* l = getActiveDBCLayout();
|
||||
if (!l) return 0xFFFFFFFF;
|
||||
const auto* fm = l->getLayout(dbcName);
|
||||
return fm ? fm->field(fieldName) : 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
97
include/pipeline/hd_pack_manager.hpp
Normal file
97
include/pipeline/hd_pack_manager.hpp
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
class AssetManager;
|
||||
|
||||
/**
|
||||
* Metadata for a single HD texture pack on disk.
|
||||
*
|
||||
* Each pack lives in Data/hd/<packDir>/ and contains:
|
||||
* pack.json - metadata (id, name, group, compatible expansions, size)
|
||||
* manifest.json - standard asset manifest with HD override textures
|
||||
* assets/ - the actual HD files
|
||||
*/
|
||||
struct HDPack {
|
||||
std::string id; // Unique identifier (e.g. "character_hd")
|
||||
std::string name; // Human-readable name
|
||||
std::string group; // Grouping label (e.g. "Character", "Terrain")
|
||||
std::vector<std::string> expansions; // Compatible expansion IDs
|
||||
uint32_t totalSizeMB = 0; // Approximate total size on disk
|
||||
std::string manifestPath; // Full path to manifest.json
|
||||
std::string packDir; // Full path to pack directory
|
||||
bool enabled = false; // User-toggled enable state
|
||||
};
|
||||
|
||||
/**
|
||||
* HDPackManager - discovers, manages, and wires HD texture packs.
|
||||
*
|
||||
* Scans Data/hd/ subdirectories for pack.json files. Each pack can be
|
||||
* enabled/disabled via setPackEnabled(). Enabled packs are wired into
|
||||
* AssetManager as high-priority overlay manifests so that HD textures
|
||||
* override the base expansion assets transparently.
|
||||
*/
|
||||
class HDPackManager {
|
||||
public:
|
||||
HDPackManager() = default;
|
||||
|
||||
/**
|
||||
* Scan the HD root directory for available packs.
|
||||
* @param hdRootPath Path to Data/hd/ directory
|
||||
*/
|
||||
void initialize(const std::string& hdRootPath);
|
||||
|
||||
/**
|
||||
* Get all discovered packs.
|
||||
*/
|
||||
const std::vector<HDPack>& getAllPacks() const { return packs_; }
|
||||
|
||||
/**
|
||||
* Get packs compatible with a specific expansion.
|
||||
*/
|
||||
std::vector<const HDPack*> getPacksForExpansion(const std::string& expansionId) const;
|
||||
|
||||
/**
|
||||
* Enable or disable a pack. Persists state in enabledPacks_ map.
|
||||
*/
|
||||
void setPackEnabled(const std::string& packId, bool enabled);
|
||||
|
||||
/**
|
||||
* Check if a pack is enabled.
|
||||
*/
|
||||
bool isPackEnabled(const std::string& packId) const;
|
||||
|
||||
/**
|
||||
* Apply enabled packs as overlays to the asset manager.
|
||||
* Removes previously applied overlays and re-adds enabled ones.
|
||||
*/
|
||||
void applyToAssetManager(AssetManager* assetManager, const std::string& expansionId);
|
||||
|
||||
/**
|
||||
* Save enabled pack state to a settings file.
|
||||
*/
|
||||
void saveSettings(const std::string& settingsPath) const;
|
||||
|
||||
/**
|
||||
* Load enabled pack state from a settings file.
|
||||
*/
|
||||
void loadSettings(const std::string& settingsPath);
|
||||
|
||||
private:
|
||||
std::vector<HDPack> packs_;
|
||||
std::unordered_map<std::string, bool> enabledState_; // packId → enabled
|
||||
|
||||
// Overlay IDs currently applied to AssetManager (for removal on re-apply)
|
||||
std::vector<std::string> appliedOverlayIds_;
|
||||
|
||||
static constexpr int HD_OVERLAY_PRIORITY_BASE = 100; // High priority, above expansion base
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include "auth/auth_handler.hpp"
|
||||
#include "rendering/video_player.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
|
@ -46,7 +47,7 @@ private:
|
|||
char username[256] = "";
|
||||
char password[256] = "";
|
||||
int port = 3724;
|
||||
int compatibilityMode = 0; // 0 = 3.3.5a
|
||||
int expansionIndex = 0; // Index into expansion registry profiles
|
||||
bool authenticating = false;
|
||||
bool showPassword = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ network::Packet LogonChallengePacket::build(const std::string& account, const Cl
|
|||
|
||||
network::Packet packet(static_cast<uint16_t>(AuthOpcode::LOGON_CHALLENGE));
|
||||
|
||||
// Protocol version (WoW 3.3.5a build 12340 uses protocol version 8)
|
||||
packet.writeUInt8(0x08);
|
||||
// Protocol version (e.g. 8 for WoW 3.3.5a build 12340)
|
||||
packet.writeUInt8(info.protocolVersion);
|
||||
|
||||
// Payload size
|
||||
packet.writeUInt16(payloadSize);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include "game/transport_manager.hpp"
|
||||
#include "game/world.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include "game/packet_parsers.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "pipeline/hd_pack_manager.hpp"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <GL/glew.h>
|
||||
#include <chrono>
|
||||
|
|
@ -48,6 +52,7 @@
|
|||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
#include <filesystem>
|
||||
|
||||
namespace wowee {
|
||||
namespace core {
|
||||
|
|
@ -118,6 +123,15 @@ bool Application::initialize() {
|
|||
gameHandler = std::make_unique<game::GameHandler>();
|
||||
world = std::make_unique<game::World>();
|
||||
|
||||
// Create and initialize expansion registry
|
||||
expansionRegistry_ = std::make_unique<game::ExpansionRegistry>();
|
||||
|
||||
// Create DBC layout
|
||||
dbcLayout_ = std::make_unique<pipeline::DBCLayout>();
|
||||
|
||||
// Create HD pack manager
|
||||
hdPackManager_ = std::make_unique<pipeline::HDPackManager>();
|
||||
|
||||
// Create asset manager
|
||||
assetManager = std::make_unique<pipeline::AssetManager>();
|
||||
|
||||
|
|
@ -125,8 +139,56 @@ bool Application::initialize() {
|
|||
const char* dataPathEnv = std::getenv("WOW_DATA_PATH");
|
||||
std::string dataPath = dataPathEnv ? dataPathEnv : "./Data";
|
||||
|
||||
LOG_INFO("Attempting to load WoW assets from: ", dataPath);
|
||||
if (assetManager->initialize(dataPath)) {
|
||||
// Scan for available expansion profiles
|
||||
expansionRegistry_->initialize(dataPath);
|
||||
|
||||
// Load expansion-specific opcode table
|
||||
if (gameHandler && expansionRegistry_) {
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
if (profile) {
|
||||
std::string opcodesPath = profile->dataPath + "/opcodes.json";
|
||||
if (!gameHandler->getOpcodeTable().loadFromJson(opcodesPath)) {
|
||||
LOG_INFO("Using built-in WotLK opcode defaults");
|
||||
}
|
||||
game::setActiveOpcodeTable(&gameHandler->getOpcodeTable());
|
||||
|
||||
// Load expansion-specific update field table
|
||||
std::string updateFieldsPath = profile->dataPath + "/update_fields.json";
|
||||
if (!gameHandler->getUpdateFieldTable().loadFromJson(updateFieldsPath)) {
|
||||
LOG_INFO("Using built-in WotLK update field defaults");
|
||||
}
|
||||
game::setActiveUpdateFieldTable(&gameHandler->getUpdateFieldTable());
|
||||
|
||||
// Create expansion-specific packet parsers
|
||||
gameHandler->setPacketParsers(game::createPacketParsers(profile->id));
|
||||
|
||||
// Load expansion-specific DBC layouts
|
||||
if (dbcLayout_) {
|
||||
std::string dbcLayoutsPath = profile->dataPath + "/dbc_layouts.json";
|
||||
if (!dbcLayout_->loadFromJson(dbcLayoutsPath)) {
|
||||
dbcLayout_->loadWotlkDefaults();
|
||||
LOG_INFO("Using built-in WotLK DBC layout defaults");
|
||||
}
|
||||
pipeline::setActiveDBCLayout(dbcLayout_.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try expansion-specific asset path first, fall back to base Data/
|
||||
std::string assetPath = dataPath;
|
||||
if (expansionRegistry_) {
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
if (profile && !profile->dataPath.empty()) {
|
||||
std::string expansionManifest = profile->dataPath + "/manifest.json";
|
||||
if (std::filesystem::exists(expansionManifest)) {
|
||||
assetPath = profile->dataPath;
|
||||
LOG_INFO("Using expansion-specific asset path: ", assetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Attempting to load WoW assets from: ", assetPath);
|
||||
if (assetManager->initialize(assetPath)) {
|
||||
LOG_INFO("Asset manager initialized successfully");
|
||||
// Eagerly load creature display DBC lookups so first spawn doesn't stall
|
||||
buildCreatureDisplayLookups();
|
||||
|
|
@ -142,6 +204,28 @@ bool Application::initialize() {
|
|||
if (gameHandler && gameHandler->getTransportManager()) {
|
||||
gameHandler->getTransportManager()->loadTransportAnimationDBC(assetManager.get());
|
||||
}
|
||||
|
||||
// Initialize HD texture packs
|
||||
if (hdPackManager_) {
|
||||
std::string hdPath = dataPath + "/hd";
|
||||
std::string settingsDir;
|
||||
const char* xdg = std::getenv("XDG_DATA_HOME");
|
||||
if (xdg && *xdg) {
|
||||
settingsDir = std::string(xdg) + "/wowee";
|
||||
} else {
|
||||
const char* home = std::getenv("HOME");
|
||||
settingsDir = std::string(home ? home : ".") + "/.local/share/wowee";
|
||||
}
|
||||
hdPackManager_->loadSettings(settingsDir + "/settings.cfg");
|
||||
hdPackManager_->initialize(hdPath);
|
||||
|
||||
// Apply enabled packs as overlays
|
||||
std::string expansionId = "wotlk";
|
||||
if (expansionRegistry_ && expansionRegistry_->getActive()) {
|
||||
expansionId = expansionRegistry_->getActive()->id;
|
||||
}
|
||||
hdPackManager_->applyToAssetManager(assetManager.get(), expansionId);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable");
|
||||
LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory");
|
||||
|
|
@ -805,7 +889,12 @@ void Application::setupUICallbacks() {
|
|||
accountName = "TESTACCOUNT";
|
||||
}
|
||||
|
||||
if (gameHandler->connect(host, port, sessionKey, accountName)) {
|
||||
uint32_t clientBuild = 12340; // default WotLK
|
||||
if (expansionRegistry_) {
|
||||
auto* profile = expansionRegistry_->getActive();
|
||||
if (profile) clientBuild = profile->build;
|
||||
}
|
||||
if (gameHandler->connect(host, port, sessionKey, accountName, clientBuild)) {
|
||||
LOG_INFO("Connected to world server, transitioning to character selection");
|
||||
setState(AppState::CHARACTER_SELECTION);
|
||||
} else {
|
||||
|
|
@ -1643,22 +1732,24 @@ void Application::spawnPlayerCharacter() {
|
|||
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
|
||||
if (charSectionsDbc) {
|
||||
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
|
||||
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
bool foundSkin = false;
|
||||
bool foundUnderwear = false;
|
||||
bool foundFaceLower = false;
|
||||
bool foundHair = false;
|
||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
|
||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
|
||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
||||
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
|
||||
// Section 0 = skin: match by colorIndex = skin byte
|
||||
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4;
|
||||
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
std::string tex1 = charSectionsDbc->getString(r, csTex1);
|
||||
if (!tex1.empty()) {
|
||||
bodySkinPath = tex1;
|
||||
foundSkin = true;
|
||||
|
|
@ -1668,7 +1759,7 @@ void Application::spawnPlayerCharacter() {
|
|||
// Section 3 = hair: match variation=hairStyle, color=hairColor
|
||||
else if (baseSection == 3 && !foundHair &&
|
||||
variationIndex == charHairStyleId && colorIndex == charHairColorId) {
|
||||
hairTexturePath = charSectionsDbc->getString(r, 4);
|
||||
hairTexturePath = charSectionsDbc->getString(r, csTex1);
|
||||
if (!hairTexturePath.empty()) {
|
||||
foundHair = true;
|
||||
LOG_INFO(" DBC hair texture: ", hairTexturePath,
|
||||
|
|
@ -1678,7 +1769,7 @@ void Application::spawnPlayerCharacter() {
|
|||
// Section 1 = face lower: match variation=faceId
|
||||
else if (baseSection == 1 && !foundFaceLower &&
|
||||
variationIndex == charFaceId && colorIndex == charSkinId) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
std::string tex1 = charSectionsDbc->getString(r, csTex1);
|
||||
if (!tex1.empty()) {
|
||||
faceLowerTexturePath = tex1;
|
||||
foundFaceLower = true;
|
||||
|
|
@ -1687,7 +1778,7 @@ void Application::spawnPlayerCharacter() {
|
|||
}
|
||||
// Section 4 = underwear
|
||||
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
|
||||
for (int f = 4; f <= 6; f++) {
|
||||
for (uint32_t f = csTex1; f <= csTex1 + 2; f++) {
|
||||
std::string tex = charSectionsDbc->getString(r, f);
|
||||
if (!tex.empty()) {
|
||||
underwearPaths.push_back(tex);
|
||||
|
|
@ -1988,10 +2079,9 @@ void Application::loadEquippedWeapons() {
|
|||
continue;
|
||||
}
|
||||
|
||||
// DBC field 1 = modelName_1 (e.g. "Sword_1H_Short_A_02.mdx")
|
||||
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 1);
|
||||
// DBC field 3 = modelTexture_1 (e.g. "Sword_1H_Short_A_02Rusty")
|
||||
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
|
||||
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModel"] : 1);
|
||||
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
|
||||
|
||||
if (modelName.empty()) {
|
||||
LOG_WARNING("loadEquippedWeapons: empty model name for displayInfoId ", displayInfoId);
|
||||
|
|
@ -2099,14 +2189,19 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
|||
}
|
||||
|
||||
// Build set of hostile parent faction IDs from Faction.dbc base reputation
|
||||
const auto* facL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr;
|
||||
const auto* ftL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("FactionTemplate") : nullptr;
|
||||
std::unordered_set<uint32_t> hostileParentFactions;
|
||||
if (fDbc && fDbc->isLoaded()) {
|
||||
const uint32_t facID = facL ? (*facL)["ID"] : 0;
|
||||
const uint32_t facRaceMask0 = facL ? (*facL)["ReputationRaceMask0"] : 2;
|
||||
const uint32_t facBase0 = facL ? (*facL)["ReputationBase0"] : 10;
|
||||
for (uint32_t i = 0; i < fDbc->getRecordCount(); i++) {
|
||||
uint32_t factionId = fDbc->getUInt32(i, 0);
|
||||
uint32_t factionId = fDbc->getUInt32(i, facID);
|
||||
for (int slot = 0; slot < 4; slot++) {
|
||||
uint32_t raceMask = fDbc->getUInt32(i, 2 + slot); // ReputationRaceMask[4] at fields 2-5
|
||||
uint32_t raceMask = fDbc->getUInt32(i, facRaceMask0 + slot);
|
||||
if (raceMask & playerRaceMask) {
|
||||
int32_t baseRep = fDbc->getInt32(i, 10 + slot); // ReputationBase[4] at fields 10-13
|
||||
int32_t baseRep = fDbc->getInt32(i, facBase0 + slot);
|
||||
if (baseRep < 0) {
|
||||
hostileParentFactions.insert(factionId);
|
||||
}
|
||||
|
|
@ -2118,14 +2213,20 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
|||
}
|
||||
|
||||
// Get player faction template data
|
||||
const uint32_t ftID = ftL ? (*ftL)["ID"] : 0;
|
||||
const uint32_t ftFaction = ftL ? (*ftL)["Faction"] : 1;
|
||||
const uint32_t ftFG = ftL ? (*ftL)["FactionGroup"] : 3;
|
||||
const uint32_t ftFriend = ftL ? (*ftL)["FriendGroup"] : 4;
|
||||
const uint32_t ftEnemy = ftL ? (*ftL)["EnemyGroup"] : 5;
|
||||
const uint32_t ftEnemy0 = ftL ? (*ftL)["Enemy0"] : 6;
|
||||
uint32_t playerFriendGroup = 0;
|
||||
uint32_t playerEnemyGroup = 0;
|
||||
uint32_t playerFactionId = 0;
|
||||
for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) {
|
||||
if (ftDbc->getUInt32(i, 0) == playerFtId) {
|
||||
playerFriendGroup = ftDbc->getUInt32(i, 4) | ftDbc->getUInt32(i, 3);
|
||||
playerEnemyGroup = ftDbc->getUInt32(i, 5);
|
||||
playerFactionId = ftDbc->getUInt32(i, 1);
|
||||
if (ftDbc->getUInt32(i, ftID) == playerFtId) {
|
||||
playerFriendGroup = ftDbc->getUInt32(i, ftFriend) | ftDbc->getUInt32(i, ftFG);
|
||||
playerEnemyGroup = ftDbc->getUInt32(i, ftEnemy);
|
||||
playerFactionId = ftDbc->getUInt32(i, ftFaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -2133,11 +2234,11 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
|||
// Build hostility map for each faction template
|
||||
std::unordered_map<uint32_t, bool> factionMap;
|
||||
for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) {
|
||||
uint32_t id = ftDbc->getUInt32(i, 0);
|
||||
uint32_t parentFaction = ftDbc->getUInt32(i, 1);
|
||||
uint32_t factionGroup = ftDbc->getUInt32(i, 3);
|
||||
uint32_t friendGroup = ftDbc->getUInt32(i, 4);
|
||||
uint32_t enemyGroup = ftDbc->getUInt32(i, 5);
|
||||
uint32_t id = ftDbc->getUInt32(i, ftID);
|
||||
uint32_t parentFaction = ftDbc->getUInt32(i, ftFaction);
|
||||
uint32_t factionGroup = ftDbc->getUInt32(i, ftFG);
|
||||
uint32_t friendGroup = ftDbc->getUInt32(i, ftFriend);
|
||||
uint32_t enemyGroup = ftDbc->getUInt32(i, ftEnemy);
|
||||
|
||||
// 1. Symmetric group check
|
||||
bool hostile = (enemyGroup & playerFriendGroup) != 0
|
||||
|
|
@ -2148,9 +2249,9 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
|||
hostile = true;
|
||||
}
|
||||
|
||||
// 3. Individual enemy faction IDs (fields 6-9)
|
||||
// 3. Individual enemy faction IDs
|
||||
if (!hostile && playerFactionId > 0) {
|
||||
for (int e = 6; e <= 9; e++) {
|
||||
for (uint32_t e = ftEnemy0; e <= ftEnemy0 + 3; e++) {
|
||||
if (ftDbc->getUInt32(i, e) == playerFactionId) {
|
||||
hostile = true;
|
||||
break;
|
||||
|
|
@ -2227,9 +2328,10 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
mapNameCacheLoaded = true;
|
||||
if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) {
|
||||
mapNameById.reserve(mapDbc->getRecordCount());
|
||||
const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr;
|
||||
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
|
||||
uint32_t id = mapDbc->getUInt32(i, 0);
|
||||
std::string internalName = mapDbc->getString(i, 1);
|
||||
uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0);
|
||||
std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
|
||||
if (!internalName.empty()) {
|
||||
mapNameById[id] = std::move(internalName);
|
||||
}
|
||||
|
|
@ -2509,14 +2611,15 @@ void Application::buildCreatureDisplayLookups() {
|
|||
// Col 7: Skin2
|
||||
// Col 8: Skin3
|
||||
if (auto cdi = assetManager->loadDBC("CreatureDisplayInfo.dbc"); cdi && cdi->isLoaded()) {
|
||||
const auto* cdiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfo") : nullptr;
|
||||
for (uint32_t i = 0; i < cdi->getRecordCount(); i++) {
|
||||
CreatureDisplayData data;
|
||||
data.modelId = cdi->getUInt32(i, 1);
|
||||
data.extraDisplayId = cdi->getUInt32(i, 3);
|
||||
data.skin1 = cdi->getString(i, 6);
|
||||
data.skin2 = cdi->getString(i, 7);
|
||||
data.skin3 = cdi->getString(i, 8);
|
||||
displayDataMap_[cdi->getUInt32(i, 0)] = data;
|
||||
data.modelId = cdi->getUInt32(i, cdiL ? (*cdiL)["ModelID"] : 1);
|
||||
data.extraDisplayId = cdi->getUInt32(i, cdiL ? (*cdiL)["ExtraDisplayId"] : 3);
|
||||
data.skin1 = cdi->getString(i, cdiL ? (*cdiL)["Skin1"] : 6);
|
||||
data.skin2 = cdi->getString(i, cdiL ? (*cdiL)["Skin2"] : 7);
|
||||
data.skin3 = cdi->getString(i, cdiL ? (*cdiL)["Skin3"] : 8);
|
||||
displayDataMap_[cdi->getUInt32(i, cdiL ? (*cdiL)["ID"] : 0)] = data;
|
||||
}
|
||||
LOG_INFO("Loaded ", displayDataMap_.size(), " display→model mappings");
|
||||
}
|
||||
|
|
@ -2534,37 +2637,38 @@ void Application::buildCreatureDisplayLookups() {
|
|||
// Col 19: Flags
|
||||
// Col 20: BakeName (pre-baked texture path)
|
||||
if (auto cdie = assetManager->loadDBC("CreatureDisplayInfoExtra.dbc"); cdie && cdie->isLoaded()) {
|
||||
const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr;
|
||||
const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8;
|
||||
uint32_t withBakeName = 0;
|
||||
for (uint32_t i = 0; i < cdie->getRecordCount(); i++) {
|
||||
HumanoidDisplayExtra extra;
|
||||
extra.raceId = static_cast<uint8_t>(cdie->getUInt32(i, 1));
|
||||
extra.sexId = static_cast<uint8_t>(cdie->getUInt32(i, 2));
|
||||
extra.skinId = static_cast<uint8_t>(cdie->getUInt32(i, 3));
|
||||
extra.faceId = static_cast<uint8_t>(cdie->getUInt32(i, 4));
|
||||
extra.hairStyleId = static_cast<uint8_t>(cdie->getUInt32(i, 5));
|
||||
extra.hairColorId = static_cast<uint8_t>(cdie->getUInt32(i, 6));
|
||||
extra.facialHairId = static_cast<uint8_t>(cdie->getUInt32(i, 7));
|
||||
// Equipment display IDs (columns 8-18)
|
||||
extra.raceId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["RaceID"] : 1));
|
||||
extra.sexId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["SexID"] : 2));
|
||||
extra.skinId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["SkinID"] : 3));
|
||||
extra.faceId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["FaceID"] : 4));
|
||||
extra.hairStyleId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["HairStyleID"] : 5));
|
||||
extra.hairColorId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["HairColorID"] : 6));
|
||||
extra.facialHairId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["FacialHairID"] : 7));
|
||||
for (int eq = 0; eq < 11; eq++) {
|
||||
extra.equipDisplayId[eq] = cdie->getUInt32(i, 8 + eq);
|
||||
extra.equipDisplayId[eq] = cdie->getUInt32(i, cdieEquip0 + eq);
|
||||
}
|
||||
extra.bakeName = cdie->getString(i, 20);
|
||||
extra.bakeName = cdie->getString(i, cdieL ? (*cdieL)["BakeName"] : 20);
|
||||
if (!extra.bakeName.empty()) withBakeName++;
|
||||
humanoidExtraMap_[cdie->getUInt32(i, 0)] = extra;
|
||||
humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra;
|
||||
}
|
||||
LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (", withBakeName, " with baked textures)");
|
||||
}
|
||||
|
||||
// CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2)
|
||||
if (auto cmd = assetManager->loadDBC("CreatureModelData.dbc"); cmd && cmd->isLoaded()) {
|
||||
const auto* cmdL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureModelData") : nullptr;
|
||||
for (uint32_t i = 0; i < cmd->getRecordCount(); i++) {
|
||||
std::string mdx = cmd->getString(i, 2);
|
||||
std::string mdx = cmd->getString(i, cmdL ? (*cmdL)["ModelPath"] : 2);
|
||||
if (mdx.empty()) continue;
|
||||
// Convert .mdx to .m2
|
||||
if (mdx.size() >= 4) {
|
||||
mdx = mdx.substr(0, mdx.size() - 4) + ".m2";
|
||||
}
|
||||
modelIdToPath_[cmd->getUInt32(i, 0)] = mdx;
|
||||
modelIdToPath_[cmd->getUInt32(i, cmdL ? (*cmdL)["ID"] : 0)] = mdx;
|
||||
}
|
||||
LOG_INFO("Loaded ", modelIdToPath_.size(), " model→path mappings");
|
||||
}
|
||||
|
|
@ -2612,11 +2716,12 @@ void Application::buildCreatureDisplayLookups() {
|
|||
// CharHairGeosets.dbc: maps (race, sex, hairStyleId) → skinSectionId for hair mesh
|
||||
// Col 0: ID, Col 1: RaceID, Col 2: SexID, Col 3: VariationID, Col 4: GeosetID, Col 5: Showscalp
|
||||
if (auto chg = assetManager->loadDBC("CharHairGeosets.dbc"); chg && chg->isLoaded()) {
|
||||
const auto* chgL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharHairGeosets") : nullptr;
|
||||
for (uint32_t i = 0; i < chg->getRecordCount(); i++) {
|
||||
uint32_t raceId = chg->getUInt32(i, 1);
|
||||
uint32_t sexId = chg->getUInt32(i, 2);
|
||||
uint32_t variation = chg->getUInt32(i, 3);
|
||||
uint32_t geosetId = chg->getUInt32(i, 4);
|
||||
uint32_t raceId = chg->getUInt32(i, chgL ? (*chgL)["RaceID"] : 1);
|
||||
uint32_t sexId = chg->getUInt32(i, chgL ? (*chgL)["SexID"] : 2);
|
||||
uint32_t variation = chg->getUInt32(i, chgL ? (*chgL)["Variation"] : 3);
|
||||
uint32_t geosetId = chg->getUInt32(i, chgL ? (*chgL)["GeosetID"] : 4);
|
||||
uint32_t key = (raceId << 16) | (sexId << 8) | variation;
|
||||
hairGeosetMap_[key] = static_cast<uint16_t>(geosetId);
|
||||
}
|
||||
|
|
@ -2634,15 +2739,16 @@ void Application::buildCreatureDisplayLookups() {
|
|||
// No ID column: Col 0: RaceID, Col 1: SexID, Col 2: VariationID
|
||||
// Col 3: Geoset100, Col 4: Geoset300, Col 5: Geoset200
|
||||
if (auto cfh = assetManager->loadDBC("CharacterFacialHairStyles.dbc"); cfh && cfh->isLoaded()) {
|
||||
const auto* cfhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
|
||||
for (uint32_t i = 0; i < cfh->getRecordCount(); i++) {
|
||||
uint32_t raceId = cfh->getUInt32(i, 0);
|
||||
uint32_t sexId = cfh->getUInt32(i, 1);
|
||||
uint32_t variation = cfh->getUInt32(i, 2);
|
||||
uint32_t raceId = cfh->getUInt32(i, cfhL ? (*cfhL)["RaceID"] : 0);
|
||||
uint32_t sexId = cfh->getUInt32(i, cfhL ? (*cfhL)["SexID"] : 1);
|
||||
uint32_t variation = cfh->getUInt32(i, cfhL ? (*cfhL)["Variation"] : 2);
|
||||
uint32_t key = (raceId << 16) | (sexId << 8) | variation;
|
||||
FacialHairGeosets fhg;
|
||||
fhg.geoset100 = static_cast<uint16_t>(cfh->getUInt32(i, 3));
|
||||
fhg.geoset300 = static_cast<uint16_t>(cfh->getUInt32(i, 4));
|
||||
fhg.geoset200 = static_cast<uint16_t>(cfh->getUInt32(i, 5));
|
||||
fhg.geoset100 = static_cast<uint16_t>(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset100"] : 3));
|
||||
fhg.geoset300 = static_cast<uint16_t>(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset300"] : 4));
|
||||
fhg.geoset200 = static_cast<uint16_t>(cfh->getUInt32(i, cfhL ? (*cfhL)["Geoset200"] : 5));
|
||||
facialHairGeosetMap_[key] = fhg;
|
||||
}
|
||||
LOG_INFO("Loaded ", facialHairGeosetMap_.size(), " facial hair geoset mappings from CharacterFacialHairStyles.dbc");
|
||||
|
|
@ -2722,9 +2828,10 @@ void Application::buildGameObjectDisplayLookups() {
|
|||
// Col 0: ID (displayId)
|
||||
// Col 1: ModelName
|
||||
if (auto godi = assetManager->loadDBC("GameObjectDisplayInfo.dbc"); godi && godi->isLoaded()) {
|
||||
const auto* godiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("GameObjectDisplayInfo") : nullptr;
|
||||
for (uint32_t i = 0; i < godi->getRecordCount(); i++) {
|
||||
uint32_t displayId = godi->getUInt32(i, 0);
|
||||
std::string modelName = godi->getString(i, 1);
|
||||
uint32_t displayId = godi->getUInt32(i, godiL ? (*godiL)["ID"] : 0);
|
||||
std::string modelName = godi->getString(i, godiL ? (*godiL)["ModelName"] : 1);
|
||||
if (modelName.empty()) continue;
|
||||
if (modelName.size() >= 4) {
|
||||
std::string ext = modelName.substr(modelName.size() - 4);
|
||||
|
|
@ -2913,23 +3020,24 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Load hair texture from CharSections.dbc (section 3)
|
||||
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
|
||||
if (charSectionsDbc) {
|
||||
const auto* csL2 = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
uint32_t targetRace = static_cast<uint32_t>(extra.raceId);
|
||||
uint32_t targetSex = static_cast<uint32_t>(extra.sexId);
|
||||
std::string hairTexPath;
|
||||
|
||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
||||
uint32_t section = charSectionsDbc->getUInt32(r, 3);
|
||||
uint32_t variation = charSectionsDbc->getUInt32(r, 8);
|
||||
uint32_t colorIdx = charSectionsDbc->getUInt32(r, 9);
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["RaceID"] : 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2);
|
||||
uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3);
|
||||
uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 8);
|
||||
uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 9);
|
||||
|
||||
if (raceId != targetRace || sexId != targetSex) continue;
|
||||
if (section != 3) continue; // Section 3 = hair
|
||||
if (variation != static_cast<uint32_t>(extra.hairStyleId)) continue;
|
||||
if (colorIdx != static_cast<uint32_t>(extra.hairColorId)) continue;
|
||||
|
||||
hairTexPath = charSectionsDbc->getString(r, 4);
|
||||
hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 4);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -3058,8 +3166,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Load equipment geosets from ItemDisplayInfo.dbc
|
||||
// DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2]
|
||||
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
||||
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
if (itemDisplayDbc) {
|
||||
// Equipment slots: 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
|
||||
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
||||
const uint32_t fGG3 = idiL ? (*idiL)["GeosetGroup3"] : 9;
|
||||
|
||||
// Helm (slot 0) - noted for helmet model attachment below
|
||||
|
||||
|
|
@ -3067,10 +3178,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (extra.equipDisplayId[3] != 0) {
|
||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
|
||||
if (idx >= 0) {
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||
if (gg > 0) geosetChest = static_cast<uint16_t>(501 + gg);
|
||||
// Robes: GeosetGroup[2] > 0 shows kilt legs
|
||||
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 9);
|
||||
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG3);
|
||||
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
||||
}
|
||||
}
|
||||
|
|
@ -3079,7 +3190,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (extra.equipDisplayId[5] != 0) {
|
||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
|
||||
if (idx >= 0) {
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
|
||||
}
|
||||
}
|
||||
|
|
@ -3088,7 +3199,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (extra.equipDisplayId[6] != 0) {
|
||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
|
||||
if (idx >= 0) {
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||
if (gg > 0) geosetBoots = static_cast<uint16_t>(401 + gg);
|
||||
}
|
||||
}
|
||||
|
|
@ -3097,7 +3208,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (extra.equipDisplayId[8] != 0) {
|
||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
|
||||
if (idx >= 0) {
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), 7);
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||
if (gg > 0) geosetGloves = static_cast<uint16_t>(301 + gg);
|
||||
}
|
||||
}
|
||||
|
|
@ -3162,8 +3273,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
|
||||
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
||||
if (helmIdx >= 0) {
|
||||
// Get helmet model name from ItemDisplayInfo.dbc (col 1 = LeftModel)
|
||||
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 1);
|
||||
// Get helmet model name from ItemDisplayInfo.dbc (LeftModel)
|
||||
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL ? (*idiL)["LeftModel"] : 1);
|
||||
if (!helmModelName.empty()) {
|
||||
// Convert .mdx to .m2
|
||||
size_t dotPos = helmModelName.rfind('.');
|
||||
|
|
@ -3209,8 +3320,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (helmModel.isValid()) {
|
||||
// Attachment point 11 = Head
|
||||
uint32_t helmModelId = nextCreatureModelId_++;
|
||||
// Get texture from ItemDisplayInfo (col 3 = LeftModelTexture)
|
||||
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 3);
|
||||
// Get texture from ItemDisplayInfo (LeftModelTexture)
|
||||
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
|
||||
std::string helmTexPath;
|
||||
if (!helmTexName.empty()) {
|
||||
// Try race/gender suffixed texture first
|
||||
|
|
@ -3245,10 +3356,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
const auto& extra = itExtra->second;
|
||||
if (extra.equipDisplayId[0] != 0) { // Helm slot
|
||||
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
||||
const auto* idiL2 = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
if (itemDisplayDbc) {
|
||||
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
||||
if (helmIdx >= 0) {
|
||||
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 1);
|
||||
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL2 ? (*idiL2)["LeftModel"] : 1);
|
||||
if (!helmModelName.empty()) {
|
||||
size_t dotPos = helmModelName.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
|
|
@ -3287,7 +3399,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
|
||||
if (helmModel.isValid()) {
|
||||
uint32_t helmModelId = nextCreatureModelId_++;
|
||||
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), 3);
|
||||
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL2 ? (*idiL2)["LeftModelTexture"] : 3);
|
||||
std::string helmTexPath;
|
||||
if (!helmTexName.empty()) {
|
||||
if (!raceSuffix.empty()) {
|
||||
|
|
|
|||
185
src/game/expansion_profile.cpp
Normal file
185
src/game/expansion_profile.cpp
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#include "game/expansion_profile.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
|
||||
// Minimal JSON parsing (no external dependency) — expansion.json is tiny and flat.
|
||||
// We parse the subset we need: strings, integers, arrays of integers.
|
||||
namespace {
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t start = s.find_first_not_of(" \t\r\n\"");
|
||||
size_t end = s.find_last_not_of(" \t\r\n\",");
|
||||
if (start == std::string::npos) return "";
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
// Quick-and-dirty JSON value extractor for flat objects.
|
||||
// Returns the raw value string for a given key, or empty.
|
||||
std::string jsonValue(const std::string& json, const std::string& key) {
|
||||
std::string needle = "\"" + key + "\"";
|
||||
auto pos = json.find(needle);
|
||||
if (pos == std::string::npos) return "";
|
||||
pos = json.find(':', pos + needle.size());
|
||||
if (pos == std::string::npos) return "";
|
||||
++pos;
|
||||
// Skip whitespace
|
||||
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || json[pos] == '\r' || json[pos] == '\n'))
|
||||
++pos;
|
||||
if (pos >= json.size()) return "";
|
||||
|
||||
if (json[pos] == '"') {
|
||||
// String value
|
||||
size_t end = json.find('"', pos + 1);
|
||||
return (end != std::string::npos) ? json.substr(pos + 1, end - pos - 1) : "";
|
||||
}
|
||||
if (json[pos] == '{') {
|
||||
// Nested object — return content between braces
|
||||
size_t depth = 1;
|
||||
size_t start = pos + 1;
|
||||
for (size_t i = start; i < json.size() && depth > 0; ++i) {
|
||||
if (json[i] == '{') ++depth;
|
||||
else if (json[i] == '}') { --depth; if (depth == 0) return json.substr(start, i - start); }
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (json[pos] == '[') {
|
||||
// Array — return content between brackets (including brackets)
|
||||
size_t end = json.find(']', pos);
|
||||
return (end != std::string::npos) ? json.substr(pos, end - pos + 1) : "";
|
||||
}
|
||||
// Number or other literal
|
||||
size_t end = json.find_first_of(",}\n\r", pos);
|
||||
return trim(json.substr(pos, end - pos));
|
||||
}
|
||||
|
||||
int jsonInt(const std::string& json, const std::string& key, int def = 0) {
|
||||
std::string v = jsonValue(json, key);
|
||||
if (v.empty()) return def;
|
||||
try { return std::stoi(v); } catch (...) { return def; }
|
||||
}
|
||||
|
||||
std::vector<uint32_t> jsonUintArray(const std::string& json, const std::string& key) {
|
||||
std::vector<uint32_t> result;
|
||||
std::string arr = jsonValue(json, key);
|
||||
if (arr.empty() || arr.front() != '[') return result;
|
||||
// Strip brackets
|
||||
arr = arr.substr(1, arr.size() - 2);
|
||||
std::istringstream ss(arr);
|
||||
std::string tok;
|
||||
while (std::getline(ss, tok, ',')) {
|
||||
std::string t = trim(tok);
|
||||
if (!t.empty()) {
|
||||
try { result.push_back(static_cast<uint32_t>(std::stoul(t))); } catch (...) {}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
std::string ExpansionProfile::versionString() const {
|
||||
std::ostringstream ss;
|
||||
ss << (int)majorVersion << "." << (int)minorVersion << "." << (int)patchVersion;
|
||||
// Append letter suffix for known builds
|
||||
if (majorVersion == 3 && minorVersion == 3 && patchVersion == 5) ss << "a";
|
||||
else if (majorVersion == 2 && minorVersion == 4 && patchVersion == 3) ss << "";
|
||||
else if (majorVersion == 1 && minorVersion == 12 && patchVersion == 1) ss << "";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
size_t ExpansionRegistry::initialize(const std::string& dataRoot) {
|
||||
profiles_.clear();
|
||||
activeId_.clear();
|
||||
|
||||
std::string expansionsDir = dataRoot + "/expansions";
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_directory(expansionsDir, ec)) {
|
||||
LOG_WARNING("ExpansionRegistry: no expansions/ directory at ", expansionsDir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (auto& entry : std::filesystem::directory_iterator(expansionsDir, ec)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
std::string jsonPath = entry.path().string() + "/expansion.json";
|
||||
if (std::filesystem::exists(jsonPath, ec)) {
|
||||
loadProfile(jsonPath, entry.path().string());
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by build number (ascending: classic < tbc < wotlk < cata)
|
||||
std::sort(profiles_.begin(), profiles_.end(),
|
||||
[](const ExpansionProfile& a, const ExpansionProfile& b) { return a.build < b.build; });
|
||||
|
||||
// Default to WotLK if available, otherwise the last (highest build)
|
||||
if (!profiles_.empty()) {
|
||||
auto it = std::find_if(profiles_.begin(), profiles_.end(),
|
||||
[](const ExpansionProfile& p) { return p.id == "wotlk"; });
|
||||
activeId_ = (it != profiles_.end()) ? it->id : profiles_.back().id;
|
||||
}
|
||||
|
||||
LOG_INFO("ExpansionRegistry: discovered ", profiles_.size(), " expansion(s), active=", activeId_);
|
||||
return profiles_.size();
|
||||
}
|
||||
|
||||
const ExpansionProfile* ExpansionRegistry::getProfile(const std::string& id) const {
|
||||
for (auto& p : profiles_) {
|
||||
if (p.id == id) return &p;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ExpansionRegistry::setActive(const std::string& id) {
|
||||
if (!getProfile(id)) return false;
|
||||
activeId_ = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
const ExpansionProfile* ExpansionRegistry::getActive() const {
|
||||
return getProfile(activeId_);
|
||||
}
|
||||
|
||||
bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::string& dirPath) {
|
||||
std::ifstream f(jsonPath);
|
||||
if (!f.is_open()) return false;
|
||||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
ExpansionProfile p;
|
||||
p.id = jsonValue(json, "id");
|
||||
p.name = jsonValue(json, "name");
|
||||
p.shortName = jsonValue(json, "shortName");
|
||||
p.dataPath = dirPath;
|
||||
|
||||
// Version nested object
|
||||
std::string ver = jsonValue(json, "version");
|
||||
if (!ver.empty()) {
|
||||
p.majorVersion = static_cast<uint8_t>(jsonInt(ver, "major"));
|
||||
p.minorVersion = static_cast<uint8_t>(jsonInt(ver, "minor"));
|
||||
p.patchVersion = static_cast<uint8_t>(jsonInt(ver, "patch"));
|
||||
}
|
||||
|
||||
p.build = static_cast<uint16_t>(jsonInt(json, "build"));
|
||||
p.protocolVersion = static_cast<uint8_t>(jsonInt(json, "protocolVersion"));
|
||||
p.maxLevel = static_cast<uint32_t>(jsonInt(json, "maxLevel", 60));
|
||||
p.races = jsonUintArray(json, "races");
|
||||
p.classes = jsonUintArray(json, "classes");
|
||||
|
||||
if (p.id.empty() || p.build == 0) {
|
||||
LOG_WARNING("ExpansionRegistry: skipping invalid profile at ", jsonPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("ExpansionRegistry: loaded '", p.name, "' (", p.shortName,
|
||||
") v", p.versionString(), " build=", p.build);
|
||||
profiles_.push_back(std::move(p));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include "game/packet_parsers.hpp"
|
||||
#include "game/transport_manager.hpp"
|
||||
#include "game/warden_crypto.hpp"
|
||||
#include "game/warden_module.hpp"
|
||||
#include "game/opcodes.hpp"
|
||||
#include "game/update_field_table.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "network/world_socket.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
|
|
@ -50,16 +53,16 @@ const char* worldStateName(WorldState state) {
|
|||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool isAuthCharPipelineOpcode(uint16_t opcode) {
|
||||
switch (opcode) {
|
||||
case static_cast<uint16_t>(Opcode::SMSG_AUTH_CHALLENGE):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_AUTH_RESPONSE):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CLIENTCACHE_VERSION):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_TUTORIAL_FLAGS):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CHAR_ENUM):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CHAR_CREATE):
|
||||
case static_cast<uint16_t>(Opcode::SMSG_CHAR_DELETE):
|
||||
bool isAuthCharPipelineOpcode(LogicalOpcode op) {
|
||||
switch (op) {
|
||||
case Opcode::SMSG_AUTH_CHALLENGE:
|
||||
case Opcode::SMSG_AUTH_RESPONSE:
|
||||
case Opcode::SMSG_CLIENTCACHE_VERSION:
|
||||
case Opcode::SMSG_TUTORIAL_FLAGS:
|
||||
case Opcode::SMSG_WARDEN_DATA:
|
||||
case Opcode::SMSG_CHAR_ENUM:
|
||||
case Opcode::SMSG_CHAR_CREATE:
|
||||
case Opcode::SMSG_CHAR_DELETE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
|
@ -71,6 +74,17 @@ bool isAuthCharPipelineOpcode(uint16_t opcode) {
|
|||
GameHandler::GameHandler() {
|
||||
LOG_DEBUG("GameHandler created");
|
||||
|
||||
// Initialize opcode table with WotLK defaults (may be overridden from JSON later)
|
||||
opcodeTable_.loadWotlkDefaults();
|
||||
setActiveOpcodeTable(&opcodeTable_);
|
||||
|
||||
// Initialize update field table with WotLK defaults (may be overridden from JSON later)
|
||||
updateFieldTable_.loadWotlkDefaults();
|
||||
setActiveUpdateFieldTable(&updateFieldTable_);
|
||||
|
||||
// Initialize packet parsers (WotLK default, may be replaced for other expansions)
|
||||
packetParsers_ = std::make_unique<WotlkPacketParsers>();
|
||||
|
||||
// Initialize transport manager
|
||||
transportManager_ = std::make_unique<TransportManager>();
|
||||
|
||||
|
|
@ -92,6 +106,10 @@ GameHandler::~GameHandler() {
|
|||
disconnect();
|
||||
}
|
||||
|
||||
void GameHandler::setPacketParsers(std::unique_ptr<PacketParsers> parsers) {
|
||||
packetParsers_ = std::move(parsers);
|
||||
}
|
||||
|
||||
bool GameHandler::connect(const std::string& host,
|
||||
uint16_t port,
|
||||
const std::vector<uint8_t>& sessionKey,
|
||||
|
|
@ -542,10 +560,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
|
||||
uint16_t opcode = packet.getOpcode();
|
||||
if (wardenGateSeen_ && opcode != static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA)) {
|
||||
auto preLogicalOp = opcodeTable_.fromWire(opcode);
|
||||
if (wardenGateSeen_ && (!preLogicalOp || *preLogicalOp != Opcode::SMSG_WARDEN_DATA)) {
|
||||
++wardenPacketsAfterGate_;
|
||||
}
|
||||
if (isAuthCharPipelineOpcode(opcode)) {
|
||||
if (preLogicalOp && isAuthCharPipelineOpcode(*preLogicalOp)) {
|
||||
LOG_INFO("AUTH/CHAR RX opcode=0x", std::hex, opcode, std::dec,
|
||||
" state=", worldStateName(state),
|
||||
" size=", packet.getSize());
|
||||
|
|
@ -554,10 +573,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
LOG_DEBUG("Received world packet: opcode=0x", std::hex, opcode, std::dec,
|
||||
" size=", packet.getSize(), " bytes");
|
||||
|
||||
// Route packet based on opcode
|
||||
Opcode opcodeEnum = static_cast<Opcode>(opcode);
|
||||
// Translate wire opcode to logical opcode via expansion table
|
||||
auto logicalOp = opcodeTable_.fromWire(opcode);
|
||||
if (!logicalOp) {
|
||||
LOG_DEBUG("Unknown wire opcode 0x", std::hex, opcode, std::dec, " - ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (opcodeEnum) {
|
||||
switch (*logicalOp) {
|
||||
case Opcode::SMSG_AUTH_CHALLENGE:
|
||||
if (state == WorldState::CONNECTED) {
|
||||
handleAuthChallenge(packet);
|
||||
|
|
@ -1132,7 +1155,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (entity->getType() != ObjectType::UNIT) continue;
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
if (unit->getNpcFlags() & 0x02) {
|
||||
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(guid);
|
||||
socket->send(qsPkt);
|
||||
}
|
||||
|
|
@ -1629,7 +1652,7 @@ void GameHandler::deleteCharacter(uint64_t characterGuid) {
|
|||
return;
|
||||
}
|
||||
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_DELETE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_DELETE));
|
||||
packet.writeUInt64(characterGuid);
|
||||
socket->send(packet);
|
||||
LOG_INFO("CMSG_CHAR_DELETE sent for GUID: 0x", std::hex, characterGuid, std::dec);
|
||||
|
|
@ -1938,7 +1961,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
std::vector<uint8_t> encryptedResponse = wardenCrypto_->encrypt(hashResponse);
|
||||
|
||||
// Send HASH_RESULT response
|
||||
network::Packet response(static_cast<uint16_t>(Opcode::CMSG_WARDEN_DATA));
|
||||
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
|
||||
for (uint8_t byte : encryptedResponse) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
|
|
@ -2151,7 +2174,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
LOG_INFO("Warden: Response encrypted (", encrypted.size(), " bytes): ", respEncHex);
|
||||
|
||||
// Build and send response packet
|
||||
network::Packet response(static_cast<uint16_t>(Opcode::CMSG_WARDEN_DATA));
|
||||
network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA));
|
||||
for (uint8_t byte : encrypted) {
|
||||
response.writeUInt8(byte);
|
||||
}
|
||||
|
|
@ -2344,7 +2367,7 @@ void GameHandler::sendMovement(Opcode opcode) {
|
|||
}
|
||||
|
||||
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
|
||||
static_cast<uint16_t>(opcode), std::dec,
|
||||
wireOpcode(opcode), std::dec,
|
||||
(isOnTransport() ? " ONTRANSPORT" : ""));
|
||||
|
||||
// Convert canonical → server coordinates for the wire
|
||||
|
|
@ -2584,9 +2607,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (block.objectType == ObjectType::PLAYER) {
|
||||
queryPlayerName(block.guid);
|
||||
} else if (block.objectType == ObjectType::UNIT) {
|
||||
// Extract creature entry from fields (UNIT_FIELD_ENTRY = index 54 in 3.3.5a,
|
||||
// but the OBJECT_FIELD_ENTRY is at index 3)
|
||||
auto it = block.fields.find(3); // OBJECT_FIELD_ENTRY
|
||||
auto it = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||
if (it != block.fields.end() && it->second != 0) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
unit->setEntry(it->second);
|
||||
|
|
@ -2603,39 +2624,44 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
|
||||
const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH);
|
||||
const uint16_t ufPower = fieldIndex(UF::UNIT_FIELD_POWER1);
|
||||
const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH);
|
||||
const uint16_t ufMaxPower = fieldIndex(UF::UNIT_FIELD_MAXPOWER1);
|
||||
const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||
const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE);
|
||||
const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS);
|
||||
const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS);
|
||||
const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID);
|
||||
const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID);
|
||||
const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS);
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 24:
|
||||
unit->setHealth(val);
|
||||
// Detect dead player on login
|
||||
if (block.guid == playerGuid && val == 0) {
|
||||
playerDead_ = true;
|
||||
LOG_INFO("Player logged in dead");
|
||||
if (key == ufHealth) {
|
||||
unit->setHealth(val);
|
||||
if (block.guid == playerGuid && val == 0) {
|
||||
playerDead_ = true;
|
||||
LOG_INFO("Player logged in dead");
|
||||
}
|
||||
} else if (key == ufPower) { unit->setPower(val); }
|
||||
else if (key == ufMaxHealth) { unit->setMaxHealth(val); }
|
||||
else if (key == ufMaxPower) { unit->setMaxPower(val); }
|
||||
else if (key == ufFaction) { unit->setFactionTemplate(val); }
|
||||
else if (key == ufFlags) { unit->setUnitFlags(val); }
|
||||
else if (key == ufDynFlags) { unit->setDynamicFlags(val); }
|
||||
else if (key == ufLevel) { unit->setLevel(val); }
|
||||
else if (key == ufDisplayId) { unit->setDisplayId(val); }
|
||||
else if (key == ufMountDisplayId) {
|
||||
if (block.guid == playerGuid) {
|
||||
uint32_t old = currentMountDisplayId_;
|
||||
currentMountDisplayId_ = val;
|
||||
if (val != old && mountCallback_) mountCallback_(val);
|
||||
if (old != 0 && val == 0) {
|
||||
for (auto& a : playerAuras)
|
||||
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
|
||||
}
|
||||
break;
|
||||
case 25: unit->setPower(val); break;
|
||||
case 32: unit->setMaxHealth(val); break;
|
||||
case 33: unit->setMaxPower(val); break;
|
||||
case 55: unit->setFactionTemplate(val); break; // UNIT_FIELD_FACTIONTEMPLATE
|
||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 147: unit->setDynamicFlags(val); break; // UNIT_DYNAMIC_FLAGS
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||
if (block.guid == playerGuid) {
|
||||
uint32_t old = currentMountDisplayId_;
|
||||
currentMountDisplayId_ = val;
|
||||
if (val != old && mountCallback_) mountCallback_(val);
|
||||
if (old != 0 && val == 0) {
|
||||
for (auto& a : playerAuras)
|
||||
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
|
||||
}
|
||||
}
|
||||
unit->setMountDisplayId(val);
|
||||
break;
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
unit->setMountDisplayId(val);
|
||||
} else if (key == ufNpcFlags) { unit->setNpcFlags(val); }
|
||||
}
|
||||
if (block.guid == playerGuid) {
|
||||
constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100;
|
||||
|
|
@ -2651,11 +2677,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
playerDead_ = true;
|
||||
LOG_INFO("Player logged in dead (dynamic flags)");
|
||||
}
|
||||
// Detect ghost state on login via PLAYER_FLAGS (field 150)
|
||||
// Detect ghost state on login via PLAYER_FLAGS
|
||||
if (block.guid == playerGuid) {
|
||||
constexpr uint32_t PLAYER_FLAGS_IDX = 150; // UNIT_END(148) + 2
|
||||
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
|
||||
auto pfIt = block.fields.find(PLAYER_FLAGS_IDX);
|
||||
auto pfIt = block.fields.find(fieldIndex(UF::PLAYER_FLAGS));
|
||||
if (pfIt != block.fields.end() && (pfIt->second & PLAYER_FLAGS_GHOST) != 0) {
|
||||
releasedSpirit_ = true;
|
||||
playerDead_ = true;
|
||||
|
|
@ -2674,7 +2699,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
// Query quest giver status for NPCs with questgiver flag (0x02)
|
||||
if ((unit->getNpcFlags() & 0x02) && socket) {
|
||||
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(block.guid);
|
||||
socket->send(qsPkt);
|
||||
}
|
||||
|
|
@ -2683,12 +2708,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
// Extract displayId and entry for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8)
|
||||
if (block.objectType == ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||
auto itDisp = block.fields.find(8);
|
||||
auto itDisp = block.fields.find(fieldIndex(UF::GAMEOBJECT_DISPLAYID));
|
||||
if (itDisp != block.fields.end()) {
|
||||
go->setDisplayId(itDisp->second);
|
||||
}
|
||||
// Extract entry and query name (OBJECT_FIELD_ENTRY = index 3)
|
||||
auto itEntry = block.fields.find(3);
|
||||
auto itEntry = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||
if (itEntry != block.fields.end() && itEntry->second != 0) {
|
||||
go->setEntry(itEntry->second);
|
||||
auto cacheIt = gameObjectInfoCache_.find(itEntry->second);
|
||||
|
|
@ -2719,8 +2743,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
// Track online item objects
|
||||
if (block.objectType == ObjectType::ITEM) {
|
||||
auto entryIt = block.fields.find(3); // OBJECT_FIELD_ENTRY
|
||||
auto stackIt = block.fields.find(14); // ITEM_FIELD_STACK_COUNT
|
||||
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
|
||||
if (entryIt != block.fields.end() && entryIt->second != 0) {
|
||||
OnlineItemInfo info;
|
||||
info.entry = entryIt->second;
|
||||
|
|
@ -2816,22 +2840,27 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
LOG_INFO(" Highest field index: ", maxField);
|
||||
|
||||
bool slotsChanged = false;
|
||||
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
|
||||
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
|
||||
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
const uint16_t ufQuestEnd = ufQuestStart + 25 * 5; // 25 quest slots, stride 5
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == 634) { playerXp_ = val; } // PLAYER_XP
|
||||
else if (key == 635) { playerNextLevelXp_ = val; } // PLAYER_NEXT_LEVEL_XP
|
||||
else if (key == 54) {
|
||||
serverPlayerLevel_ = val; // UNIT_FIELD_LEVEL
|
||||
if (key == ufPlayerXp) { playerXp_ = val; }
|
||||
else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; }
|
||||
else if (key == ufPlayerLevel) {
|
||||
serverPlayerLevel_ = val;
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == playerGuid) { ch.level = val; break; }
|
||||
}
|
||||
}
|
||||
else if (key == 1170) {
|
||||
else if (key == ufCoinage) {
|
||||
playerMoneyCopper_ = val;
|
||||
LOG_INFO("Money set from update fields: ", val, " copper");
|
||||
} // PLAYER_FIELD_COINAGE
|
||||
// Parse quest log fields (PLAYER_QUEST_LOG_1_1 = UNIT_END + 10 = 158, stride 5)
|
||||
// Quest slots: 158, 163, 168, 173, ... (25 slots max = up to index 278)
|
||||
else if (key >= 158 && key < 283 && (key - 158) % 5 == 0) {
|
||||
}
|
||||
// Parse quest log fields (stride 5, 25 slots)
|
||||
else if (key >= ufQuestStart && key < ufQuestEnd && (key - ufQuestStart) % 5 == 0) {
|
||||
uint32_t questId = val;
|
||||
if (questId != 0) {
|
||||
// Check if quest is already in log
|
||||
|
|
@ -2853,7 +2882,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
|
||||
// Request quest details from server
|
||||
if (socket) {
|
||||
network::Packet qPkt(static_cast<uint16_t>(Opcode::CMSG_QUEST_QUERY));
|
||||
network::Packet qPkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
|
||||
qPkt.writeUInt32(questId);
|
||||
socket->send(qPkt);
|
||||
}
|
||||
|
|
@ -2907,92 +2936,89 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008;
|
||||
uint32_t oldDisplayId = unit->getDisplayId();
|
||||
bool displayIdChanged = false;
|
||||
const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH);
|
||||
const uint16_t ufPower = fieldIndex(UF::UNIT_FIELD_POWER1);
|
||||
const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH);
|
||||
const uint16_t ufMaxPower = fieldIndex(UF::UNIT_FIELD_MAXPOWER1);
|
||||
const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||
const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE);
|
||||
const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS);
|
||||
const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS);
|
||||
const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID);
|
||||
const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID);
|
||||
const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS);
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
switch (key) {
|
||||
case 24: {
|
||||
uint32_t oldHealth = unit->getHealth();
|
||||
unit->setHealth(val);
|
||||
if (val == 0) {
|
||||
if (block.guid == autoAttackTarget) {
|
||||
stopAutoAttack();
|
||||
}
|
||||
hostileAttackers_.erase(block.guid);
|
||||
// Player death
|
||||
if (block.guid == playerGuid) {
|
||||
playerDead_ = true;
|
||||
releasedSpirit_ = false;
|
||||
stopAutoAttack();
|
||||
LOG_INFO("Player died!");
|
||||
}
|
||||
// Trigger death animation for NPC units
|
||||
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
|
||||
npcDeathCallback_(block.guid);
|
||||
}
|
||||
} else if (oldHealth == 0 && val > 0) {
|
||||
// Player resurrection or ghost form
|
||||
if (block.guid == playerGuid) {
|
||||
playerDead_ = false;
|
||||
if (!releasedSpirit_) {
|
||||
LOG_INFO("Player resurrected!");
|
||||
} else {
|
||||
LOG_INFO("Player entered ghost form");
|
||||
}
|
||||
}
|
||||
// Respawn: health went from 0 to >0, reset animation
|
||||
if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) {
|
||||
npcRespawnCallback_(block.guid);
|
||||
}
|
||||
if (key == ufHealth) {
|
||||
uint32_t oldHealth = unit->getHealth();
|
||||
unit->setHealth(val);
|
||||
if (val == 0) {
|
||||
if (block.guid == autoAttackTarget) {
|
||||
stopAutoAttack();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 25: unit->setPower(val); break;
|
||||
case 32: unit->setMaxHealth(val); break;
|
||||
case 33: unit->setMaxPower(val); break;
|
||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 147: {
|
||||
uint32_t oldDyn = unit->getDynamicFlags();
|
||||
unit->setDynamicFlags(val);
|
||||
hostileAttackers_.erase(block.guid);
|
||||
if (block.guid == playerGuid) {
|
||||
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
|
||||
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
|
||||
if (!wasDead && nowDead) {
|
||||
playerDead_ = true;
|
||||
releasedSpirit_ = false;
|
||||
LOG_INFO("Player died (dynamic flags)");
|
||||
} else if (wasDead && !nowDead) {
|
||||
playerDead_ = false;
|
||||
releasedSpirit_ = false;
|
||||
LOG_INFO("Player resurrected (dynamic flags)");
|
||||
}
|
||||
playerDead_ = true;
|
||||
releasedSpirit_ = false;
|
||||
stopAutoAttack();
|
||||
LOG_INFO("Player died!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 55: // UNIT_FIELD_FACTIONTEMPLATE
|
||||
unit->setFactionTemplate(val);
|
||||
unit->setHostile(isHostileFaction(val));
|
||||
break;
|
||||
case 67:
|
||||
if (val != unit->getDisplayId()) {
|
||||
unit->setDisplayId(val);
|
||||
displayIdChanged = true;
|
||||
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
|
||||
npcDeathCallback_(block.guid);
|
||||
}
|
||||
break; // UNIT_FIELD_DISPLAYID
|
||||
case 69: // UNIT_FIELD_MOUNTDISPLAYID
|
||||
} else if (oldHealth == 0 && val > 0) {
|
||||
if (block.guid == playerGuid) {
|
||||
uint32_t old = currentMountDisplayId_;
|
||||
currentMountDisplayId_ = val;
|
||||
if (val != old && mountCallback_) mountCallback_(val);
|
||||
if (old != 0 && val == 0) {
|
||||
for (auto& a : playerAuras)
|
||||
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
|
||||
playerDead_ = false;
|
||||
if (!releasedSpirit_) {
|
||||
LOG_INFO("Player resurrected!");
|
||||
} else {
|
||||
LOG_INFO("Player entered ghost form");
|
||||
}
|
||||
}
|
||||
unit->setMountDisplayId(val);
|
||||
break;
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) {
|
||||
npcRespawnCallback_(block.guid);
|
||||
}
|
||||
}
|
||||
} else if (key == ufPower) { unit->setPower(val); }
|
||||
else if (key == ufMaxHealth) { unit->setMaxHealth(val); }
|
||||
else if (key == ufMaxPower) { unit->setMaxPower(val); }
|
||||
else if (key == ufFlags) { unit->setUnitFlags(val); }
|
||||
else if (key == ufDynFlags) {
|
||||
uint32_t oldDyn = unit->getDynamicFlags();
|
||||
unit->setDynamicFlags(val);
|
||||
if (block.guid == playerGuid) {
|
||||
bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0;
|
||||
bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0;
|
||||
if (!wasDead && nowDead) {
|
||||
playerDead_ = true;
|
||||
releasedSpirit_ = false;
|
||||
LOG_INFO("Player died (dynamic flags)");
|
||||
} else if (wasDead && !nowDead) {
|
||||
playerDead_ = false;
|
||||
releasedSpirit_ = false;
|
||||
LOG_INFO("Player resurrected (dynamic flags)");
|
||||
}
|
||||
}
|
||||
} else if (key == ufLevel) { unit->setLevel(val); }
|
||||
else if (key == ufFaction) {
|
||||
unit->setFactionTemplate(val);
|
||||
unit->setHostile(isHostileFaction(val));
|
||||
} else if (key == ufDisplayId) {
|
||||
if (val != unit->getDisplayId()) {
|
||||
unit->setDisplayId(val);
|
||||
displayIdChanged = true;
|
||||
}
|
||||
} else if (key == ufMountDisplayId) {
|
||||
if (block.guid == playerGuid) {
|
||||
uint32_t old = currentMountDisplayId_;
|
||||
currentMountDisplayId_ = val;
|
||||
if (val != old && mountCallback_) mountCallback_(val);
|
||||
if (old != 0 && val == 0) {
|
||||
for (auto& a : playerAuras)
|
||||
if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{};
|
||||
}
|
||||
}
|
||||
unit->setMountDisplayId(val);
|
||||
} else if (key == ufNpcFlags) { unit->setNpcFlags(val); }
|
||||
}
|
||||
|
||||
// Some units are created without displayId and get it later via VALUES.
|
||||
|
|
@ -3005,7 +3031,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
||||
}
|
||||
if ((unit->getNpcFlags() & 0x02) && socket) {
|
||||
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(block.guid);
|
||||
socket->send(qsPkt);
|
||||
}
|
||||
|
|
@ -3031,19 +3057,23 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
detectInventorySlotBases(block.fields);
|
||||
bool slotsChanged = false;
|
||||
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
|
||||
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
|
||||
const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL);
|
||||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == 634) {
|
||||
if (key == ufPlayerXp) {
|
||||
playerXp_ = val;
|
||||
LOG_INFO("XP updated: ", val);
|
||||
}
|
||||
else if (key == 635) {
|
||||
else if (key == ufPlayerNextXp) {
|
||||
playerNextLevelXp_ = val;
|
||||
LOG_INFO("Next level XP updated: ", val);
|
||||
}
|
||||
else if (key == 54) {
|
||||
else if (key == ufPlayerLevel) {
|
||||
serverPlayerLevel_ = val;
|
||||
LOG_INFO("Level updated: ", val);
|
||||
// Update Character struct for character selection screen
|
||||
for (auto& ch : characters) {
|
||||
if (ch.guid == playerGuid) {
|
||||
ch.level = val;
|
||||
|
|
@ -3051,11 +3081,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
else if (key == 1170) {
|
||||
else if (key == ufCoinage) {
|
||||
playerMoneyCopper_ = val;
|
||||
LOG_INFO("Money updated via VALUES: ", val, " copper");
|
||||
}
|
||||
else if (key == 150) { // PLAYER_FLAGS (UNIT_END+2)
|
||||
else if (key == ufPlayerFlags) {
|
||||
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
|
||||
bool wasGhost = releasedSpirit_;
|
||||
bool nowGhost = (val & PLAYER_FLAGS_GHOST) != 0;
|
||||
|
|
@ -3080,7 +3110,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
// Update item stack count for online items
|
||||
if (entity->getType() == ObjectType::ITEM) {
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
if (key == 14) { // ITEM_FIELD_STACK_COUNT
|
||||
if (key == fieldIndex(UF::ITEM_FIELD_STACK_COUNT)) {
|
||||
auto it = onlineItems_.find(block.guid);
|
||||
if (it != onlineItems_.end()) it->second.stackCount = val;
|
||||
}
|
||||
|
|
@ -3240,7 +3270,7 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
|
|||
LOG_DEBUG(" Decompressed ", compressedSize, " -> ", destLen, " bytes");
|
||||
|
||||
// Create packet from decompressed data and parse it
|
||||
network::Packet decompressedPacket(static_cast<uint16_t>(Opcode::SMSG_UPDATE_OBJECT), decompressed);
|
||||
network::Packet decompressedPacket(wireOpcode(Opcode::SMSG_UPDATE_OBJECT), decompressed);
|
||||
handleUpdateObject(decompressedPacket);
|
||||
}
|
||||
|
||||
|
|
@ -3908,15 +3938,12 @@ void GameHandler::assistTarget() {
|
|||
}
|
||||
|
||||
// Try to read target GUID from update fields (UNIT_FIELD_TARGET)
|
||||
// Field offset 6 is typically UNIT_FIELD_TARGET in 3.3.5a
|
||||
uint64_t assistTargetGuid = 0;
|
||||
const auto& fields = target->getFields();
|
||||
auto it = fields.find(6);
|
||||
auto it = fields.find(fieldIndex(UF::UNIT_FIELD_TARGET_LO));
|
||||
if (it != fields.end()) {
|
||||
// Low 32 bits
|
||||
assistTargetGuid = it->second;
|
||||
// Try to get high 32 bits from next field
|
||||
auto it2 = fields.find(7);
|
||||
auto it2 = fields.find(fieldIndex(UF::UNIT_FIELD_TARGET_HI));
|
||||
if (it2 != fields.end()) {
|
||||
assistTargetGuid |= (static_cast<uint64_t>(it2->second) << 32);
|
||||
}
|
||||
|
|
@ -4581,8 +4608,7 @@ void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& f
|
|||
// The lowest matching field is the first EQUIPPED slot (not necessarily HEAD).
|
||||
// With 2+ matches we can derive the true base: all matches must be at
|
||||
// even offsets from the base, spaced 2 fields per slot.
|
||||
// Use the known 3.3.5a default (324) and verify matches align to it.
|
||||
constexpr int knownBase = 324;
|
||||
const int knownBase = static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
|
||||
constexpr int slotStride = 2;
|
||||
bool allAlign = true;
|
||||
for (uint16_t p : matchingPairs) {
|
||||
|
|
@ -4628,10 +4654,8 @@ void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& f
|
|||
|
||||
bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& fields) {
|
||||
bool slotsChanged = false;
|
||||
// WoW 3.3.5a: PLAYER_FIELD_INV_SLOT_HEAD = UNIT_END + 0x00B0 = 324
|
||||
// PLAYER_FIELD_PACK_SLOT_1 = UNIT_END + 0x00DE = 370
|
||||
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : 324;
|
||||
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : 370;
|
||||
int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD));
|
||||
int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast<int>(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1));
|
||||
|
||||
for (const auto& [key, val] : fields) {
|
||||
if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) {
|
||||
|
|
@ -4854,7 +4878,7 @@ void GameHandler::dismount() {
|
|||
taxiClientActive_ = false;
|
||||
LOG_INFO("Dismount desync recovery: force-cleared local mount state");
|
||||
}
|
||||
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_CANCEL_MOUNT_AURA));
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA));
|
||||
socket->send(pkt);
|
||||
LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA");
|
||||
}
|
||||
|
|
@ -4886,7 +4910,7 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
|
|||
// Always ACK the speed change to prevent server stall.
|
||||
// Packet format mirrors movement packets: packed guid + counter + movement info + new speed.
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
|
||||
network::Packet ack(wireOpcode(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
|
||||
MovementPacket::writePackedGuid(ack, playerGuid);
|
||||
ack.writeUInt32(counter);
|
||||
|
||||
|
|
@ -5781,7 +5805,7 @@ void GameHandler::selectGossipQuest(uint32_t questId) {
|
|||
if (isInLog && isCompletable) {
|
||||
// Quest is ready to turn in - request reward
|
||||
LOG_INFO("Turning in quest: questId=", questId, " npcGuid=", currentGossip.npcGuid);
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
|
||||
packet.writeUInt64(currentGossip.npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
socket->send(packet);
|
||||
|
|
@ -5831,7 +5855,7 @@ void GameHandler::acceptQuest() {
|
|||
|
||||
// Re-query quest giver status so marker updates (! → ?)
|
||||
if (npcGuid) {
|
||||
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
socket->send(qsPkt);
|
||||
}
|
||||
|
|
@ -5849,7 +5873,7 @@ void GameHandler::abandonQuest(uint32_t questId) {
|
|||
// Tell server to remove it (slot index in server quest log)
|
||||
// We send the local index; server maps it via PLAYER_QUEST_LOG fields
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST));
|
||||
pkt.writeUInt8(static_cast<uint8_t>(i));
|
||||
socket->send(pkt);
|
||||
}
|
||||
|
|
@ -5923,7 +5947,7 @@ void GameHandler::chooseQuestReward(uint32_t rewardIndex) {
|
|||
|
||||
// Re-query quest giver status so markers update
|
||||
if (npcGuid) {
|
||||
network::Packet qsPkt(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
socket->send(qsPkt);
|
||||
}
|
||||
|
|
@ -6274,13 +6298,13 @@ void GameHandler::loadSpellNameCache() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Fields: 0=SpellID, 136=SpellName_enUS, 153=RankText_enUS
|
||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t id = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
|
||||
if (id == 0) continue;
|
||||
std::string name = dbc->getString(i, 136);
|
||||
std::string rank = dbc->getString(i, 153);
|
||||
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
||||
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
||||
if (!name.empty()) {
|
||||
spellNameCache_[id] = {std::move(name), std::move(rank)};
|
||||
}
|
||||
|
|
@ -6295,12 +6319,12 @@ void GameHandler::loadSkillLineAbilityDbc() {
|
|||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (!am || !am->isInitialized()) return;
|
||||
|
||||
// SkillLineAbility.dbc: field 1=skillLineID, field 2=spellID
|
||||
auto slaDbc = am->loadDBC("SkillLineAbility.dbc");
|
||||
if (slaDbc && slaDbc->isLoaded()) {
|
||||
const auto* slaL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLineAbility") : nullptr;
|
||||
for (uint32_t i = 0; i < slaDbc->getRecordCount(); i++) {
|
||||
uint32_t skillLineId = slaDbc->getUInt32(i, 1);
|
||||
uint32_t spellId = slaDbc->getUInt32(i, 2);
|
||||
uint32_t skillLineId = slaDbc->getUInt32(i, slaL ? (*slaL)["SkillLineID"] : 1);
|
||||
uint32_t spellId = slaDbc->getUInt32(i, slaL ? (*slaL)["SpellID"] : 2);
|
||||
if (spellId > 0 && skillLineId > 0) {
|
||||
spellToSkillLine_[spellId] = skillLineId;
|
||||
}
|
||||
|
|
@ -6380,25 +6404,34 @@ void GameHandler::loadTalentDbc() {
|
|||
// 12-14: PrereqRank[0-2]
|
||||
// (other fields less relevant for basic functionality)
|
||||
|
||||
const auto* talL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Talent") : nullptr;
|
||||
const uint32_t tID = talL ? (*talL)["ID"] : 0;
|
||||
const uint32_t tTabID = talL ? (*talL)["TabID"] : 1;
|
||||
const uint32_t tRow = talL ? (*talL)["Row"] : 2;
|
||||
const uint32_t tCol = talL ? (*talL)["Column"] : 3;
|
||||
const uint32_t tRank0 = talL ? (*talL)["RankSpell0"] : 4;
|
||||
const uint32_t tPrereq0 = talL ? (*talL)["PrereqTalent0"] : 9;
|
||||
const uint32_t tPrereqR0 = talL ? (*talL)["PrereqRank0"] : 12;
|
||||
|
||||
uint32_t count = talentDbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
TalentEntry entry;
|
||||
entry.talentId = talentDbc->getUInt32(i, 0);
|
||||
entry.talentId = talentDbc->getUInt32(i, tID);
|
||||
if (entry.talentId == 0) continue;
|
||||
|
||||
entry.tabId = talentDbc->getUInt32(i, 1);
|
||||
entry.row = static_cast<uint8_t>(talentDbc->getUInt32(i, 2));
|
||||
entry.column = static_cast<uint8_t>(talentDbc->getUInt32(i, 3));
|
||||
entry.tabId = talentDbc->getUInt32(i, tTabID);
|
||||
entry.row = static_cast<uint8_t>(talentDbc->getUInt32(i, tRow));
|
||||
entry.column = static_cast<uint8_t>(talentDbc->getUInt32(i, tCol));
|
||||
|
||||
// Rank spells (1-5 ranks)
|
||||
for (int r = 0; r < 5; ++r) {
|
||||
entry.rankSpells[r] = talentDbc->getUInt32(i, 4 + r);
|
||||
entry.rankSpells[r] = talentDbc->getUInt32(i, tRank0 + r);
|
||||
}
|
||||
|
||||
// Prerequisites
|
||||
for (int p = 0; p < 3; ++p) {
|
||||
entry.prereqTalent[p] = talentDbc->getUInt32(i, 9 + p);
|
||||
entry.prereqRank[p] = static_cast<uint8_t>(talentDbc->getUInt32(i, 12 + p));
|
||||
entry.prereqTalent[p] = talentDbc->getUInt32(i, tPrereq0 + p);
|
||||
entry.prereqRank[p] = static_cast<uint8_t>(talentDbc->getUInt32(i, tPrereqR0 + p));
|
||||
}
|
||||
|
||||
// Calculate max rank
|
||||
|
|
@ -6429,16 +6462,17 @@ void GameHandler::loadTalentDbc() {
|
|||
// 22: OrderIndex
|
||||
// 23-39: BackgroundFile (16 localized strings + flags = 17 fields)
|
||||
|
||||
const auto* ttL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TalentTab") : nullptr;
|
||||
uint32_t count = tabDbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
TalentTabEntry entry;
|
||||
entry.tabId = tabDbc->getUInt32(i, 0);
|
||||
entry.tabId = tabDbc->getUInt32(i, ttL ? (*ttL)["ID"] : 0);
|
||||
if (entry.tabId == 0) continue;
|
||||
|
||||
entry.name = tabDbc->getString(i, 1);
|
||||
entry.classMask = tabDbc->getUInt32(i, 20);
|
||||
entry.orderIndex = static_cast<uint8_t>(tabDbc->getUInt32(i, 22));
|
||||
entry.backgroundFile = tabDbc->getString(i, 23);
|
||||
entry.name = tabDbc->getString(i, ttL ? (*ttL)["Name"] : 1);
|
||||
entry.classMask = tabDbc->getUInt32(i, ttL ? (*ttL)["ClassMask"] : 20);
|
||||
entry.orderIndex = static_cast<uint8_t>(tabDbc->getUInt32(i, ttL ? (*ttL)["OrderIndex"] : 22));
|
||||
entry.backgroundFile = tabDbc->getString(i, ttL ? (*ttL)["BackgroundFile"] : 23);
|
||||
|
||||
talentTabCache_[entry.tabId] = entry;
|
||||
|
||||
|
|
@ -6616,7 +6650,7 @@ void GameHandler::handleTeleportAck(network::Packet& packet) {
|
|||
// Send the ack back to the server
|
||||
// Client→server MSG_MOVE_TELEPORT_ACK: u64 guid + u32 counter + u32 time
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::MSG_MOVE_TELEPORT_ACK));
|
||||
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK));
|
||||
// Write packed guid
|
||||
uint8_t mask = 0;
|
||||
uint8_t bytes[8];
|
||||
|
|
@ -6698,7 +6732,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
|||
|
||||
// Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready
|
||||
if (socket) {
|
||||
network::Packet ack(static_cast<uint16_t>(Opcode::MSG_MOVE_WORLDPORT_ACK));
|
||||
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
|
||||
socket->send(ack);
|
||||
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK");
|
||||
}
|
||||
|
|
@ -6720,25 +6754,28 @@ void GameHandler::loadTaxiDbc() {
|
|||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (!am || !am->isInitialized()) return;
|
||||
|
||||
// Load TaxiNodes.dbc: 0=ID, 1=mapId, 2=x, 3=y, 4=z, 5=name(enUS locale)
|
||||
auto nodesDbc = am->loadDBC("TaxiNodes.dbc");
|
||||
if (nodesDbc && nodesDbc->isLoaded()) {
|
||||
const auto* tnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiNodes") : nullptr;
|
||||
uint32_t fieldCount = nodesDbc->getFieldCount();
|
||||
for (uint32_t i = 0; i < nodesDbc->getRecordCount(); i++) {
|
||||
TaxiNode node;
|
||||
node.id = nodesDbc->getUInt32(i, 0);
|
||||
node.mapId = nodesDbc->getUInt32(i, 1);
|
||||
node.x = nodesDbc->getFloat(i, 2);
|
||||
node.y = nodesDbc->getFloat(i, 3);
|
||||
node.z = nodesDbc->getFloat(i, 4);
|
||||
node.name = nodesDbc->getString(i, 5);
|
||||
// TaxiNodes.dbc (3.3.5a): last two fields are mount display IDs (Alliance, Horde)
|
||||
if (fieldCount >= 24) {
|
||||
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, 22);
|
||||
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, 23);
|
||||
if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount >= 22) {
|
||||
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, 20);
|
||||
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, 21);
|
||||
node.id = nodesDbc->getUInt32(i, tnL ? (*tnL)["ID"] : 0);
|
||||
node.mapId = nodesDbc->getUInt32(i, tnL ? (*tnL)["MapID"] : 1);
|
||||
node.x = nodesDbc->getFloat(i, tnL ? (*tnL)["X"] : 2);
|
||||
node.y = nodesDbc->getFloat(i, tnL ? (*tnL)["Y"] : 3);
|
||||
node.z = nodesDbc->getFloat(i, tnL ? (*tnL)["Z"] : 4);
|
||||
node.name = nodesDbc->getString(i, tnL ? (*tnL)["Name"] : 5);
|
||||
const uint32_t mountAllianceField = tnL ? (*tnL)["MountDisplayIdAlliance"] : 22;
|
||||
const uint32_t mountHordeField = tnL ? (*tnL)["MountDisplayIdHorde"] : 23;
|
||||
const uint32_t mountAllianceFB = tnL ? (*tnL)["MountDisplayIdAllianceFallback"] : 20;
|
||||
const uint32_t mountHordeFB = tnL ? (*tnL)["MountDisplayIdHordeFallback"] : 21;
|
||||
if (fieldCount > mountHordeField) {
|
||||
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceField);
|
||||
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeField);
|
||||
if (node.mountDisplayIdAlliance == 0 && node.mountDisplayIdHorde == 0 && fieldCount > mountHordeFB) {
|
||||
node.mountDisplayIdAlliance = nodesDbc->getUInt32(i, mountAllianceFB);
|
||||
node.mountDisplayIdHorde = nodesDbc->getUInt32(i, mountHordeFB);
|
||||
}
|
||||
}
|
||||
if (node.id > 0) {
|
||||
|
|
@ -6757,15 +6794,15 @@ void GameHandler::loadTaxiDbc() {
|
|||
LOG_WARNING("Could not load TaxiNodes.dbc");
|
||||
}
|
||||
|
||||
// Load TaxiPath.dbc: 0=pathId, 1=fromNode, 2=toNode, 3=cost
|
||||
auto pathDbc = am->loadDBC("TaxiPath.dbc");
|
||||
if (pathDbc && pathDbc->isLoaded()) {
|
||||
const auto* tpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPath") : nullptr;
|
||||
for (uint32_t i = 0; i < pathDbc->getRecordCount(); i++) {
|
||||
TaxiPathEdge edge;
|
||||
edge.pathId = pathDbc->getUInt32(i, 0);
|
||||
edge.fromNode = pathDbc->getUInt32(i, 1);
|
||||
edge.toNode = pathDbc->getUInt32(i, 2);
|
||||
edge.cost = pathDbc->getUInt32(i, 3);
|
||||
edge.pathId = pathDbc->getUInt32(i, tpL ? (*tpL)["ID"] : 0);
|
||||
edge.fromNode = pathDbc->getUInt32(i, tpL ? (*tpL)["FromNode"] : 1);
|
||||
edge.toNode = pathDbc->getUInt32(i, tpL ? (*tpL)["ToNode"] : 2);
|
||||
edge.cost = pathDbc->getUInt32(i, tpL ? (*tpL)["Cost"] : 3);
|
||||
taxiPathEdges_.push_back(edge);
|
||||
}
|
||||
LOG_INFO("Loaded ", taxiPathEdges_.size(), " taxi path edges from TaxiPath.dbc");
|
||||
|
|
@ -6773,19 +6810,18 @@ void GameHandler::loadTaxiDbc() {
|
|||
LOG_WARNING("Could not load TaxiPath.dbc");
|
||||
}
|
||||
|
||||
// Load TaxiPathNode.dbc: actual spline waypoints for each path
|
||||
// 0=ID, 1=PathID, 2=NodeIndex, 3=MapID, 4=X, 5=Y, 6=Z
|
||||
auto pathNodeDbc = am->loadDBC("TaxiPathNode.dbc");
|
||||
if (pathNodeDbc && pathNodeDbc->isLoaded()) {
|
||||
const auto* tpnL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("TaxiPathNode") : nullptr;
|
||||
for (uint32_t i = 0; i < pathNodeDbc->getRecordCount(); i++) {
|
||||
TaxiPathNode node;
|
||||
node.id = pathNodeDbc->getUInt32(i, 0);
|
||||
node.pathId = pathNodeDbc->getUInt32(i, 1);
|
||||
node.nodeIndex = pathNodeDbc->getUInt32(i, 2);
|
||||
node.mapId = pathNodeDbc->getUInt32(i, 3);
|
||||
node.x = pathNodeDbc->getFloat(i, 4);
|
||||
node.y = pathNodeDbc->getFloat(i, 5);
|
||||
node.z = pathNodeDbc->getFloat(i, 6);
|
||||
node.id = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["ID"] : 0);
|
||||
node.pathId = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["PathID"] : 1);
|
||||
node.nodeIndex = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["NodeIndex"] : 2);
|
||||
node.mapId = pathNodeDbc->getUInt32(i, tpnL ? (*tpnL)["MapID"] : 3);
|
||||
node.x = pathNodeDbc->getFloat(i, tpnL ? (*tpnL)["X"] : 4);
|
||||
node.y = pathNodeDbc->getFloat(i, tpnL ? (*tpnL)["Y"] : 5);
|
||||
node.z = pathNodeDbc->getFloat(i, tpnL ? (*tpnL)["Z"] : 6);
|
||||
taxiPathNodes_[node.pathId].push_back(node);
|
||||
}
|
||||
// Sort waypoints by nodeIndex for each path
|
||||
|
|
@ -7667,10 +7703,11 @@ void GameHandler::loadSkillLineDbc() {
|
|||
return;
|
||||
}
|
||||
|
||||
const auto* slL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t category = dbc->getUInt32(i, 1);
|
||||
std::string name = dbc->getString(i, 3);
|
||||
uint32_t id = dbc->getUInt32(i, slL ? (*slL)["ID"] : 0);
|
||||
uint32_t category = dbc->getUInt32(i, slL ? (*slL)["Category"] : 1);
|
||||
std::string name = dbc->getString(i, slL ? (*slL)["Name"] : 3);
|
||||
if (id > 0 && !name.empty()) {
|
||||
skillLineNames_[id] = name;
|
||||
skillLineCategories_[id] = category;
|
||||
|
|
@ -7682,8 +7719,7 @@ void GameHandler::loadSkillLineDbc() {
|
|||
void GameHandler::extractSkillFields(const std::map<uint16_t, uint32_t>& fields) {
|
||||
loadSkillLineDbc();
|
||||
|
||||
// PLAYER_SKILL_INFO_1_1 = field 636, 128 slots x 3 fields each (636..1019)
|
||||
static constexpr uint16_t PLAYER_SKILL_INFO_START = 636;
|
||||
const uint16_t PLAYER_SKILL_INFO_START = fieldIndex(UF::PLAYER_SKILL_INFO_START);
|
||||
static constexpr int MAX_SKILL_SLOTS = 128;
|
||||
|
||||
std::map<uint32_t, PlayerSkill> newSkills;
|
||||
|
|
@ -7745,7 +7781,7 @@ void GameHandler::extractExploredZoneFields(const std::map<uint16_t, uint32_t>&
|
|||
|
||||
bool foundAny = false;
|
||||
for (size_t i = 0; i < PLAYER_EXPLORED_ZONES_COUNT; i++) {
|
||||
const uint16_t fieldIdx = static_cast<uint16_t>(PLAYER_EXPLORED_ZONES_START + i);
|
||||
const uint16_t fieldIdx = static_cast<uint16_t>(fieldIndex(UF::PLAYER_EXPLORED_ZONES_START) + i);
|
||||
auto it = fields.find(fieldIdx);
|
||||
if (it == fields.end()) continue;
|
||||
playerExploredZones_[i] = it->second;
|
||||
|
|
|
|||
649
src/game/opcode_table.cpp
Normal file
649
src/game/opcode_table.cpp
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
#include "game/opcode_table.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
// Global active opcode table pointer
|
||||
static const OpcodeTable* g_activeOpcodeTable = nullptr;
|
||||
|
||||
void setActiveOpcodeTable(const OpcodeTable* table) { g_activeOpcodeTable = table; }
|
||||
const OpcodeTable* getActiveOpcodeTable() { return g_activeOpcodeTable; }
|
||||
|
||||
// Name ↔ LogicalOpcode mapping table (generated from the enum)
|
||||
struct OpcodeNameEntry {
|
||||
const char* name;
|
||||
LogicalOpcode op;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
static const OpcodeNameEntry kOpcodeNames[] = {
|
||||
{"CMSG_PING", LogicalOpcode::CMSG_PING},
|
||||
{"CMSG_AUTH_SESSION", LogicalOpcode::CMSG_AUTH_SESSION},
|
||||
{"CMSG_CHAR_CREATE", LogicalOpcode::CMSG_CHAR_CREATE},
|
||||
{"CMSG_CHAR_ENUM", LogicalOpcode::CMSG_CHAR_ENUM},
|
||||
{"CMSG_CHAR_DELETE", LogicalOpcode::CMSG_CHAR_DELETE},
|
||||
{"CMSG_PLAYER_LOGIN", LogicalOpcode::CMSG_PLAYER_LOGIN},
|
||||
{"CMSG_MOVE_START_FORWARD", LogicalOpcode::CMSG_MOVE_START_FORWARD},
|
||||
{"CMSG_MOVE_START_BACKWARD", LogicalOpcode::CMSG_MOVE_START_BACKWARD},
|
||||
{"CMSG_MOVE_STOP", LogicalOpcode::CMSG_MOVE_STOP},
|
||||
{"CMSG_MOVE_START_STRAFE_LEFT", LogicalOpcode::CMSG_MOVE_START_STRAFE_LEFT},
|
||||
{"CMSG_MOVE_START_STRAFE_RIGHT", LogicalOpcode::CMSG_MOVE_START_STRAFE_RIGHT},
|
||||
{"CMSG_MOVE_STOP_STRAFE", LogicalOpcode::CMSG_MOVE_STOP_STRAFE},
|
||||
{"CMSG_MOVE_JUMP", LogicalOpcode::CMSG_MOVE_JUMP},
|
||||
{"CMSG_MOVE_START_TURN_LEFT", LogicalOpcode::CMSG_MOVE_START_TURN_LEFT},
|
||||
{"CMSG_MOVE_START_TURN_RIGHT", LogicalOpcode::CMSG_MOVE_START_TURN_RIGHT},
|
||||
{"CMSG_MOVE_STOP_TURN", LogicalOpcode::CMSG_MOVE_STOP_TURN},
|
||||
{"CMSG_MOVE_SET_FACING", LogicalOpcode::CMSG_MOVE_SET_FACING},
|
||||
{"CMSG_MOVE_FALL_LAND", LogicalOpcode::CMSG_MOVE_FALL_LAND},
|
||||
{"CMSG_MOVE_START_SWIM", LogicalOpcode::CMSG_MOVE_START_SWIM},
|
||||
{"CMSG_MOVE_STOP_SWIM", LogicalOpcode::CMSG_MOVE_STOP_SWIM},
|
||||
{"CMSG_MOVE_HEARTBEAT", LogicalOpcode::CMSG_MOVE_HEARTBEAT},
|
||||
{"SMSG_AUTH_CHALLENGE", LogicalOpcode::SMSG_AUTH_CHALLENGE},
|
||||
{"SMSG_AUTH_RESPONSE", LogicalOpcode::SMSG_AUTH_RESPONSE},
|
||||
{"SMSG_CHAR_CREATE", LogicalOpcode::SMSG_CHAR_CREATE},
|
||||
{"SMSG_CHAR_ENUM", LogicalOpcode::SMSG_CHAR_ENUM},
|
||||
{"SMSG_CHAR_DELETE", LogicalOpcode::SMSG_CHAR_DELETE},
|
||||
{"SMSG_PONG", LogicalOpcode::SMSG_PONG},
|
||||
{"SMSG_LOGIN_VERIFY_WORLD", LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD},
|
||||
{"SMSG_LOGIN_SETTIMESPEED", LogicalOpcode::SMSG_LOGIN_SETTIMESPEED},
|
||||
{"SMSG_TUTORIAL_FLAGS", LogicalOpcode::SMSG_TUTORIAL_FLAGS},
|
||||
{"SMSG_WARDEN_DATA", LogicalOpcode::SMSG_WARDEN_DATA},
|
||||
{"CMSG_WARDEN_DATA", LogicalOpcode::CMSG_WARDEN_DATA},
|
||||
{"SMSG_ACCOUNT_DATA_TIMES", LogicalOpcode::SMSG_ACCOUNT_DATA_TIMES},
|
||||
{"SMSG_CLIENTCACHE_VERSION", LogicalOpcode::SMSG_CLIENTCACHE_VERSION},
|
||||
{"SMSG_FEATURE_SYSTEM_STATUS", LogicalOpcode::SMSG_FEATURE_SYSTEM_STATUS},
|
||||
{"SMSG_MOTD", LogicalOpcode::SMSG_MOTD},
|
||||
{"SMSG_UPDATE_OBJECT", LogicalOpcode::SMSG_UPDATE_OBJECT},
|
||||
{"SMSG_COMPRESSED_UPDATE_OBJECT", LogicalOpcode::SMSG_COMPRESSED_UPDATE_OBJECT},
|
||||
{"SMSG_MONSTER_MOVE_TRANSPORT", LogicalOpcode::SMSG_MONSTER_MOVE_TRANSPORT},
|
||||
{"SMSG_DESTROY_OBJECT", LogicalOpcode::SMSG_DESTROY_OBJECT},
|
||||
{"CMSG_MESSAGECHAT", LogicalOpcode::CMSG_MESSAGECHAT},
|
||||
{"SMSG_MESSAGECHAT", LogicalOpcode::SMSG_MESSAGECHAT},
|
||||
{"CMSG_WHO", LogicalOpcode::CMSG_WHO},
|
||||
{"SMSG_WHO", LogicalOpcode::SMSG_WHO},
|
||||
{"CMSG_REQUEST_PLAYED_TIME", LogicalOpcode::CMSG_REQUEST_PLAYED_TIME},
|
||||
{"SMSG_PLAYED_TIME", LogicalOpcode::SMSG_PLAYED_TIME},
|
||||
{"CMSG_QUERY_TIME", LogicalOpcode::CMSG_QUERY_TIME},
|
||||
{"SMSG_QUERY_TIME_RESPONSE", LogicalOpcode::SMSG_QUERY_TIME_RESPONSE},
|
||||
{"SMSG_FRIEND_STATUS", LogicalOpcode::SMSG_FRIEND_STATUS},
|
||||
{"CMSG_ADD_FRIEND", LogicalOpcode::CMSG_ADD_FRIEND},
|
||||
{"CMSG_DEL_FRIEND", LogicalOpcode::CMSG_DEL_FRIEND},
|
||||
{"CMSG_SET_CONTACT_NOTES", LogicalOpcode::CMSG_SET_CONTACT_NOTES},
|
||||
{"CMSG_ADD_IGNORE", LogicalOpcode::CMSG_ADD_IGNORE},
|
||||
{"CMSG_DEL_IGNORE", LogicalOpcode::CMSG_DEL_IGNORE},
|
||||
{"CMSG_PLAYER_LOGOUT", LogicalOpcode::CMSG_PLAYER_LOGOUT},
|
||||
{"CMSG_LOGOUT_REQUEST", LogicalOpcode::CMSG_LOGOUT_REQUEST},
|
||||
{"CMSG_LOGOUT_CANCEL", LogicalOpcode::CMSG_LOGOUT_CANCEL},
|
||||
{"SMSG_LOGOUT_RESPONSE", LogicalOpcode::SMSG_LOGOUT_RESPONSE},
|
||||
{"SMSG_LOGOUT_COMPLETE", LogicalOpcode::SMSG_LOGOUT_COMPLETE},
|
||||
{"CMSG_STAND_STATE_CHANGE", LogicalOpcode::CMSG_STAND_STATE_CHANGE},
|
||||
{"CMSG_SHOWING_HELM", LogicalOpcode::CMSG_SHOWING_HELM},
|
||||
{"CMSG_SHOWING_CLOAK", LogicalOpcode::CMSG_SHOWING_CLOAK},
|
||||
{"CMSG_TOGGLE_PVP", LogicalOpcode::CMSG_TOGGLE_PVP},
|
||||
{"CMSG_GUILD_INVITE", LogicalOpcode::CMSG_GUILD_INVITE},
|
||||
{"CMSG_GUILD_ACCEPT", LogicalOpcode::CMSG_GUILD_ACCEPT},
|
||||
{"CMSG_GUILD_DECLINE_INVITATION", LogicalOpcode::CMSG_GUILD_DECLINE_INVITATION},
|
||||
{"CMSG_GUILD_INFO", LogicalOpcode::CMSG_GUILD_INFO},
|
||||
{"CMSG_GUILD_GET_ROSTER", LogicalOpcode::CMSG_GUILD_GET_ROSTER},
|
||||
{"CMSG_GUILD_PROMOTE_MEMBER", LogicalOpcode::CMSG_GUILD_PROMOTE_MEMBER},
|
||||
{"CMSG_GUILD_DEMOTE_MEMBER", LogicalOpcode::CMSG_GUILD_DEMOTE_MEMBER},
|
||||
{"CMSG_GUILD_LEAVE", LogicalOpcode::CMSG_GUILD_LEAVE},
|
||||
{"CMSG_GUILD_MOTD", LogicalOpcode::CMSG_GUILD_MOTD},
|
||||
{"SMSG_GUILD_INFO", LogicalOpcode::SMSG_GUILD_INFO},
|
||||
{"SMSG_GUILD_ROSTER", LogicalOpcode::SMSG_GUILD_ROSTER},
|
||||
{"MSG_RAID_READY_CHECK", LogicalOpcode::MSG_RAID_READY_CHECK},
|
||||
{"MSG_RAID_READY_CHECK_CONFIRM", LogicalOpcode::MSG_RAID_READY_CHECK_CONFIRM},
|
||||
{"CMSG_DUEL_PROPOSED", LogicalOpcode::CMSG_DUEL_PROPOSED},
|
||||
{"CMSG_DUEL_ACCEPTED", LogicalOpcode::CMSG_DUEL_ACCEPTED},
|
||||
{"CMSG_DUEL_CANCELLED", LogicalOpcode::CMSG_DUEL_CANCELLED},
|
||||
{"SMSG_DUEL_REQUESTED", LogicalOpcode::SMSG_DUEL_REQUESTED},
|
||||
{"CMSG_INITIATE_TRADE", LogicalOpcode::CMSG_INITIATE_TRADE},
|
||||
{"MSG_RANDOM_ROLL", LogicalOpcode::MSG_RANDOM_ROLL},
|
||||
{"CMSG_SET_SELECTION", LogicalOpcode::CMSG_SET_SELECTION},
|
||||
{"CMSG_NAME_QUERY", LogicalOpcode::CMSG_NAME_QUERY},
|
||||
{"SMSG_NAME_QUERY_RESPONSE", LogicalOpcode::SMSG_NAME_QUERY_RESPONSE},
|
||||
{"CMSG_CREATURE_QUERY", LogicalOpcode::CMSG_CREATURE_QUERY},
|
||||
{"SMSG_CREATURE_QUERY_RESPONSE", LogicalOpcode::SMSG_CREATURE_QUERY_RESPONSE},
|
||||
{"CMSG_GAMEOBJECT_QUERY", LogicalOpcode::CMSG_GAMEOBJECT_QUERY},
|
||||
{"SMSG_GAMEOBJECT_QUERY_RESPONSE", LogicalOpcode::SMSG_GAMEOBJECT_QUERY_RESPONSE},
|
||||
{"CMSG_SET_ACTIVE_MOVER", LogicalOpcode::CMSG_SET_ACTIVE_MOVER},
|
||||
{"CMSG_BINDER_ACTIVATE", LogicalOpcode::CMSG_BINDER_ACTIVATE},
|
||||
{"SMSG_LOG_XPGAIN", LogicalOpcode::SMSG_LOG_XPGAIN},
|
||||
{"SMSG_MONSTER_MOVE", LogicalOpcode::SMSG_MONSTER_MOVE},
|
||||
{"CMSG_ATTACKSWING", LogicalOpcode::CMSG_ATTACKSWING},
|
||||
{"CMSG_ATTACKSTOP", LogicalOpcode::CMSG_ATTACKSTOP},
|
||||
{"SMSG_ATTACKSTART", LogicalOpcode::SMSG_ATTACKSTART},
|
||||
{"SMSG_ATTACKSTOP", LogicalOpcode::SMSG_ATTACKSTOP},
|
||||
{"SMSG_ATTACKERSTATEUPDATE", LogicalOpcode::SMSG_ATTACKERSTATEUPDATE},
|
||||
{"SMSG_SPELLNONMELEEDAMAGELOG", LogicalOpcode::SMSG_SPELLNONMELEEDAMAGELOG},
|
||||
{"SMSG_SPELLHEALLOG", LogicalOpcode::SMSG_SPELLHEALLOG},
|
||||
{"SMSG_SPELLENERGIZELOG", LogicalOpcode::SMSG_SPELLENERGIZELOG},
|
||||
{"SMSG_PERIODICAURALOG", LogicalOpcode::SMSG_PERIODICAURALOG},
|
||||
{"SMSG_ENVIRONMENTALDAMAGELOG", LogicalOpcode::SMSG_ENVIRONMENTALDAMAGELOG},
|
||||
{"CMSG_CAST_SPELL", LogicalOpcode::CMSG_CAST_SPELL},
|
||||
{"CMSG_CANCEL_CAST", LogicalOpcode::CMSG_CANCEL_CAST},
|
||||
{"CMSG_CANCEL_AURA", LogicalOpcode::CMSG_CANCEL_AURA},
|
||||
{"SMSG_CAST_FAILED", LogicalOpcode::SMSG_CAST_FAILED},
|
||||
{"SMSG_SPELL_START", LogicalOpcode::SMSG_SPELL_START},
|
||||
{"SMSG_SPELL_GO", LogicalOpcode::SMSG_SPELL_GO},
|
||||
{"SMSG_SPELL_FAILURE", LogicalOpcode::SMSG_SPELL_FAILURE},
|
||||
{"SMSG_SPELL_COOLDOWN", LogicalOpcode::SMSG_SPELL_COOLDOWN},
|
||||
{"SMSG_COOLDOWN_EVENT", LogicalOpcode::SMSG_COOLDOWN_EVENT},
|
||||
{"SMSG_UPDATE_AURA_DURATION", LogicalOpcode::SMSG_UPDATE_AURA_DURATION},
|
||||
{"SMSG_INITIAL_SPELLS", LogicalOpcode::SMSG_INITIAL_SPELLS},
|
||||
{"SMSG_LEARNED_SPELL", LogicalOpcode::SMSG_LEARNED_SPELL},
|
||||
{"SMSG_SUPERCEDED_SPELL", LogicalOpcode::SMSG_SUPERCEDED_SPELL},
|
||||
{"SMSG_REMOVED_SPELL", LogicalOpcode::SMSG_REMOVED_SPELL},
|
||||
{"SMSG_SEND_UNLEARN_SPELLS", LogicalOpcode::SMSG_SEND_UNLEARN_SPELLS},
|
||||
{"SMSG_SPELL_DELAYED", LogicalOpcode::SMSG_SPELL_DELAYED},
|
||||
{"SMSG_AURA_UPDATE", LogicalOpcode::SMSG_AURA_UPDATE},
|
||||
{"SMSG_AURA_UPDATE_ALL", LogicalOpcode::SMSG_AURA_UPDATE_ALL},
|
||||
{"SMSG_SET_FLAT_SPELL_MODIFIER", LogicalOpcode::SMSG_SET_FLAT_SPELL_MODIFIER},
|
||||
{"SMSG_SET_PCT_SPELL_MODIFIER", LogicalOpcode::SMSG_SET_PCT_SPELL_MODIFIER},
|
||||
{"SMSG_TALENTS_INFO", LogicalOpcode::SMSG_TALENTS_INFO},
|
||||
{"CMSG_LEARN_TALENT", LogicalOpcode::CMSG_LEARN_TALENT},
|
||||
{"MSG_TALENT_WIPE_CONFIRM", LogicalOpcode::MSG_TALENT_WIPE_CONFIRM},
|
||||
{"CMSG_GROUP_INVITE", LogicalOpcode::CMSG_GROUP_INVITE},
|
||||
{"SMSG_GROUP_INVITE", LogicalOpcode::SMSG_GROUP_INVITE},
|
||||
{"CMSG_GROUP_ACCEPT", LogicalOpcode::CMSG_GROUP_ACCEPT},
|
||||
{"CMSG_GROUP_DECLINE", LogicalOpcode::CMSG_GROUP_DECLINE},
|
||||
{"SMSG_GROUP_DECLINE", LogicalOpcode::SMSG_GROUP_DECLINE},
|
||||
{"CMSG_GROUP_UNINVITE_GUID", LogicalOpcode::CMSG_GROUP_UNINVITE_GUID},
|
||||
{"SMSG_GROUP_UNINVITE", LogicalOpcode::SMSG_GROUP_UNINVITE},
|
||||
{"CMSG_GROUP_SET_LEADER", LogicalOpcode::CMSG_GROUP_SET_LEADER},
|
||||
{"SMSG_GROUP_SET_LEADER", LogicalOpcode::SMSG_GROUP_SET_LEADER},
|
||||
{"CMSG_GROUP_DISBAND", LogicalOpcode::CMSG_GROUP_DISBAND},
|
||||
{"SMSG_GROUP_LIST", LogicalOpcode::SMSG_GROUP_LIST},
|
||||
{"SMSG_PARTY_COMMAND_RESULT", LogicalOpcode::SMSG_PARTY_COMMAND_RESULT},
|
||||
{"MSG_RAID_TARGET_UPDATE", LogicalOpcode::MSG_RAID_TARGET_UPDATE},
|
||||
{"CMSG_REQUEST_RAID_INFO", LogicalOpcode::CMSG_REQUEST_RAID_INFO},
|
||||
{"SMSG_RAID_INSTANCE_INFO", LogicalOpcode::SMSG_RAID_INSTANCE_INFO},
|
||||
{"CMSG_AUTOSTORE_LOOT_ITEM", LogicalOpcode::CMSG_AUTOSTORE_LOOT_ITEM},
|
||||
{"CMSG_LOOT", LogicalOpcode::CMSG_LOOT},
|
||||
{"CMSG_LOOT_MONEY", LogicalOpcode::CMSG_LOOT_MONEY},
|
||||
{"CMSG_LOOT_RELEASE", LogicalOpcode::CMSG_LOOT_RELEASE},
|
||||
{"SMSG_LOOT_RESPONSE", LogicalOpcode::SMSG_LOOT_RESPONSE},
|
||||
{"SMSG_LOOT_RELEASE_RESPONSE", LogicalOpcode::SMSG_LOOT_RELEASE_RESPONSE},
|
||||
{"SMSG_LOOT_REMOVED", LogicalOpcode::SMSG_LOOT_REMOVED},
|
||||
{"SMSG_LOOT_MONEY_NOTIFY", LogicalOpcode::SMSG_LOOT_MONEY_NOTIFY},
|
||||
{"SMSG_LOOT_CLEAR_MONEY", LogicalOpcode::SMSG_LOOT_CLEAR_MONEY},
|
||||
{"CMSG_ACTIVATETAXI", LogicalOpcode::CMSG_ACTIVATETAXI},
|
||||
{"CMSG_GOSSIP_HELLO", LogicalOpcode::CMSG_GOSSIP_HELLO},
|
||||
{"CMSG_GOSSIP_SELECT_OPTION", LogicalOpcode::CMSG_GOSSIP_SELECT_OPTION},
|
||||
{"SMSG_GOSSIP_MESSAGE", LogicalOpcode::SMSG_GOSSIP_MESSAGE},
|
||||
{"SMSG_GOSSIP_COMPLETE", LogicalOpcode::SMSG_GOSSIP_COMPLETE},
|
||||
{"SMSG_NPC_TEXT_UPDATE", LogicalOpcode::SMSG_NPC_TEXT_UPDATE},
|
||||
{"CMSG_GAMEOBJECT_USE", LogicalOpcode::CMSG_GAMEOBJECT_USE},
|
||||
{"CMSG_QUESTGIVER_STATUS_QUERY", LogicalOpcode::CMSG_QUESTGIVER_STATUS_QUERY},
|
||||
{"SMSG_QUESTGIVER_STATUS", LogicalOpcode::SMSG_QUESTGIVER_STATUS},
|
||||
{"SMSG_QUESTGIVER_STATUS_MULTIPLE", LogicalOpcode::SMSG_QUESTGIVER_STATUS_MULTIPLE},
|
||||
{"CMSG_QUESTGIVER_HELLO", LogicalOpcode::CMSG_QUESTGIVER_HELLO},
|
||||
{"CMSG_QUESTGIVER_QUERY_QUEST", LogicalOpcode::CMSG_QUESTGIVER_QUERY_QUEST},
|
||||
{"SMSG_QUESTGIVER_QUEST_DETAILS", LogicalOpcode::SMSG_QUESTGIVER_QUEST_DETAILS},
|
||||
{"CMSG_QUESTGIVER_ACCEPT_QUEST", LogicalOpcode::CMSG_QUESTGIVER_ACCEPT_QUEST},
|
||||
{"CMSG_QUESTGIVER_COMPLETE_QUEST", LogicalOpcode::CMSG_QUESTGIVER_COMPLETE_QUEST},
|
||||
{"SMSG_QUESTGIVER_REQUEST_ITEMS", LogicalOpcode::SMSG_QUESTGIVER_REQUEST_ITEMS},
|
||||
{"CMSG_QUESTGIVER_REQUEST_REWARD", LogicalOpcode::CMSG_QUESTGIVER_REQUEST_REWARD},
|
||||
{"SMSG_QUESTGIVER_OFFER_REWARD", LogicalOpcode::SMSG_QUESTGIVER_OFFER_REWARD},
|
||||
{"CMSG_QUESTGIVER_CHOOSE_REWARD", LogicalOpcode::CMSG_QUESTGIVER_CHOOSE_REWARD},
|
||||
{"SMSG_QUESTGIVER_QUEST_INVALID", LogicalOpcode::SMSG_QUESTGIVER_QUEST_INVALID},
|
||||
{"SMSG_QUESTGIVER_QUEST_COMPLETE", LogicalOpcode::SMSG_QUESTGIVER_QUEST_COMPLETE},
|
||||
{"CMSG_QUESTLOG_REMOVE_QUEST", LogicalOpcode::CMSG_QUESTLOG_REMOVE_QUEST},
|
||||
{"SMSG_QUESTUPDATE_ADD_KILL", LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL},
|
||||
{"SMSG_QUESTUPDATE_COMPLETE", LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE},
|
||||
{"CMSG_QUEST_QUERY", LogicalOpcode::CMSG_QUEST_QUERY},
|
||||
{"SMSG_QUEST_QUERY_RESPONSE", LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE},
|
||||
{"SMSG_QUESTLOG_FULL", LogicalOpcode::SMSG_QUESTLOG_FULL},
|
||||
{"CMSG_LIST_INVENTORY", LogicalOpcode::CMSG_LIST_INVENTORY},
|
||||
{"SMSG_LIST_INVENTORY", LogicalOpcode::SMSG_LIST_INVENTORY},
|
||||
{"CMSG_SELL_ITEM", LogicalOpcode::CMSG_SELL_ITEM},
|
||||
{"SMSG_SELL_ITEM", LogicalOpcode::SMSG_SELL_ITEM},
|
||||
{"CMSG_BUY_ITEM", LogicalOpcode::CMSG_BUY_ITEM},
|
||||
{"SMSG_BUY_FAILED", LogicalOpcode::SMSG_BUY_FAILED},
|
||||
{"CMSG_TRAINER_LIST", LogicalOpcode::CMSG_TRAINER_LIST},
|
||||
{"SMSG_TRAINER_LIST", LogicalOpcode::SMSG_TRAINER_LIST},
|
||||
{"CMSG_TRAINER_BUY_SPELL", LogicalOpcode::CMSG_TRAINER_BUY_SPELL},
|
||||
{"SMSG_TRAINER_BUY_FAILED", LogicalOpcode::SMSG_TRAINER_BUY_FAILED},
|
||||
{"CMSG_ITEM_QUERY_SINGLE", LogicalOpcode::CMSG_ITEM_QUERY_SINGLE},
|
||||
{"SMSG_ITEM_QUERY_SINGLE_RESPONSE", LogicalOpcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE},
|
||||
{"CMSG_USE_ITEM", LogicalOpcode::CMSG_USE_ITEM},
|
||||
{"CMSG_AUTOEQUIP_ITEM", LogicalOpcode::CMSG_AUTOEQUIP_ITEM},
|
||||
{"CMSG_SWAP_ITEM", LogicalOpcode::CMSG_SWAP_ITEM},
|
||||
{"CMSG_SWAP_INV_ITEM", LogicalOpcode::CMSG_SWAP_INV_ITEM},
|
||||
{"SMSG_INVENTORY_CHANGE_FAILURE", LogicalOpcode::SMSG_INVENTORY_CHANGE_FAILURE},
|
||||
{"CMSG_INSPECT", LogicalOpcode::CMSG_INSPECT},
|
||||
{"SMSG_INSPECT_RESULTS", LogicalOpcode::SMSG_INSPECT_RESULTS},
|
||||
{"CMSG_REPOP_REQUEST", LogicalOpcode::CMSG_REPOP_REQUEST},
|
||||
{"SMSG_RESURRECT_REQUEST", LogicalOpcode::SMSG_RESURRECT_REQUEST},
|
||||
{"CMSG_RESURRECT_RESPONSE", LogicalOpcode::CMSG_RESURRECT_RESPONSE},
|
||||
{"CMSG_SPIRIT_HEALER_ACTIVATE", LogicalOpcode::CMSG_SPIRIT_HEALER_ACTIVATE},
|
||||
{"SMSG_SPIRIT_HEALER_CONFIRM", LogicalOpcode::SMSG_SPIRIT_HEALER_CONFIRM},
|
||||
{"SMSG_RESURRECT_CANCEL", LogicalOpcode::SMSG_RESURRECT_CANCEL},
|
||||
{"MSG_MOVE_TELEPORT_ACK", LogicalOpcode::MSG_MOVE_TELEPORT_ACK},
|
||||
{"SMSG_TRANSFER_PENDING", LogicalOpcode::SMSG_TRANSFER_PENDING},
|
||||
{"SMSG_NEW_WORLD", LogicalOpcode::SMSG_NEW_WORLD},
|
||||
{"MSG_MOVE_WORLDPORT_ACK", LogicalOpcode::MSG_MOVE_WORLDPORT_ACK},
|
||||
{"SMSG_TRANSFER_ABORTED", LogicalOpcode::SMSG_TRANSFER_ABORTED},
|
||||
{"SMSG_FORCE_RUN_SPEED_CHANGE", LogicalOpcode::SMSG_FORCE_RUN_SPEED_CHANGE},
|
||||
{"CMSG_FORCE_RUN_SPEED_CHANGE_ACK", LogicalOpcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK},
|
||||
{"CMSG_CANCEL_MOUNT_AURA", LogicalOpcode::CMSG_CANCEL_MOUNT_AURA},
|
||||
{"SMSG_SHOWTAXINODES", LogicalOpcode::SMSG_SHOWTAXINODES},
|
||||
{"SMSG_ACTIVATETAXIREPLY", LogicalOpcode::SMSG_ACTIVATETAXIREPLY},
|
||||
{"SMSG_ACTIVATETAXIREPLY_ALT", LogicalOpcode::SMSG_ACTIVATETAXIREPLY_ALT},
|
||||
{"SMSG_NEW_TAXI_PATH", LogicalOpcode::SMSG_NEW_TAXI_PATH},
|
||||
{"CMSG_ACTIVATETAXIEXPRESS", LogicalOpcode::CMSG_ACTIVATETAXIEXPRESS},
|
||||
{"SMSG_BATTLEFIELD_PORT_DENIED", LogicalOpcode::SMSG_BATTLEFIELD_PORT_DENIED},
|
||||
{"SMSG_REMOVED_FROM_PVP_QUEUE", LogicalOpcode::SMSG_REMOVED_FROM_PVP_QUEUE},
|
||||
{"SMSG_TRAINER_BUY_SUCCEEDED", LogicalOpcode::SMSG_TRAINER_BUY_SUCCEEDED},
|
||||
{"SMSG_BINDPOINTUPDATE", LogicalOpcode::SMSG_BINDPOINTUPDATE},
|
||||
{"CMSG_BATTLEFIELD_LIST", LogicalOpcode::CMSG_BATTLEFIELD_LIST},
|
||||
{"SMSG_BATTLEFIELD_LIST", LogicalOpcode::SMSG_BATTLEFIELD_LIST},
|
||||
{"CMSG_BATTLEFIELD_JOIN", LogicalOpcode::CMSG_BATTLEFIELD_JOIN},
|
||||
{"CMSG_BATTLEFIELD_STATUS", LogicalOpcode::CMSG_BATTLEFIELD_STATUS},
|
||||
{"SMSG_BATTLEFIELD_STATUS", LogicalOpcode::SMSG_BATTLEFIELD_STATUS},
|
||||
{"CMSG_BATTLEFIELD_PORT", LogicalOpcode::CMSG_BATTLEFIELD_PORT},
|
||||
{"CMSG_BATTLEMASTER_HELLO", LogicalOpcode::CMSG_BATTLEMASTER_HELLO},
|
||||
{"MSG_PVP_LOG_DATA", LogicalOpcode::MSG_PVP_LOG_DATA},
|
||||
{"CMSG_LEAVE_BATTLEFIELD", LogicalOpcode::CMSG_LEAVE_BATTLEFIELD},
|
||||
{"SMSG_GROUP_JOINED_BATTLEGROUND", LogicalOpcode::SMSG_GROUP_JOINED_BATTLEGROUND},
|
||||
{"MSG_BATTLEGROUND_PLAYER_POSITIONS", LogicalOpcode::MSG_BATTLEGROUND_PLAYER_POSITIONS},
|
||||
{"SMSG_BATTLEGROUND_PLAYER_JOINED", LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_JOINED},
|
||||
{"SMSG_BATTLEGROUND_PLAYER_LEFT", LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_LEFT},
|
||||
{"CMSG_BATTLEMASTER_JOIN", LogicalOpcode::CMSG_BATTLEMASTER_JOIN},
|
||||
{"SMSG_JOINED_BATTLEGROUND_QUEUE", LogicalOpcode::SMSG_JOINED_BATTLEGROUND_QUEUE},
|
||||
{"CMSG_ARENA_TEAM_CREATE", LogicalOpcode::CMSG_ARENA_TEAM_CREATE},
|
||||
{"SMSG_ARENA_TEAM_COMMAND_RESULT", LogicalOpcode::SMSG_ARENA_TEAM_COMMAND_RESULT},
|
||||
{"CMSG_ARENA_TEAM_QUERY", LogicalOpcode::CMSG_ARENA_TEAM_QUERY},
|
||||
{"SMSG_ARENA_TEAM_QUERY_RESPONSE", LogicalOpcode::SMSG_ARENA_TEAM_QUERY_RESPONSE},
|
||||
{"CMSG_ARENA_TEAM_ROSTER", LogicalOpcode::CMSG_ARENA_TEAM_ROSTER},
|
||||
{"SMSG_ARENA_TEAM_ROSTER", LogicalOpcode::SMSG_ARENA_TEAM_ROSTER},
|
||||
{"CMSG_ARENA_TEAM_INVITE", LogicalOpcode::CMSG_ARENA_TEAM_INVITE},
|
||||
{"SMSG_ARENA_TEAM_INVITE", LogicalOpcode::SMSG_ARENA_TEAM_INVITE},
|
||||
{"CMSG_ARENA_TEAM_ACCEPT", LogicalOpcode::CMSG_ARENA_TEAM_ACCEPT},
|
||||
{"CMSG_ARENA_TEAM_DECLINE", LogicalOpcode::CMSG_ARENA_TEAM_DECLINE},
|
||||
{"CMSG_ARENA_TEAM_LEAVE", LogicalOpcode::CMSG_ARENA_TEAM_LEAVE},
|
||||
{"CMSG_ARENA_TEAM_REMOVE", LogicalOpcode::CMSG_ARENA_TEAM_REMOVE},
|
||||
{"CMSG_ARENA_TEAM_DISBAND", LogicalOpcode::CMSG_ARENA_TEAM_DISBAND},
|
||||
{"CMSG_ARENA_TEAM_LEADER", LogicalOpcode::CMSG_ARENA_TEAM_LEADER},
|
||||
{"SMSG_ARENA_TEAM_EVENT", LogicalOpcode::SMSG_ARENA_TEAM_EVENT},
|
||||
{"CMSG_BATTLEMASTER_JOIN_ARENA", LogicalOpcode::CMSG_BATTLEMASTER_JOIN_ARENA},
|
||||
{"SMSG_ARENA_TEAM_STATS", LogicalOpcode::SMSG_ARENA_TEAM_STATS},
|
||||
{"SMSG_ARENA_ERROR", LogicalOpcode::SMSG_ARENA_ERROR},
|
||||
{"MSG_INSPECT_ARENA_TEAMS", LogicalOpcode::MSG_INSPECT_ARENA_TEAMS},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static constexpr size_t kOpcodeNameCount = sizeof(kOpcodeNames) / sizeof(kOpcodeNames[0]);
|
||||
|
||||
std::optional<LogicalOpcode> OpcodeTable::nameToLogical(const std::string& name) {
|
||||
for (size_t i = 0; i < kOpcodeNameCount; ++i) {
|
||||
if (name == kOpcodeNames[i].name) return kOpcodeNames[i].op;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* OpcodeTable::logicalToName(LogicalOpcode op) {
|
||||
uint16_t val = static_cast<uint16_t>(op);
|
||||
for (size_t i = 0; i < kOpcodeNameCount; ++i) {
|
||||
if (static_cast<uint16_t>(kOpcodeNames[i].op) == val) return kOpcodeNames[i].name;
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
void OpcodeTable::loadWotlkDefaults() {
|
||||
// WotLK 3.3.5a wire values — matches the original hardcoded Opcode enum
|
||||
struct { LogicalOpcode op; uint16_t wire; } defaults[] = {
|
||||
{LogicalOpcode::CMSG_PING, 0x1DC},
|
||||
{LogicalOpcode::CMSG_AUTH_SESSION, 0x1ED},
|
||||
{LogicalOpcode::CMSG_CHAR_CREATE, 0x036},
|
||||
{LogicalOpcode::CMSG_CHAR_ENUM, 0x037},
|
||||
{LogicalOpcode::CMSG_CHAR_DELETE, 0x038},
|
||||
{LogicalOpcode::CMSG_PLAYER_LOGIN, 0x03D},
|
||||
{LogicalOpcode::CMSG_MOVE_START_FORWARD, 0x0B5},
|
||||
{LogicalOpcode::CMSG_MOVE_START_BACKWARD, 0x0B6},
|
||||
{LogicalOpcode::CMSG_MOVE_STOP, 0x0B7},
|
||||
{LogicalOpcode::CMSG_MOVE_START_STRAFE_LEFT, 0x0B8},
|
||||
{LogicalOpcode::CMSG_MOVE_START_STRAFE_RIGHT, 0x0B9},
|
||||
{LogicalOpcode::CMSG_MOVE_STOP_STRAFE, 0x0BA},
|
||||
{LogicalOpcode::CMSG_MOVE_JUMP, 0x0BB},
|
||||
{LogicalOpcode::CMSG_MOVE_START_TURN_LEFT, 0x0BC},
|
||||
{LogicalOpcode::CMSG_MOVE_START_TURN_RIGHT, 0x0BD},
|
||||
{LogicalOpcode::CMSG_MOVE_STOP_TURN, 0x0BE},
|
||||
{LogicalOpcode::CMSG_MOVE_SET_FACING, 0x0DA},
|
||||
{LogicalOpcode::CMSG_MOVE_FALL_LAND, 0x0C9},
|
||||
{LogicalOpcode::CMSG_MOVE_START_SWIM, 0x0CA},
|
||||
{LogicalOpcode::CMSG_MOVE_STOP_SWIM, 0x0CB},
|
||||
{LogicalOpcode::CMSG_MOVE_HEARTBEAT, 0x0EE},
|
||||
{LogicalOpcode::SMSG_AUTH_CHALLENGE, 0x1EC},
|
||||
{LogicalOpcode::SMSG_AUTH_RESPONSE, 0x1EE},
|
||||
{LogicalOpcode::SMSG_CHAR_CREATE, 0x03A},
|
||||
{LogicalOpcode::SMSG_CHAR_ENUM, 0x03B},
|
||||
{LogicalOpcode::SMSG_CHAR_DELETE, 0x03C},
|
||||
{LogicalOpcode::SMSG_PONG, 0x1DD},
|
||||
{LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD, 0x236},
|
||||
{LogicalOpcode::SMSG_LOGIN_SETTIMESPEED, 0x042},
|
||||
{LogicalOpcode::SMSG_TUTORIAL_FLAGS, 0x0FD},
|
||||
{LogicalOpcode::SMSG_WARDEN_DATA, 0x2E6},
|
||||
{LogicalOpcode::CMSG_WARDEN_DATA, 0x2E7},
|
||||
{LogicalOpcode::SMSG_ACCOUNT_DATA_TIMES, 0x209},
|
||||
{LogicalOpcode::SMSG_CLIENTCACHE_VERSION, 0x4AB},
|
||||
{LogicalOpcode::SMSG_FEATURE_SYSTEM_STATUS, 0x3ED},
|
||||
{LogicalOpcode::SMSG_MOTD, 0x33D},
|
||||
{LogicalOpcode::SMSG_UPDATE_OBJECT, 0x0A9},
|
||||
{LogicalOpcode::SMSG_COMPRESSED_UPDATE_OBJECT, 0x1F6},
|
||||
{LogicalOpcode::SMSG_MONSTER_MOVE_TRANSPORT, 0x2AE},
|
||||
{LogicalOpcode::SMSG_DESTROY_OBJECT, 0x0AA},
|
||||
{LogicalOpcode::CMSG_MESSAGECHAT, 0x095},
|
||||
{LogicalOpcode::SMSG_MESSAGECHAT, 0x096},
|
||||
{LogicalOpcode::CMSG_WHO, 0x062},
|
||||
{LogicalOpcode::SMSG_WHO, 0x063},
|
||||
{LogicalOpcode::CMSG_REQUEST_PLAYED_TIME, 0x1CC},
|
||||
{LogicalOpcode::SMSG_PLAYED_TIME, 0x1CD},
|
||||
{LogicalOpcode::CMSG_QUERY_TIME, 0x1CE},
|
||||
{LogicalOpcode::SMSG_QUERY_TIME_RESPONSE, 0x1CF},
|
||||
{LogicalOpcode::SMSG_FRIEND_STATUS, 0x068},
|
||||
{LogicalOpcode::CMSG_ADD_FRIEND, 0x069},
|
||||
{LogicalOpcode::CMSG_DEL_FRIEND, 0x06A},
|
||||
{LogicalOpcode::CMSG_SET_CONTACT_NOTES, 0x06B},
|
||||
{LogicalOpcode::CMSG_ADD_IGNORE, 0x06C},
|
||||
{LogicalOpcode::CMSG_DEL_IGNORE, 0x06D},
|
||||
{LogicalOpcode::CMSG_PLAYER_LOGOUT, 0x04A},
|
||||
{LogicalOpcode::CMSG_LOGOUT_REQUEST, 0x04B},
|
||||
{LogicalOpcode::CMSG_LOGOUT_CANCEL, 0x04E},
|
||||
{LogicalOpcode::SMSG_LOGOUT_RESPONSE, 0x04C},
|
||||
{LogicalOpcode::SMSG_LOGOUT_COMPLETE, 0x04D},
|
||||
{LogicalOpcode::CMSG_STAND_STATE_CHANGE, 0x101},
|
||||
{LogicalOpcode::CMSG_SHOWING_HELM, 0x2B9},
|
||||
{LogicalOpcode::CMSG_SHOWING_CLOAK, 0x2BA},
|
||||
{LogicalOpcode::CMSG_TOGGLE_PVP, 0x253},
|
||||
{LogicalOpcode::CMSG_GUILD_INVITE, 0x082},
|
||||
{LogicalOpcode::CMSG_GUILD_ACCEPT, 0x084},
|
||||
{LogicalOpcode::CMSG_GUILD_DECLINE_INVITATION, 0x085},
|
||||
{LogicalOpcode::CMSG_GUILD_INFO, 0x087},
|
||||
{LogicalOpcode::CMSG_GUILD_GET_ROSTER, 0x089},
|
||||
{LogicalOpcode::CMSG_GUILD_PROMOTE_MEMBER, 0x08B},
|
||||
{LogicalOpcode::CMSG_GUILD_DEMOTE_MEMBER, 0x08C},
|
||||
{LogicalOpcode::CMSG_GUILD_LEAVE, 0x08D},
|
||||
{LogicalOpcode::CMSG_GUILD_MOTD, 0x091},
|
||||
{LogicalOpcode::SMSG_GUILD_INFO, 0x088},
|
||||
{LogicalOpcode::SMSG_GUILD_ROSTER, 0x08A},
|
||||
{LogicalOpcode::MSG_RAID_READY_CHECK, 0x322},
|
||||
{LogicalOpcode::MSG_RAID_READY_CHECK_CONFIRM, 0x3AE},
|
||||
{LogicalOpcode::CMSG_DUEL_PROPOSED, 0x166},
|
||||
{LogicalOpcode::CMSG_DUEL_ACCEPTED, 0x16C},
|
||||
{LogicalOpcode::CMSG_DUEL_CANCELLED, 0x16D},
|
||||
{LogicalOpcode::SMSG_DUEL_REQUESTED, 0x167},
|
||||
{LogicalOpcode::CMSG_INITIATE_TRADE, 0x116},
|
||||
{LogicalOpcode::MSG_RANDOM_ROLL, 0x1FB},
|
||||
{LogicalOpcode::CMSG_SET_SELECTION, 0x13D},
|
||||
{LogicalOpcode::CMSG_NAME_QUERY, 0x050},
|
||||
{LogicalOpcode::SMSG_NAME_QUERY_RESPONSE, 0x051},
|
||||
{LogicalOpcode::CMSG_CREATURE_QUERY, 0x060},
|
||||
{LogicalOpcode::SMSG_CREATURE_QUERY_RESPONSE, 0x061},
|
||||
{LogicalOpcode::CMSG_GAMEOBJECT_QUERY, 0x05E},
|
||||
{LogicalOpcode::SMSG_GAMEOBJECT_QUERY_RESPONSE, 0x05F},
|
||||
{LogicalOpcode::CMSG_SET_ACTIVE_MOVER, 0x26A},
|
||||
{LogicalOpcode::CMSG_BINDER_ACTIVATE, 0x1B5},
|
||||
{LogicalOpcode::SMSG_LOG_XPGAIN, 0x1D0},
|
||||
{LogicalOpcode::SMSG_MONSTER_MOVE, 0x0DD},
|
||||
{LogicalOpcode::CMSG_ATTACKSWING, 0x141},
|
||||
{LogicalOpcode::CMSG_ATTACKSTOP, 0x142},
|
||||
{LogicalOpcode::SMSG_ATTACKSTART, 0x143},
|
||||
{LogicalOpcode::SMSG_ATTACKSTOP, 0x144},
|
||||
{LogicalOpcode::SMSG_ATTACKERSTATEUPDATE, 0x14A},
|
||||
{LogicalOpcode::SMSG_SPELLNONMELEEDAMAGELOG, 0x250},
|
||||
{LogicalOpcode::SMSG_SPELLHEALLOG, 0x150},
|
||||
{LogicalOpcode::SMSG_SPELLENERGIZELOG, 0x25B},
|
||||
{LogicalOpcode::SMSG_PERIODICAURALOG, 0x24E},
|
||||
{LogicalOpcode::SMSG_ENVIRONMENTALDAMAGELOG, 0x1FC},
|
||||
{LogicalOpcode::CMSG_CAST_SPELL, 0x12E},
|
||||
{LogicalOpcode::CMSG_CANCEL_CAST, 0x12F},
|
||||
{LogicalOpcode::CMSG_CANCEL_AURA, 0x033},
|
||||
{LogicalOpcode::SMSG_CAST_FAILED, 0x130},
|
||||
{LogicalOpcode::SMSG_SPELL_START, 0x131},
|
||||
{LogicalOpcode::SMSG_SPELL_GO, 0x132},
|
||||
{LogicalOpcode::SMSG_SPELL_FAILURE, 0x133},
|
||||
{LogicalOpcode::SMSG_SPELL_COOLDOWN, 0x134},
|
||||
{LogicalOpcode::SMSG_COOLDOWN_EVENT, 0x135},
|
||||
{LogicalOpcode::SMSG_UPDATE_AURA_DURATION, 0x137},
|
||||
{LogicalOpcode::SMSG_INITIAL_SPELLS, 0x12A},
|
||||
{LogicalOpcode::SMSG_LEARNED_SPELL, 0x12B},
|
||||
{LogicalOpcode::SMSG_SUPERCEDED_SPELL, 0x12C},
|
||||
{LogicalOpcode::SMSG_REMOVED_SPELL, 0x203},
|
||||
{LogicalOpcode::SMSG_SEND_UNLEARN_SPELLS, 0x41F},
|
||||
{LogicalOpcode::SMSG_SPELL_DELAYED, 0x1E2},
|
||||
{LogicalOpcode::SMSG_AURA_UPDATE, 0x3FA},
|
||||
{LogicalOpcode::SMSG_AURA_UPDATE_ALL, 0x495},
|
||||
{LogicalOpcode::SMSG_SET_FLAT_SPELL_MODIFIER, 0x266},
|
||||
{LogicalOpcode::SMSG_SET_PCT_SPELL_MODIFIER, 0x267},
|
||||
{LogicalOpcode::SMSG_TALENTS_INFO, 0x4C0},
|
||||
{LogicalOpcode::CMSG_LEARN_TALENT, 0x251},
|
||||
{LogicalOpcode::MSG_TALENT_WIPE_CONFIRM, 0x2AB},
|
||||
{LogicalOpcode::CMSG_GROUP_INVITE, 0x06E},
|
||||
{LogicalOpcode::SMSG_GROUP_INVITE, 0x06F},
|
||||
{LogicalOpcode::CMSG_GROUP_ACCEPT, 0x072},
|
||||
{LogicalOpcode::CMSG_GROUP_DECLINE, 0x073},
|
||||
{LogicalOpcode::SMSG_GROUP_DECLINE, 0x074},
|
||||
{LogicalOpcode::CMSG_GROUP_UNINVITE_GUID, 0x076},
|
||||
{LogicalOpcode::SMSG_GROUP_UNINVITE, 0x077},
|
||||
{LogicalOpcode::CMSG_GROUP_SET_LEADER, 0x078},
|
||||
{LogicalOpcode::SMSG_GROUP_SET_LEADER, 0x079},
|
||||
{LogicalOpcode::CMSG_GROUP_DISBAND, 0x07B},
|
||||
{LogicalOpcode::SMSG_GROUP_LIST, 0x07D},
|
||||
{LogicalOpcode::SMSG_PARTY_COMMAND_RESULT, 0x07E},
|
||||
{LogicalOpcode::MSG_RAID_TARGET_UPDATE, 0x321},
|
||||
{LogicalOpcode::CMSG_REQUEST_RAID_INFO, 0x2CD},
|
||||
{LogicalOpcode::SMSG_RAID_INSTANCE_INFO, 0x2CC},
|
||||
{LogicalOpcode::CMSG_AUTOSTORE_LOOT_ITEM, 0x108},
|
||||
{LogicalOpcode::CMSG_LOOT, 0x15D},
|
||||
{LogicalOpcode::CMSG_LOOT_MONEY, 0x15E},
|
||||
{LogicalOpcode::CMSG_LOOT_RELEASE, 0x15F},
|
||||
{LogicalOpcode::SMSG_LOOT_RESPONSE, 0x160},
|
||||
{LogicalOpcode::SMSG_LOOT_RELEASE_RESPONSE, 0x161},
|
||||
{LogicalOpcode::SMSG_LOOT_REMOVED, 0x162},
|
||||
{LogicalOpcode::SMSG_LOOT_MONEY_NOTIFY, 0x163},
|
||||
{LogicalOpcode::SMSG_LOOT_CLEAR_MONEY, 0x165},
|
||||
{LogicalOpcode::CMSG_ACTIVATETAXI, 0x19D},
|
||||
{LogicalOpcode::CMSG_GOSSIP_HELLO, 0x17B},
|
||||
{LogicalOpcode::CMSG_GOSSIP_SELECT_OPTION, 0x17C},
|
||||
{LogicalOpcode::SMSG_GOSSIP_MESSAGE, 0x17D},
|
||||
{LogicalOpcode::SMSG_GOSSIP_COMPLETE, 0x17E},
|
||||
{LogicalOpcode::SMSG_NPC_TEXT_UPDATE, 0x180},
|
||||
{LogicalOpcode::CMSG_GAMEOBJECT_USE, 0x01B},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_STATUS_QUERY, 0x182},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_STATUS, 0x183},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_STATUS_MULTIPLE, 0x198},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_HELLO, 0x184},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_QUERY_QUEST, 0x186},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_QUEST_DETAILS, 0x188},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_ACCEPT_QUEST, 0x189},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_COMPLETE_QUEST, 0x18A},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_REQUEST_ITEMS, 0x18B},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_REQUEST_REWARD, 0x18C},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_OFFER_REWARD, 0x18D},
|
||||
{LogicalOpcode::CMSG_QUESTGIVER_CHOOSE_REWARD, 0x18E},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_QUEST_INVALID, 0x18F},
|
||||
{LogicalOpcode::SMSG_QUESTGIVER_QUEST_COMPLETE, 0x191},
|
||||
{LogicalOpcode::CMSG_QUESTLOG_REMOVE_QUEST, 0x194},
|
||||
{LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL, 0x196},
|
||||
{LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE, 0x195},
|
||||
{LogicalOpcode::CMSG_QUEST_QUERY, 0x05C},
|
||||
{LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE, 0x05D},
|
||||
{LogicalOpcode::SMSG_QUESTLOG_FULL, 0x1A3},
|
||||
{LogicalOpcode::CMSG_LIST_INVENTORY, 0x19E},
|
||||
{LogicalOpcode::SMSG_LIST_INVENTORY, 0x19F},
|
||||
{LogicalOpcode::CMSG_SELL_ITEM, 0x1A0},
|
||||
{LogicalOpcode::SMSG_SELL_ITEM, 0x1A1},
|
||||
{LogicalOpcode::CMSG_BUY_ITEM, 0x1A2},
|
||||
{LogicalOpcode::SMSG_BUY_FAILED, 0x1A5},
|
||||
{LogicalOpcode::CMSG_TRAINER_LIST, 0x01B0},
|
||||
{LogicalOpcode::SMSG_TRAINER_LIST, 0x01B1},
|
||||
{LogicalOpcode::CMSG_TRAINER_BUY_SPELL, 0x01B2},
|
||||
{LogicalOpcode::SMSG_TRAINER_BUY_FAILED, 0x01B4},
|
||||
{LogicalOpcode::CMSG_ITEM_QUERY_SINGLE, 0x056},
|
||||
{LogicalOpcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE, 0x058},
|
||||
{LogicalOpcode::CMSG_USE_ITEM, 0x00AB},
|
||||
{LogicalOpcode::CMSG_AUTOEQUIP_ITEM, 0x10A},
|
||||
{LogicalOpcode::CMSG_SWAP_ITEM, 0x10C},
|
||||
{LogicalOpcode::CMSG_SWAP_INV_ITEM, 0x10D},
|
||||
{LogicalOpcode::SMSG_INVENTORY_CHANGE_FAILURE, 0x112},
|
||||
{LogicalOpcode::CMSG_INSPECT, 0x114},
|
||||
{LogicalOpcode::SMSG_INSPECT_RESULTS, 0x115},
|
||||
{LogicalOpcode::CMSG_REPOP_REQUEST, 0x015A},
|
||||
{LogicalOpcode::SMSG_RESURRECT_REQUEST, 0x015B},
|
||||
{LogicalOpcode::CMSG_RESURRECT_RESPONSE, 0x015C},
|
||||
{LogicalOpcode::CMSG_SPIRIT_HEALER_ACTIVATE, 0x021C},
|
||||
{LogicalOpcode::SMSG_SPIRIT_HEALER_CONFIRM, 0x0222},
|
||||
{LogicalOpcode::SMSG_RESURRECT_CANCEL, 0x0390},
|
||||
{LogicalOpcode::MSG_MOVE_TELEPORT_ACK, 0x0C7},
|
||||
{LogicalOpcode::SMSG_TRANSFER_PENDING, 0x003F},
|
||||
{LogicalOpcode::SMSG_NEW_WORLD, 0x003E},
|
||||
{LogicalOpcode::MSG_MOVE_WORLDPORT_ACK, 0x00DC},
|
||||
{LogicalOpcode::SMSG_TRANSFER_ABORTED, 0x0040},
|
||||
{LogicalOpcode::SMSG_FORCE_RUN_SPEED_CHANGE, 0x00E2},
|
||||
{LogicalOpcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, 0x00E3},
|
||||
{LogicalOpcode::CMSG_CANCEL_MOUNT_AURA, 0x0375},
|
||||
{LogicalOpcode::SMSG_SHOWTAXINODES, 0x01A9},
|
||||
{LogicalOpcode::SMSG_ACTIVATETAXIREPLY, 0x01AE},
|
||||
{LogicalOpcode::SMSG_ACTIVATETAXIREPLY_ALT, 0x029D},
|
||||
{LogicalOpcode::SMSG_NEW_TAXI_PATH, 0x01AF},
|
||||
{LogicalOpcode::CMSG_ACTIVATETAXIEXPRESS, 0x0312},
|
||||
{LogicalOpcode::SMSG_BATTLEFIELD_PORT_DENIED, 0x014B},
|
||||
{LogicalOpcode::SMSG_REMOVED_FROM_PVP_QUEUE, 0x0170},
|
||||
{LogicalOpcode::SMSG_TRAINER_BUY_SUCCEEDED, 0x01B3},
|
||||
{LogicalOpcode::SMSG_BINDPOINTUPDATE, 0x0155},
|
||||
{LogicalOpcode::CMSG_BATTLEFIELD_LIST, 0x023C},
|
||||
{LogicalOpcode::SMSG_BATTLEFIELD_LIST, 0x023D},
|
||||
{LogicalOpcode::CMSG_BATTLEFIELD_JOIN, 0x023E},
|
||||
{LogicalOpcode::CMSG_BATTLEFIELD_STATUS, 0x02D3},
|
||||
{LogicalOpcode::SMSG_BATTLEFIELD_STATUS, 0x02D4},
|
||||
{LogicalOpcode::CMSG_BATTLEFIELD_PORT, 0x02D5},
|
||||
{LogicalOpcode::CMSG_BATTLEMASTER_HELLO, 0x02D7},
|
||||
{LogicalOpcode::MSG_PVP_LOG_DATA, 0x02E0},
|
||||
{LogicalOpcode::CMSG_LEAVE_BATTLEFIELD, 0x02E1},
|
||||
{LogicalOpcode::SMSG_GROUP_JOINED_BATTLEGROUND, 0x02E8},
|
||||
{LogicalOpcode::MSG_BATTLEGROUND_PLAYER_POSITIONS, 0x02E9},
|
||||
{LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_JOINED, 0x02EC},
|
||||
{LogicalOpcode::SMSG_BATTLEGROUND_PLAYER_LEFT, 0x02ED},
|
||||
{LogicalOpcode::CMSG_BATTLEMASTER_JOIN, 0x02EE},
|
||||
{LogicalOpcode::SMSG_JOINED_BATTLEGROUND_QUEUE, 0x038A},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_CREATE, 0x0348},
|
||||
{LogicalOpcode::SMSG_ARENA_TEAM_COMMAND_RESULT, 0x0349},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_QUERY, 0x034B},
|
||||
{LogicalOpcode::SMSG_ARENA_TEAM_QUERY_RESPONSE, 0x034C},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_ROSTER, 0x034D},
|
||||
{LogicalOpcode::SMSG_ARENA_TEAM_ROSTER, 0x034E},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_INVITE, 0x034F},
|
||||
{LogicalOpcode::SMSG_ARENA_TEAM_INVITE, 0x0350},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_ACCEPT, 0x0351},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_DECLINE, 0x0352},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_LEAVE, 0x0353},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_REMOVE, 0x0354},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_DISBAND, 0x0355},
|
||||
{LogicalOpcode::CMSG_ARENA_TEAM_LEADER, 0x0356},
|
||||
{LogicalOpcode::SMSG_ARENA_TEAM_EVENT, 0x0357},
|
||||
{LogicalOpcode::CMSG_BATTLEMASTER_JOIN_ARENA, 0x0358},
|
||||
{LogicalOpcode::SMSG_ARENA_TEAM_STATS, 0x035B},
|
||||
{LogicalOpcode::SMSG_ARENA_ERROR, 0x0376},
|
||||
{LogicalOpcode::MSG_INSPECT_ARENA_TEAMS, 0x0377},
|
||||
};
|
||||
|
||||
logicalToWire_.clear();
|
||||
wireToLogical_.clear();
|
||||
for (auto& d : defaults) {
|
||||
uint16_t logIdx = static_cast<uint16_t>(d.op);
|
||||
logicalToWire_[logIdx] = d.wire;
|
||||
wireToLogical_[d.wire] = logIdx;
|
||||
}
|
||||
LOG_INFO("OpcodeTable: loaded ", logicalToWire_.size(), " WotLK default opcodes");
|
||||
}
|
||||
|
||||
bool OpcodeTable::loadFromJson(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) {
|
||||
LOG_WARNING("OpcodeTable: cannot open ", path, ", using defaults");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
logicalToWire_.clear();
|
||||
wireToLogical_.clear();
|
||||
|
||||
// Parse simple JSON: { "NAME": "0xHEX", ... } or { "NAME": 123, ... }
|
||||
size_t pos = 0;
|
||||
size_t loaded = 0;
|
||||
while (pos < json.size()) {
|
||||
// Find next quoted key
|
||||
size_t keyStart = json.find('"', pos);
|
||||
if (keyStart == std::string::npos) break;
|
||||
size_t keyEnd = json.find('"', keyStart + 1);
|
||||
if (keyEnd == std::string::npos) break;
|
||||
std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1);
|
||||
|
||||
// Find colon then value
|
||||
size_t colon = json.find(':', keyEnd);
|
||||
if (colon == std::string::npos) break;
|
||||
|
||||
// Skip whitespace
|
||||
size_t valStart = colon + 1;
|
||||
while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' ||
|
||||
json[valStart] == '\r' || json[valStart] == '\n' || json[valStart] == '"'))
|
||||
++valStart;
|
||||
|
||||
size_t valEnd = json.find_first_of(",}\"\r\n", valStart);
|
||||
if (valEnd == std::string::npos) valEnd = json.size();
|
||||
std::string valStr = json.substr(valStart, valEnd - valStart);
|
||||
|
||||
// Parse hex or decimal value
|
||||
uint16_t wire = 0;
|
||||
try {
|
||||
if (valStr.size() > 2 && (valStr[0] == '0' && (valStr[1] == 'x' || valStr[1] == 'X'))) {
|
||||
wire = static_cast<uint16_t>(std::stoul(valStr, nullptr, 16));
|
||||
} else {
|
||||
wire = static_cast<uint16_t>(std::stoul(valStr));
|
||||
}
|
||||
} catch (...) {
|
||||
pos = valEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto logOp = nameToLogical(key);
|
||||
if (logOp) {
|
||||
uint16_t logIdx = static_cast<uint16_t>(*logOp);
|
||||
logicalToWire_[logIdx] = wire;
|
||||
wireToLogical_[wire] = logIdx;
|
||||
++loaded;
|
||||
}
|
||||
|
||||
pos = valEnd + 1;
|
||||
}
|
||||
|
||||
LOG_INFO("OpcodeTable: loaded ", loaded, " opcodes from ", path);
|
||||
return loaded > 0;
|
||||
}
|
||||
|
||||
uint16_t OpcodeTable::toWire(LogicalOpcode op) const {
|
||||
auto it = logicalToWire_.find(static_cast<uint16_t>(op));
|
||||
return (it != logicalToWire_.end()) ? it->second : 0xFFFF;
|
||||
}
|
||||
|
||||
std::optional<LogicalOpcode> OpcodeTable::fromWire(uint16_t wireValue) const {
|
||||
auto it = wireToLogical_.find(wireValue);
|
||||
if (it != wireToLogical_.end()) {
|
||||
return static_cast<LogicalOpcode>(it->second);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool OpcodeTable::hasOpcode(LogicalOpcode op) const {
|
||||
return logicalToWire_.count(static_cast<uint16_t>(op)) > 0;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
344
src/game/packet_parsers_classic.cpp
Normal file
344
src/game/packet_parsers_classic.cpp
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
#include "game/packet_parsers.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
// ============================================================================
|
||||
// Classic 1.12.1 movement flag constants
|
||||
// Key differences from TBC:
|
||||
// - SPLINE_ENABLED at 0x00400000 (TBC/WotLK: 0x08000000)
|
||||
// - No FLYING flag (flight was added in TBC)
|
||||
// - ONTRANSPORT at 0x02000000 (not used for pitch in Classic)
|
||||
// Same as TBC: ON_TRANSPORT=0x200, JUMPING=0x2000, SWIMMING=0x200000,
|
||||
// SPLINE_ELEVATION=0x04000000
|
||||
// ============================================================================
|
||||
namespace ClassicMoveFlags {
|
||||
constexpr uint32_t ONTRANSPORT = 0x02000000; // Gates transport data (vmangos authoritative)
|
||||
constexpr uint32_t JUMPING = 0x00002000; // Gates jump data
|
||||
constexpr uint32_t SWIMMING = 0x00200000; // Gates pitch
|
||||
constexpr uint32_t SPLINE_ENABLED = 0x00400000; // TBC/WotLK: 0x08000000
|
||||
constexpr uint32_t SPLINE_ELEVATION = 0x04000000; // Same as TBC
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic parseMovementBlock
|
||||
// Key differences from TBC:
|
||||
// - NO moveFlags2 (TBC reads u8, WotLK reads u16)
|
||||
// - SPLINE_ENABLED at 0x00400000 (not 0x08000000)
|
||||
// - Transport data: NO timestamp (TBC adds u32 timestamp)
|
||||
// - Pitch: only SWIMMING (no ONTRANSPORT secondary pitch, no FLYING)
|
||||
// Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate
|
||||
// ============================================================================
|
||||
bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
// Classic: UpdateFlags is uint8 (same as TBC)
|
||||
uint8_t updateFlags = packet.readUInt8();
|
||||
block.updateFlags = static_cast<uint16_t>(updateFlags);
|
||||
|
||||
LOG_DEBUG(" [Classic] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec);
|
||||
|
||||
const uint8_t UPDATEFLAG_LIVING = 0x20;
|
||||
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
|
||||
const uint8_t UPDATEFLAG_HAS_TARGET = 0x04;
|
||||
const uint8_t UPDATEFLAG_TRANSPORT = 0x02;
|
||||
const uint8_t UPDATEFLAG_LOWGUID = 0x08;
|
||||
const uint8_t UPDATEFLAG_HIGHGUID = 0x10;
|
||||
|
||||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||||
// Movement flags (u32 only — NO extra flags byte in Classic)
|
||||
uint32_t moveFlags = packet.readUInt32();
|
||||
/*uint32_t time =*/ packet.readUInt32();
|
||||
|
||||
// Position
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
block.orientation = packet.readFloat();
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_DEBUG(" [Classic] LIVING: (", block.x, ", ", block.y, ", ", block.z,
|
||||
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
|
||||
|
||||
// Transport data (Classic: ONTRANSPORT=0x02000000, no timestamp)
|
||||
if (moveFlags & ClassicMoveFlags::ONTRANSPORT) {
|
||||
block.onTransport = true;
|
||||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
block.transportO = packet.readFloat();
|
||||
// Classic: NO transport timestamp (TBC adds u32 timestamp)
|
||||
// Classic: NO transport seat byte
|
||||
}
|
||||
|
||||
// Pitch (Classic: only SWIMMING, no FLYING or ONTRANSPORT pitch)
|
||||
if (moveFlags & ClassicMoveFlags::SWIMMING) {
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||||
|
||||
// Jumping (Classic: JUMPING=0x2000, same as TBC)
|
||||
if (moveFlags & ClassicMoveFlags::JUMPING) {
|
||||
/*float jumpVelocity =*/ packet.readFloat();
|
||||
/*float jumpSinAngle =*/ packet.readFloat();
|
||||
/*float jumpCosAngle =*/ packet.readFloat();
|
||||
/*float jumpXYSpeed =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Spline elevation
|
||||
if (moveFlags & ClassicMoveFlags::SPLINE_ELEVATION) {
|
||||
/*float splineElevation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Speeds (Classic: 6 values — no flight speeds, no pitchRate)
|
||||
// TBC added flying_speed + backwards_flying_speed (8 total)
|
||||
// WotLK added pitchRate (9 total)
|
||||
/*float walkSpeed =*/ packet.readFloat();
|
||||
float runSpeed = packet.readFloat();
|
||||
/*float runBackSpeed =*/ packet.readFloat();
|
||||
/*float swimSpeed =*/ packet.readFloat();
|
||||
/*float swimBackSpeed =*/ packet.readFloat();
|
||||
/*float turnRate =*/ packet.readFloat();
|
||||
|
||||
block.runSpeed = runSpeed;
|
||||
|
||||
// Spline data (Classic: SPLINE_ENABLED=0x00400000)
|
||||
if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) {
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) { // FINAL_POINT
|
||||
/*float finalX =*/ packet.readFloat();
|
||||
/*float finalY =*/ packet.readFloat();
|
||||
/*float finalZ =*/ packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
|
||||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
|
||||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Classic spline: timePassed, duration, id, nodes, finalNode (same as TBC)
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) {
|
||||
LOG_WARNING(" [Classic] Spline pointCount=", pointCount, " exceeds max, capping");
|
||||
pointCount = 0;
|
||||
}
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
/*float pz =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Classic: NO splineMode byte
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
block.orientation = packet.readFloat();
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_DEBUG(" [Classic] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
|
||||
}
|
||||
|
||||
// Target GUID
|
||||
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
|
||||
/*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
// Transport time
|
||||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// Low GUID
|
||||
if (updateFlags & UPDATEFLAG_LOWGUID) {
|
||||
/*uint32_t lowGuid =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// High GUID
|
||||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||||
/*uint32_t highGuid =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic writeMovementPayload
|
||||
// Key differences from TBC:
|
||||
// - NO flags2 byte (TBC writes u8)
|
||||
// - Transport data: NO timestamp
|
||||
// - Pitch: only SWIMMING (no ONTRANSPORT pitch)
|
||||
// ============================================================================
|
||||
void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
|
||||
// Movement flags (uint32)
|
||||
packet.writeUInt32(info.flags);
|
||||
|
||||
// Classic: NO flags2 byte (TBC has u8, WotLK has u16)
|
||||
|
||||
// Timestamp
|
||||
packet.writeUInt32(info.time);
|
||||
|
||||
// Position
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.x), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.y), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.z), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
|
||||
|
||||
// Transport data (Classic ONTRANSPORT = 0x02000000, no timestamp)
|
||||
if (info.flags & ClassicMoveFlags::ONTRANSPORT) {
|
||||
// Packed transport GUID
|
||||
uint8_t transMask = 0;
|
||||
uint8_t transGuidBytes[8];
|
||||
int transGuidByteCount = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = static_cast<uint8_t>((info.transportGuid >> (i * 8)) & 0xFF);
|
||||
if (byte != 0) {
|
||||
transMask |= (1 << i);
|
||||
transGuidBytes[transGuidByteCount++] = byte;
|
||||
}
|
||||
}
|
||||
packet.writeUInt8(transMask);
|
||||
for (int i = 0; i < transGuidByteCount; i++) {
|
||||
packet.writeUInt8(transGuidBytes[i]);
|
||||
}
|
||||
|
||||
// Transport local position
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportX), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportY), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportZ), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportO), sizeof(float));
|
||||
|
||||
// Classic: NO transport timestamp
|
||||
// Classic: NO transport seat byte
|
||||
}
|
||||
|
||||
// Pitch (Classic: only SWIMMING)
|
||||
if (info.flags & ClassicMoveFlags::SWIMMING) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
packet.writeUInt32(info.fallTime);
|
||||
|
||||
// Jump data (Classic JUMPING = 0x2000)
|
||||
if (info.flags & ClassicMoveFlags::JUMPING) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic buildMovementPacket
|
||||
// Classic/TBC: client movement packets do NOT include PackedGuid prefix
|
||||
// (WotLK added PackedGuid to client packets)
|
||||
// ============================================================================
|
||||
network::Packet ClassicPacketParsers::buildMovementPacket(LogicalOpcode opcode,
|
||||
const MovementInfo& info,
|
||||
uint64_t /*playerGuid*/) {
|
||||
network::Packet packet(wireOpcode(opcode));
|
||||
|
||||
// Classic: NO PackedGuid prefix for client packets
|
||||
writeMovementPayload(packet, info);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classic 1.12.1 parseCharEnum
|
||||
// Differences from TBC:
|
||||
// - Equipment: 20 items, but NO enchantment field per slot
|
||||
// Classic: displayId(u32) + inventoryType(u8) = 5 bytes/slot
|
||||
// TBC/WotLK: displayId(u32) + inventoryType(u8) + enchant(u32) = 9 bytes/slot
|
||||
// - After flags: uint8 firstLogin (same as TBC)
|
||||
// ============================================================================
|
||||
bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
|
||||
uint8_t count = packet.readUInt8();
|
||||
|
||||
LOG_INFO("[Classic] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
|
||||
|
||||
response.characters.clear();
|
||||
response.characters.reserve(count);
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
Character character;
|
||||
|
||||
// GUID (8 bytes)
|
||||
character.guid = packet.readUInt64();
|
||||
|
||||
// Name (null-terminated string)
|
||||
character.name = packet.readString();
|
||||
|
||||
// Race, class, gender
|
||||
character.race = static_cast<Race>(packet.readUInt8());
|
||||
character.characterClass = static_cast<Class>(packet.readUInt8());
|
||||
character.gender = static_cast<Gender>(packet.readUInt8());
|
||||
|
||||
// Appearance (5 bytes: skin, face, hairStyle, hairColor packed + facialFeatures)
|
||||
character.appearanceBytes = packet.readUInt32();
|
||||
character.facialFeatures = packet.readUInt8();
|
||||
|
||||
// Level
|
||||
character.level = packet.readUInt8();
|
||||
|
||||
// Location
|
||||
character.zoneId = packet.readUInt32();
|
||||
character.mapId = packet.readUInt32();
|
||||
character.x = packet.readFloat();
|
||||
character.y = packet.readFloat();
|
||||
character.z = packet.readFloat();
|
||||
|
||||
// Guild ID
|
||||
character.guildId = packet.readUInt32();
|
||||
|
||||
// Flags
|
||||
character.flags = packet.readUInt32();
|
||||
|
||||
// Classic: uint8 firstLogin (same as TBC)
|
||||
/*uint8_t firstLogin =*/ packet.readUInt8();
|
||||
|
||||
// Pet data (always present)
|
||||
character.pet.displayModel = packet.readUInt32();
|
||||
character.pet.level = packet.readUInt32();
|
||||
character.pet.family = packet.readUInt32();
|
||||
|
||||
// Equipment (Classic: 20 items, NO enchantment field)
|
||||
character.equipment.reserve(20);
|
||||
for (int j = 0; j < 20; ++j) {
|
||||
EquipmentItem item;
|
||||
item.displayModel = packet.readUInt32();
|
||||
item.inventoryType = packet.readUInt8();
|
||||
item.enchantment = 0; // Classic has no enchant field in char enum
|
||||
character.equipment.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO(" Character ", (int)(i + 1), ": ", character.name);
|
||||
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
|
||||
LOG_INFO(" ", getRaceName(character.race), " ",
|
||||
getClassName(character.characterClass), " (",
|
||||
getGenderName(character.gender), ")");
|
||||
LOG_INFO(" Level: ", (int)character.level);
|
||||
LOG_INFO(" Location: Zone ", character.zoneId, ", Map ", character.mapId);
|
||||
|
||||
response.characters.push_back(character);
|
||||
}
|
||||
|
||||
LOG_INFO("[Classic] Parsed ", response.characters.size(), " characters");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
424
src/game/packet_parsers_tbc.cpp
Normal file
424
src/game/packet_parsers_tbc.cpp
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
#include "game/packet_parsers.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
// ============================================================================
|
||||
// TBC 2.4.3 movement flag constants (shifted relative to WotLK 3.3.5a)
|
||||
// ============================================================================
|
||||
namespace TbcMoveFlags {
|
||||
constexpr uint32_t ON_TRANSPORT = 0x00000200; // Gates transport data (same as WotLK)
|
||||
constexpr uint32_t JUMPING = 0x00002000; // Gates jump data (WotLK: FALLING=0x1000)
|
||||
constexpr uint32_t SWIMMING = 0x00200000; // Same as WotLK
|
||||
constexpr uint32_t FLYING = 0x01000000; // WotLK: 0x02000000
|
||||
constexpr uint32_t ONTRANSPORT = 0x02000000; // Secondary pitch check
|
||||
constexpr uint32_t SPLINE_ELEVATION = 0x04000000; // Same as WotLK
|
||||
constexpr uint32_t SPLINE_ENABLED = 0x08000000; // Same as WotLK
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TBC parseMovementBlock
|
||||
// Key differences from WotLK:
|
||||
// - UpdateFlags is uint8 (not uint16)
|
||||
// - No VEHICLE (0x0080), POSITION (0x0100), ROTATION (0x0200) flags
|
||||
// - moveFlags2 is uint8 (not uint16)
|
||||
// - No transport seat byte
|
||||
// - No interpolated movement (flags2 & 0x0200) check
|
||||
// - Pitch check: SWIMMING, else ONTRANSPORT(0x02000000)
|
||||
// - Spline data: has splineId, no durationMod/durationModNext/verticalAccel/effectStartTime/splineMode
|
||||
// - Flag 0x08 (HIGH_GUID) reads 2 u32s (Classic: 1 u32)
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||||
// TBC 2.4.3: UpdateFlags is uint8 (1 byte)
|
||||
uint8_t updateFlags = packet.readUInt8();
|
||||
block.updateFlags = static_cast<uint16_t>(updateFlags);
|
||||
|
||||
LOG_DEBUG(" [TBC] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec);
|
||||
|
||||
// TBC UpdateFlag bit values (same as lower byte of WotLK):
|
||||
// 0x01 = SELF
|
||||
// 0x02 = TRANSPORT
|
||||
// 0x04 = HAS_TARGET
|
||||
// 0x08 = LOWGUID
|
||||
// 0x10 = HIGHGUID
|
||||
// 0x20 = LIVING
|
||||
// 0x40 = HAS_POSITION (stationary)
|
||||
const uint8_t UPDATEFLAG_LIVING = 0x20;
|
||||
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
|
||||
const uint8_t UPDATEFLAG_HAS_TARGET = 0x04;
|
||||
const uint8_t UPDATEFLAG_TRANSPORT = 0x02;
|
||||
const uint8_t UPDATEFLAG_LOWGUID = 0x08;
|
||||
const uint8_t UPDATEFLAG_HIGHGUID = 0x10;
|
||||
|
||||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||||
// Full movement block for living units
|
||||
uint32_t moveFlags = packet.readUInt32();
|
||||
uint8_t moveFlags2 = packet.readUInt8(); // TBC: uint8, not uint16
|
||||
(void)moveFlags2;
|
||||
/*uint32_t time =*/ packet.readUInt32();
|
||||
|
||||
// Position
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
block.orientation = packet.readFloat();
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_DEBUG(" [TBC] LIVING: (", block.x, ", ", block.y, ", ", block.z,
|
||||
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
|
||||
|
||||
// Transport data
|
||||
if (moveFlags & TbcMoveFlags::ON_TRANSPORT) {
|
||||
block.onTransport = true;
|
||||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
block.transportO = packet.readFloat();
|
||||
/*uint32_t tTime =*/ packet.readUInt32();
|
||||
// TBC: NO transport seat byte
|
||||
// TBC: NO interpolated movement check
|
||||
}
|
||||
|
||||
// Pitch: SWIMMING, or else ONTRANSPORT (TBC-specific secondary pitch)
|
||||
if (moveFlags & TbcMoveFlags::SWIMMING) {
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
} else if (moveFlags & TbcMoveFlags::ONTRANSPORT) {
|
||||
/*float pitch =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||||
|
||||
// Jumping (TBC: JUMPING=0x2000, WotLK: FALLING=0x1000)
|
||||
if (moveFlags & TbcMoveFlags::JUMPING) {
|
||||
/*float jumpVelocity =*/ packet.readFloat();
|
||||
/*float jumpSinAngle =*/ packet.readFloat();
|
||||
/*float jumpCosAngle =*/ packet.readFloat();
|
||||
/*float jumpXYSpeed =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Spline elevation (TBC: 0x02000000, WotLK: 0x04000000)
|
||||
if (moveFlags & TbcMoveFlags::SPLINE_ELEVATION) {
|
||||
/*float splineElevation =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Speeds (TBC: 8 values — walk, run, runBack, swim, fly, flyBack, swimBack, turn)
|
||||
// WotLK adds pitchRate (9 total)
|
||||
/*float walkSpeed =*/ packet.readFloat();
|
||||
float runSpeed = packet.readFloat();
|
||||
/*float runBackSpeed =*/ packet.readFloat();
|
||||
/*float swimSpeed =*/ packet.readFloat();
|
||||
/*float flySpeed =*/ packet.readFloat();
|
||||
/*float flyBackSpeed =*/ packet.readFloat();
|
||||
/*float swimBackSpeed =*/ packet.readFloat();
|
||||
/*float turnRate =*/ packet.readFloat();
|
||||
|
||||
block.runSpeed = runSpeed;
|
||||
|
||||
// Spline data (TBC/WotLK: SPLINE_ENABLED = 0x08000000)
|
||||
if (moveFlags & TbcMoveFlags::SPLINE_ENABLED) {
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [TBC] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) { // FINAL_POINT
|
||||
/*float finalX =*/ packet.readFloat();
|
||||
/*float finalY =*/ packet.readFloat();
|
||||
/*float finalZ =*/ packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
|
||||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
|
||||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// TBC spline: timePassed, duration, id, nodes, finalNode
|
||||
// (no durationMod, durationModNext, verticalAccel, effectStartTime, splineMode)
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) {
|
||||
LOG_WARNING(" [TBC] Spline pointCount=", pointCount, " exceeds max, capping");
|
||||
pointCount = 0;
|
||||
}
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
/*float px =*/ packet.readFloat();
|
||||
/*float py =*/ packet.readFloat();
|
||||
/*float pz =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// TBC: NO splineMode byte (WotLK adds it)
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||||
// TBC: Simple stationary position (same as WotLK STATIONARY)
|
||||
block.x = packet.readFloat();
|
||||
block.y = packet.readFloat();
|
||||
block.z = packet.readFloat();
|
||||
block.orientation = packet.readFloat();
|
||||
block.hasMovement = true;
|
||||
|
||||
LOG_DEBUG(" [TBC] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
|
||||
}
|
||||
// TBC: No UPDATEFLAG_POSITION (0x0100) code path
|
||||
|
||||
// Target GUID
|
||||
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
|
||||
/*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||||
}
|
||||
|
||||
// Transport time
|
||||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// TBC: No VEHICLE flag (WotLK 0x0080)
|
||||
// TBC: No ROTATION flag (WotLK 0x0200)
|
||||
|
||||
// HIGH_GUID (0x08) — TBC has 2 u32s, Classic has 1 u32
|
||||
if (updateFlags & UPDATEFLAG_LOWGUID) {
|
||||
/*uint32_t unknown0 =*/ packet.readUInt32();
|
||||
/*uint32_t unknown1 =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
// ALL (0x10)
|
||||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||||
/*uint32_t unknown2 =*/ packet.readUInt32();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TBC writeMovementPayload
|
||||
// Key differences from WotLK:
|
||||
// - flags2 is uint8 (not uint16)
|
||||
// - No transport seat byte
|
||||
// - No interpolated movement (flags2 & 0x0200) write
|
||||
// - Pitch check uses TBC flag positions
|
||||
// ============================================================================
|
||||
void TbcPacketParsers::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
|
||||
// Movement flags (uint32, same as WotLK)
|
||||
packet.writeUInt32(info.flags);
|
||||
|
||||
// TBC: flags2 is uint8 (WotLK: uint16)
|
||||
packet.writeUInt8(static_cast<uint8_t>(info.flags2 & 0xFF));
|
||||
|
||||
// Timestamp
|
||||
packet.writeUInt32(info.time);
|
||||
|
||||
// Position
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.x), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.y), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.z), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
|
||||
|
||||
// Transport data (TBC ON_TRANSPORT = 0x200, same bit as WotLK)
|
||||
if (info.flags & TbcMoveFlags::ON_TRANSPORT) {
|
||||
// Packed transport GUID
|
||||
uint8_t transMask = 0;
|
||||
uint8_t transGuidBytes[8];
|
||||
int transGuidByteCount = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = static_cast<uint8_t>((info.transportGuid >> (i * 8)) & 0xFF);
|
||||
if (byte != 0) {
|
||||
transMask |= (1 << i);
|
||||
transGuidBytes[transGuidByteCount++] = byte;
|
||||
}
|
||||
}
|
||||
packet.writeUInt8(transMask);
|
||||
for (int i = 0; i < transGuidByteCount; i++) {
|
||||
packet.writeUInt8(transGuidBytes[i]);
|
||||
}
|
||||
|
||||
// Transport local position
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportX), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportY), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportZ), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportO), sizeof(float));
|
||||
|
||||
// Transport time
|
||||
packet.writeUInt32(info.transportTime);
|
||||
|
||||
// TBC: NO transport seat byte
|
||||
// TBC: NO interpolated movement time
|
||||
}
|
||||
|
||||
// Pitch: SWIMMING or else ONTRANSPORT (TBC flag positions)
|
||||
if (info.flags & TbcMoveFlags::SWIMMING) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
|
||||
} else if (info.flags & TbcMoveFlags::ONTRANSPORT) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
|
||||
}
|
||||
|
||||
// Fall time (always present)
|
||||
packet.writeUInt32(info.fallTime);
|
||||
|
||||
// Jump data (TBC JUMPING = 0x2000, WotLK FALLING = 0x1000)
|
||||
if (info.flags & TbcMoveFlags::JUMPING) {
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
|
||||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TBC buildMovementPacket
|
||||
// Classic/TBC: client movement packets do NOT include PackedGuid prefix
|
||||
// (WotLK added PackedGuid to client packets)
|
||||
// ============================================================================
|
||||
network::Packet TbcPacketParsers::buildMovementPacket(LogicalOpcode opcode,
|
||||
const MovementInfo& info,
|
||||
uint64_t /*playerGuid*/) {
|
||||
network::Packet packet(wireOpcode(opcode));
|
||||
|
||||
// TBC: NO PackedGuid prefix for client packets
|
||||
writeMovementPayload(packet, info);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TBC parseCharEnum
|
||||
// Differences from WotLK:
|
||||
// - After flags: uint8 firstLogin (not uint32 customization + uint8 unknown)
|
||||
// - Equipment: 20 items (not 23)
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
|
||||
uint8_t count = packet.readUInt8();
|
||||
|
||||
LOG_INFO("[TBC] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
|
||||
|
||||
response.characters.clear();
|
||||
response.characters.reserve(count);
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
Character character;
|
||||
|
||||
// GUID (8 bytes)
|
||||
character.guid = packet.readUInt64();
|
||||
|
||||
// Name (null-terminated string)
|
||||
character.name = packet.readString();
|
||||
|
||||
// Race, class, gender
|
||||
character.race = static_cast<Race>(packet.readUInt8());
|
||||
character.characterClass = static_cast<Class>(packet.readUInt8());
|
||||
character.gender = static_cast<Gender>(packet.readUInt8());
|
||||
|
||||
// Appearance (5 bytes: skin, face, hairStyle, hairColor packed + facialFeatures)
|
||||
character.appearanceBytes = packet.readUInt32();
|
||||
character.facialFeatures = packet.readUInt8();
|
||||
|
||||
// Level
|
||||
character.level = packet.readUInt8();
|
||||
|
||||
// Location
|
||||
character.zoneId = packet.readUInt32();
|
||||
character.mapId = packet.readUInt32();
|
||||
character.x = packet.readFloat();
|
||||
character.y = packet.readFloat();
|
||||
character.z = packet.readFloat();
|
||||
|
||||
// Guild ID
|
||||
character.guildId = packet.readUInt32();
|
||||
|
||||
// Flags
|
||||
character.flags = packet.readUInt32();
|
||||
|
||||
// TBC: uint8 firstLogin (WotLK: uint32 customization + uint8 unknown)
|
||||
/*uint8_t firstLogin =*/ packet.readUInt8();
|
||||
|
||||
// Pet data (always present)
|
||||
character.pet.displayModel = packet.readUInt32();
|
||||
character.pet.level = packet.readUInt32();
|
||||
character.pet.family = packet.readUInt32();
|
||||
|
||||
// Equipment (TBC: 20 items, WotLK: 23 items)
|
||||
character.equipment.reserve(20);
|
||||
for (int j = 0; j < 20; ++j) {
|
||||
EquipmentItem item;
|
||||
item.displayModel = packet.readUInt32();
|
||||
item.inventoryType = packet.readUInt8();
|
||||
item.enchantment = packet.readUInt32();
|
||||
character.equipment.push_back(item);
|
||||
}
|
||||
|
||||
LOG_INFO(" Character ", (int)(i + 1), ": ", character.name);
|
||||
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
|
||||
LOG_INFO(" ", getRaceName(character.race), " ",
|
||||
getClassName(character.characterClass), " (",
|
||||
getGenderName(character.gender), ")");
|
||||
LOG_INFO(" Level: ", (int)character.level);
|
||||
LOG_INFO(" Location: Zone ", character.zoneId, ", Map ", character.mapId);
|
||||
|
||||
response.characters.push_back(character);
|
||||
}
|
||||
|
||||
LOG_INFO("[TBC] Parsed ", response.characters.size(), " characters");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TBC parseUpdateObject
|
||||
// Key difference from WotLK: u8 has_transport byte after blockCount
|
||||
// (WotLK removed this field)
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||||
// Read block count
|
||||
data.blockCount = packet.readUInt32();
|
||||
|
||||
// TBC/Classic: has_transport byte (WotLK removed this)
|
||||
/*uint8_t hasTransport =*/ packet.readUInt8();
|
||||
|
||||
LOG_DEBUG("[TBC] SMSG_UPDATE_OBJECT: objectCount=", data.blockCount);
|
||||
|
||||
// Check for out-of-range objects first
|
||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||
uint8_t firstByte = packet.readUInt8();
|
||||
|
||||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||
uint32_t count = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||
data.outOfRangeGuids.push_back(guid);
|
||||
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
|
||||
}
|
||||
} else {
|
||||
packet.setReadPos(packet.getReadPos() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse update blocks
|
||||
data.blocks.reserve(data.blockCount);
|
||||
for (uint32_t i = 0; i < data.blockCount; ++i) {
|
||||
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
|
||||
UpdateBlock block;
|
||||
if (!UpdateObjectParser::parseUpdateBlock(packet, block)) {
|
||||
LOG_ERROR("Failed to parse update block ", i + 1);
|
||||
return false;
|
||||
}
|
||||
data.blocks.push_back(block);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TBC parseAuraUpdate - SMSG_AURA_UPDATE doesn't exist in TBC
|
||||
// TBC uses inline aura update fields + SMSG_INIT_EXTRA_AURA_INFO (0x3A3) /
|
||||
// SMSG_SET_EXTRA_AURA_INFO (0x3A4) instead
|
||||
// ============================================================================
|
||||
bool TbcPacketParsers::parseAuraUpdate(network::Packet& /*packet*/, AuraUpdateData& /*data*/, bool /*isAll*/) {
|
||||
LOG_WARNING("[TBC] parseAuraUpdate called but SMSG_AURA_UPDATE does not exist in TBC 2.4.3");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
156
src/game/update_field_table.cpp
Normal file
156
src/game/update_field_table.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#include "game/update_field_table.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
namespace game {
|
||||
|
||||
static const UpdateFieldTable* g_activeUpdateFieldTable = nullptr;
|
||||
|
||||
void setActiveUpdateFieldTable(const UpdateFieldTable* table) { g_activeUpdateFieldTable = table; }
|
||||
const UpdateFieldTable* getActiveUpdateFieldTable() { return g_activeUpdateFieldTable; }
|
||||
|
||||
struct UFNameEntry {
|
||||
const char* name;
|
||||
UF field;
|
||||
};
|
||||
|
||||
static const UFNameEntry kUFNames[] = {
|
||||
{"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY},
|
||||
{"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO},
|
||||
{"UNIT_FIELD_TARGET_HI", UF::UNIT_FIELD_TARGET_HI},
|
||||
{"UNIT_FIELD_HEALTH", UF::UNIT_FIELD_HEALTH},
|
||||
{"UNIT_FIELD_POWER1", UF::UNIT_FIELD_POWER1},
|
||||
{"UNIT_FIELD_MAXHEALTH", UF::UNIT_FIELD_MAXHEALTH},
|
||||
{"UNIT_FIELD_MAXPOWER1", UF::UNIT_FIELD_MAXPOWER1},
|
||||
{"UNIT_FIELD_LEVEL", UF::UNIT_FIELD_LEVEL},
|
||||
{"UNIT_FIELD_FACTIONTEMPLATE", UF::UNIT_FIELD_FACTIONTEMPLATE},
|
||||
{"UNIT_FIELD_FLAGS", UF::UNIT_FIELD_FLAGS},
|
||||
{"UNIT_FIELD_FLAGS_2", UF::UNIT_FIELD_FLAGS_2},
|
||||
{"UNIT_FIELD_DISPLAYID", UF::UNIT_FIELD_DISPLAYID},
|
||||
{"UNIT_FIELD_MOUNTDISPLAYID", UF::UNIT_FIELD_MOUNTDISPLAYID},
|
||||
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
|
||||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||
{"UNIT_END", UF::UNIT_END},
|
||||
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
|
||||
{"PLAYER_XP", UF::PLAYER_XP},
|
||||
{"PLAYER_NEXT_LEVEL_XP", UF::PLAYER_NEXT_LEVEL_XP},
|
||||
{"PLAYER_FIELD_COINAGE", UF::PLAYER_FIELD_COINAGE},
|
||||
{"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START},
|
||||
{"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD},
|
||||
{"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1},
|
||||
{"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START},
|
||||
{"PLAYER_EXPLORED_ZONES_START", UF::PLAYER_EXPLORED_ZONES_START},
|
||||
{"GAMEOBJECT_DISPLAYID", UF::GAMEOBJECT_DISPLAYID},
|
||||
{"ITEM_FIELD_STACK_COUNT", UF::ITEM_FIELD_STACK_COUNT},
|
||||
};
|
||||
|
||||
static constexpr size_t kUFNameCount = sizeof(kUFNames) / sizeof(kUFNames[0]);
|
||||
|
||||
void UpdateFieldTable::loadWotlkDefaults() {
|
||||
fieldMap_.clear();
|
||||
struct { UF field; uint16_t idx; } defaults[] = {
|
||||
{UF::OBJECT_FIELD_ENTRY, 3},
|
||||
{UF::UNIT_FIELD_TARGET_LO, 6},
|
||||
{UF::UNIT_FIELD_TARGET_HI, 7},
|
||||
{UF::UNIT_FIELD_HEALTH, 24},
|
||||
{UF::UNIT_FIELD_POWER1, 25},
|
||||
{UF::UNIT_FIELD_MAXHEALTH, 32},
|
||||
{UF::UNIT_FIELD_MAXPOWER1, 33},
|
||||
{UF::UNIT_FIELD_LEVEL, 54},
|
||||
{UF::UNIT_FIELD_FACTIONTEMPLATE, 55},
|
||||
{UF::UNIT_FIELD_FLAGS, 59},
|
||||
{UF::UNIT_FIELD_FLAGS_2, 60},
|
||||
{UF::UNIT_FIELD_DISPLAYID, 67},
|
||||
{UF::UNIT_FIELD_MOUNTDISPLAYID, 69},
|
||||
{UF::UNIT_NPC_FLAGS, 82},
|
||||
{UF::UNIT_DYNAMIC_FLAGS, 147},
|
||||
{UF::UNIT_END, 148},
|
||||
{UF::PLAYER_FLAGS, 150},
|
||||
{UF::PLAYER_XP, 634},
|
||||
{UF::PLAYER_NEXT_LEVEL_XP, 635},
|
||||
{UF::PLAYER_FIELD_COINAGE, 1170},
|
||||
{UF::PLAYER_QUEST_LOG_START, 158},
|
||||
{UF::PLAYER_FIELD_INV_SLOT_HEAD, 324},
|
||||
{UF::PLAYER_FIELD_PACK_SLOT_1, 370},
|
||||
{UF::PLAYER_SKILL_INFO_START, 636},
|
||||
{UF::PLAYER_EXPLORED_ZONES_START, 1041},
|
||||
{UF::GAMEOBJECT_DISPLAYID, 8},
|
||||
{UF::ITEM_FIELD_STACK_COUNT, 14},
|
||||
};
|
||||
for (auto& d : defaults) {
|
||||
fieldMap_[static_cast<uint16_t>(d.field)] = d.idx;
|
||||
}
|
||||
LOG_INFO("UpdateFieldTable: loaded ", fieldMap_.size(), " WotLK default fields");
|
||||
}
|
||||
|
||||
bool UpdateFieldTable::loadFromJson(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) {
|
||||
LOG_WARNING("UpdateFieldTable: cannot open ", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
fieldMap_.clear();
|
||||
size_t loaded = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < json.size()) {
|
||||
size_t keyStart = json.find('"', pos);
|
||||
if (keyStart == std::string::npos) break;
|
||||
size_t keyEnd = json.find('"', keyStart + 1);
|
||||
if (keyEnd == std::string::npos) break;
|
||||
std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1);
|
||||
|
||||
size_t colon = json.find(':', keyEnd);
|
||||
if (colon == std::string::npos) break;
|
||||
|
||||
size_t valStart = colon + 1;
|
||||
while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' ||
|
||||
json[valStart] == '\r' || json[valStart] == '\n'))
|
||||
++valStart;
|
||||
|
||||
size_t valEnd = json.find_first_of(",}\r\n", valStart);
|
||||
if (valEnd == std::string::npos) valEnd = json.size();
|
||||
std::string valStr = json.substr(valStart, valEnd - valStart);
|
||||
// Trim whitespace
|
||||
while (!valStr.empty() && (valStr.back() == ' ' || valStr.back() == '\t'))
|
||||
valStr.pop_back();
|
||||
|
||||
uint16_t idx = 0;
|
||||
try { idx = static_cast<uint16_t>(std::stoul(valStr)); } catch (...) {
|
||||
pos = valEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find matching UF enum
|
||||
for (size_t i = 0; i < kUFNameCount; ++i) {
|
||||
if (key == kUFNames[i].name) {
|
||||
fieldMap_[static_cast<uint16_t>(kUFNames[i].field)] = idx;
|
||||
++loaded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pos = valEnd + 1;
|
||||
}
|
||||
|
||||
LOG_INFO("UpdateFieldTable: loaded ", loaded, " fields from ", path);
|
||||
return loaded > 0;
|
||||
}
|
||||
|
||||
uint16_t UpdateFieldTable::index(UF field) const {
|
||||
auto it = fieldMap_.find(static_cast<uint16_t>(field));
|
||||
return (it != fieldMap_.end()) ? it->second : 0xFFFF;
|
||||
}
|
||||
|
||||
bool UpdateFieldTable::hasField(UF field) const {
|
||||
return fieldMap_.count(static_cast<uint16_t>(field)) > 0;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
@ -39,40 +39,33 @@ network::Packet AuthSessionPacket::build(uint32_t build,
|
|||
LOG_DEBUG(" Auth hash: ", authHash.size(), " bytes");
|
||||
|
||||
// Create packet (opcode will be added by WorldSocket)
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTH_SESSION));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_AUTH_SESSION));
|
||||
|
||||
// AzerothCore 3.3.5a expects this exact field order:
|
||||
// Build, LoginServerID, Account, LoginServerType, LocalChallenge,
|
||||
// RegionID, BattlegroupID, RealmID, DosResponse, Digest, AddonInfo
|
||||
bool isTbc = (build <= 8606); // TBC 2.4.3 = 8606, WotLK starts at 11159+
|
||||
|
||||
// Build number (uint32, little-endian)
|
||||
packet.writeUInt32(build);
|
||||
|
||||
// Login server ID (uint32, always 0)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
// Account name (null-terminated string)
|
||||
packet.writeString(upperAccount);
|
||||
|
||||
// Login server type (uint32, always 0)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
// LocalChallenge / Client seed (uint32, little-endian)
|
||||
packet.writeUInt32(clientSeed);
|
||||
|
||||
// Region ID (uint32)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
// Battlegroup ID (uint32)
|
||||
packet.writeUInt32(0);
|
||||
|
||||
// Realm ID (uint32)
|
||||
packet.writeUInt32(realmId);
|
||||
LOG_DEBUG(" Realm ID: ", realmId);
|
||||
|
||||
// DOS response (uint64) - required for 3.x
|
||||
packet.writeUInt32(0);
|
||||
packet.writeUInt32(0);
|
||||
if (isTbc) {
|
||||
// TBC 2.4.3 format (6 fields):
|
||||
// Build, ServerID, Account, ClientSeed, Digest, AddonInfo
|
||||
packet.writeUInt32(build);
|
||||
packet.writeUInt32(realmId); // server_id
|
||||
packet.writeString(upperAccount);
|
||||
packet.writeUInt32(clientSeed);
|
||||
} else {
|
||||
// WotLK 3.3.5a format (11 fields):
|
||||
// Build, LoginServerID, Account, LoginServerType, LocalChallenge,
|
||||
// RegionID, BattlegroupID, RealmID, DosResponse, Digest, AddonInfo
|
||||
packet.writeUInt32(build);
|
||||
packet.writeUInt32(0); // LoginServerID
|
||||
packet.writeString(upperAccount);
|
||||
packet.writeUInt32(0); // LoginServerType
|
||||
packet.writeUInt32(clientSeed);
|
||||
packet.writeUInt32(0); // RegionID
|
||||
packet.writeUInt32(0); // BattlegroupID
|
||||
packet.writeUInt32(realmId); // RealmID
|
||||
LOG_DEBUG(" Realm ID: ", realmId);
|
||||
packet.writeUInt32(0); // DOS response (uint64)
|
||||
packet.writeUInt32(0);
|
||||
}
|
||||
|
||||
// Authentication hash/digest (20 bytes)
|
||||
packet.writeBytes(authHash.data(), authHash.size());
|
||||
|
|
@ -160,25 +153,30 @@ std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
|
|||
}
|
||||
|
||||
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
|
||||
// SMSG_AUTH_CHALLENGE format (WoW 3.3.5a):
|
||||
// uint32 unknown1 (always 1?)
|
||||
// uint32 serverSeed
|
||||
// SMSG_AUTH_CHALLENGE format varies by expansion:
|
||||
// TBC 2.4.3: uint32 serverSeed (4 bytes)
|
||||
// WotLK 3.3.5a: uint32 one + uint32 serverSeed + seeds (40 bytes)
|
||||
|
||||
if (packet.getSize() < 8) {
|
||||
if (packet.getSize() < 4) {
|
||||
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
data.unknown1 = packet.readUInt32();
|
||||
data.serverSeed = packet.readUInt32();
|
||||
if (packet.getSize() < 8) {
|
||||
// TBC format: just the server seed (4 bytes)
|
||||
data.unknown1 = 0;
|
||||
data.serverSeed = packet.readUInt32();
|
||||
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (TBC format):");
|
||||
} else {
|
||||
// WotLK format: unknown1 + serverSeed + encryption seeds
|
||||
data.unknown1 = packet.readUInt32();
|
||||
data.serverSeed = packet.readUInt32();
|
||||
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (WotLK format):");
|
||||
LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
|
||||
}
|
||||
|
||||
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE:");
|
||||
LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
|
||||
LOG_INFO(" Server seed: 0x", std::hex, data.serverSeed, std::dec);
|
||||
|
||||
// Note: 3.3.5a has additional data after this (seed2, etc.)
|
||||
// but we only need the first seed for authentication
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +255,7 @@ const char* getAuthResultString(AuthResult result) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet CharCreatePacket::build(const CharCreateData& data) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_CREATE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_CREATE));
|
||||
|
||||
// Convert nonbinary gender to server-compatible value (servers only support male/female)
|
||||
Gender serverGender = toServerGender(data.gender);
|
||||
|
|
@ -305,7 +303,7 @@ bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponse
|
|||
|
||||
network::Packet CharEnumPacket::build() {
|
||||
// CMSG_CHAR_ENUM has no body - just the opcode
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_ENUM));
|
||||
|
||||
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
|
||||
|
||||
|
|
@ -399,7 +397,7 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response)
|
|||
}
|
||||
|
||||
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PLAYER_LOGIN));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_PLAYER_LOGIN));
|
||||
|
||||
// Write character GUID (8 bytes, little-endian)
|
||||
packet.writeUInt64(characterGuid);
|
||||
|
|
@ -491,7 +489,7 @@ bool MotdParser::parse(network::Packet& packet, MotdData& data) {
|
|||
}
|
||||
|
||||
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PING));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_PING));
|
||||
|
||||
// Write sequence number (uint32, little-endian)
|
||||
packet.writeUInt32(sequence);
|
||||
|
|
@ -618,7 +616,7 @@ void MovementPacket::writeMovementPayload(network::Packet& packet, const Movemen
|
|||
}
|
||||
|
||||
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(opcode));
|
||||
network::Packet packet(wireOpcode(opcode));
|
||||
|
||||
// Movement packet format (WoW 3.3.5a):
|
||||
// packed GUID + movement payload
|
||||
|
|
@ -634,7 +632,7 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u
|
|||
char b[4]; snprintf(b, sizeof(b), "%02x ", raw[i]);
|
||||
hex += b;
|
||||
}
|
||||
LOG_INFO("MOVEPKT opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec,
|
||||
LOG_INFO("MOVEPKT opcode=0x", std::hex, wireOpcode(opcode), std::dec,
|
||||
" guid=0x", std::hex, playerGuid, std::dec,
|
||||
" payload=", raw.size(), " bytes",
|
||||
" flags=0x", std::hex, info.flags, std::dec,
|
||||
|
|
@ -1096,7 +1094,7 @@ network::Packet MessageChatPacket::build(ChatType type,
|
|||
ChatLanguage language,
|
||||
const std::string& message,
|
||||
const std::string& target) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_MESSAGECHAT));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_MESSAGECHAT));
|
||||
|
||||
// Write chat type
|
||||
packet.writeUInt32(static_cast<uint32_t>(type));
|
||||
|
|
@ -1267,21 +1265,21 @@ const char* getChatTypeString(ChatType type) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_SELECTION));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SET_SELECTION));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_SET_SELECTION: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SetActiveMoverPacket::build(uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_ACTIVE_MOVER));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTIVE_MOVER));
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_SET_ACTIVE_MOVER: guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet InspectPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_INSPECT));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_INSPECT));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_INSPECT: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
|
|
@ -1292,7 +1290,7 @@ network::Packet InspectPacket::build(uint64_t targetGuid) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet QueryTimePacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUERY_TIME));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_TIME));
|
||||
LOG_DEBUG("Built CMSG_QUERY_TIME");
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -1305,7 +1303,7 @@ bool QueryTimeResponseParser::parse(network::Packet& packet, QueryTimeResponseDa
|
|||
}
|
||||
|
||||
network::Packet RequestPlayedTimePacket::build(bool sendToChat) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_REQUEST_PLAYED_TIME));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REQUEST_PLAYED_TIME));
|
||||
packet.writeUInt8(sendToChat ? 1 : 0);
|
||||
LOG_DEBUG("Built CMSG_REQUEST_PLAYED_TIME: sendToChat=", sendToChat);
|
||||
return packet;
|
||||
|
|
@ -1324,7 +1322,7 @@ network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel,
|
|||
const std::string& guildName,
|
||||
uint32_t raceMask, uint32_t classMask,
|
||||
uint32_t zones) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_WHO));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_WHO));
|
||||
packet.writeUInt32(minLevel);
|
||||
packet.writeUInt32(maxLevel);
|
||||
packet.writeString(playerName);
|
||||
|
|
@ -1341,7 +1339,7 @@ network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel,
|
|||
// ============================================================
|
||||
|
||||
network::Packet AddFriendPacket::build(const std::string& playerName, const std::string& note) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ADD_FRIEND));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_FRIEND));
|
||||
packet.writeString(playerName);
|
||||
packet.writeString(note);
|
||||
LOG_DEBUG("Built CMSG_ADD_FRIEND: player=", playerName);
|
||||
|
|
@ -1349,14 +1347,14 @@ network::Packet AddFriendPacket::build(const std::string& playerName, const std:
|
|||
}
|
||||
|
||||
network::Packet DelFriendPacket::build(uint64_t friendGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DEL_FRIEND));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_DEL_FRIEND));
|
||||
packet.writeUInt64(friendGuid);
|
||||
LOG_DEBUG("Built CMSG_DEL_FRIEND: guid=0x", std::hex, friendGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SetContactNotesPacket::build(uint64_t friendGuid, const std::string& note) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SET_CONTACT_NOTES));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SET_CONTACT_NOTES));
|
||||
packet.writeUInt64(friendGuid);
|
||||
packet.writeString(note);
|
||||
LOG_DEBUG("Built CMSG_SET_CONTACT_NOTES: guid=0x", std::hex, friendGuid, std::dec);
|
||||
|
|
@ -1375,14 +1373,14 @@ bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data)
|
|||
}
|
||||
|
||||
network::Packet AddIgnorePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ADD_IGNORE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ADD_IGNORE));
|
||||
packet.writeString(playerName);
|
||||
LOG_DEBUG("Built CMSG_ADD_IGNORE: player=", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DEL_IGNORE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_DEL_IGNORE));
|
||||
packet.writeUInt64(ignoreGuid);
|
||||
LOG_DEBUG("Built CMSG_DEL_IGNORE: guid=0x", std::hex, ignoreGuid, std::dec);
|
||||
return packet;
|
||||
|
|
@ -1393,13 +1391,13 @@ network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet LogoutRequestPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOGOUT_REQUEST));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_REQUEST));
|
||||
LOG_DEBUG("Built CMSG_LOGOUT_REQUEST");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet LogoutCancelPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOGOUT_CANCEL));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LOGOUT_CANCEL));
|
||||
LOG_DEBUG("Built CMSG_LOGOUT_CANCEL");
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -1416,7 +1414,7 @@ bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& da
|
|||
// ============================================================
|
||||
|
||||
network::Packet StandStateChangePacket::build(uint8_t state) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_STAND_STATE_CHANGE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_STAND_STATE_CHANGE));
|
||||
packet.writeUInt32(state);
|
||||
LOG_DEBUG("Built CMSG_STAND_STATE_CHANGE: state=", (int)state);
|
||||
return packet;
|
||||
|
|
@ -1427,14 +1425,14 @@ network::Packet StandStateChangePacket::build(uint8_t state) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet ShowingHelmPacket::build(bool show) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SHOWING_HELM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_HELM));
|
||||
packet.writeUInt8(show ? 1 : 0);
|
||||
LOG_DEBUG("Built CMSG_SHOWING_HELM: show=", show);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet ShowingCloakPacket::build(bool show) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SHOWING_CLOAK));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SHOWING_CLOAK));
|
||||
packet.writeUInt8(show ? 1 : 0);
|
||||
LOG_DEBUG("Built CMSG_SHOWING_CLOAK: show=", show);
|
||||
return packet;
|
||||
|
|
@ -1445,7 +1443,7 @@ network::Packet ShowingCloakPacket::build(bool show) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet TogglePvpPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_TOGGLE_PVP));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_TOGGLE_PVP));
|
||||
LOG_DEBUG("Built CMSG_TOGGLE_PVP");
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -1455,46 +1453,46 @@ network::Packet TogglePvpPacket::build() {
|
|||
// ============================================================
|
||||
|
||||
network::Packet GuildInfoPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_INFO));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INFO));
|
||||
LOG_DEBUG("Built CMSG_GUILD_INFO");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildRosterPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_GET_ROSTER));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_GET_ROSTER));
|
||||
LOG_DEBUG("Built CMSG_GUILD_GET_ROSTER");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildMotdPacket::build(const std::string& motd) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_MOTD));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_MOTD));
|
||||
packet.writeString(motd);
|
||||
LOG_DEBUG("Built CMSG_GUILD_MOTD: ", motd);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildPromotePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_PROMOTE_MEMBER));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_PROMOTE_MEMBER));
|
||||
packet.writeString(playerName);
|
||||
LOG_DEBUG("Built CMSG_GUILD_PROMOTE_MEMBER: ", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildDemotePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_DEMOTE_MEMBER));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DEMOTE_MEMBER));
|
||||
packet.writeString(playerName);
|
||||
LOG_DEBUG("Built CMSG_GUILD_DEMOTE_MEMBER: ", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildLeavePacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_LEAVE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_LEAVE));
|
||||
LOG_DEBUG("Built CMSG_GUILD_LEAVE");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GuildInvitePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GUILD_INVITE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_INVITE));
|
||||
packet.writeString(playerName);
|
||||
LOG_DEBUG("Built CMSG_GUILD_INVITE: ", playerName);
|
||||
return packet;
|
||||
|
|
@ -1505,13 +1503,13 @@ network::Packet GuildInvitePacket::build(const std::string& playerName) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet ReadyCheckPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RAID_READY_CHECK));
|
||||
network::Packet packet(wireOpcode(Opcode::MSG_RAID_READY_CHECK));
|
||||
LOG_DEBUG("Built MSG_RAID_READY_CHECK");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet ReadyCheckConfirmPacket::build(bool ready) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RAID_READY_CHECK_CONFIRM));
|
||||
network::Packet packet(wireOpcode(Opcode::MSG_RAID_READY_CHECK_CONFIRM));
|
||||
packet.writeUInt8(ready ? 1 : 0);
|
||||
LOG_DEBUG("Built MSG_RAID_READY_CHECK_CONFIRM: ready=", ready);
|
||||
return packet;
|
||||
|
|
@ -1522,7 +1520,7 @@ network::Packet ReadyCheckConfirmPacket::build(bool ready) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet DuelCancelPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DUEL_CANCELLED));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED));
|
||||
LOG_DEBUG("Built CMSG_DUEL_CANCELLED");
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -1532,20 +1530,20 @@ network::Packet DuelCancelPacket::build() {
|
|||
// ============================================================
|
||||
|
||||
network::Packet GroupUninvitePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_UNINVITE_GUID));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_UNINVITE_GUID));
|
||||
packet.writeString(playerName);
|
||||
LOG_DEBUG("Built CMSG_GROUP_UNINVITE_GUID for player: ", playerName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GroupDisbandPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DISBAND));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DISBAND));
|
||||
LOG_DEBUG("Built CMSG_GROUP_DISBAND");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RAID_TARGET_UPDATE));
|
||||
network::Packet packet(wireOpcode(Opcode::MSG_RAID_TARGET_UPDATE));
|
||||
packet.writeUInt8(targetIndex);
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", (uint32_t)targetIndex, ", guid: 0x", std::hex, targetGuid, std::dec);
|
||||
|
|
@ -1553,7 +1551,7 @@ network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targ
|
|||
}
|
||||
|
||||
network::Packet RequestRaidInfoPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_REQUEST_RAID_INFO));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REQUEST_RAID_INFO));
|
||||
LOG_DEBUG("Built CMSG_REQUEST_RAID_INFO");
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -1563,34 +1561,34 @@ network::Packet RequestRaidInfoPacket::build() {
|
|||
// ============================================================
|
||||
|
||||
network::Packet DuelProposedPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_DUEL_PROPOSED));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_PROPOSED));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_DUEL_PROPOSED for target: 0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_INITIATE_TRADE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_INITIATE_TRADE for target: 0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AttackSwingPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSWING));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ATTACKSWING));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_ATTACKSWING for target: 0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AttackStopPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ATTACKSTOP));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ATTACKSTOP));
|
||||
LOG_DEBUG("Built CMSG_ATTACKSTOP");
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet CancelCastPacket::build(uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_CAST));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_CAST));
|
||||
packet.writeUInt32(0); // cast count/sequence
|
||||
packet.writeUInt32(spellId);
|
||||
LOG_DEBUG("Built CMSG_CANCEL_CAST for spell: ", spellId);
|
||||
|
|
@ -1602,7 +1600,7 @@ network::Packet CancelCastPacket::build(uint32_t spellId) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet RandomRollPacket::build(uint32_t minRoll, uint32_t maxRoll) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_RANDOM_ROLL));
|
||||
network::Packet packet(wireOpcode(Opcode::MSG_RANDOM_ROLL));
|
||||
packet.writeUInt32(minRoll);
|
||||
packet.writeUInt32(maxRoll);
|
||||
LOG_DEBUG("Built MSG_RANDOM_ROLL: ", minRoll, "-", maxRoll);
|
||||
|
|
@ -1621,7 +1619,7 @@ bool RandomRollParser::parse(network::Packet& packet, RandomRollData& data) {
|
|||
}
|
||||
|
||||
network::Packet NameQueryPacket::build(uint64_t playerGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_NAME_QUERY));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_NAME_QUERY));
|
||||
packet.writeUInt64(playerGuid);
|
||||
LOG_DEBUG("Built CMSG_NAME_QUERY: guid=0x", std::hex, playerGuid, std::dec);
|
||||
return packet;
|
||||
|
|
@ -1650,7 +1648,7 @@ bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseDa
|
|||
}
|
||||
|
||||
network::Packet CreatureQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CREATURE_QUERY));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CREATURE_QUERY));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_CREATURE_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
|
|
@ -1691,7 +1689,7 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
|
|||
// ---- GameObject Query ----
|
||||
|
||||
network::Packet GameObjectQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GAMEOBJECT_QUERY));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJECT_QUERY));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_GAMEOBJECT_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
|
|
@ -1725,7 +1723,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue
|
|||
// ---- Item Query ----
|
||||
|
||||
network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ITEM_QUERY_SINGLE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ITEM_QUERY_SINGLE));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_ITEM_QUERY_SINGLE: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
|
|
@ -2116,7 +2114,7 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data
|
|||
}
|
||||
|
||||
network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CAST_SPELL));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CAST_SPELL));
|
||||
packet.writeUInt8(castCount);
|
||||
packet.writeUInt32(spellId);
|
||||
packet.writeUInt8(0x00); // castFlags = 0 for normal cast
|
||||
|
|
@ -2152,7 +2150,7 @@ network::Packet CastSpellPacket::build(uint32_t spellId, uint64_t targetGuid, ui
|
|||
}
|
||||
|
||||
network::Packet CancelAuraPacket::build(uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CANCEL_AURA));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_AURA));
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -2273,7 +2271,7 @@ bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data
|
|||
// ============================================================
|
||||
|
||||
network::Packet GroupInvitePacket::build(const std::string& playerName) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_INVITE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_INVITE));
|
||||
packet.writeString(playerName);
|
||||
packet.writeUInt32(0); // unused
|
||||
LOG_DEBUG("Built CMSG_GROUP_INVITE: ", playerName);
|
||||
|
|
@ -2288,13 +2286,13 @@ bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteRespon
|
|||
}
|
||||
|
||||
network::Packet GroupAcceptPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_ACCEPT));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_ACCEPT));
|
||||
packet.writeUInt32(0); // unused in 3.3.5a
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GroupDeclinePacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GROUP_DECLINE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GROUP_DECLINE));
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
@ -2365,20 +2363,20 @@ bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData
|
|||
// ============================================================
|
||||
|
||||
network::Packet LootPacket::build(uint64_t targetGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT));
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_LOOT: target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOSTORE_LOOT_ITEM));
|
||||
packet.writeUInt8(slotIndex);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_USE_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM));
|
||||
packet.writeUInt8(bagIndex);
|
||||
packet.writeUInt8(slotIndex);
|
||||
packet.writeUInt8(0); // cast count
|
||||
|
|
@ -2392,14 +2390,14 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64
|
|||
}
|
||||
|
||||
network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTOEQUIP_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_AUTOEQUIP_ITEM));
|
||||
packet.writeUInt8(srcBag);
|
||||
packet.writeUInt8(srcSlot);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SWAP_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_ITEM));
|
||||
packet.writeUInt8(dstBag);
|
||||
packet.writeUInt8(dstSlot);
|
||||
packet.writeUInt8(srcBag);
|
||||
|
|
@ -2408,19 +2406,19 @@ network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t s
|
|||
}
|
||||
|
||||
network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SWAP_INV_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM));
|
||||
packet.writeUInt8(srcSlot);
|
||||
packet.writeUInt8(dstSlot);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet LootMoneyPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_MONEY));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_MONEY));
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet LootReleasePacket::build(uint64_t lootGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LOOT_RELEASE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LOOT_RELEASE));
|
||||
packet.writeUInt64(lootGuid);
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -2453,19 +2451,19 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data)
|
|||
// ============================================================
|
||||
|
||||
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_HELLO));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_HELLO));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet QuestgiverHelloPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_HELLO));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_HELLO));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(menuId);
|
||||
packet.writeUInt32(optionId);
|
||||
|
|
@ -2476,7 +2474,7 @@ network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuI
|
|||
}
|
||||
|
||||
network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
packet.writeUInt8(1); // isDialogContinued = 1 (from gossip)
|
||||
|
|
@ -2484,7 +2482,7 @@ network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t que
|
|||
}
|
||||
|
||||
network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
packet.writeUInt32(0); // unused
|
||||
|
|
@ -2584,7 +2582,7 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data
|
|||
// ============================================================
|
||||
|
||||
network::Packet BinderActivatePacket::build(uint64_t npcGuid) {
|
||||
network::Packet pkt(static_cast<uint16_t>(Opcode::CMSG_BINDER_ACTIVATE));
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_BINDER_ACTIVATE));
|
||||
pkt.writeUInt64(npcGuid);
|
||||
return pkt;
|
||||
}
|
||||
|
|
@ -2703,14 +2701,14 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
|
|||
}
|
||||
|
||||
network::Packet QuestgiverCompleteQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_COMPLETE_QUEST));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_COMPLETE_QUEST));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t questId, uint32_t rewardIndex) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
packet.writeUInt32(rewardIndex);
|
||||
|
|
@ -2722,13 +2720,13 @@ network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t q
|
|||
// ============================================================
|
||||
|
||||
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LIST_INVENTORY));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LIST_INVENTORY));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_BUY_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_BUY_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt32(itemId);
|
||||
packet.writeUInt32(slot);
|
||||
|
|
@ -2738,7 +2736,7 @@ network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint3
|
|||
}
|
||||
|
||||
network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SELL_ITEM));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SELL_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt32(count);
|
||||
|
|
@ -2812,7 +2810,7 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) {
|
|||
}
|
||||
|
||||
network::Packet TrainerBuySpellPacket::build(uint64_t trainerGuid, uint32_t spellId) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_TRAINER_BUY_SPELL));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_TRAINER_BUY_SPELL));
|
||||
packet.writeUInt64(trainerGuid);
|
||||
packet.writeUInt32(spellId);
|
||||
return packet;
|
||||
|
|
@ -2919,14 +2917,14 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) {
|
|||
}
|
||||
|
||||
network::Packet LearnTalentPacket::build(uint32_t talentId, uint32_t requestedRank) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_LEARN_TALENT));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LEARN_TALENT));
|
||||
packet.writeUInt32(talentId);
|
||||
packet.writeUInt32(requestedRank);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet TalentWipeConfirmPacket::build(bool accept) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::MSG_TALENT_WIPE_CONFIRM));
|
||||
network::Packet packet(wireOpcode(Opcode::MSG_TALENT_WIPE_CONFIRM));
|
||||
packet.writeUInt32(accept ? 1 : 0);
|
||||
return packet;
|
||||
}
|
||||
|
|
@ -2936,19 +2934,19 @@ network::Packet TalentWipeConfirmPacket::build(bool accept) {
|
|||
// ============================================================
|
||||
|
||||
network::Packet RepopRequestPacket::build() {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_REPOP_REQUEST));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REPOP_REQUEST));
|
||||
packet.writeUInt8(1); // request release (1 = manual)
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE));
|
||||
packet.writeUInt64(npcGuid);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet ResurrectResponsePacket::build(uint64_t casterGuid, bool accept) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_RESURRECT_RESPONSE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_RESURRECT_RESPONSE));
|
||||
packet.writeUInt64(casterGuid);
|
||||
packet.writeUInt8(accept ? 1 : 0);
|
||||
return packet;
|
||||
|
|
@ -2989,7 +2987,7 @@ bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyDa
|
|||
}
|
||||
|
||||
network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t totalCost, const std::vector<uint32_t>& pathNodes) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ACTIVATETAXIEXPRESS));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXIEXPRESS));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(totalCost);
|
||||
packet.writeUInt32(static_cast<uint32_t>(pathNodes.size()));
|
||||
|
|
@ -3002,7 +3000,7 @@ network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, uint32_t tota
|
|||
}
|
||||
|
||||
network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_ACTIVATETAXI));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_ACTIVATETAXI));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(srcNode);
|
||||
packet.writeUInt32(destNode);
|
||||
|
|
@ -3010,7 +3008,7 @@ network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, ui
|
|||
}
|
||||
|
||||
network::Packet GameObjectUsePacket::build(uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GAMEOBJECT_USE));
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_GAMEOBJECT_USE));
|
||||
packet.writeUInt64(guid);
|
||||
return packet;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,9 +103,77 @@ void AssetManager::shutdown() {
|
|||
}
|
||||
|
||||
clearCache();
|
||||
overlayLayers_.clear();
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
bool AssetManager::addOverlayManifest(const std::string& manifestPath, int priority, const std::string& id) {
|
||||
// Check for duplicate
|
||||
for (const auto& layer : overlayLayers_) {
|
||||
if (layer.id == id) {
|
||||
LOG_WARNING("Overlay '", id, "' already loaded, skipping");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ManifestLayer layer;
|
||||
layer.priority = priority;
|
||||
layer.id = id;
|
||||
|
||||
if (!layer.manifest.load(manifestPath)) {
|
||||
LOG_ERROR("Failed to load overlay manifest: ", manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
overlayLayers_.push_back(std::move(layer));
|
||||
|
||||
// Sort by priority descending (highest priority first)
|
||||
std::sort(overlayLayers_.begin(), overlayLayers_.end(),
|
||||
[](const ManifestLayer& a, const ManifestLayer& b) {
|
||||
return a.priority > b.priority;
|
||||
});
|
||||
|
||||
LOG_INFO("Added overlay '", id, "' (priority ", priority, ", ",
|
||||
overlayLayers_.back().manifest.getEntryCount(), " files) from ", manifestPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AssetManager::removeOverlay(const std::string& id) {
|
||||
auto it = std::remove_if(overlayLayers_.begin(), overlayLayers_.end(),
|
||||
[&id](const ManifestLayer& layer) { return layer.id == id; });
|
||||
if (it != overlayLayers_.end()) {
|
||||
overlayLayers_.erase(it, overlayLayers_.end());
|
||||
// Clear file cache since overlay removal changes file resolution
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cacheMutex);
|
||||
fileCache.clear();
|
||||
fileCacheTotalBytes = 0;
|
||||
}
|
||||
LOG_INFO("Removed overlay '", id, "', file cache cleared");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> AssetManager::getOverlayIds() const {
|
||||
std::vector<std::string> ids;
|
||||
ids.reserve(overlayLayers_.size());
|
||||
for (const auto& layer : overlayLayers_) {
|
||||
ids.push_back(layer.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::string AssetManager::resolveLayeredPath(const std::string& normalizedPath) const {
|
||||
// Check overlay manifests first (sorted by priority desc)
|
||||
for (const auto& layer : overlayLayers_) {
|
||||
std::string fsPath = layer.manifest.resolveFilesystemPath(normalizedPath);
|
||||
if (!fsPath.empty()) {
|
||||
return fsPath;
|
||||
}
|
||||
}
|
||||
// Fall back to base manifest
|
||||
return manifest_.resolveFilesystemPath(normalizedPath);
|
||||
}
|
||||
|
||||
BLPImage AssetManager::loadTexture(const std::string& path) {
|
||||
if (!initialized) {
|
||||
LOG_ERROR("AssetManager not initialized");
|
||||
|
|
@ -144,7 +212,7 @@ BLPImage AssetManager::tryLoadPngOverride(const std::string& normalizedPath) con
|
|||
std::string ext = normalizedPath.substr(normalizedPath.size() - 4);
|
||||
if (ext != ".blp") return BLPImage();
|
||||
|
||||
std::string fsPath = manifest_.resolveFilesystemPath(normalizedPath);
|
||||
std::string fsPath = resolveLayeredPath(normalizedPath);
|
||||
if (fsPath.empty()) return BLPImage();
|
||||
|
||||
// Replace .blp/.BLP extension with .png
|
||||
|
|
@ -219,7 +287,14 @@ bool AssetManager::fileExists(const std::string& path) const {
|
|||
if (!initialized) {
|
||||
return false;
|
||||
}
|
||||
return manifest_.hasEntry(normalizePath(path));
|
||||
std::string normalized = normalizePath(path);
|
||||
// Check overlay manifests first
|
||||
for (const auto& layer : overlayLayers_) {
|
||||
if (layer.manifest.hasEntry(normalized)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return manifest_.hasEntry(normalized);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
|
||||
|
|
@ -240,8 +315,8 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
|
|||
}
|
||||
}
|
||||
|
||||
// Read from filesystem (fully parallel, no serialization needed)
|
||||
std::string fsPath = manifest_.resolveFilesystemPath(normalized);
|
||||
// Read from filesystem using layered resolution (overlays first, then base)
|
||||
std::string fsPath = resolveLayeredPath(normalized);
|
||||
if (fsPath.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
226
src/pipeline/dbc_layout.cpp
Normal file
226
src/pipeline/dbc_layout.cpp
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
static const DBCLayout* g_activeDBCLayout = nullptr;
|
||||
|
||||
void setActiveDBCLayout(const DBCLayout* layout) { g_activeDBCLayout = layout; }
|
||||
const DBCLayout* getActiveDBCLayout() { return g_activeDBCLayout; }
|
||||
|
||||
void DBCLayout::loadWotlkDefaults() {
|
||||
layouts_.clear();
|
||||
|
||||
// Spell.dbc
|
||||
layouts_["Spell"] = {{{ "ID", 0 }, { "Attributes", 4 }, { "IconID", 133 },
|
||||
{ "Name", 136 }, { "Tooltip", 139 }, { "Rank", 153 }}};
|
||||
|
||||
// ItemDisplayInfo.dbc
|
||||
layouts_["ItemDisplayInfo"] = {{{ "ID", 0 }, { "LeftModel", 1 }, { "LeftModelTexture", 3 },
|
||||
{ "InventoryIcon", 5 }, { "GeosetGroup1", 7 }, { "GeosetGroup3", 9 }}};
|
||||
|
||||
// CharSections.dbc
|
||||
layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 },
|
||||
{ "Texture1", 4 }, { "Texture2", 5 }, { "Texture3", 6 },
|
||||
{ "VariationIndex", 8 }, { "ColorIndex", 9 }}};
|
||||
|
||||
// SpellIcon.dbc (Icon.dbc in code but actually SpellIcon)
|
||||
layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}};
|
||||
|
||||
// FactionTemplate.dbc
|
||||
layouts_["FactionTemplate"] = {{{ "ID", 0 }, { "Faction", 1 }, { "FactionGroup", 3 },
|
||||
{ "FriendGroup", 4 }, { "EnemyGroup", 5 },
|
||||
{ "Enemy0", 6 }, { "Enemy1", 7 }, { "Enemy2", 8 }, { "Enemy3", 9 }}};
|
||||
|
||||
// Faction.dbc
|
||||
layouts_["Faction"] = {{{ "ID", 0 }, { "ReputationRaceMask0", 2 }, { "ReputationRaceMask1", 3 },
|
||||
{ "ReputationRaceMask2", 4 }, { "ReputationRaceMask3", 5 },
|
||||
{ "ReputationBase0", 10 }, { "ReputationBase1", 11 },
|
||||
{ "ReputationBase2", 12 }, { "ReputationBase3", 13 }}};
|
||||
|
||||
// AreaTable.dbc
|
||||
layouts_["AreaTable"] = {{{ "ID", 0 }, { "ExploreFlag", 3 }}};
|
||||
|
||||
// CreatureDisplayInfoExtra.dbc
|
||||
layouts_["CreatureDisplayInfoExtra"] = {{{ "ID", 0 }, { "RaceID", 1 }, { "SexID", 2 },
|
||||
{ "SkinID", 3 }, { "FaceID", 4 }, { "HairStyleID", 5 }, { "HairColorID", 6 },
|
||||
{ "FacialHairID", 7 }, { "EquipDisplay0", 8 }, { "EquipDisplay1", 9 },
|
||||
{ "EquipDisplay2", 10 }, { "EquipDisplay3", 11 }, { "EquipDisplay4", 12 },
|
||||
{ "EquipDisplay5", 13 }, { "EquipDisplay6", 14 }, { "EquipDisplay7", 15 },
|
||||
{ "EquipDisplay8", 16 }, { "EquipDisplay9", 17 }, { "EquipDisplay10", 18 },
|
||||
{ "BakeName", 20 }}};
|
||||
|
||||
// CreatureDisplayInfo.dbc
|
||||
layouts_["CreatureDisplayInfo"] = {{{ "ID", 0 }, { "ModelID", 1 }, { "ExtraDisplayId", 3 },
|
||||
{ "Skin1", 6 }, { "Skin2", 7 }, { "Skin3", 8 }}};
|
||||
|
||||
// TaxiNodes.dbc
|
||||
layouts_["TaxiNodes"] = {{{ "ID", 0 }, { "MapID", 1 }, { "X", 2 }, { "Y", 3 }, { "Z", 4 },
|
||||
{ "Name", 5 }, { "MountDisplayIdAllianceFallback", 20 },
|
||||
{ "MountDisplayIdHordeFallback", 21 },
|
||||
{ "MountDisplayIdAlliance", 22 }, { "MountDisplayIdHorde", 23 }}};
|
||||
|
||||
// TaxiPath.dbc
|
||||
layouts_["TaxiPath"] = {{{ "ID", 0 }, { "FromNode", 1 }, { "ToNode", 2 }, { "Cost", 3 }}};
|
||||
|
||||
// TaxiPathNode.dbc
|
||||
layouts_["TaxiPathNode"] = {{{ "ID", 0 }, { "PathID", 1 }, { "NodeIndex", 2 },
|
||||
{ "MapID", 3 }, { "X", 4 }, { "Y", 5 }, { "Z", 6 }}};
|
||||
|
||||
// TalentTab.dbc
|
||||
layouts_["TalentTab"] = {{{ "ID", 0 }, { "Name", 1 }, { "ClassMask", 20 },
|
||||
{ "OrderIndex", 22 }, { "BackgroundFile", 23 }}};
|
||||
|
||||
// Talent.dbc
|
||||
layouts_["Talent"] = {{{ "ID", 0 }, { "TabID", 1 }, { "Row", 2 }, { "Column", 3 },
|
||||
{ "RankSpell0", 4 }, { "PrereqTalent0", 9 }, { "PrereqRank0", 12 }}};
|
||||
|
||||
// SkillLineAbility.dbc
|
||||
layouts_["SkillLineAbility"] = {{{ "SkillLineID", 1 }, { "SpellID", 2 }}};
|
||||
|
||||
// SkillLine.dbc
|
||||
layouts_["SkillLine"] = {{{ "ID", 0 }, { "Category", 1 }, { "Name", 3 }}};
|
||||
|
||||
// Map.dbc
|
||||
layouts_["Map"] = {{{ "ID", 0 }, { "InternalName", 1 }}};
|
||||
|
||||
// CreatureModelData.dbc
|
||||
layouts_["CreatureModelData"] = {{{ "ID", 0 }, { "ModelPath", 2 }}};
|
||||
|
||||
// CharHairGeosets.dbc
|
||||
layouts_["CharHairGeosets"] = {{{ "RaceID", 1 }, { "SexID", 2 },
|
||||
{ "Variation", 3 }, { "GeosetID", 4 }}};
|
||||
|
||||
// CharacterFacialHairStyles.dbc
|
||||
layouts_["CharacterFacialHairStyles"] = {{{ "RaceID", 0 }, { "SexID", 1 },
|
||||
{ "Variation", 2 }, { "Geoset100", 3 }, { "Geoset300", 4 }, { "Geoset200", 5 }}};
|
||||
|
||||
// GameObjectDisplayInfo.dbc
|
||||
layouts_["GameObjectDisplayInfo"] = {{{ "ID", 0 }, { "ModelName", 1 }}};
|
||||
|
||||
// Emotes.dbc
|
||||
layouts_["Emotes"] = {{{ "ID", 0 }, { "AnimID", 2 }}};
|
||||
|
||||
// EmotesText.dbc
|
||||
layouts_["EmotesText"] = {{{ "Command", 1 }, { "EmoteRef", 2 },
|
||||
{ "SenderTargetTextID", 5 }, { "SenderNoTargetTextID", 9 }}};
|
||||
|
||||
// EmotesTextData.dbc
|
||||
layouts_["EmotesTextData"] = {{{ "ID", 0 }, { "Text", 1 }}};
|
||||
|
||||
// Light.dbc
|
||||
layouts_["Light"] = {{{ "ID", 0 }, { "MapID", 1 }, { "X", 2 }, { "Z", 3 }, { "Y", 4 },
|
||||
{ "InnerRadius", 5 }, { "OuterRadius", 6 }, { "LightParamsID", 7 },
|
||||
{ "LightParamsIDRain", 8 }, { "LightParamsIDUnderwater", 9 }}};
|
||||
|
||||
// LightParams.dbc
|
||||
layouts_["LightParams"] = {{{ "LightParamsID", 0 }}};
|
||||
|
||||
// LightParamsBands.dbc (custom split from LightIntBand/LightFloatBand)
|
||||
layouts_["LightParamsBands"] = {{{ "BlockIndex", 1 }, { "NumKeyframes", 2 },
|
||||
{ "TimeKey0", 3 }, { "Value0", 19 }}};
|
||||
|
||||
// LightIntBand.dbc (same structure as LightParamsBands)
|
||||
layouts_["LightIntBand"] = {{{ "BlockIndex", 1 }, { "NumKeyframes", 2 },
|
||||
{ "TimeKey0", 3 }, { "Value0", 19 }}};
|
||||
|
||||
// LightFloatBand.dbc
|
||||
layouts_["LightFloatBand"] = {{{ "BlockIndex", 1 }, { "NumKeyframes", 2 },
|
||||
{ "TimeKey0", 3 }, { "Value0", 19 }}};
|
||||
|
||||
// WorldMapArea.dbc
|
||||
layouts_["WorldMapArea"] = {{{ "ID", 0 }, { "MapID", 1 }, { "AreaID", 2 },
|
||||
{ "AreaName", 3 }, { "LocLeft", 4 }, { "LocRight", 5 }, { "LocTop", 6 },
|
||||
{ "LocBottom", 7 }, { "DisplayMapID", 8 }, { "ParentWorldMapID", 10 }}};
|
||||
|
||||
LOG_INFO("DBCLayout: loaded ", layouts_.size(), " WotLK default layouts");
|
||||
}
|
||||
|
||||
bool DBCLayout::loadFromJson(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) {
|
||||
LOG_WARNING("DBCLayout: cannot open ", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
layouts_.clear();
|
||||
size_t loaded = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
// Parse top-level object: { "DbcName": { "FieldName": index, ... }, ... }
|
||||
// Find the first '{'
|
||||
pos = json.find('{', pos);
|
||||
if (pos == std::string::npos) return false;
|
||||
++pos;
|
||||
|
||||
while (pos < json.size()) {
|
||||
// Find DBC name key
|
||||
size_t dbcKeyStart = json.find('"', pos);
|
||||
if (dbcKeyStart == std::string::npos) break;
|
||||
size_t dbcKeyEnd = json.find('"', dbcKeyStart + 1);
|
||||
if (dbcKeyEnd == std::string::npos) break;
|
||||
std::string dbcName = json.substr(dbcKeyStart + 1, dbcKeyEnd - dbcKeyStart - 1);
|
||||
|
||||
// Find the nested object '{'
|
||||
size_t objStart = json.find('{', dbcKeyEnd);
|
||||
if (objStart == std::string::npos) break;
|
||||
|
||||
// Find the matching '}'
|
||||
size_t objEnd = json.find('}', objStart);
|
||||
if (objEnd == std::string::npos) break;
|
||||
|
||||
// Parse the inner object
|
||||
std::string inner = json.substr(objStart + 1, objEnd - objStart - 1);
|
||||
DBCFieldMap fieldMap;
|
||||
size_t ipos = 0;
|
||||
while (ipos < inner.size()) {
|
||||
size_t fkStart = inner.find('"', ipos);
|
||||
if (fkStart == std::string::npos) break;
|
||||
size_t fkEnd = inner.find('"', fkStart + 1);
|
||||
if (fkEnd == std::string::npos) break;
|
||||
std::string fieldName = inner.substr(fkStart + 1, fkEnd - fkStart - 1);
|
||||
|
||||
size_t colon = inner.find(':', fkEnd);
|
||||
if (colon == std::string::npos) break;
|
||||
size_t valStart = colon + 1;
|
||||
while (valStart < inner.size() && (inner[valStart] == ' ' || inner[valStart] == '\t' ||
|
||||
inner[valStart] == '\r' || inner[valStart] == '\n'))
|
||||
++valStart;
|
||||
size_t valEnd = inner.find_first_of(",}\r\n", valStart);
|
||||
if (valEnd == std::string::npos) valEnd = inner.size();
|
||||
std::string valStr = inner.substr(valStart, valEnd - valStart);
|
||||
while (!valStr.empty() && (valStr.back() == ' ' || valStr.back() == '\t'))
|
||||
valStr.pop_back();
|
||||
|
||||
try {
|
||||
uint32_t idx = static_cast<uint32_t>(std::stoul(valStr));
|
||||
fieldMap.fields[fieldName] = idx;
|
||||
} catch (...) {}
|
||||
|
||||
ipos = valEnd + 1;
|
||||
}
|
||||
|
||||
if (!fieldMap.fields.empty()) {
|
||||
layouts_[dbcName] = std::move(fieldMap);
|
||||
++loaded;
|
||||
}
|
||||
|
||||
pos = objEnd + 1;
|
||||
}
|
||||
|
||||
LOG_INFO("DBCLayout: loaded ", loaded, " layouts from ", path);
|
||||
return loaded > 0;
|
||||
}
|
||||
|
||||
const DBCFieldMap* DBCLayout::getLayout(const std::string& dbcName) const {
|
||||
auto it = layouts_.find(dbcName);
|
||||
return (it != layouts_.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
204
src/pipeline/hd_pack_manager.cpp
Normal file
204
src/pipeline/hd_pack_manager.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
#include "pipeline/hd_pack_manager.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
// Minimal JSON string value parser (key must be unique in the flat object)
|
||||
std::string jsonStringValue(const std::string& json, const std::string& key) {
|
||||
std::string needle = "\"" + key + "\"";
|
||||
size_t pos = json.find(needle);
|
||||
if (pos == std::string::npos) return "";
|
||||
pos = json.find(':', pos + needle.size());
|
||||
if (pos == std::string::npos) return "";
|
||||
pos = json.find('"', pos + 1);
|
||||
if (pos == std::string::npos) return "";
|
||||
size_t end = json.find('"', pos + 1);
|
||||
if (end == std::string::npos) return "";
|
||||
return json.substr(pos + 1, end - pos - 1);
|
||||
}
|
||||
|
||||
// Parse a JSON number value
|
||||
uint32_t jsonUintValue(const std::string& json, const std::string& key) {
|
||||
std::string needle = "\"" + key + "\"";
|
||||
size_t pos = json.find(needle);
|
||||
if (pos == std::string::npos) return 0;
|
||||
pos = json.find(':', pos + needle.size());
|
||||
if (pos == std::string::npos) return 0;
|
||||
++pos;
|
||||
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) ++pos;
|
||||
return static_cast<uint32_t>(std::strtoul(json.c_str() + pos, nullptr, 10));
|
||||
}
|
||||
|
||||
// Parse a JSON string array value
|
||||
std::vector<std::string> jsonStringArray(const std::string& json, const std::string& key) {
|
||||
std::vector<std::string> result;
|
||||
std::string needle = "\"" + key + "\"";
|
||||
size_t pos = json.find(needle);
|
||||
if (pos == std::string::npos) return result;
|
||||
pos = json.find('[', pos + needle.size());
|
||||
if (pos == std::string::npos) return result;
|
||||
size_t end = json.find(']', pos);
|
||||
if (end == std::string::npos) return result;
|
||||
std::string arr = json.substr(pos + 1, end - pos - 1);
|
||||
size_t p = 0;
|
||||
while (p < arr.size()) {
|
||||
size_t qs = arr.find('"', p);
|
||||
if (qs == std::string::npos) break;
|
||||
size_t qe = arr.find('"', qs + 1);
|
||||
if (qe == std::string::npos) break;
|
||||
result.push_back(arr.substr(qs + 1, qe - qs - 1));
|
||||
p = qe + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void HDPackManager::initialize(const std::string& hdRootPath) {
|
||||
packs_.clear();
|
||||
|
||||
if (!std::filesystem::exists(hdRootPath) || !std::filesystem::is_directory(hdRootPath)) {
|
||||
LOG_DEBUG("HD pack directory not found: ", hdRootPath);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(hdRootPath)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
|
||||
std::string packJsonPath = entry.path().string() + "/pack.json";
|
||||
if (!std::filesystem::exists(packJsonPath)) continue;
|
||||
|
||||
std::ifstream f(packJsonPath);
|
||||
if (!f.is_open()) continue;
|
||||
|
||||
std::string json((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
|
||||
HDPack pack;
|
||||
pack.id = jsonStringValue(json, "id");
|
||||
pack.name = jsonStringValue(json, "name");
|
||||
pack.group = jsonStringValue(json, "group");
|
||||
pack.totalSizeMB = jsonUintValue(json, "totalSizeMB");
|
||||
pack.expansions = jsonStringArray(json, "expansions");
|
||||
pack.packDir = entry.path().string();
|
||||
pack.manifestPath = entry.path().string() + "/manifest.json";
|
||||
|
||||
if (pack.id.empty()) {
|
||||
LOG_WARNING("HD pack in ", entry.path().string(), " has no id, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(pack.manifestPath)) {
|
||||
LOG_WARNING("HD pack '", pack.id, "' missing manifest.json, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply saved enabled state if available
|
||||
auto it = enabledState_.find(pack.id);
|
||||
if (it != enabledState_.end()) {
|
||||
pack.enabled = it->second;
|
||||
}
|
||||
|
||||
LOG_INFO("Discovered HD pack: '", pack.id, "' (", pack.name, ") ",
|
||||
pack.totalSizeMB, " MB, ", pack.expansions.size(), " expansions");
|
||||
packs_.push_back(std::move(pack));
|
||||
}
|
||||
|
||||
LOG_INFO("HDPackManager: found ", packs_.size(), " packs in ", hdRootPath);
|
||||
}
|
||||
|
||||
std::vector<const HDPack*> HDPackManager::getPacksForExpansion(const std::string& expansionId) const {
|
||||
std::vector<const HDPack*> result;
|
||||
for (const auto& pack : packs_) {
|
||||
if (pack.expansions.empty()) {
|
||||
// No expansion filter = compatible with all
|
||||
result.push_back(&pack);
|
||||
} else {
|
||||
for (const auto& exp : pack.expansions) {
|
||||
if (exp == expansionId) {
|
||||
result.push_back(&pack);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void HDPackManager::setPackEnabled(const std::string& packId, bool enabled) {
|
||||
enabledState_[packId] = enabled;
|
||||
for (auto& pack : packs_) {
|
||||
if (pack.id == packId) {
|
||||
pack.enabled = enabled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool HDPackManager::isPackEnabled(const std::string& packId) const {
|
||||
auto it = enabledState_.find(packId);
|
||||
return it != enabledState_.end() && it->second;
|
||||
}
|
||||
|
||||
void HDPackManager::applyToAssetManager(AssetManager* assetManager, const std::string& expansionId) {
|
||||
if (!assetManager) return;
|
||||
|
||||
// Remove previously applied overlays
|
||||
for (const auto& overlayId : appliedOverlayIds_) {
|
||||
assetManager->removeOverlay(overlayId);
|
||||
}
|
||||
appliedOverlayIds_.clear();
|
||||
|
||||
// Get packs compatible with current expansion
|
||||
auto compatiblePacks = getPacksForExpansion(expansionId);
|
||||
int priorityOffset = 0;
|
||||
|
||||
for (const auto* pack : compatiblePacks) {
|
||||
if (!pack->enabled) continue;
|
||||
|
||||
std::string overlayId = "hd_" + pack->id;
|
||||
int priority = HD_OVERLAY_PRIORITY_BASE + priorityOffset;
|
||||
|
||||
if (assetManager->addOverlayManifest(pack->manifestPath, priority, overlayId)) {
|
||||
appliedOverlayIds_.push_back(overlayId);
|
||||
LOG_INFO("Applied HD pack '", pack->id, "' as overlay (priority ", priority, ")");
|
||||
}
|
||||
++priorityOffset;
|
||||
}
|
||||
|
||||
if (!appliedOverlayIds_.empty()) {
|
||||
LOG_INFO("Applied ", appliedOverlayIds_.size(), " HD pack overlays");
|
||||
}
|
||||
}
|
||||
|
||||
void HDPackManager::saveSettings(const std::string& settingsPath) const {
|
||||
std::ofstream f(settingsPath, std::ios::app);
|
||||
if (!f.is_open()) return;
|
||||
|
||||
for (const auto& [packId, enabled] : enabledState_) {
|
||||
f << "hd_pack_" << packId << "=" << (enabled ? "1" : "0") << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void HDPackManager::loadSettings(const std::string& settingsPath) {
|
||||
std::ifstream f(settingsPath);
|
||||
if (!f.is_open()) return;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
if (line.substr(0, 8) != "hd_pack_") continue;
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string packId = line.substr(8, eq - 8);
|
||||
bool enabled = (line.substr(eq + 1) == "1");
|
||||
enabledState_[packId] = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
|
@ -164,19 +165,21 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
bool foundHair = false;
|
||||
bool foundUnderwear = false;
|
||||
|
||||
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
|
||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
|
||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
|
||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
||||
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
|
||||
// Section 0: Body skin (variation=0, colorIndex = skin color)
|
||||
if (baseSection == 0 && !foundSkin &&
|
||||
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
|
||||
if (!tex1.empty()) {
|
||||
bodySkinPath_ = tex1;
|
||||
foundSkin = true;
|
||||
|
|
@ -186,8 +189,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
else if (baseSection == 1 && !foundFace &&
|
||||
variationIndex == static_cast<uint32_t>(face) &&
|
||||
colorIndex == static_cast<uint32_t>(skin)) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
std::string tex2 = charSectionsDbc->getString(r, 5);
|
||||
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
|
||||
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5);
|
||||
if (!tex1.empty()) faceLowerPath = tex1;
|
||||
if (!tex2.empty()) faceUpperPath = tex2;
|
||||
foundFace = true;
|
||||
|
|
@ -196,7 +199,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
else if (baseSection == 3 && !foundHair &&
|
||||
variationIndex == static_cast<uint32_t>(hairStyle) &&
|
||||
colorIndex == static_cast<uint32_t>(hairColor)) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
|
||||
if (!tex1.empty()) {
|
||||
hairScalpPath = tex1;
|
||||
foundHair = true;
|
||||
|
|
@ -205,7 +208,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
// Section 4: Underwear (variation=0, colorIndex = skin color)
|
||||
else if (baseSection == 4 && !foundUnderwear &&
|
||||
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
|
||||
for (int f = 4; f <= 6; f++) {
|
||||
uint32_t texBase = csL ? (*csL)["Texture1"] : 4;
|
||||
for (uint32_t f = texBase; f <= texBase + 2; f++) {
|
||||
std::string tex = charSectionsDbc->getString(r, f);
|
||||
if (!tex.empty()) {
|
||||
underwearPaths.push_back(tex);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "rendering/lighting_manager.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -78,27 +79,30 @@ bool LightingManager::loadLightDbc(pipeline::AssetManager* assetManager) {
|
|||
// 9: uint32 LightParamsID (underwater)
|
||||
// ... more params for death, phases, etc.
|
||||
|
||||
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
||||
const auto* lL = activeLayout ? activeLayout->getLayout("Light") : nullptr;
|
||||
|
||||
for (uint32_t i = 0; i < recordCount; ++i) {
|
||||
LightVolume volume;
|
||||
volume.lightId = dbc->getUInt32(i, 0);
|
||||
volume.mapId = dbc->getUInt32(i, 1);
|
||||
volume.lightId = dbc->getUInt32(i, lL ? (*lL)["ID"] : 0);
|
||||
volume.mapId = dbc->getUInt32(i, lL ? (*lL)["MapID"] : 1);
|
||||
|
||||
// Position (note: DBC stores as x,z,y - need to swap!)
|
||||
float x = dbc->getFloat(i, 2);
|
||||
float z = dbc->getFloat(i, 3);
|
||||
float y = dbc->getFloat(i, 4);
|
||||
float x = dbc->getFloat(i, lL ? (*lL)["X"] : 2);
|
||||
float z = dbc->getFloat(i, lL ? (*lL)["Z"] : 3);
|
||||
float y = dbc->getFloat(i, lL ? (*lL)["Y"] : 4);
|
||||
volume.position = glm::vec3(x, y, z); // Convert to x,y,z
|
||||
|
||||
volume.innerRadius = dbc->getFloat(i, 5);
|
||||
volume.outerRadius = dbc->getFloat(i, 6);
|
||||
volume.innerRadius = dbc->getFloat(i, lL ? (*lL)["InnerRadius"] : 5);
|
||||
volume.outerRadius = dbc->getFloat(i, lL ? (*lL)["OuterRadius"] : 6);
|
||||
|
||||
// LightParams IDs for different conditions
|
||||
volume.lightParamsId = dbc->getUInt32(i, 7);
|
||||
volume.lightParamsId = dbc->getUInt32(i, lL ? (*lL)["LightParamsID"] : 7);
|
||||
if (dbc->getFieldCount() > 8) {
|
||||
volume.lightParamsIdRain = dbc->getUInt32(i, 8);
|
||||
volume.lightParamsIdRain = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDRain"] : 8);
|
||||
}
|
||||
if (dbc->getFieldCount() > 9) {
|
||||
volume.lightParamsIdUnderwater = dbc->getUInt32(i, 9);
|
||||
volume.lightParamsIdUnderwater = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDUnderwater"] : 9);
|
||||
}
|
||||
|
||||
// Add to map-specific list
|
||||
|
|
@ -126,8 +130,9 @@ bool LightingManager::loadLightParamsDbc(pipeline::AssetManager* assetManager) {
|
|||
LOG_INFO("Loaded LightParams.dbc: ", recordCount, " profiles");
|
||||
|
||||
// Create profile entries (will be populated by band loading)
|
||||
const auto* lpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightParams") : nullptr;
|
||||
for (uint32_t i = 0; i < recordCount; ++i) {
|
||||
uint32_t paramId = dbc->getUInt32(i, 0);
|
||||
uint32_t paramId = dbc->getUInt32(i, lpL ? (*lpL)["LightParamsID"] : 0);
|
||||
LightParamsProfile profile;
|
||||
profile.lightParamsId = paramId;
|
||||
lightParamsProfiles_[paramId] = profile;
|
||||
|
|
@ -147,8 +152,9 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
|
|||
// Parse int bands
|
||||
// Structure: ID, Entry (block index), NumValues, Time[16], Color[16]
|
||||
// Block index = LightParamsID * 18 + channel
|
||||
const auto* libL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightIntBand") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t blockIndex = dbc->getUInt32(i, 1);
|
||||
uint32_t blockIndex = dbc->getUInt32(i, libL ? (*libL)["BlockIndex"] : 1);
|
||||
uint32_t lightParamsId = blockIndex / 18;
|
||||
uint32_t channelIndex = blockIndex % 18;
|
||||
|
||||
|
|
@ -158,18 +164,20 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
|
|||
if (channelIndex >= LightParamsProfile::COLOR_CHANNEL_COUNT) continue;
|
||||
|
||||
ColorBand& band = it->second.colorBands[channelIndex];
|
||||
band.numKeyframes = dbc->getUInt32(i, 2);
|
||||
band.numKeyframes = dbc->getUInt32(i, libL ? (*libL)["NumKeyframes"] : 2);
|
||||
if (band.numKeyframes > 16) band.numKeyframes = 16;
|
||||
|
||||
// Read time keys (field 3-18) - stored as uint16 half-minutes
|
||||
uint32_t timeKeyBase = libL ? (*libL)["TimeKey0"] : 3;
|
||||
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
|
||||
uint32_t timeValue = dbc->getUInt32(i, 3 + k);
|
||||
uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k);
|
||||
band.times[k] = static_cast<uint16_t>(timeValue % 2880); // Clamp to valid range
|
||||
}
|
||||
|
||||
// Read color values (field 19-34) - stored as BGRA packed uint32
|
||||
uint32_t valueBase = libL ? (*libL)["Value0"] : 19;
|
||||
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
|
||||
uint32_t colorBGRA = dbc->getUInt32(i, 19 + k);
|
||||
uint32_t colorBGRA = dbc->getUInt32(i, valueBase + k);
|
||||
band.colors[k] = dbcColorToVec3(colorBGRA);
|
||||
}
|
||||
}
|
||||
|
|
@ -186,8 +194,9 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
|
|||
// Parse float bands
|
||||
// Structure: ID, Entry (block index), NumValues, Time[16], Value[16]
|
||||
// Block index = LightParamsID * 6 + channel
|
||||
const auto* lfbL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightFloatBand") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
||||
uint32_t blockIndex = dbc->getUInt32(i, 1);
|
||||
uint32_t blockIndex = dbc->getUInt32(i, lfbL ? (*lfbL)["BlockIndex"] : 1);
|
||||
uint32_t lightParamsId = blockIndex / 6;
|
||||
uint32_t channelIndex = blockIndex % 6;
|
||||
|
||||
|
|
@ -197,18 +206,20 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
|
|||
if (channelIndex >= LightParamsProfile::FLOAT_CHANNEL_COUNT) continue;
|
||||
|
||||
FloatBand& band = it->second.floatBands[channelIndex];
|
||||
band.numKeyframes = dbc->getUInt32(i, 2);
|
||||
band.numKeyframes = dbc->getUInt32(i, lfbL ? (*lfbL)["NumKeyframes"] : 2);
|
||||
if (band.numKeyframes > 16) band.numKeyframes = 16;
|
||||
|
||||
// Read time keys (field 3-18)
|
||||
uint32_t timeKeyBase = lfbL ? (*lfbL)["TimeKey0"] : 3;
|
||||
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
|
||||
uint32_t timeValue = dbc->getUInt32(i, 3 + k);
|
||||
uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k);
|
||||
band.times[k] = static_cast<uint16_t>(timeValue % 2880); // Clamp to valid range
|
||||
}
|
||||
|
||||
// Read float values (field 19-34)
|
||||
uint32_t valueBase = lfbL ? (*lfbL)["Value0"] : 19;
|
||||
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
|
||||
band.values[k] = dbc->getFloat(i, 19 + k);
|
||||
band.values[k] = dbc->getFloat(i, valueBase + k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include <algorithm>
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
|
|
@ -156,11 +157,16 @@ static void loadEmotesFromDbc() {
|
|||
return;
|
||||
}
|
||||
|
||||
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
||||
const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr;
|
||||
const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr;
|
||||
const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr;
|
||||
|
||||
std::unordered_map<uint32_t, std::string> textData;
|
||||
textData.reserve(emotesTextDataDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
|
||||
uint32_t id = emotesTextDataDbc->getUInt32(r, 0);
|
||||
std::string text = emotesTextDataDbc->getString(r, 1);
|
||||
uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0);
|
||||
std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1);
|
||||
if (!text.empty()) textData.emplace(id, std::move(text));
|
||||
}
|
||||
|
||||
|
|
@ -168,8 +174,8 @@ static void loadEmotesFromDbc() {
|
|||
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
|
||||
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
|
||||
uint32_t emoteId = emotesDbc->getUInt32(r, 0);
|
||||
uint32_t animId = emotesDbc->getUInt32(r, 2);
|
||||
uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0);
|
||||
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
|
||||
if (animId != 0) emoteIdToAnim[emoteId] = animId;
|
||||
}
|
||||
}
|
||||
|
|
@ -177,10 +183,10 @@ static void loadEmotesFromDbc() {
|
|||
EMOTE_TABLE.clear();
|
||||
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
|
||||
std::string cmdRaw = emotesTextDbc->getString(r, 1);
|
||||
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
|
||||
if (cmdRaw.empty()) continue;
|
||||
|
||||
uint32_t emoteRef = emotesTextDbc->getUInt32(r, 2);
|
||||
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
|
||||
uint32_t animId = 0;
|
||||
auto animIt = emoteIdToAnim.find(emoteRef);
|
||||
if (animIt != emoteIdToAnim.end()) {
|
||||
|
|
@ -189,8 +195,8 @@ static void loadEmotesFromDbc() {
|
|||
animId = emoteRef; // fallback if EmotesText stores animation id directly
|
||||
}
|
||||
|
||||
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, 5); // unisex, target, sender
|
||||
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, 9); // unisex, no target, sender
|
||||
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); // unisex, target, sender
|
||||
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); // unisex, no target, sender
|
||||
|
||||
std::string textTarget;
|
||||
std::string textNoTarget;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "rendering/world_map.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
|
@ -182,13 +183,16 @@ void WorldMap::loadZonesFromDBC() {
|
|||
if (!zones.empty() || !assetManager) return;
|
||||
|
||||
// Step 1: Resolve mapID from Map.dbc
|
||||
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
||||
const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr;
|
||||
|
||||
int mapID = -1;
|
||||
auto mapDbc = assetManager->loadDBC("Map.dbc");
|
||||
if (mapDbc && mapDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
|
||||
std::string dir = mapDbc->getString(i, 1);
|
||||
std::string dir = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
|
||||
if (dir == mapName) {
|
||||
mapID = static_cast<int>(mapDbc->getUInt32(i, 0));
|
||||
mapID = static_cast<int>(mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0));
|
||||
LOG_INFO("WorldMap: Map.dbc '", mapName, "' -> mapID=", mapID);
|
||||
break;
|
||||
}
|
||||
|
|
@ -207,12 +211,13 @@ void WorldMap::loadZonesFromDBC() {
|
|||
}
|
||||
|
||||
// Step 2: Load AreaTable explore flags by areaID.
|
||||
const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr;
|
||||
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
|
||||
auto areaDbc = assetManager->loadDBC("AreaTable.dbc");
|
||||
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
|
||||
for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) {
|
||||
const uint32_t areaId = areaDbc->getUInt32(i, 0);
|
||||
const uint32_t exploreFlag = areaDbc->getUInt32(i, 3);
|
||||
const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0);
|
||||
const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3);
|
||||
if (areaId != 0) {
|
||||
exploreFlagByAreaId[areaId] = exploreFlag;
|
||||
}
|
||||
|
|
@ -236,20 +241,22 @@ void WorldMap::loadZonesFromDBC() {
|
|||
// 4: locLeft, 5: locRight, 6: locTop, 7: locBottom
|
||||
// 8: displayMapID, 9: defaultDungeonFloor, 10: parentWorldMapID
|
||||
|
||||
const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr;
|
||||
|
||||
for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) {
|
||||
uint32_t recMapID = wmaDbc->getUInt32(i, 1);
|
||||
uint32_t recMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["MapID"] : 1);
|
||||
if (static_cast<int>(recMapID) != mapID) continue;
|
||||
|
||||
WorldMapZone zone;
|
||||
zone.wmaID = wmaDbc->getUInt32(i, 0);
|
||||
zone.areaID = wmaDbc->getUInt32(i, 2);
|
||||
zone.areaName = wmaDbc->getString(i, 3);
|
||||
zone.locLeft = wmaDbc->getFloat(i, 4);
|
||||
zone.locRight = wmaDbc->getFloat(i, 5);
|
||||
zone.locTop = wmaDbc->getFloat(i, 6);
|
||||
zone.locBottom = wmaDbc->getFloat(i, 7);
|
||||
zone.displayMapID = wmaDbc->getUInt32(i, 8);
|
||||
zone.parentWorldMapID = wmaDbc->getUInt32(i, 10);
|
||||
zone.wmaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ID"] : 0);
|
||||
zone.areaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["AreaID"] : 2);
|
||||
zone.areaName = wmaDbc->getString(i, wmaL ? (*wmaL)["AreaName"] : 3);
|
||||
zone.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4);
|
||||
zone.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5);
|
||||
zone.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6);
|
||||
zone.locBottom = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocBottom"] : 7);
|
||||
zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8);
|
||||
zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10);
|
||||
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
|
||||
if (exploreIt != exploreFlagByAreaId.end()) {
|
||||
zone.exploreFlag = exploreIt->second;
|
||||
|
|
@ -258,10 +265,10 @@ void WorldMap::loadZonesFromDBC() {
|
|||
int idx = static_cast<int>(zones.size());
|
||||
|
||||
// Debug: also log raw uint32 values for bounds fields
|
||||
uint32_t raw4 = wmaDbc->getUInt32(i, 4);
|
||||
uint32_t raw5 = wmaDbc->getUInt32(i, 5);
|
||||
uint32_t raw6 = wmaDbc->getUInt32(i, 6);
|
||||
uint32_t raw7 = wmaDbc->getUInt32(i, 7);
|
||||
uint32_t raw4 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocLeft"] : 4);
|
||||
uint32_t raw5 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocRight"] : 5);
|
||||
uint32_t raw6 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocTop"] : 6);
|
||||
uint32_t raw7 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocBottom"] : 7);
|
||||
|
||||
LOG_INFO("WorldMap: zone[", idx, "] areaID=", zone.areaID,
|
||||
" '", zone.areaName, "' L=", zone.locLeft,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "rendering/renderer.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include <imgui.h>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
|
|
@ -148,9 +149,30 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
if (port < 1) port = 1;
|
||||
if (port > 65535) port = 65535;
|
||||
|
||||
// Compatibility mode dropdown
|
||||
const char* compatModes[] = { "3.3.5a" };
|
||||
ImGui::Combo("Compatibility Mode", &compatibilityMode, compatModes, IM_ARRAYSIZE(compatModes));
|
||||
// Expansion selector (populated from ExpansionRegistry)
|
||||
auto* registry = core::Application::getInstance().getExpansionRegistry();
|
||||
if (registry && !registry->getAllProfiles().empty()) {
|
||||
auto& profiles = registry->getAllProfiles();
|
||||
// Build combo items: "WotLK (3.3.5a)"
|
||||
std::string preview;
|
||||
if (expansionIndex >= 0 && expansionIndex < static_cast<int>(profiles.size())) {
|
||||
preview = profiles[expansionIndex].shortName + " (" + profiles[expansionIndex].versionString() + ")";
|
||||
}
|
||||
if (ImGui::BeginCombo("Expansion", preview.c_str())) {
|
||||
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
|
||||
std::string label = profiles[i].shortName + " (" + profiles[i].versionString() + ")";
|
||||
bool selected = (expansionIndex == i);
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
expansionIndex = i;
|
||||
registry->setActive(profiles[i].id);
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("Expansion: WotLK 3.3.5a (default)");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
|
@ -291,6 +313,21 @@ void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) {
|
|||
failureReason = reason;
|
||||
});
|
||||
|
||||
// Configure client version from active expansion profile
|
||||
auto* reg = core::Application::getInstance().getExpansionRegistry();
|
||||
if (reg) {
|
||||
auto* profile = reg->getActive();
|
||||
if (profile) {
|
||||
auth::ClientInfo info;
|
||||
info.majorVersion = profile->majorVersion;
|
||||
info.minorVersion = profile->minorVersion;
|
||||
info.patchVersion = profile->patchVersion;
|
||||
info.build = profile->build;
|
||||
info.protocolVersion = profile->protocolVersion;
|
||||
authHandler.setClientInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
if (authHandler.connect(hostname, static_cast<uint16_t>(port))) {
|
||||
authenticating = true;
|
||||
authTimer = 0.0f;
|
||||
|
|
@ -350,6 +387,11 @@ void AuthScreen::saveLoginInfo() {
|
|||
if (!savedPasswordHash.empty()) {
|
||||
out << "password_hash=" << savedPasswordHash << "\n";
|
||||
}
|
||||
// Save active expansion id
|
||||
auto* expReg = core::Application::getInstance().getExpansionRegistry();
|
||||
if (expReg && !expReg->getActiveId().empty()) {
|
||||
out << "expansion=" << expReg->getActiveId() << "\n";
|
||||
}
|
||||
|
||||
LOG_INFO("Login info saved to ", path);
|
||||
}
|
||||
|
|
@ -376,6 +418,15 @@ void AuthScreen::loadLoginInfo() {
|
|||
username[sizeof(username) - 1] = '\0';
|
||||
} else if (key == "password_hash" && !val.empty()) {
|
||||
savedPasswordHash = val;
|
||||
} else if (key == "expansion" && !val.empty()) {
|
||||
auto* expReg = core::Application::getInstance().getExpansionRegistry();
|
||||
if (expReg && expReg->setActive(val)) {
|
||||
// Find matching index
|
||||
auto& profiles = expReg->getAllProfiles();
|
||||
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
|
||||
if (profiles[i].id == val) { expansionIndex = i; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "rendering/character_preview.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstring>
|
||||
|
||||
|
|
@ -169,16 +170,17 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
uint32_t targetRaceId = static_cast<uint32_t>(allRaces[raceIndex]);
|
||||
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
|
||||
|
||||
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
int skinMax = -1;
|
||||
int hairStyleMax = -1;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = dbc->getUInt32(r, 1);
|
||||
uint32_t sexId = dbc->getUInt32(r, 2);
|
||||
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
|
||||
uint32_t baseSection = dbc->getUInt32(r, 3);
|
||||
uint32_t variationIndex = dbc->getUInt32(r, 8);
|
||||
uint32_t colorIndex = dbc->getUInt32(r, 9);
|
||||
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
||||
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
||||
|
||||
if (baseSection == 0 && variationIndex == 0) {
|
||||
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
|
||||
|
|
@ -199,13 +201,13 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
int faceMax = -1;
|
||||
std::vector<uint8_t> hairColorIds;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = dbc->getUInt32(r, 1);
|
||||
uint32_t sexId = dbc->getUInt32(r, 2);
|
||||
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
|
||||
uint32_t baseSection = dbc->getUInt32(r, 3);
|
||||
uint32_t variationIndex = dbc->getUInt32(r, 8);
|
||||
uint32_t colorIndex = dbc->getUInt32(r, 9);
|
||||
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
||||
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
||||
|
||||
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
|
||||
faceMax = std::max(faceMax, static_cast<int>(variationIndex));
|
||||
|
|
@ -232,12 +234,13 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
}
|
||||
int facialMax = -1;
|
||||
auto facialDbc = assetManager_->loadDBC("CharacterFacialHairStyles.dbc");
|
||||
const auto* fhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
|
||||
if (facialDbc) {
|
||||
for (uint32_t r = 0; r < facialDbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = facialDbc->getUInt32(r, 0);
|
||||
uint32_t sexId = facialDbc->getUInt32(r, 1);
|
||||
uint32_t raceId = facialDbc->getUInt32(r, fhL ? (*fhL)["RaceID"] : 0);
|
||||
uint32_t sexId = facialDbc->getUInt32(r, fhL ? (*fhL)["SexID"] : 1);
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
uint32_t variation = facialDbc->getUInt32(r, 2);
|
||||
uint32_t variation = facialDbc->getUInt32(r, fhL ? (*fhL)["Variation"] : 2);
|
||||
facialMax = std::max(facialMax, static_cast<int>(variation));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "pipeline/hd_pack_manager.hpp"
|
||||
#include "game/expansion_profile.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
|
@ -2360,7 +2363,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
|||
int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId);
|
||||
if (recIdx >= 0) {
|
||||
// DBC field 3 = modelTexture_1 (cape texture name)
|
||||
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
|
||||
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["LeftModelTexture"] : 3);
|
||||
if (!capeName.empty()) {
|
||||
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
|
||||
GLuint capeTex = charRenderer->loadTexture(capePath);
|
||||
|
|
@ -2422,10 +2426,11 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
|
|||
|
||||
// Load SpellIcon.dbc: field 0 = ID, field 1 = icon path
|
||||
auto iconDbc = am->loadDBC("SpellIcon.dbc");
|
||||
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
|
||||
if (iconDbc && iconDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) {
|
||||
uint32_t id = iconDbc->getUInt32(i, 0);
|
||||
std::string path = iconDbc->getString(i, 1);
|
||||
uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
|
||||
std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1);
|
||||
if (!path.empty() && id > 0) {
|
||||
spellIconPaths_[id] = path;
|
||||
}
|
||||
|
|
@ -2434,10 +2439,11 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
|
|||
|
||||
// Load Spell.dbc: field 133 = SpellIconID
|
||||
auto spellDbc = am->loadDBC("Spell.dbc");
|
||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
if (spellDbc && spellDbc->isLoaded() && spellDbc->getFieldCount() > 133) {
|
||||
for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) {
|
||||
uint32_t id = spellDbc->getUInt32(i, 0);
|
||||
uint32_t iconId = spellDbc->getUInt32(i, 133);
|
||||
uint32_t id = spellDbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
|
||||
uint32_t iconId = spellDbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
|
||||
if (id > 0 && iconId > 0) {
|
||||
spellIconIds_[id] = iconId;
|
||||
}
|
||||
|
|
@ -2530,8 +2536,9 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|||
if (assetMgr && assetMgr->isInitialized()) {
|
||||
auto dbc = assetMgr->loadDBC("Spell.dbc");
|
||||
if (dbc && dbc->isLoaded()) {
|
||||
const auto* actionSpellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
uint32_t fieldCount = dbc->getFieldCount();
|
||||
uint32_t nameField = 136;
|
||||
uint32_t nameField = actionSpellL ? (*actionSpellL)["Name"] : 136;
|
||||
if (fieldCount < 137) {
|
||||
if (fieldCount > 10) {
|
||||
nameField = fieldCount > 140 ? 136 : 1;
|
||||
|
|
@ -2542,7 +2549,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
|||
uint32_t count = dbc->getRecordCount();
|
||||
actionSpellNames.reserve(count);
|
||||
for (uint32_t r = 0; r < count; ++r) {
|
||||
uint32_t id = dbc->getUInt32(r, 0);
|
||||
uint32_t id = dbc->getUInt32(r, actionSpellL ? (*actionSpellL)["ID"] : 0);
|
||||
std::string name = dbc->getString(r, nameField);
|
||||
if (!name.empty() && id > 0) {
|
||||
actionSpellNames[id] = name;
|
||||
|
|
@ -4835,6 +4842,79 @@ void GameScreen::renderSettingsWindow() {
|
|||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HD TEXTURES TAB
|
||||
// ============================================================
|
||||
if (ImGui::BeginTabItem("HD Textures")) {
|
||||
ImGui::Spacing();
|
||||
|
||||
auto& app = core::Application::getInstance();
|
||||
auto* hdMgr = app.getHDPackManager();
|
||||
|
||||
if (hdMgr) {
|
||||
const auto& packs = hdMgr->getAllPacks();
|
||||
if (packs.empty()) {
|
||||
ImGui::TextWrapped("No HD texture packs found.");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("Place packs in Data/hd/<pack_name>/ with a pack.json and manifest.json.");
|
||||
} else {
|
||||
ImGui::Text("Available HD Texture Packs:");
|
||||
ImGui::Spacing();
|
||||
|
||||
bool changed = false;
|
||||
for (const auto& pack : packs) {
|
||||
bool enabled = pack.enabled;
|
||||
if (ImGui::Checkbox(pack.name.c_str(), &enabled)) {
|
||||
hdMgr->setPackEnabled(pack.id, enabled);
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::TextDisabled("(%u MB)", pack.totalSizeMB);
|
||||
if (!pack.group.empty()) {
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::TextDisabled("[%s]", pack.group.c_str());
|
||||
}
|
||||
if (!pack.expansions.empty()) {
|
||||
std::string expList;
|
||||
for (const auto& e : pack.expansions) {
|
||||
if (!expList.empty()) expList += ", ";
|
||||
expList += e;
|
||||
}
|
||||
ImGui::TextDisabled(" Compatible: %s", expList.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Apply HD Packs", ImVec2(-1, 0))) {
|
||||
std::string expansionId = "wotlk";
|
||||
if (app.getExpansionRegistry() && app.getExpansionRegistry()->getActive()) {
|
||||
expansionId = app.getExpansionRegistry()->getActive()->id;
|
||||
}
|
||||
hdMgr->applyToAssetManager(app.getAssetManager(), expansionId);
|
||||
|
||||
// Save settings
|
||||
std::string settingsDir;
|
||||
const char* xdg = std::getenv("XDG_DATA_HOME");
|
||||
if (xdg && *xdg) {
|
||||
settingsDir = std::string(xdg) + "/wowee";
|
||||
} else {
|
||||
const char* home = std::getenv("HOME");
|
||||
settingsDir = std::string(home ? home : ".") + "/.local/share/wowee";
|
||||
}
|
||||
hdMgr->saveSettings(settingsDir + "/settings.cfg");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("HD Pack Manager not available.");
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
|
@ -60,7 +61,8 @@ GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
|
|||
}
|
||||
|
||||
// Field 5 = inventoryIcon_1
|
||||
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 5);
|
||||
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["InventoryIcon"] : 5);
|
||||
if (iconName.empty()) {
|
||||
iconCache_[displayInfoId] = 0;
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
|
|
@ -30,17 +31,18 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
|
||||
// WoW 3.3.5a Spell.dbc fields (0-based):
|
||||
// 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 136 = SpellName_enUS, 153 = RankText_enUS
|
||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t spellId = dbc->getUInt32(i, 0);
|
||||
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
|
||||
if (spellId == 0) continue;
|
||||
|
||||
SpellInfo info;
|
||||
info.spellId = spellId;
|
||||
info.attributes = dbc->getUInt32(i, 4);
|
||||
info.iconId = dbc->getUInt32(i, 133);
|
||||
info.name = dbc->getString(i, 136);
|
||||
info.rank = dbc->getString(i, 153);
|
||||
info.attributes = dbc->getUInt32(i, spellL ? (*spellL)["Attributes"] : 4);
|
||||
info.iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
|
||||
info.name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
||||
info.rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
||||
|
||||
if (!info.name.empty()) {
|
||||
spellData[spellId] = std::move(info);
|
||||
|
|
@ -63,9 +65,10 @@ void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
return;
|
||||
}
|
||||
|
||||
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
std::string path = dbc->getString(i, 1);
|
||||
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
|
||||
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
|
||||
if (!path.empty() && id > 0) {
|
||||
spellIconPaths[id] = path;
|
||||
}
|
||||
|
|
@ -82,11 +85,12 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
|
||||
// Load SkillLine.dbc: field 0 = ID, field 1 = categoryID, field 3 = name_enUS
|
||||
auto skillLineDbc = assetManager->loadDBC("SkillLine.dbc");
|
||||
const auto* slL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
|
||||
if (skillLineDbc && skillLineDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < skillLineDbc->getRecordCount(); i++) {
|
||||
uint32_t id = skillLineDbc->getUInt32(i, 0);
|
||||
uint32_t category = skillLineDbc->getUInt32(i, 1);
|
||||
std::string name = skillLineDbc->getString(i, 3);
|
||||
uint32_t id = skillLineDbc->getUInt32(i, slL ? (*slL)["ID"] : 0);
|
||||
uint32_t category = skillLineDbc->getUInt32(i, slL ? (*slL)["Category"] : 1);
|
||||
std::string name = skillLineDbc->getString(i, slL ? (*slL)["Name"] : 3);
|
||||
if (id > 0 && !name.empty()) {
|
||||
skillLineNames[id] = name;
|
||||
skillLineCategories[id] = category;
|
||||
|
|
@ -99,10 +103,11 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
|
||||
// Load SkillLineAbility.dbc: field 0 = ID, field 1 = skillLineID, field 2 = spellID
|
||||
auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc");
|
||||
const auto* slaL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLineAbility") : nullptr;
|
||||
if (slaDbc && slaDbc->isLoaded()) {
|
||||
for (uint32_t i = 0; i < slaDbc->getRecordCount(); i++) {
|
||||
uint32_t skillLineId = slaDbc->getUInt32(i, 1);
|
||||
uint32_t spellId = slaDbc->getUInt32(i, 2);
|
||||
uint32_t skillLineId = slaDbc->getUInt32(i, slaL ? (*slaL)["SkillLineID"] : 1);
|
||||
uint32_t spellId = slaDbc->getUInt32(i, slaL ? (*slaL)["SpellID"] : 2);
|
||||
if (spellId > 0 && skillLineId > 0) {
|
||||
spellToSkillLine[spellId] = skillLineId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "core/logger.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <algorithm>
|
||||
#include <GL/glew.h>
|
||||
|
||||
|
|
@ -448,15 +449,16 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
|
|||
}
|
||||
|
||||
// WoW 3.3.5a Spell.dbc fields: 0=SpellID, 133=SpellIconID, 136=SpellName_enUS, 139=Tooltip_enUS
|
||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||
uint32_t count = dbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t spellId = dbc->getUInt32(i, 0);
|
||||
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
|
||||
if (spellId == 0) continue;
|
||||
|
||||
uint32_t iconId = dbc->getUInt32(i, 133);
|
||||
uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
|
||||
spellIconIds[spellId] = iconId;
|
||||
|
||||
std::string tooltip = dbc->getString(i, 139);
|
||||
std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139);
|
||||
if (!tooltip.empty()) {
|
||||
spellTooltips[spellId] = tooltip;
|
||||
}
|
||||
|
|
@ -477,9 +479,10 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
|
|||
return;
|
||||
}
|
||||
|
||||
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
std::string path = dbc->getString(i, 1);
|
||||
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
|
||||
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
|
||||
if (!path.empty() && id > 0) {
|
||||
spellIconPaths[id] = path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ static void printUsage(const char* prog) {
|
|||
<< " --output <path> Output directory for extracted assets\n"
|
||||
<< "\n"
|
||||
<< "Options:\n"
|
||||
<< " --expansion <id> Expansion ID (classic/tbc/wotlk/cata).\n"
|
||||
<< " Output goes to <output>/expansions/<id>/\n"
|
||||
<< " --verify CRC32 verify all extracted files\n"
|
||||
<< " --threads <N> Number of extraction threads (default: auto)\n"
|
||||
<< " --verbose Verbose output\n"
|
||||
|
|
@ -21,12 +23,15 @@ static void printUsage(const char* prog) {
|
|||
|
||||
int main(int argc, char** argv) {
|
||||
wowee::tools::Extractor::Options opts;
|
||||
std::string expansion;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (std::strcmp(argv[i], "--mpq-dir") == 0 && i + 1 < argc) {
|
||||
opts.mpqDir = argv[++i];
|
||||
} else if (std::strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
|
||||
opts.outputDir = argv[++i];
|
||||
} else if (std::strcmp(argv[i], "--expansion") == 0 && i + 1 < argc) {
|
||||
expansion = argv[++i];
|
||||
} else if (std::strcmp(argv[i], "--threads") == 0 && i + 1 < argc) {
|
||||
opts.threads = std::atoi(argv[++i]);
|
||||
} else if (std::strcmp(argv[i], "--verify") == 0) {
|
||||
|
|
@ -49,9 +54,17 @@ int main(int argc, char** argv) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// If --expansion given, redirect output into expansions/<id>/ subdirectory
|
||||
if (!expansion.empty()) {
|
||||
opts.outputDir += "/expansions/" + expansion;
|
||||
}
|
||||
|
||||
std::cout << "=== Wowee Asset Extractor ===\n";
|
||||
std::cout << "MPQ directory: " << opts.mpqDir << "\n";
|
||||
std::cout << "Output: " << opts.outputDir << "\n";
|
||||
if (!expansion.empty()) {
|
||||
std::cout << "Expansion: " << expansion << "\n";
|
||||
}
|
||||
|
||||
if (!wowee::tools::Extractor::run(opts)) {
|
||||
std::cerr << "Extraction failed!\n";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue