From f9c4cbddee4309187665babbc0c069e60dc58340 Mon Sep 17 00:00:00 2001 From: kelsi davis Date: Sat, 7 Feb 2026 12:43:32 -0800 Subject: [PATCH] Add server info commands: /time, /played, and /who - Add CMSG_QUERY_TIME (0x1CE) and SMSG_QUERY_TIME_RESPONSE (0x1CF) opcodes - Add CMSG_REQUEST_PLAYED_TIME (0x1CC) and SMSG_PLAYED_TIME (0x1CD) opcodes - Add CMSG_WHO (0x062) and SMSG_WHO (0x063) opcodes - Implement /time command to query and display server time - Implement /played command to show total and level playtime statistics - Implement /who [name] command to list online players with level and guild - Add packet builders: QueryTimePacket, RequestPlayedTimePacket, WhoPacket - Add response parsers for all three server info packet types - Add handlers that format and display responses in chat as system messages - Format played time as "X days, Y hours, Z minutes" for readability - Format server time as "YYYY-MM-DD HH:MM:SS" for readability --- include/game/game_handler.hpp | 10 +++ include/game/opcodes.hpp | 8 ++ include/game/world_packets.hpp | 52 ++++++++++++ src/game/game_handler.cpp | 143 +++++++++++++++++++++++++++++++++ src/game/world_packets.cpp | 49 +++++++++++ src/ui/game_screen.cpp | 25 ++++++ 6 files changed, 287 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 064ec58d..5ee20627 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -205,6 +205,11 @@ public: // Inspection void inspectTarget(); + // Server info commands + void queryServerTime(); + void requestPlayedTime(); + void queryWho(const std::string& playerName = ""); + // ---- Phase 1: Name queries ---- void queryPlayerName(uint64_t guid); void queryCreatureInfo(uint32_t entry, uint64_t guid); @@ -503,6 +508,11 @@ private: void handleListInventory(network::Packet& packet); void addMoneyCopper(uint32_t amount); + // ---- Server info handlers ---- + void handleQueryTimeResponse(network::Packet& packet); + void handlePlayedTime(network::Packet& packet); + void handleWho(network::Packet& packet); + void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource); void addSystemChatMessage(const std::string& message); diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index b0f01bb2..217552de 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -55,6 +55,14 @@ enum class Opcode : uint16_t { CMSG_MESSAGECHAT = 0x095, SMSG_MESSAGECHAT = 0x096, + // ---- Server Info Commands ---- + CMSG_WHO = 0x062, + SMSG_WHO = 0x063, + CMSG_REQUEST_PLAYED_TIME = 0x1CC, + SMSG_PLAYED_TIME = 0x1CD, + CMSG_QUERY_TIME = 0x1CE, + SMSG_QUERY_TIME_RESPONSE = 0x1CF, + // ---- Phase 1: Foundation (Targeting, Queries) ---- CMSG_SET_SELECTION = 0x13D, CMSG_NAME_QUERY = 0x050, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 70900c78..2f38c253 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -648,6 +648,58 @@ public: */ const char* getChatTypeString(ChatType type); +// ============================================================ +// Server Info Commands +// ============================================================ + +/** CMSG_QUERY_TIME packet builder */ +class QueryTimePacket { +public: + static network::Packet build(); +}; + +/** SMSG_QUERY_TIME_RESPONSE data */ +struct QueryTimeResponseData { + uint32_t serverTime = 0; // Unix timestamp + uint32_t timeOffset = 0; // Time until next daily reset +}; + +/** SMSG_QUERY_TIME_RESPONSE parser */ +class QueryTimeResponseParser { +public: + static bool parse(network::Packet& packet, QueryTimeResponseData& data); +}; + +/** CMSG_REQUEST_PLAYED_TIME packet builder */ +class RequestPlayedTimePacket { +public: + static network::Packet build(bool sendToChat = true); +}; + +/** SMSG_PLAYED_TIME data */ +struct PlayedTimeData { + uint32_t totalTimePlayed = 0; // Total seconds played + uint32_t levelTimePlayed = 0; // Seconds played at current level + bool triggerMessage = false; // Whether to show in chat +}; + +/** SMSG_PLAYED_TIME parser */ +class PlayedTimeParser { +public: + static bool parse(network::Packet& packet, PlayedTimeData& data); +}; + +/** CMSG_WHO packet builder */ +class WhoPacket { +public: + static network::Packet build(uint32_t minLevel = 0, uint32_t maxLevel = 0, + const std::string& playerName = "", + const std::string& guildName = "", + uint32_t raceMask = 0xFFFFFFFF, + uint32_t classMask = 0xFFFFFFFF, + uint32_t zones = 0); +}; + // ============================================================ // Phase 1: Foundation — Targeting, Name Queries // ============================================================ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d53b3f9b..0d062da1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -286,6 +287,24 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_QUERY_TIME_RESPONSE: + if (state == WorldState::IN_WORLD) { + handleQueryTimeResponse(packet); + } + break; + + case Opcode::SMSG_PLAYED_TIME: + if (state == WorldState::IN_WORLD) { + handlePlayedTime(packet); + } + break; + + case Opcode::SMSG_WHO: + if (state == WorldState::IN_WORLD) { + handleWho(packet); + } + break; + // ---- Phase 1: Foundation ---- case Opcode::SMSG_NAME_QUERY_RESPONSE: handleNameQueryResponse(packet); @@ -1530,6 +1549,39 @@ void GameHandler::inspectTarget() { LOG_INFO("Sent inspect request for player: ", name, " (GUID: 0x", std::hex, targetGuid, std::dec, ")"); } +void GameHandler::queryServerTime() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot query time: not in world or not connected"); + return; + } + + auto packet = QueryTimePacket::build(); + socket->send(packet); + LOG_INFO("Requested server time"); +} + +void GameHandler::requestPlayedTime() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot request played time: not in world or not connected"); + return; + } + + auto packet = RequestPlayedTimePacket::build(true); + socket->send(packet); + LOG_INFO("Requested played time"); +} + +void GameHandler::queryWho(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot query who: not in world or not connected"); + return; + } + + auto packet = WhoPacket::build(0, 0, playerName); + socket->send(packet); + LOG_INFO("Sent WHO query", playerName.empty() ? "" : " for: " + playerName); +} + void GameHandler::releaseSpirit() { if (!playerDead_) return; if (socket && state == WorldState::IN_WORLD) { @@ -2792,6 +2844,97 @@ void GameHandler::addSystemChatMessage(const std::string& message) { addLocalChatMessage(msg); } +// ============================================================ +// Server Info Command Handlers +// ============================================================ + +void GameHandler::handleQueryTimeResponse(network::Packet& packet) { + QueryTimeResponseData data; + if (!QueryTimeResponseParser::parse(packet, data)) { + LOG_WARNING("Failed to parse SMSG_QUERY_TIME_RESPONSE"); + return; + } + + // Convert Unix timestamp to readable format + time_t serverTime = static_cast(data.serverTime); + struct tm* timeInfo = localtime(&serverTime); + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", timeInfo); + + std::string msg = "Server time: " + std::string(timeStr); + addSystemChatMessage(msg); + LOG_INFO("Server time: ", data.serverTime, " (", timeStr, ")"); +} + +void GameHandler::handlePlayedTime(network::Packet& packet) { + PlayedTimeData data; + if (!PlayedTimeParser::parse(packet, data)) { + LOG_WARNING("Failed to parse SMSG_PLAYED_TIME"); + return; + } + + if (data.triggerMessage) { + // Format total time played + uint32_t totalDays = data.totalTimePlayed / 86400; + uint32_t totalHours = (data.totalTimePlayed % 86400) / 3600; + uint32_t totalMinutes = (data.totalTimePlayed % 3600) / 60; + + // Format level time played + uint32_t levelDays = data.levelTimePlayed / 86400; + uint32_t levelHours = (data.levelTimePlayed % 86400) / 3600; + uint32_t levelMinutes = (data.levelTimePlayed % 3600) / 60; + + std::string totalMsg = "Total time played: "; + if (totalDays > 0) totalMsg += std::to_string(totalDays) + " days, "; + if (totalHours > 0 || totalDays > 0) totalMsg += std::to_string(totalHours) + " hours, "; + totalMsg += std::to_string(totalMinutes) + " minutes"; + + std::string levelMsg = "Time played this level: "; + if (levelDays > 0) levelMsg += std::to_string(levelDays) + " days, "; + if (levelHours > 0 || levelDays > 0) levelMsg += std::to_string(levelHours) + " hours, "; + levelMsg += std::to_string(levelMinutes) + " minutes"; + + addSystemChatMessage(totalMsg); + addSystemChatMessage(levelMsg); + } + + LOG_INFO("Played time: total=", data.totalTimePlayed, "s, level=", data.levelTimePlayed, "s"); +} + +void GameHandler::handleWho(network::Packet& packet) { + // Parse WHO response + uint32_t displayCount = packet.readUInt32(); + uint32_t onlineCount = packet.readUInt32(); + + LOG_INFO("WHO response: ", displayCount, " players displayed, ", onlineCount, " total online"); + + if (displayCount == 0) { + addSystemChatMessage("No players found."); + return; + } + + addSystemChatMessage(std::to_string(onlineCount) + " player(s) online:"); + + for (uint32_t i = 0; i < displayCount; ++i) { + std::string playerName = packet.readString(); + std::string guildName = packet.readString(); + uint32_t level = packet.readUInt32(); + uint32_t classId = packet.readUInt32(); + uint32_t raceId = packet.readUInt32(); + packet.readUInt8(); // gender (unused) + packet.readUInt32(); // zoneId (unused) + + std::string msg = " " + playerName; + if (!guildName.empty()) { + msg += " <" + guildName + ">"; + } + msg += " - Level " + std::to_string(level); + + addSystemChatMessage(msg); + LOG_INFO(" ", playerName, " (", guildName, ") Lv", level, " Class:", classId, " Race:", raceId); + } +} + 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 b096d40f..20bca0c3 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1181,6 +1181,55 @@ network::Packet InspectPacket::build(uint64_t targetGuid) { return packet; } +// ============================================================ +// Server Info Commands +// ============================================================ + +network::Packet QueryTimePacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_QUERY_TIME)); + LOG_DEBUG("Built CMSG_QUERY_TIME"); + return packet; +} + +bool QueryTimeResponseParser::parse(network::Packet& packet, QueryTimeResponseData& data) { + data.serverTime = packet.readUInt32(); + data.timeOffset = packet.readUInt32(); + LOG_DEBUG("Parsed SMSG_QUERY_TIME_RESPONSE: time=", data.serverTime, " offset=", data.timeOffset); + return true; +} + +network::Packet RequestPlayedTimePacket::build(bool sendToChat) { + network::Packet packet(static_cast(Opcode::CMSG_REQUEST_PLAYED_TIME)); + packet.writeUInt8(sendToChat ? 1 : 0); + LOG_DEBUG("Built CMSG_REQUEST_PLAYED_TIME: sendToChat=", sendToChat); + return packet; +} + +bool PlayedTimeParser::parse(network::Packet& packet, PlayedTimeData& data) { + data.totalTimePlayed = packet.readUInt32(); + data.levelTimePlayed = packet.readUInt32(); + data.triggerMessage = packet.readUInt8() != 0; + LOG_DEBUG("Parsed SMSG_PLAYED_TIME: total=", data.totalTimePlayed, " level=", data.levelTimePlayed); + return true; +} + +network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel, + const std::string& playerName, + const std::string& guildName, + uint32_t raceMask, uint32_t classMask, + uint32_t zones) { + network::Packet packet(static_cast(Opcode::CMSG_WHO)); + packet.writeUInt32(minLevel); + packet.writeUInt32(maxLevel); + packet.writeString(playerName); + packet.writeString(guildName); + packet.writeUInt32(raceMask); + packet.writeUInt32(classMask); + packet.writeUInt32(zones); // Number of zones + LOG_DEBUG("Built CMSG_WHO: player=", playerName); + return packet; +} + network::Packet NameQueryPacket::build(uint64_t playerGuid) { network::Packet packet(static_cast(Opcode::CMSG_NAME_QUERY)); packet.writeUInt64(playerGuid); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 1740ed79..e0b1c65b 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -963,6 +963,31 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /time command + if (cmdLower == "time") { + gameHandler.queryServerTime(); + chatInputBuffer[0] = '\0'; + return; + } + + // /played command + if (cmdLower == "played") { + gameHandler.requestPlayedTime(); + chatInputBuffer[0] = '\0'; + return; + } + + // /who command + if (cmdLower == "who") { + std::string playerName; + if (spacePos != std::string::npos) { + playerName = command.substr(spacePos + 1); + } + gameHandler.queryWho(playerName); + chatInputBuffer[0] = '\0'; + return; + } + // Chat channel slash commands bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") {