From ec32286b0d45642f5692d819491fdb147389dfd3 Mon Sep 17 00:00:00 2001 From: kelsi davis Date: Sat, 7 Feb 2026 12:58:11 -0800 Subject: [PATCH] Add Tier 1 utility commands: ignore, sit/stand, and logout Ignore Commands: - Add /ignore to block messages from players - Add /unignore to unblock players - Maintain ignoreCache for name-to-GUID lookups - Show confirmation and error messages for ignore actions - Use CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) Sit/Stand/Kneel Commands: - Add /sit to sit down (stand state 1) - Add /stand to stand up (stand state 0) - Add /kneel to kneel (stand state 8) - Instant visual feedback with CMSG_STAND_STATE_CHANGE (0x101) - Support for additional stand states (chair, sleep, etc.) Logout Commands: - Add /logout and /camp to initiate logout with countdown - Add /cancellogout to cancel pending logout - Show "Logging out in 20 seconds..." or "Logout complete" messages - Track logout state with loggingOut_ flag to prevent duplicate requests - Handle instant logout (in inn/city) vs countdown logout - Use opcodes: - CMSG_LOGOUT_REQUEST (0x4B) - CMSG_LOGOUT_CANCEL (0x4E) - SMSG_LOGOUT_RESPONSE (0x4C) - SMSG_LOGOUT_COMPLETE (0x4D) Implementation: - Add LogoutRequestPacket, LogoutCancelPacket builders - Add LogoutResponseParser to parse server logout responses - Add StandStateChangePacket builder for stance changes - Add AddIgnorePacket and DelIgnorePacket for ignore list management - Add handleLogoutResponse() and handleLogoutComplete() handlers - Add ignoreCache map and loggingOut_ state tracking - All commands display feedback in chat window --- include/game/game_handler.hpp | 19 +++++ include/game/opcodes.hpp | 10 +++ include/game/world_packets.hpp | 50 +++++++++++++ src/game/game_handler.cpp | 127 +++++++++++++++++++++++++++++++++ src/game/world_packets.cpp | 48 +++++++++++++ src/ui/game_screen.cpp | 71 ++++++++++++++++++ 6 files changed, 325 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index dfc9b072..dd9afc8c 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -214,10 +214,19 @@ public: void addFriend(const std::string& playerName, const std::string& note = ""); void removeFriend(const std::string& playerName); void setFriendNote(const std::string& playerName, const std::string& note); + void addIgnore(const std::string& playerName); + void removeIgnore(const std::string& playerName); // Random roll void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100); + // Logout commands + void requestLogout(); + void cancelLogout(); + + // Stand state + void setStandState(uint8_t state); // 0=stand, 1=sit, 2=sit_chair, 3=sleep, 4=sit_low_chair, 5=sit_medium_chair, 6=sit_high_chair, 7=dead, 8=kneel, 9=submerged + // ---- Phase 1: Name queries ---- void queryPlayerName(uint64_t guid); void queryCreatureInfo(uint32_t entry, uint64_t guid); @@ -525,6 +534,10 @@ private: void handleFriendStatus(network::Packet& packet); void handleRandomRoll(network::Packet& packet); + // ---- Logout handlers ---- + void handleLogoutResponse(network::Packet& packet); + void handleLogoutComplete(network::Packet& packet); + void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource); void addSystemChatMessage(const std::string& message); @@ -607,6 +620,12 @@ private: // ---- Friend list cache ---- std::unordered_map friendsCache; // name -> guid + // ---- Ignore list cache ---- + std::unordered_map ignoreCache; // name -> guid + + // ---- Logout state ---- + bool loggingOut_ = false; + // ---- Online item tracking ---- struct OnlineItemInfo { uint32_t entry = 0; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index a4fc6978..26a623c0 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -71,6 +71,16 @@ enum class Opcode : uint16_t { 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, + // ---- Random Roll ---- MSG_RANDOM_ROLL = 0x1FB, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 99d07d66..347e0d3e 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -736,6 +736,56 @@ public: static bool parse(network::Packet& packet, FriendStatusData& data); }; +/** CMSG_ADD_IGNORE packet builder */ +class AddIgnorePacket { +public: + static network::Packet build(const std::string& playerName); +}; + +/** CMSG_DEL_IGNORE packet builder */ +class DelIgnorePacket { +public: + static network::Packet build(uint64_t ignoreGuid); +}; + +// ============================================================ +// Logout Commands +// ============================================================ + +/** CMSG_LOGOUT_REQUEST packet builder */ +class LogoutRequestPacket { +public: + static network::Packet build(); +}; + +/** CMSG_LOGOUT_CANCEL packet builder */ +class LogoutCancelPacket { +public: + static network::Packet build(); +}; + +/** SMSG_LOGOUT_RESPONSE data */ +struct LogoutResponseData { + uint32_t result = 0; // 0 = success, 1 = failure + uint8_t instant = 0; // 1 = instant logout +}; + +/** SMSG_LOGOUT_RESPONSE parser */ +class LogoutResponseParser { +public: + static bool parse(network::Packet& packet, LogoutResponseData& data); +}; + +// ============================================================ +// Stand State +// ============================================================ + +/** CMSG_STAND_STATE_CHANGE packet builder */ +class StandStateChangePacket { +public: + static network::Packet build(uint8_t state); +}; + // ============================================================ // Random Roll // ============================================================ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4af697f6..501a7bb2 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -317,6 +317,14 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_LOGOUT_RESPONSE: + handleLogoutResponse(packet); + break; + + case Opcode::SMSG_LOGOUT_COMPLETE: + handleLogoutComplete(packet); + break; + // ---- Phase 1: Foundation ---- case Opcode::SMSG_NAME_QUERY_RESPONSE: handleNameQueryResponse(packet); @@ -1679,6 +1687,95 @@ void GameHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) { LOG_INFO("Rolled ", minRoll, "-", maxRoll); } +void GameHandler::addIgnore(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot add ignore: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name."); + return; + } + + auto packet = AddIgnorePacket::build(playerName); + socket->send(packet); + addSystemChatMessage("Adding " + playerName + " to ignore list..."); + LOG_INFO("Sent ignore request for: ", playerName); +} + +void GameHandler::removeIgnore(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot remove ignore: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name."); + return; + } + + // Look up GUID from cache + auto it = ignoreCache.find(playerName); + if (it == ignoreCache.end()) { + addSystemChatMessage(playerName + " is not in your ignore list."); + LOG_WARNING("Ignored player not found in cache: ", playerName); + return; + } + + auto packet = DelIgnorePacket::build(it->second); + socket->send(packet); + addSystemChatMessage("Removing " + playerName + " from ignore list..."); + ignoreCache.erase(it); + LOG_INFO("Sent remove ignore request for: ", playerName, " (GUID: 0x", std::hex, it->second, std::dec, ")"); +} + +void GameHandler::requestLogout() { + if (!socket) { + LOG_WARNING("Cannot logout: not connected"); + return; + } + + if (loggingOut_) { + addSystemChatMessage("Already logging out."); + return; + } + + auto packet = LogoutRequestPacket::build(); + socket->send(packet); + loggingOut_ = true; + LOG_INFO("Sent logout request"); +} + +void GameHandler::cancelLogout() { + if (!socket) { + LOG_WARNING("Cannot cancel logout: not connected"); + return; + } + + if (!loggingOut_) { + addSystemChatMessage("Not currently logging out."); + return; + } + + auto packet = LogoutCancelPacket::build(); + socket->send(packet); + loggingOut_ = false; + addSystemChatMessage("Logout cancelled."); + LOG_INFO("Cancelled logout"); +} + +void GameHandler::setStandState(uint8_t standState) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot change stand state: not in world or not connected"); + return; + } + + auto packet = StandStateChangePacket::build(standState); + socket->send(packet); + LOG_INFO("Changed stand state to: ", (int)standState); +} + void GameHandler::releaseSpirit() { if (!playerDead_) return; if (socket && state == WorldState::IN_WORLD) { @@ -3123,6 +3220,36 @@ void GameHandler::handleRandomRoll(network::Packet& packet) { LOG_INFO("Random roll: ", rollerName, " rolled ", data.result, " (", data.minRoll, "-", data.maxRoll, ")"); } +void GameHandler::handleLogoutResponse(network::Packet& packet) { + LogoutResponseData data; + if (!LogoutResponseParser::parse(packet, data)) { + LOG_WARNING("Failed to parse SMSG_LOGOUT_RESPONSE"); + return; + } + + if (data.result == 0) { + // Success - logout initiated + if (data.instant) { + addSystemChatMessage("Logging out..."); + } else { + addSystemChatMessage("Logging out in 20 seconds..."); + } + LOG_INFO("Logout response: success, instant=", (int)data.instant); + } else { + // Failure + addSystemChatMessage("Cannot logout right now."); + loggingOut_ = false; + LOG_WARNING("Logout failed, result=", data.result); + } +} + +void GameHandler::handleLogoutComplete(network::Packet& /*packet*/) { + addSystemChatMessage("Logout complete."); + loggingOut_ = false; + LOG_INFO("Logout complete"); + // Server will disconnect us +} + uint32_t GameHandler::generateClientSeed() { // Generate cryptographically random seed std::random_device rd; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 7c1b1627..6f3982f3 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1268,6 +1268,54 @@ bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data) return true; } +network::Packet AddIgnorePacket::build(const std::string& playerName) { + network::Packet packet(static_cast(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(Opcode::CMSG_DEL_IGNORE)); + packet.writeUInt64(ignoreGuid); + LOG_DEBUG("Built CMSG_DEL_IGNORE: guid=0x", std::hex, ignoreGuid, std::dec); + return packet; +} + +// ============================================================ +// Logout Commands +// ============================================================ + +network::Packet LogoutRequestPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_LOGOUT_REQUEST)); + LOG_DEBUG("Built CMSG_LOGOUT_REQUEST"); + return packet; +} + +network::Packet LogoutCancelPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_LOGOUT_CANCEL)); + LOG_DEBUG("Built CMSG_LOGOUT_CANCEL"); + return packet; +} + +bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& data) { + data.result = packet.readUInt32(); + data.instant = packet.readUInt8(); + LOG_DEBUG("Parsed SMSG_LOGOUT_RESPONSE: result=", data.result, " instant=", (int)data.instant); + return true; +} + +// ============================================================ +// Stand State +// ============================================================ + +network::Packet StandStateChangePacket::build(uint8_t state) { + network::Packet packet(static_cast(Opcode::CMSG_STAND_STATE_CHANGE)); + packet.writeUInt32(state); + LOG_DEBUG("Built CMSG_STAND_STATE_CHANGE: state=", (int)state); + return packet; +} + // ============================================================ // Random Roll // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 9ee7e323..e67eafe2 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1079,6 +1079,77 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /ignore command + if (cmdLower == "ignore") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.addIgnore(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /ignore "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /unignore command + if (cmdLower == "unignore") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.removeIgnore(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /unignore "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /sit command + if (cmdLower == "sit") { + gameHandler.setStandState(1); // 1 = sit + chatInputBuffer[0] = '\0'; + return; + } + + // /stand command + if (cmdLower == "stand") { + gameHandler.setStandState(0); // 0 = stand + chatInputBuffer[0] = '\0'; + return; + } + + // /kneel command + if (cmdLower == "kneel") { + gameHandler.setStandState(8); // 8 = kneel + chatInputBuffer[0] = '\0'; + return; + } + + // /logout command (already exists but using /logout instead of going to login) + if (cmdLower == "logout" || cmdLower == "camp") { + gameHandler.requestLogout(); + chatInputBuffer[0] = '\0'; + return; + } + + // /cancellogout command + if (cmdLower == "cancellogout") { + gameHandler.cancelLogout(); + chatInputBuffer[0] = '\0'; + return; + } + // Chat channel slash commands bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") {