From 9b60108fa62d968b0aba29dd6e716c15faced41e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 22:07:03 -0700 Subject: [PATCH] feat: handle SMSG_MEETINGSTONE, LFG timeout, SMSG_WHOIS, and SMSG_MIRRORIMAGE_DATA Add handlers for 14 previously-unhandled server opcodes: LFG error/timeout states (WotLK Dungeon Finder): - SMSG_LFG_TIMEDOUT: invite timed out, shows message and re-opens LFG UI - SMSG_LFG_OTHER_TIMEDOUT: another player's response timed out - SMSG_LFG_AUTOJOIN_FAILED: auto-join failed with reason code - SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER: no players available for auto-join - SMSG_LFG_LEADER_IS_LFM: party leader is in LFM mode Meeting Stone (Classic/TBC era group-finding feature): - SMSG_MEETINGSTONE_SETQUEUE: shows zone and level range in chat - SMSG_MEETINGSTONE_COMPLETE: group ready notification - SMSG_MEETINGSTONE_IN_PROGRESS: search ongoing notification - SMSG_MEETINGSTONE_MEMBER_ADDED: player name resolved and shown in chat - SMSG_MEETINGSTONE_JOINFAILED: localized error message (4 reason codes) - SMSG_MEETINGSTONE_LEAVE: queue departure notification Other: - SMSG_WHOIS: displays GM /whois result line-by-line in system chat - SMSG_MIRRORIMAGE_DATA: parses WotLK mirror image unit display ID and applies it to the entity so mirror images render with correct appearance --- src/game/game_handler.cpp | 162 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index aa834bfe..fd0f4b56 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1646,6 +1646,29 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_WHOIS: { + // GM/admin response to /whois command: cstring with account/IP info + // Format: string (the whois result text, typically "Name: ...\nAccount: ...\nIP: ...") + if (packet.getReadPos() < packet.getSize()) { + std::string whoisText = packet.readString(); + if (!whoisText.empty()) { + // Display each line of the whois response in system chat + std::string line; + for (char c : whoisText) { + if (c == '\n') { + if (!line.empty()) addSystemChatMessage("[Whois] " + line); + line.clear(); + } else { + line += c; + } + } + if (!line.empty()) addSystemChatMessage("[Whois] " + line); + LOG_INFO("SMSG_WHOIS: ", whoisText); + } + } + break; + } + case Opcode::SMSG_FRIEND_STATUS: if (state == WorldState::IN_WORLD) { handleFriendStatus(packet); @@ -5609,6 +5632,110 @@ void GameHandler::handlePacket(network::Packet& packet) { packet.setReadPos(packet.getSize()); break; + // ---- LFG error/timeout states ---- + case Opcode::SMSG_LFG_TIMEDOUT: + // Server-side LFG invite timed out (no response within time limit) + addSystemChatMessage("Dungeon Finder: Invite timed out."); + if (openLfgCallback_) openLfgCallback_(); + packet.setReadPos(packet.getSize()); + break; + case Opcode::SMSG_LFG_OTHER_TIMEDOUT: + // Another party member failed to respond to a LFG role-check in time + addSystemChatMessage("Dungeon Finder: Another player's invite timed out."); + if (openLfgCallback_) openLfgCallback_(); + packet.setReadPos(packet.getSize()); + break; + case Opcode::SMSG_LFG_AUTOJOIN_FAILED: { + // uint32 result — LFG auto-join attempt failed (player selected auto-join at queue time) + if (packet.getSize() - packet.getReadPos() >= 4) { + uint32_t result = packet.readUInt32(); + (void)result; + } + addSystemChatMessage("Dungeon Finder: Auto-join failed."); + packet.setReadPos(packet.getSize()); + break; + } + case Opcode::SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER: + // No eligible players found for auto-join + addSystemChatMessage("Dungeon Finder: No players available for auto-join."); + packet.setReadPos(packet.getSize()); + break; + case Opcode::SMSG_LFG_LEADER_IS_LFM: + // Party leader is currently set to Looking for More (LFM) mode + addSystemChatMessage("Your party leader is currently Looking for More."); + packet.setReadPos(packet.getSize()); + break; + + // ---- Meeting stone (Classic/TBC group-finding via summon stone) ---- + case Opcode::SMSG_MEETINGSTONE_SETQUEUE: { + // uint32 zoneId + uint8 level_min + uint8 level_max — player queued for meeting stone + if (packet.getSize() - packet.getReadPos() >= 6) { + uint32_t zoneId = packet.readUInt32(); + uint8_t levelMin = packet.readUInt8(); + uint8_t levelMax = packet.readUInt8(); + char buf[128]; + std::snprintf(buf, sizeof(buf), + "You are now in the Meeting Stone queue for zone %u (levels %u-%u).", + zoneId, levelMin, levelMax); + addSystemChatMessage(buf); + LOG_INFO("SMSG_MEETINGSTONE_SETQUEUE: zone=", zoneId, + " levels=", (int)levelMin, "-", (int)levelMax); + } + packet.setReadPos(packet.getSize()); + break; + } + case Opcode::SMSG_MEETINGSTONE_COMPLETE: + // Server confirms group found and teleport summon is ready + addSystemChatMessage("Meeting Stone: Your group is ready! Use the Meeting Stone to summon."); + LOG_INFO("SMSG_MEETINGSTONE_COMPLETE"); + packet.setReadPos(packet.getSize()); + break; + case Opcode::SMSG_MEETINGSTONE_IN_PROGRESS: + // Meeting stone search is still ongoing + addSystemChatMessage("Meeting Stone: Searching for group members..."); + LOG_DEBUG("SMSG_MEETINGSTONE_IN_PROGRESS"); + packet.setReadPos(packet.getSize()); + break; + case Opcode::SMSG_MEETINGSTONE_MEMBER_ADDED: { + // uint64 memberGuid — a player was added to your group via meeting stone + if (packet.getSize() - packet.getReadPos() >= 8) { + uint64_t memberGuid = packet.readUInt64(); + auto nit = playerNameCache.find(memberGuid); + if (nit != playerNameCache.end() && !nit->second.empty()) { + addSystemChatMessage("Meeting Stone: " + nit->second + + " has been added to your group."); + } else { + addSystemChatMessage("Meeting Stone: A new player has been added to your group."); + } + LOG_INFO("SMSG_MEETINGSTONE_MEMBER_ADDED: guid=0x", std::hex, memberGuid, std::dec); + } + break; + } + case Opcode::SMSG_MEETINGSTONE_JOINFAILED: { + // uint8 reason — failed to join group via meeting stone + // 0=target_not_in_lfg, 1=target_in_party, 2=target_invalid_map, 3=target_not_available + static const char* kMeetingstoneErrors[] = { + "Target player is not using the Meeting Stone.", + "Target player is already in a group.", + "You are not in a valid zone for that Meeting Stone.", + "Target player is not available.", + }; + if (packet.getSize() - packet.getReadPos() >= 1) { + uint8_t reason = packet.readUInt8(); + const char* msg = (reason < 4) ? kMeetingstoneErrors[reason] + : "Meeting Stone: Could not join group."; + addSystemChatMessage(msg); + LOG_INFO("SMSG_MEETINGSTONE_JOINFAILED: reason=", (int)reason); + } + break; + } + case Opcode::SMSG_MEETINGSTONE_LEAVE: + // Player was removed from the meeting stone queue (left, or group disbanded) + addSystemChatMessage("You have left the Meeting Stone queue."); + LOG_DEBUG("SMSG_MEETINGSTONE_LEAVE"); + packet.setReadPos(packet.getSize()); + break; + // ---- GM Ticket responses ---- case Opcode::SMSG_GMTICKET_CREATE: { if (packet.getSize() - packet.getReadPos() >= 1) { @@ -6596,6 +6723,41 @@ void GameHandler::handlePacket(network::Packet& packet) { packet.setReadPos(packet.getSize()); break; + // ---- Mirror image data (WotLK: Mage ability Mirror Image) ---- + case Opcode::SMSG_MIRRORIMAGE_DATA: { + // WotLK 3.3.5a format: + // uint64 mirrorGuid — GUID of the mirror image unit + // uint32 displayId — display ID to render the image with + // uint8 raceId — race of caster + // uint8 genderFlag — gender of caster + // uint8 classId — class of caster + // uint64 casterGuid — GUID of the player who cast the spell + // Followed by equipped item display IDs (11 × uint32) if casterGuid != 0 + // Purpose: tells client how to render the image (same appearance as caster). + // We parse the GUIDs so units render correctly via their existing display IDs. + if (packet.getSize() - packet.getReadPos() < 8) break; + uint64_t mirrorGuid = packet.readUInt64(); + if (packet.getSize() - packet.getReadPos() < 4) break; + uint32_t displayId = packet.readUInt32(); + if (packet.getSize() - packet.getReadPos() < 3) break; + /*uint8_t raceId =*/ packet.readUInt8(); + /*uint8_t gender =*/ packet.readUInt8(); + /*uint8_t classId =*/ packet.readUInt8(); + // Apply display ID to the mirror image unit so it renders correctly + if (mirrorGuid != 0 && displayId != 0) { + auto entity = entityManager.getEntity(mirrorGuid); + if (entity) { + auto unit = std::dynamic_pointer_cast(entity); + if (unit && unit->getDisplayId() == 0) + unit->setDisplayId(displayId); + } + } + LOG_DEBUG("SMSG_MIRRORIMAGE_DATA: mirrorGuid=0x", std::hex, mirrorGuid, + " displayId=", std::dec, displayId); + packet.setReadPos(packet.getSize()); + break; + } + // ---- Player movement flag changes (server-pushed) ---- case Opcode::SMSG_MOVE_GRAVITY_DISABLE: handleForceMoveFlagChange(packet, "GRAVITY_DISABLE", Opcode::CMSG_MOVE_GRAVITY_DISABLE_ACK,