From 6ede9a2968c948bf8d4793d3895ec2bdfac51654 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 15 Mar 2026 02:55:05 -0700 Subject: [PATCH] refactor: derive turtle opcodes from classic --- Data/expansions/classic/opcodes.json | 18 +- Data/expansions/turtle/opcodes.json | 304 +--------------------- Data/opcodes/aliases.json | 1 - include/game/opcode_aliases_generated.inc | 1 - include/game/opcode_table.hpp | 5 +- include/game/packet_parsers.hpp | 14 +- src/game/game_handler.cpp | 19 +- src/game/opcode_table.cpp | 218 +++++++++++----- src/game/packet_parsers_classic.cpp | 11 +- tools/diff_classic_turtle_opcodes.py | 175 +++++++++++++ tools/opcode_map_utils.py | 46 ++++ tools/validate_opcode_maps.py | 10 +- 12 files changed, 428 insertions(+), 394 deletions(-) create mode 100644 tools/diff_classic_turtle_opcodes.py create mode 100644 tools/opcode_map_utils.py diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index b99e4223..760647d9 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -273,7 +273,7 @@ "SMSG_INVENTORY_CHANGE_FAILURE": "0x112", "SMSG_OPEN_CONTAINER": "0x113", "CMSG_INSPECT": "0x114", - "SMSG_INSPECT": "0x115", + "SMSG_INSPECT_RESULTS_UPDATE": "0x115", "CMSG_INITIATE_TRADE": "0x116", "CMSG_BEGIN_TRADE": "0x117", "CMSG_BUSY_TRADE": "0x118", @@ -300,7 +300,7 @@ "CMSG_NEW_SPELL_SLOT": "0x12D", "CMSG_CAST_SPELL": "0x12E", "CMSG_CANCEL_CAST": "0x12F", - "SMSG_CAST_RESULT": "0x130", + "SMSG_CAST_FAILED": "0x130", "SMSG_SPELL_START": "0x131", "SMSG_SPELL_GO": "0x132", "SMSG_SPELL_FAILURE": "0x133", @@ -504,8 +504,7 @@ "CMSG_GM_SET_SECURITY_GROUP": "0x1F9", "CMSG_GM_NUKE": "0x1FA", "MSG_RANDOM_ROLL": "0x1FB", - "SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC", - "CMSG_RWHOIS_OBSOLETE": "0x1FD", + "SMSG_ENVIRONMENTAL_DAMAGE_LOG": "0x1FC", "SMSG_RWHOIS": "0x1FE", "MSG_LOOKING_FOR_GROUP": "0x1FF", "CMSG_SET_LOOKING_FOR_GROUP": "0x200", @@ -528,7 +527,6 @@ "CMSG_GMTICKET_GETTICKET": "0x211", "SMSG_GMTICKET_GETTICKET": "0x212", "CMSG_UNLEARN_TALENTS": "0x213", - "SMSG_GAMEOBJECT_SPAWN_ANIM_OBSOLETE": "0x214", "SMSG_GAMEOBJECT_DESPAWN_ANIM": "0x215", "MSG_CORPSE_QUERY": "0x216", "CMSG_GMTICKET_DELETETICKET": "0x217", @@ -538,7 +536,7 @@ "SMSG_GMTICKET_SYSTEMSTATUS": "0x21B", "CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C", "CMSG_SET_STAT_CHEAT": "0x21D", - "SMSG_SET_REST_START": "0x21E", + "SMSG_QUEST_FORCE_REMOVE": "0x21E", "CMSG_SKILL_BUY_STEP": "0x21F", "CMSG_SKILL_BUY_RANK": "0x220", "CMSG_XP_CHEAT": "0x221", @@ -571,8 +569,6 @@ "CMSG_BATTLEFIELD_LIST": "0x23C", "SMSG_BATTLEFIELD_LIST": "0x23D", "CMSG_BATTLEFIELD_JOIN": "0x23E", - "SMSG_BATTLEFIELD_WIN_OBSOLETE": "0x23F", - "SMSG_BATTLEFIELD_LOSE_OBSOLETE": "0x240", "CMSG_TAXICLEARNODE": "0x241", "CMSG_TAXIENABLENODE": "0x242", "CMSG_ITEM_TEXT_QUERY": "0x243", @@ -605,7 +601,6 @@ "SMSG_AUCTION_BIDDER_NOTIFICATION": "0x25E", "SMSG_AUCTION_OWNER_NOTIFICATION": "0x25F", "SMSG_PROCRESIST": "0x260", - "SMSG_STANDSTATE_CHANGE_FAILURE_OBSOLETE": "0x261", "SMSG_DISPEL_FAILED": "0x262", "SMSG_SPELLORDAMAGE_IMMUNE": "0x263", "CMSG_AUCTION_LIST_BIDDER_ITEMS": "0x264", @@ -693,8 +688,8 @@ "SMSG_SCRIPT_MESSAGE": "0x2B6", "SMSG_DUEL_COUNTDOWN": "0x2B7", "SMSG_AREA_TRIGGER_MESSAGE": "0x2B8", - "CMSG_TOGGLE_HELM": "0x2B9", - "CMSG_TOGGLE_CLOAK": "0x2BA", + "CMSG_SHOWING_HELM": "0x2B9", + "CMSG_SHOWING_CLOAK": "0x2BA", "SMSG_MEETINGSTONE_JOINFAILED": "0x2BB", "SMSG_PLAYER_SKINNED": "0x2BC", "SMSG_DURABILITY_DAMAGE_DEATH": "0x2BD", @@ -821,6 +816,5 @@ "SMSG_LOTTERY_RESULT_OBSOLETE": "0x337", "SMSG_CHARACTER_PROFILE": "0x338", "SMSG_CHARACTER_PROFILE_REALM_CONNECTED": "0x339", - "SMSG_UNK": "0x33A", "SMSG_DEFENSE_MESSAGE": "0x33B" } diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index b39f7b8f..d0f84599 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -1,302 +1,6 @@ { - "CMSG_PING": "0x1DC", - "CMSG_AUTH_SESSION": "0x1ED", - "CMSG_CHAR_CREATE": "0x036", - "CMSG_CHAR_ENUM": "0x037", - "CMSG_CHAR_DELETE": "0x038", - "CMSG_PLAYER_LOGIN": "0x03D", - "MSG_MOVE_START_FORWARD": "0x0B5", - "MSG_MOVE_START_BACKWARD": "0x0B6", - "MSG_MOVE_STOP": "0x0B7", - "MSG_MOVE_START_STRAFE_LEFT": "0x0B8", - "MSG_MOVE_START_STRAFE_RIGHT": "0x0B9", - "MSG_MOVE_STOP_STRAFE": "0x0BA", - "MSG_MOVE_JUMP": "0x0BB", - "MSG_MOVE_START_TURN_LEFT": "0x0BC", - "MSG_MOVE_START_TURN_RIGHT": "0x0BD", - "MSG_MOVE_STOP_TURN": "0x0BE", - "MSG_MOVE_SET_FACING": "0x0DA", - "MSG_MOVE_FALL_LAND": "0x0C9", - "MSG_MOVE_START_SWIM": "0x0CA", - "MSG_MOVE_STOP_SWIM": "0x0CB", - "MSG_MOVE_HEARTBEAT": "0x0EE", - "SMSG_AUTH_CHALLENGE": "0x1EC", - "SMSG_AUTH_RESPONSE": "0x1EE", - "SMSG_CHAR_CREATE": "0x03A", - "SMSG_CHAR_ENUM": "0x03B", - "SMSG_CHAR_DELETE": "0x03C", - "SMSG_CHARACTER_LOGIN_FAILED": "0x041", - "SMSG_PONG": "0x1DD", - "SMSG_LOGIN_VERIFY_WORLD": "0x236", - "SMSG_INIT_WORLD_STATES": "0x2C2", - "SMSG_LOGIN_SETTIMESPEED": "0x042", - "SMSG_TUTORIAL_FLAGS": "0x0FD", - "SMSG_INITIALIZE_FACTIONS": "0x122", - "SMSG_WARDEN_DATA": "0x2E6", - "CMSG_WARDEN_DATA": "0x2E7", - "SMSG_NOTIFICATION": "0x1CB", - "SMSG_ACCOUNT_DATA_TIMES": "0x209", - "SMSG_UPDATE_OBJECT": "0x0A9", - "SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6", - "SMSG_PARTYKILLLOG": "0x1F5", - "SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE", - "SMSG_SPLINE_MOVE_SET_WALK_MODE": "0x30E", - "SMSG_SPLINE_MOVE_SET_RUN_MODE": "0x30D", - "SMSG_SPLINE_SET_RUN_SPEED": "0x2FE", - "SMSG_SPLINE_SET_RUN_BACK_SPEED": "0x2FF", - "SMSG_SPLINE_SET_SWIM_SPEED": "0x300", - "SMSG_DESTROY_OBJECT": "0x0AA", - "CMSG_MESSAGECHAT": "0x095", - "SMSG_MESSAGECHAT": "0x096", - "CMSG_WHO": "0x062", - "SMSG_WHO": "0x063", - "CMSG_PLAYED_TIME": "0x1CC", - "SMSG_PLAYED_TIME": "0x1CD", - "CMSG_QUERY_TIME": "0x1CE", - "SMSG_QUERY_TIME_RESPONSE": "0x1CF", - "SMSG_FRIEND_STATUS": "0x068", - "SMSG_CONTACT_LIST": "0x067", - "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_STANDSTATECHANGE": "0x101", - "CMSG_SHOWING_HELM": "0x2B9", - "CMSG_SHOWING_CLOAK": "0x2BA", - "CMSG_TOGGLE_PVP": "0x253", - "CMSG_GUILD_INVITE": "0x082", - "CMSG_GUILD_ACCEPT": "0x084", - "CMSG_GUILD_DECLINE": "0x085", - "CMSG_GUILD_INFO": "0x087", - "CMSG_GUILD_ROSTER": "0x089", - "CMSG_GUILD_PROMOTE": "0x08B", - "CMSG_GUILD_DEMOTE": "0x08C", - "CMSG_GUILD_LEAVE": "0x08D", - "CMSG_GUILD_MOTD": "0x091", - "SMSG_GUILD_INFO": "0x088", - "SMSG_GUILD_ROSTER": "0x08A", - "CMSG_GUILD_QUERY": "0x054", - "SMSG_GUILD_QUERY_RESPONSE": "0x055", - "SMSG_GUILD_INVITE": "0x083", - "CMSG_GUILD_REMOVE": "0x08E", - "SMSG_GUILD_EVENT": "0x092", - "SMSG_GUILD_COMMAND_RESULT": "0x093", - "MSG_RAID_READY_CHECK": "0x322", - "SMSG_ITEM_PUSH_RESULT": "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", - "_NOTE_MONSTER_MOVE": "These look swapped vs vanilla (0x0DD/0x2FB) but may be intentional Turtle WoW changes. Check if NPC movement breaks.", - "SMSG_MONSTER_MOVE": "0x2FB", - "SMSG_COMPRESSED_MOVES": "0x06B", - "CMSG_ATTACKSWING": "0x141", - "CMSG_ATTACKSTOP": "0x142", - "SMSG_ATTACKSTART": "0x143", - "SMSG_ATTACKSTOP": "0x144", - "SMSG_ATTACKERSTATEUPDATE": "0x14A", - "SMSG_AI_REACTION": "0x13C", - "SMSG_SPELLNONMELEEDAMAGELOG": "0x250", - "SMSG_PLAY_SPELL_VISUAL": "0x1F3", - "SMSG_SPELLHEALLOG": "0x150", - "SMSG_SPELLENERGIZELOG": "0x151", - "SMSG_PERIODICAURALOG": "0x24E", - "SMSG_ENVIRONMENTAL_DAMAGE_LOG": "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_GAMEOBJ_USE": "0x0B1", - "CMSG_QUESTGIVER_STATUS_QUERY": "0x182", - "SMSG_QUESTGIVER_STATUS": "0x183", - "CMSG_QUESTGIVER_HELLO": "0x184", - "SMSG_QUESTGIVER_QUEST_LIST": "0x185", - "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", - "SMSG_QUEST_FORCE_REMOVE": "0x21E", - "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", - "CMSG_BUYBACK_ITEM": "0x1A6", - "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_UPDATE": "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", - "SMSG_CLIENT_CONTROL_UPDATE": "0x159", - "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", - "SMSG_PLAY_SOUND": "0x2D2", - "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", - "SMSG_SPELL_FAILED_OTHER": "0x2A6", - "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_ADDON_INFO": "0x2EF", - "CMSG_EMOTE": "0x102", - "SMSG_EMOTE": "0x103", - "CMSG_TEXT_EMOTE": "0x104", - "SMSG_TEXT_EMOTE": "0x105", - "CMSG_JOIN_CHANNEL": "0x097", - "CMSG_LEAVE_CHANNEL": "0x098", - "SMSG_CHANNEL_NOTIFY": "0x099", - "CMSG_CHANNEL_LIST": "0x09A", - "SMSG_CHANNEL_LIST": "0x09B", - "SMSG_INSPECT_TALENT": "0x3F4", - "SMSG_SHOW_MAILBOX": "0x297", - "CMSG_GET_MAIL_LIST": "0x23A", - "SMSG_MAIL_LIST_RESULT": "0x23B", - "CMSG_SEND_MAIL": "0x238", - "SMSG_SEND_MAIL_RESULT": "0x239", - "CMSG_MAIL_TAKE_MONEY": "0x245", - "CMSG_MAIL_TAKE_ITEM": "0x246", - "CMSG_MAIL_DELETE": "0x249", - "CMSG_MAIL_MARK_AS_READ": "0x247", - "SMSG_RECEIVED_MAIL": "0x285", - "MSG_QUERY_NEXT_MAIL_TIME": "0x284", - "CMSG_BANKER_ACTIVATE": "0x1B7", - "SMSG_SHOW_BANK": "0x1B8", - "CMSG_BUY_BANK_SLOT": "0x1B9", - "SMSG_BUY_BANK_SLOT_RESULT": "0x1BA", - "CMSG_AUTOSTORE_BANK_ITEM": "0x282", - "CMSG_AUTOBANK_ITEM": "0x283", - "MSG_AUCTION_HELLO": "0x255", - "CMSG_AUCTION_SELL_ITEM": "0x256", - "CMSG_AUCTION_REMOVE_ITEM": "0x257", - "CMSG_AUCTION_LIST_ITEMS": "0x258", - "CMSG_AUCTION_LIST_OWNER_ITEMS": "0x259", - "CMSG_AUCTION_PLACE_BID": "0x25A", - "SMSG_AUCTION_COMMAND_RESULT": "0x25B", - "SMSG_AUCTION_LIST_RESULT": "0x25C", - "SMSG_AUCTION_OWNER_LIST_RESULT": "0x25D", - "SMSG_AUCTION_OWNER_NOTIFICATION": "0x25E", - "SMSG_AUCTION_BIDDER_NOTIFICATION": "0x260", - "CMSG_AUCTION_LIST_BIDDER_ITEMS": "0x264", - "SMSG_AUCTION_BIDDER_LIST_RESULT": "0x265", - "MSG_MOVE_TIME_SKIPPED": "0x319", - "SMSG_CANCEL_AUTO_REPEAT": "0x29C", - "SMSG_WEATHER": "0x2F4", - "SMSG_QUESTUPDATE_ADD_ITEM": "0x19A", - "CMSG_GUILD_DISBAND": "0x08F", - "CMSG_GUILD_LEADER": "0x090", - "CMSG_GUILD_SET_PUBLIC_NOTE": "0x234", - "CMSG_GUILD_SET_OFFICER_NOTE": "0x235" + "_extends": "../classic/opcodes.json", + "_remove": [ + "MSG_SET_DUNGEON_DIFFICULTY" + ] } diff --git a/Data/opcodes/aliases.json b/Data/opcodes/aliases.json index e3a67348..4677cd5d 100644 --- a/Data/opcodes/aliases.json +++ b/Data/opcodes/aliases.json @@ -41,7 +41,6 @@ "SMSG_SPLINE_MOVE_SET_RUN_BACK_SPEED": "SMSG_SPLINE_SET_RUN_BACK_SPEED", "SMSG_SPLINE_MOVE_SET_RUN_SPEED": "SMSG_SPLINE_SET_RUN_SPEED", "SMSG_SPLINE_MOVE_SET_SWIM_SPEED": "SMSG_SPLINE_SET_SWIM_SPEED", - "SMSG_UPDATE_AURA_DURATION": "SMSG_EQUIPMENT_SET_SAVED", "SMSG_VICTIMSTATEUPDATE_OBSOLETE": "SMSG_BATTLEFIELD_PORT_DENIED" } } diff --git a/include/game/opcode_aliases_generated.inc b/include/game/opcode_aliases_generated.inc index ad488110..ed20d098 100644 --- a/include/game/opcode_aliases_generated.inc +++ b/include/game/opcode_aliases_generated.inc @@ -41,5 +41,4 @@ {"SMSG_SPLINE_MOVE_SET_RUN_BACK_SPEED", "SMSG_SPLINE_SET_RUN_BACK_SPEED"}, {"SMSG_SPLINE_MOVE_SET_RUN_SPEED", "SMSG_SPLINE_SET_RUN_SPEED"}, {"SMSG_SPLINE_MOVE_SET_SWIM_SPEED", "SMSG_SPLINE_SET_SWIM_SPEED"}, - {"SMSG_UPDATE_AURA_DURATION", "SMSG_EQUIPMENT_SET_SAVED"}, {"SMSG_VICTIMSTATEUPDATE_OBSOLETE", "SMSG_BATTLEFIELD_PORT_DENIED"}, diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index aaecc837..966542b9 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -33,7 +33,10 @@ class OpcodeTable { public: /** * Load opcode mappings from a JSON file. - * Format: { "CMSG_PING": "0x1DC", "SMSG_AUTH_CHALLENGE": "0x1EC", ... } + * Format: + * { "CMSG_PING": "0x1DC", "SMSG_AUTH_CHALLENGE": "0x1EC", ... } + * or a delta file with: + * { "_extends": "../classic/opcodes.json", "_remove": ["MSG_FOO"], ...overrides } */ bool loadFromJson(const std::string& path); diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 4446deba..fe033101 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -439,14 +439,16 @@ public: }; /** - * Turtle WoW (build 7234) packet parsers. + * Turtle WoW packet parsers. * - * Turtle WoW is a heavily modified vanilla server that sends TBC-style - * movement blocks (moveFlags2, transport timestamps, 8 speeds including flight) - * while keeping all other Classic packet formats. + * Turtle is Classic-based but not wire-identical to vanilla MaNGOS. It keeps + * most Classic packet formats, while overriding the movement-bearing paths that + * have proven to vary in live traffic: + * - update-object movement blocks use a Turtle-specific hybrid layout + * - update-object parsing falls back through Classic/TBC/WotLK movement layouts + * - monster-move parsing falls back through Vanilla, TBC, and guarded WotLK layouts * - * Inherits all Classic overrides (charEnum, chat, gossip, mail, items, etc.) - * but delegates movement block parsing to TBC format. + * Everything else inherits the Classic parser behavior. */ class TurtlePacketParsers : public ClassicPacketParsers { public: diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0bd11890..c6c8cc56 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4261,8 +4261,11 @@ void GameHandler::handlePacket(network::Packet& packet) { } case Opcode::SMSG_ACTION_BUTTONS: { - // packed: bits 0-23 = actionId, bits 24-31 = type - // 0x00 = spell (when id != 0), 0x80 = item, 0x40 = macro (skip) + // Slot encoding differs by expansion: + // Classic/Turtle: uint16 actionId + uint8 type + uint8 misc + // type: 0=spell, 1=item, 64=macro + // TBC/WotLK: uint32 packed = actionId | (type << 24) + // type: 0x00=spell, 0x80=item, 0x40=macro // Format differences: // Classic 1.12: no mode byte, 120 slots (480 bytes) // TBC 2.4.3: no mode byte, 132 slots (528 bytes) @@ -4292,12 +4295,20 @@ void GameHandler::handlePacket(network::Packet& packet) { // so we don't wipe hardcoded fallbacks when the server sends zeros. continue; } - uint8_t type = static_cast((packed >> 24) & 0xFF); - uint32_t id = packed & 0x00FFFFFFu; + uint8_t type = 0; + uint32_t id = 0; + if (isClassicLikeExpansion()) { + id = packed & 0x0000FFFFu; + type = static_cast((packed >> 16) & 0xFF); + } else { + type = static_cast((packed >> 24) & 0xFF); + id = packed & 0x00FFFFFFu; + } if (id == 0) continue; ActionBarSlot slot; switch (type) { case 0x00: slot.type = ActionBarSlot::SPELL; slot.id = id; break; + case 0x01: slot.type = ActionBarSlot::ITEM; slot.id = id; break; case 0x80: slot.type = ActionBarSlot::ITEM; slot.id = id; break; default: continue; // macro or unknown — leave as-is } diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 8178f0f5..ad9b639f 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include namespace wowee { namespace game { @@ -47,6 +49,155 @@ static std::string_view canonicalOpcodeName(std::string_view name) { return name; } +static std::optional resolveLogicalOpcodeIndex(std::string_view name) { + const std::string_view canonical = canonicalOpcodeName(name); + for (size_t i = 0; i < kOpcodeNameCount; ++i) { + if (canonical == kOpcodeNames[i].name) { + return static_cast(kOpcodeNames[i].op); + } + } + return std::nullopt; +} + +static std::optional parseStringField(const std::string& json, const char* fieldName) { + const std::string needle = std::string("\"") + fieldName + "\""; + size_t keyPos = json.find(needle); + if (keyPos == std::string::npos) return std::nullopt; + + size_t colon = json.find(':', keyPos + needle.size()); + if (colon == std::string::npos) return std::nullopt; + + size_t valueStart = json.find('"', colon + 1); + if (valueStart == std::string::npos) return std::nullopt; + size_t valueEnd = json.find('"', valueStart + 1); + if (valueEnd == std::string::npos) return std::nullopt; + return json.substr(valueStart + 1, valueEnd - valueStart - 1); +} + +static std::vector parseStringArrayField(const std::string& json, const char* fieldName) { + std::vector values; + const std::string needle = std::string("\"") + fieldName + "\""; + size_t keyPos = json.find(needle); + if (keyPos == std::string::npos) return values; + + size_t colon = json.find(':', keyPos + needle.size()); + if (colon == std::string::npos) return values; + + size_t arrayStart = json.find('[', colon + 1); + if (arrayStart == std::string::npos) return values; + size_t arrayEnd = json.find(']', arrayStart + 1); + if (arrayEnd == std::string::npos) return values; + + size_t pos = arrayStart + 1; + while (pos < arrayEnd) { + size_t valueStart = json.find('"', pos); + if (valueStart == std::string::npos || valueStart >= arrayEnd) break; + size_t valueEnd = json.find('"', valueStart + 1); + if (valueEnd == std::string::npos || valueEnd > arrayEnd) break; + values.push_back(json.substr(valueStart + 1, valueEnd - valueStart - 1)); + pos = valueEnd + 1; + } + return values; +} + +static bool loadOpcodeJsonRecursive(const std::filesystem::path& path, + std::unordered_map& logicalToWire, + std::unordered_map& wireToLogical, + std::unordered_set& loadingStack) { + const std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(path); + const std::string canonicalKey = canonicalPath.string(); + if (!loadingStack.insert(canonicalKey).second) { + LOG_WARNING("OpcodeTable: inheritance cycle at ", canonicalKey); + return false; + } + + std::ifstream f(canonicalPath); + if (!f.is_open()) { + LOG_WARNING("OpcodeTable: cannot open ", canonicalPath.string()); + loadingStack.erase(canonicalKey); + return false; + } + + std::string json((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + bool ok = true; + + if (auto extends = parseStringField(json, "_extends")) { + ok = loadOpcodeJsonRecursive(canonicalPath.parent_path() / *extends, + logicalToWire, wireToLogical, loadingStack) && ok; + } + + for (const std::string& removeName : parseStringArrayField(json, "_remove")) { + auto logical = resolveLogicalOpcodeIndex(removeName); + if (!logical) continue; + auto it = logicalToWire.find(*logical); + if (it != logicalToWire.end()) { + const uint16_t oldWire = it->second; + logicalToWire.erase(it); + auto wireIt = wireToLogical.find(oldWire); + if (wireIt != wireToLogical.end() && wireIt->second == *logical) { + wireToLogical.erase(wireIt); + } + } + } + + 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' || 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); + + uint16_t wire = 0; + try { + if (valStr.size() > 2 && (valStr[0] == '0' && (valStr[1] == 'x' || valStr[1] == 'X'))) { + wire = static_cast(std::stoul(valStr, nullptr, 16)); + } else { + wire = static_cast(std::stoul(valStr)); + } + } catch (...) { + pos = valEnd + 1; + continue; + } + + auto logical = resolveLogicalOpcodeIndex(key); + if (logical) { + auto oldLogicalIt = logicalToWire.find(*logical); + if (oldLogicalIt != logicalToWire.end()) { + const uint16_t oldWire = oldLogicalIt->second; + auto oldWireIt = wireToLogical.find(oldWire); + if (oldWireIt != wireToLogical.end() && oldWireIt->second == *logical) { + wireToLogical.erase(oldWireIt); + } + } + auto oldWireIt = wireToLogical.find(wire); + if (oldWireIt != wireToLogical.end() && oldWireIt->second != *logical) { + logicalToWire.erase(oldWireIt->second); + wireToLogical.erase(oldWireIt); + } + logicalToWire[*logical] = wire; + wireToLogical[wire] = *logical; + } + + pos = valEnd + 1; + } + + loadingStack.erase(canonicalKey); + return ok; +} + std::optional OpcodeTable::nameToLogical(const std::string& name) { const std::string_view canonical = canonicalOpcodeName(name); for (size_t i = 0; i < kOpcodeNameCount; ++i) { @@ -64,73 +215,18 @@ const char* OpcodeTable::logicalToName(LogicalOpcode op) { } bool OpcodeTable::loadFromJson(const std::string& path) { - std::ifstream f(path); - if (!f.is_open()) { - LOG_WARNING("OpcodeTable: cannot open ", path, ", using defaults"); - return false; - } - - std::string json((std::istreambuf_iterator(f)), std::istreambuf_iterator()); - - // Start fresh — JSON is the single source of truth for opcode mappings. + // Start fresh — resolved JSON inheritance is the single source of truth for opcode mappings. logicalToWire_.clear(); wireToLogical_.clear(); - - // Parse simple JSON: { "NAME": "0xHEX", ... } or { "NAME": 123, ... } - size_t pos = 0; - size_t loaded = 0; - while (pos < json.size()) { - // Find next quoted key - size_t keyStart = json.find('"', pos); - if (keyStart == std::string::npos) break; - size_t keyEnd = json.find('"', keyStart + 1); - if (keyEnd == std::string::npos) break; - std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1); - - // Find colon then value - size_t colon = json.find(':', keyEnd); - if (colon == std::string::npos) break; - - // Skip whitespace - size_t valStart = colon + 1; - while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' || - json[valStart] == '\r' || json[valStart] == '\n' || json[valStart] == '"')) - ++valStart; - - size_t valEnd = json.find_first_of(",}\"\r\n", valStart); - if (valEnd == std::string::npos) valEnd = json.size(); - std::string valStr = json.substr(valStart, valEnd - valStart); - - // Parse hex or decimal value - uint16_t wire = 0; - try { - if (valStr.size() > 2 && (valStr[0] == '0' && (valStr[1] == 'x' || valStr[1] == 'X'))) { - wire = static_cast(std::stoul(valStr, nullptr, 16)); - } else { - wire = static_cast(std::stoul(valStr)); - } - } catch (...) { - pos = valEnd + 1; - continue; - } - - auto logOp = nameToLogical(key); - if (logOp) { - uint16_t logIdx = static_cast(*logOp); - logicalToWire_[logIdx] = wire; - wireToLogical_[wire] = logIdx; - ++loaded; - } - - pos = valEnd + 1; - } - - if (loaded == 0) { + std::unordered_set loadingStack; + if (!loadOpcodeJsonRecursive(std::filesystem::path(path), + logicalToWire_, wireToLogical_, loadingStack) || + logicalToWire_.empty()) { LOG_WARNING("OpcodeTable: no opcodes loaded from ", path); return false; } - LOG_INFO("OpcodeTable: loaded ", loaded, " opcodes from ", path); + LOG_INFO("OpcodeTable: loaded ", logicalToWire_.size(), " opcodes from ", path); return true; } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 663bafd3..59c2d0f8 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -2007,13 +2007,20 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec } bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) { - // Turtle realms can emit both vanilla-like and WotLK-like monster move bodies. - // Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout. + // Turtle realms can emit vanilla-like, TBC-like, and WotLK-like monster move + // bodies. Try the lower-expansion layouts first before the WotLK parser that + // expects an extra unk byte after the packed GUID. size_t start = packet.getReadPos(); if (MonsterMoveParser::parseVanilla(packet, data)) { return true; } + packet.setReadPos(start); + if (TbcPacketParsers::parseMonsterMove(packet, data)) { + LOG_DEBUG("[Turtle] SMSG_MONSTER_MOVE parsed via TBC fallback layout"); + return true; + } + auto looksLikeWotlkMonsterMove = [&](network::Packet& probe) -> bool { const size_t probeStart = probe.getReadPos(); uint64_t guid = UpdateObjectParser::readPackedGuid(probe); diff --git a/tools/diff_classic_turtle_opcodes.py b/tools/diff_classic_turtle_opcodes.py new file mode 100644 index 00000000..0e548b47 --- /dev/null +++ b/tools/diff_classic_turtle_opcodes.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Report the semantic opcode diff between the Classic and Turtle expansion maps. + +The report normalizes: +- hex formatting differences (0x67 vs 0x067) +- alias names that collapse to the same canonical opcode + +It highlights: +- true wire differences for the same canonical opcode +- canonical opcodes present only in Classic or only in Turtle +- name-only differences where the wire matches after aliasing +""" + +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Tuple + +from opcode_map_utils import load_opcode_map + + +RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$") + + +def read_aliases(path: Path) -> Dict[str, str]: + data = json.loads(path.read_text()) + aliases = data.get("aliases", {}) + out: Dict[str, str] = {} + for key, value in aliases.items(): + if isinstance(key, str) and isinstance(value, str): + out[key] = value + return out + + +def canonicalize(name: str, aliases: Dict[str, str]) -> str: + seen = set() + current = name + while current in aliases and current not in seen: + seen.add(current) + current = aliases[current] + return current + + +def load_map(path: Path) -> Dict[str, int]: + data = load_opcode_map(path) + out: Dict[str, int] = {} + for key, value in data.items(): + if not isinstance(key, str) or not RE_OPCODE_NAME.match(key): + continue + if not isinstance(value, str) or not value.lower().startswith("0x"): + continue + out[key] = int(value, 16) + return out + + +@dataclass(frozen=True) +class CanonicalEntry: + canonical_name: str + raw_value: int + raw_names: Tuple[str, ...] + + +def build_canonical_entries( + raw_map: Dict[str, int], aliases: Dict[str, str] +) -> Dict[str, CanonicalEntry]: + grouped: Dict[str, List[Tuple[str, int]]] = {} + for raw_name, raw_value in raw_map.items(): + canonical_name = canonicalize(raw_name, aliases) + grouped.setdefault(canonical_name, []).append((raw_name, raw_value)) + + out: Dict[str, CanonicalEntry] = {} + for canonical_name, entries in grouped.items(): + raw_values = {raw_value for _, raw_value in entries} + if len(raw_values) != 1: + formatted = ", ".join( + f"{name}=0x{raw_value:03X}" for name, raw_value in sorted(entries) + ) + raise ValueError( + f"Expansion map contains multiple wires for canonical opcode " + f"{canonical_name}: {formatted}" + ) + raw_value = next(iter(raw_values)) + raw_names = tuple(sorted(name for name, _ in entries)) + out[canonical_name] = CanonicalEntry(canonical_name, raw_value, raw_names) + return out + + +def format_hex(raw_value: int) -> str: + return f"0x{raw_value:03X}" + + +def emit_section(title: str, rows: Iterable[str], limit: int | None) -> None: + rows = list(rows) + print(f"{title}: {len(rows)}") + if not rows: + return + shown = rows if limit is None else rows[:limit] + for row in shown: + print(f" {row}") + if limit is not None and len(rows) > limit: + print(f" ... {len(rows) - limit} more") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", default=".") + parser.add_argument( + "--limit", + type=int, + default=80, + help="Maximum rows to print per section; use -1 for no limit.", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + aliases = read_aliases(root / "Data/opcodes/aliases.json") + classic_raw = load_map(root / "Data/expansions/classic/opcodes.json") + turtle_raw = load_map(root / "Data/expansions/turtle/opcodes.json") + + classic = build_canonical_entries(classic_raw, aliases) + turtle = build_canonical_entries(turtle_raw, aliases) + + classic_names = set(classic) + turtle_names = set(turtle) + shared_names = classic_names & turtle_names + + different_wire = [] + same_wire_name_only = [] + for canonical_name in sorted(shared_names): + c = classic[canonical_name] + t = turtle[canonical_name] + if c.raw_value != t.raw_value: + different_wire.append( + f"{canonical_name}: classic={format_hex(c.raw_value)} " + f"turtle={format_hex(t.raw_value)}" + ) + elif c.raw_names != t.raw_names: + same_wire_name_only.append( + f"{canonical_name}: wire={format_hex(c.raw_value)} " + f"classic_names={list(c.raw_names)} turtle_names={list(t.raw_names)}" + ) + + classic_only = [ + f"{name}: {format_hex(classic[name].raw_value)} names={list(classic[name].raw_names)}" + for name in sorted(classic_names - turtle_names) + ] + turtle_only = [ + f"{name}: {format_hex(turtle[name].raw_value)} names={list(turtle[name].raw_names)}" + for name in sorted(turtle_names - classic_names) + ] + + limit = None if args.limit < 0 else args.limit + + print(f"classic canonical entries: {len(classic)}") + print(f"turtle canonical entries: {len(turtle)}") + print(f"shared canonical entries: {len(shared_names)}") + print() + emit_section("Different wire", different_wire, limit) + print() + emit_section("Classic only", classic_only, limit) + print() + emit_section("Turtle only", turtle_only, limit) + print() + emit_section("Same wire, name-only differences", same_wire_name_only, limit) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/opcode_map_utils.py b/tools/opcode_map_utils.py new file mode 100644 index 00000000..c3566057 --- /dev/null +++ b/tools/opcode_map_utils.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Dict, Set + + +RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$") + + +def load_opcode_map(path: Path, _seen: Set[Path] | None = None) -> Dict[str, str]: + if _seen is None: + _seen = set() + + path = path.resolve() + if path in _seen: + chain = " -> ".join(str(p) for p in list(_seen) + [path]) + raise ValueError(f"Opcode map inheritance cycle: {chain}") + _seen.add(path) + + data = json.loads(path.read_text()) + merged: Dict[str, str] = {} + + extends = data.get("_extends") + if isinstance(extends, str) and extends: + merged.update(load_opcode_map(path.parent / extends, _seen)) + + remove = data.get("_remove", []) + if isinstance(remove, list): + for name in remove: + if isinstance(name, str): + merged.pop(name, None) + + for key, value in data.items(): + if not isinstance(key, str) or not RE_OPCODE_NAME.match(key): + continue + if isinstance(value, str): + merged[key] = value + elif isinstance(value, int): + merged[key] = str(value) + + _seen.remove(path) + return merged diff --git a/tools/validate_opcode_maps.py b/tools/validate_opcode_maps.py index a562439b..7acb62ed 100644 --- a/tools/validate_opcode_maps.py +++ b/tools/validate_opcode_maps.py @@ -17,6 +17,8 @@ import re from pathlib import Path from typing import Dict, Iterable, List, Set +from opcode_map_utils import load_opcode_map + RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$") RE_CODE_REF = re.compile(r"\bOpcode::((?:CMSG|SMSG|MSG)_[A-Z0-9_]+)\b") @@ -53,12 +55,8 @@ def iter_expansion_files(expansions_dir: Path) -> Iterable[Path]: def load_expansion_names(path: Path) -> Dict[str, str]: - data = json.loads(path.read_text()) - out: Dict[str, str] = {} - for k, v in data.items(): - if RE_OPCODE_NAME.match(k): - out[k] = str(v) - return out + data = load_opcode_map(path) + return {k: str(v) for k, v in data.items() if RE_OPCODE_NAME.match(k)} def collect_code_refs(root: Path) -> Set[str]: