refactor: derive turtle opcodes from classic

This commit is contained in:
Kelsi 2026-03-15 02:55:05 -07:00
parent 0b6265bc55
commit 6ede9a2968
12 changed files with 428 additions and 394 deletions

View file

@ -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"
}

View file

@ -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"
]
}

View file

@ -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"
}
}

View file

@ -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"},

View file

@ -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);

View file

@ -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:

View file

@ -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<uint8_t>((packed >> 24) & 0xFF);
uint32_t id = packed & 0x00FFFFFFu;
uint8_t type = 0;
uint32_t id = 0;
if (isClassicLikeExpansion()) {
id = packed & 0x0000FFFFu;
type = static_cast<uint8_t>((packed >> 16) & 0xFF);
} else {
type = static_cast<uint8_t>((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
}

View file

@ -4,7 +4,9 @@
#include <sstream>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <string_view>
#include <unordered_set>
namespace wowee {
namespace game {
@ -47,6 +49,155 @@ static std::string_view canonicalOpcodeName(std::string_view name) {
return name;
}
static std::optional<uint16_t> 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<uint16_t>(kOpcodeNames[i].op);
}
}
return std::nullopt;
}
static std::optional<std::string> 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<std::string> parseStringArrayField(const std::string& json, const char* fieldName) {
std::vector<std::string> 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<uint16_t, uint16_t>& logicalToWire,
std::unordered_map<uint16_t, uint16_t>& wireToLogical,
std::unordered_set<std::string>& 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<char>(f)), std::istreambuf_iterator<char>());
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<uint16_t>(std::stoul(valStr, nullptr, 16));
} else {
wire = static_cast<uint16_t>(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<LogicalOpcode> 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<char>(f)), std::istreambuf_iterator<char>());
// 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<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;
}
if (loaded == 0) {
std::unordered_set<std::string> 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;
}

View file

@ -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);

View file

@ -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())

46
tools/opcode_map_utils.py Normal file
View file

@ -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

View file

@ -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]: