mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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
This commit is contained in:
parent
8b8e32e716
commit
f9c4cbddee
6 changed files with 287 additions and 0 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <ctime>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
|
@ -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<time_t>(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;
|
||||
|
|
|
|||
|
|
@ -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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(Opcode::CMSG_NAME_QUERY));
|
||||
packet.writeUInt64(playerGuid);
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue