From a1c16762afbba843a4be74b141aac6a674384d03 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Feb 2026 23:26:58 -0800 Subject: [PATCH] Handle remaining Turtle world opcodes with safe minimal parsers --- Data/expansions/turtle/opcodes.json | 5 ++ include/game/opcode_table.hpp | 5 ++ src/game/game_handler.cpp | 89 +++++++++++++++++++++++++++++ src/game/opcode_table.cpp | 10 ++++ 4 files changed, 109 insertions(+) diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index ebadec31..9e40fbf7 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -28,8 +28,10 @@ "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_ACCOUNT_DATA_TIMES": "0x209", @@ -46,6 +48,7 @@ "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", @@ -171,6 +174,7 @@ "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", @@ -204,6 +208,7 @@ "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", diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 7d96e3ae..b4a4466c 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -50,8 +50,10 @@ enum class LogicalOpcode : uint16_t { SMSG_CHARACTER_LOGIN_FAILED, SMSG_PONG, SMSG_LOGIN_VERIFY_WORLD, + SMSG_INIT_WORLD_STATES, SMSG_LOGIN_SETTIMESPEED, SMSG_TUTORIAL_FLAGS, + SMSG_INITIALIZE_FACTIONS, SMSG_WARDEN_DATA, CMSG_WARDEN_DATA, SMSG_ACCOUNT_DATA_TIMES, @@ -79,6 +81,7 @@ enum class LogicalOpcode : uint16_t { // ---- Social Commands ---- SMSG_FRIEND_STATUS, + SMSG_CONTACT_LIST, CMSG_ADD_FRIEND, CMSG_DEL_FRIEND, CMSG_SET_CONTACT_NOTES, @@ -259,6 +262,7 @@ enum class LogicalOpcode : uint16_t { SMSG_QUESTUPDATE_ADD_KILL, SMSG_QUESTUPDATE_ADD_ITEM, SMSG_QUESTUPDATE_COMPLETE, + SMSG_QUEST_FORCE_REMOVE, CMSG_QUEST_QUERY, SMSG_QUEST_QUERY_RESPONSE, SMSG_QUESTLOG_FULL, @@ -306,6 +310,7 @@ enum class LogicalOpcode : uint16_t { // ---- Speed Changes ---- SMSG_FORCE_RUN_SPEED_CHANGE, + SMSG_CLIENT_CONTROL_UPDATE, CMSG_FORCE_RUN_SPEED_CHANGE_ACK, // ---- Mount ---- diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b42b7f03..e575887b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -965,6 +965,22 @@ void GameHandler::handlePacket(network::Packet& packet) { handleFriendStatus(packet); } break; + case Opcode::SMSG_CONTACT_LIST: { + // Known variants: + // - Full form: uint32 listMask, uint32 count, then variable-size entries. + // - Minimal/legacy keepalive-ish form observed on some servers: 1 byte. + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining >= 8) { + /*uint32_t listMask =*/ packet.readUInt32(); + /*uint32_t count =*/ packet.readUInt32(); + } else if (remaining == 1) { + /*uint8_t marker =*/ packet.readUInt8(); + } else if (remaining > 0) { + // Unknown short variant: consume to keep stream aligned, no warning spam. + packet.setReadPos(packet.getSize()); + } + break; + } case Opcode::MSG_RANDOM_ROLL: if (state == WorldState::IN_WORLD) { @@ -1019,6 +1035,28 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE: handleForceRunSpeedChange(packet); break; + case Opcode::SMSG_CLIENT_CONTROL_UPDATE: { + // Minimal parse: PackedGuid + uint8 allowMovement. + if (packet.getSize() - packet.getReadPos() < 2) { + LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE too short: ", packet.getSize(), " bytes"); + break; + } + uint8_t guidMask = packet.readUInt8(); + size_t guidBytes = 0; + for (int i = 0; i < 8; ++i) { + if (guidMask & (1u << i)) ++guidBytes; + } + if (packet.getSize() - packet.getReadPos() < guidBytes + 1) { + LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE malformed (truncated packed guid)"); + packet.setReadPos(packet.getSize()); + break; + } + for (size_t i = 0; i < guidBytes; ++i) { + packet.readUInt8(); + } + /*uint8_t allowMovement =*/ packet.readUInt8(); + break; + } // ---- Phase 2: Combat ---- case Opcode::SMSG_ATTACKSTART: @@ -1253,6 +1291,48 @@ void GameHandler::handlePacket(network::Packet& packet) { } // Silently ignore common packets we don't handle yet + case Opcode::SMSG_INIT_WORLD_STATES: { + // Minimal parse: uint32 mapId, uint32 zoneId, uint16 count, repeated (uint32 key, uint32 val) + if (packet.getSize() - packet.getReadPos() < 10) { + LOG_WARNING("SMSG_INIT_WORLD_STATES too short: ", packet.getSize(), " bytes"); + break; + } + /*uint32_t mapId =*/ packet.readUInt32(); + /*uint32_t zoneId =*/ packet.readUInt32(); + uint16_t count = packet.readUInt16(); + size_t needed = static_cast(count) * 8; + if (packet.getSize() - packet.getReadPos() < needed) { + LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed, + " bytes of state pairs, got ", packet.getSize() - packet.getReadPos()); + packet.setReadPos(packet.getSize()); + break; + } + for (uint16_t i = 0; i < count; ++i) { + packet.readUInt32(); + packet.readUInt32(); + } + break; + } + case Opcode::SMSG_INITIALIZE_FACTIONS: { + // Minimal parse: uint32 count, repeated (uint8 flags, int32 standing) + if (packet.getSize() - packet.getReadPos() < 4) { + LOG_WARNING("SMSG_INITIALIZE_FACTIONS too short: ", packet.getSize(), " bytes"); + break; + } + uint32_t count = packet.readUInt32(); + size_t needed = static_cast(count) * 5; + if (packet.getSize() - packet.getReadPos() < needed) { + LOG_WARNING("SMSG_INITIALIZE_FACTIONS truncated: expected ", needed, + " bytes of faction data, got ", packet.getSize() - packet.getReadPos()); + packet.setReadPos(packet.getSize()); + break; + } + for (uint32_t i = 0; i < count; ++i) { + packet.readUInt8(); + packet.readUInt32(); + } + break; + } case Opcode::SMSG_FEATURE_SYSTEM_STATUS: case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: case Opcode::SMSG_SET_PCT_SPELL_MODIFIER: @@ -1581,6 +1661,15 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; } + case Opcode::SMSG_QUEST_FORCE_REMOVE: { + // Minimal parse: uint32 questId + if (packet.getSize() - packet.getReadPos() < 4) { + LOG_WARNING("SMSG_QUEST_FORCE_REMOVE too short"); + break; + } + /*uint32_t questId =*/ packet.readUInt32(); + break; + } case Opcode::SMSG_QUEST_QUERY_RESPONSE: { // Quest data from server (big packet with title, objectives, rewards, etc.) LOG_INFO("SMSG_QUEST_QUERY_RESPONSE: packet size=", packet.getSize()); diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 88b212b2..ec480124 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -51,8 +51,10 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"SMSG_CHARACTER_LOGIN_FAILED", LogicalOpcode::SMSG_CHARACTER_LOGIN_FAILED}, {"SMSG_PONG", LogicalOpcode::SMSG_PONG}, {"SMSG_LOGIN_VERIFY_WORLD", LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD}, + {"SMSG_INIT_WORLD_STATES", LogicalOpcode::SMSG_INIT_WORLD_STATES}, {"SMSG_LOGIN_SETTIMESPEED", LogicalOpcode::SMSG_LOGIN_SETTIMESPEED}, {"SMSG_TUTORIAL_FLAGS", LogicalOpcode::SMSG_TUTORIAL_FLAGS}, + {"SMSG_INITIALIZE_FACTIONS", LogicalOpcode::SMSG_INITIALIZE_FACTIONS}, {"SMSG_WARDEN_DATA", LogicalOpcode::SMSG_WARDEN_DATA}, {"CMSG_WARDEN_DATA", LogicalOpcode::CMSG_WARDEN_DATA}, {"SMSG_ACCOUNT_DATA_TIMES", LogicalOpcode::SMSG_ACCOUNT_DATA_TIMES}, @@ -72,6 +74,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"CMSG_QUERY_TIME", LogicalOpcode::CMSG_QUERY_TIME}, {"SMSG_QUERY_TIME_RESPONSE", LogicalOpcode::SMSG_QUERY_TIME_RESPONSE}, {"SMSG_FRIEND_STATUS", LogicalOpcode::SMSG_FRIEND_STATUS}, + {"SMSG_CONTACT_LIST", LogicalOpcode::SMSG_CONTACT_LIST}, {"CMSG_ADD_FRIEND", LogicalOpcode::CMSG_ADD_FRIEND}, {"CMSG_DEL_FRIEND", LogicalOpcode::CMSG_DEL_FRIEND}, {"CMSG_SET_CONTACT_NOTES", LogicalOpcode::CMSG_SET_CONTACT_NOTES}, @@ -210,6 +213,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"SMSG_QUESTUPDATE_ADD_KILL", LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL}, {"SMSG_QUESTUPDATE_ADD_ITEM", LogicalOpcode::SMSG_QUESTUPDATE_ADD_ITEM}, {"SMSG_QUESTUPDATE_COMPLETE", LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE}, + {"SMSG_QUEST_FORCE_REMOVE", LogicalOpcode::SMSG_QUEST_FORCE_REMOVE}, {"CMSG_QUEST_QUERY", LogicalOpcode::CMSG_QUEST_QUERY}, {"SMSG_QUEST_QUERY_RESPONSE", LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE}, {"SMSG_QUESTLOG_FULL", LogicalOpcode::SMSG_QUESTLOG_FULL}, @@ -244,6 +248,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"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}, + {"SMSG_CLIENT_CONTROL_UPDATE", LogicalOpcode::SMSG_CLIENT_CONTROL_UPDATE}, {"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}, @@ -400,8 +405,10 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_CHARACTER_LOGIN_FAILED, 0x041}, {LogicalOpcode::SMSG_PONG, 0x1DD}, {LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD, 0x236}, + {LogicalOpcode::SMSG_INIT_WORLD_STATES, 0x2C2}, {LogicalOpcode::SMSG_LOGIN_SETTIMESPEED, 0x042}, {LogicalOpcode::SMSG_TUTORIAL_FLAGS, 0x0FD}, + {LogicalOpcode::SMSG_INITIALIZE_FACTIONS, 0x122}, {LogicalOpcode::SMSG_WARDEN_DATA, 0x2E6}, {LogicalOpcode::CMSG_WARDEN_DATA, 0x2E7}, {LogicalOpcode::SMSG_ACCOUNT_DATA_TIMES, 0x209}, @@ -421,6 +428,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::CMSG_QUERY_TIME, 0x1CE}, {LogicalOpcode::SMSG_QUERY_TIME_RESPONSE, 0x1CF}, {LogicalOpcode::SMSG_FRIEND_STATUS, 0x068}, + {LogicalOpcode::SMSG_CONTACT_LIST, 0x067}, {LogicalOpcode::CMSG_ADD_FRIEND, 0x069}, {LogicalOpcode::CMSG_DEL_FRIEND, 0x06A}, {LogicalOpcode::CMSG_SET_CONTACT_NOTES, 0x06B}, @@ -558,6 +566,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL, 0x196}, {LogicalOpcode::SMSG_QUESTUPDATE_ADD_ITEM, 0x197}, {LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE, 0x195}, + {LogicalOpcode::SMSG_QUEST_FORCE_REMOVE, 0x21E}, {LogicalOpcode::CMSG_QUEST_QUERY, 0x05C}, {LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE, 0x05D}, {LogicalOpcode::SMSG_QUESTLOG_FULL, 0x1A3}, @@ -592,6 +601,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::MSG_MOVE_WORLDPORT_ACK, 0x00DC}, {LogicalOpcode::SMSG_TRANSFER_ABORTED, 0x0040}, {LogicalOpcode::SMSG_FORCE_RUN_SPEED_CHANGE, 0x00E2}, + {LogicalOpcode::SMSG_CLIENT_CONTROL_UPDATE, 0x0159}, {LogicalOpcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK, 0x00E3}, {LogicalOpcode::CMSG_CANCEL_MOUNT_AURA, 0x0375}, {LogicalOpcode::SMSG_SHOWTAXINODES, 0x01A9},