diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5ee20627..dfc9b072 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -210,6 +210,14 @@ public: void requestPlayedTime(); void queryWho(const std::string& playerName = ""); + // Social commands + 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); + + // Random roll + void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100); + // ---- Phase 1: Name queries ---- void queryPlayerName(uint64_t guid); void queryCreatureInfo(uint32_t entry, uint64_t guid); @@ -513,6 +521,10 @@ private: void handlePlayedTime(network::Packet& packet); void handleWho(network::Packet& packet); + // ---- Social handlers ---- + void handleFriendStatus(network::Packet& packet); + void handleRandomRoll(network::Packet& packet); + void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource); void addSystemChatMessage(const std::string& message); @@ -592,6 +604,9 @@ private: std::unordered_map creatureInfoCache; std::unordered_set pendingCreatureQueries; + // ---- Friend list cache ---- + std::unordered_map friendsCache; // name -> guid + // ---- Online item tracking ---- struct OnlineItemInfo { uint32_t entry = 0; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 217552de..a4fc6978 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -63,6 +63,17 @@ enum class Opcode : uint16_t { CMSG_QUERY_TIME = 0x1CE, SMSG_QUERY_TIME_RESPONSE = 0x1CF, + // ---- Social Commands ---- + SMSG_FRIEND_STATUS = 0x068, + CMSG_ADD_FRIEND = 0x069, + CMSG_DEL_FRIEND = 0x06A, + CMSG_SET_CONTACT_NOTES = 0x06B, + CMSG_ADD_IGNORE = 0x06C, + CMSG_DEL_IGNORE = 0x06D, + + // ---- Random Roll ---- + MSG_RANDOM_ROLL = 0x1FB, + // ---- 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 2f38c253..99d07d66 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -700,6 +700,67 @@ public: uint32_t zones = 0); }; +// ============================================================ +// Social Commands +// ============================================================ + +/** CMSG_ADD_FRIEND packet builder */ +class AddFriendPacket { +public: + static network::Packet build(const std::string& playerName, const std::string& note = ""); +}; + +/** CMSG_DEL_FRIEND packet builder */ +class DelFriendPacket { +public: + static network::Packet build(uint64_t friendGuid); +}; + +/** CMSG_SET_CONTACT_NOTES packet builder */ +class SetContactNotesPacket { +public: + static network::Packet build(uint64_t friendGuid, const std::string& note); +}; + +/** SMSG_FRIEND_STATUS data */ +struct FriendStatusData { + uint8_t status = 0; // 0 = offline, 1 = online, etc. + uint64_t guid = 0; + std::string note; + uint8_t chatFlag = 0; +}; + +/** SMSG_FRIEND_STATUS parser */ +class FriendStatusParser { +public: + static bool parse(network::Packet& packet, FriendStatusData& data); +}; + +// ============================================================ +// Random Roll +// ============================================================ + +/** CMSG_RANDOM_ROLL packet builder */ +class RandomRollPacket { +public: + static network::Packet build(uint32_t minRoll, uint32_t maxRoll); +}; + +/** SMSG_RANDOM_ROLL data */ +struct RandomRollData { + uint64_t rollerGuid = 0; + uint64_t targetGuid = 0; // 0 for party roll + uint32_t minRoll = 0; + uint32_t maxRoll = 0; + uint32_t result = 0; +}; + +/** SMSG_RANDOM_ROLL parser */ +class RandomRollParser { +public: + static bool parse(network::Packet& packet, RandomRollData& data); +}; + // ============================================================ // Phase 1: Foundation — Targeting, Name Queries // ============================================================ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0d062da1..4af697f6 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -305,6 +305,18 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_FRIEND_STATUS: + if (state == WorldState::IN_WORLD) { + handleFriendStatus(packet); + } + break; + + case Opcode::MSG_RANDOM_ROLL: + if (state == WorldState::IN_WORLD) { + handleRandomRoll(packet); + } + break; + // ---- Phase 1: Foundation ---- case Opcode::SMSG_NAME_QUERY_RESPONSE: handleNameQueryResponse(packet); @@ -1582,6 +1594,91 @@ void GameHandler::queryWho(const std::string& playerName) { LOG_INFO("Sent WHO query", playerName.empty() ? "" : " for: " + playerName); } +void GameHandler::addFriend(const std::string& playerName, const std::string& note) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot add friend: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name."); + return; + } + + auto packet = AddFriendPacket::build(playerName, note); + socket->send(packet); + addSystemChatMessage("Sending friend request to " + playerName + "..."); + LOG_INFO("Sent friend request to: ", playerName); +} + +void GameHandler::removeFriend(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot remove friend: 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 = friendsCache.find(playerName); + if (it == friendsCache.end()) { + addSystemChatMessage(playerName + " is not in your friends list."); + LOG_WARNING("Friend not found in cache: ", playerName); + return; + } + + auto packet = DelFriendPacket::build(it->second); + socket->send(packet); + addSystemChatMessage("Removing " + playerName + " from friends list..."); + LOG_INFO("Sent remove friend request for: ", playerName, " (GUID: 0x", std::hex, it->second, std::dec, ")"); +} + +void GameHandler::setFriendNote(const std::string& playerName, const std::string& note) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot set friend note: 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 = friendsCache.find(playerName); + if (it == friendsCache.end()) { + addSystemChatMessage(playerName + " is not in your friends list."); + return; + } + + auto packet = SetContactNotesPacket::build(it->second, note); + socket->send(packet); + addSystemChatMessage("Updated note for " + playerName); + LOG_INFO("Set friend note for: ", playerName); +} + +void GameHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot roll: not in world or not connected"); + return; + } + + if (minRoll > maxRoll) { + std::swap(minRoll, maxRoll); + } + + if (maxRoll > 10000) { + maxRoll = 10000; // Cap at reasonable value + } + + auto packet = RandomRollPacket::build(minRoll, maxRoll); + socket->send(packet); + LOG_INFO("Rolled ", minRoll, "-", maxRoll); +} + void GameHandler::releaseSpirit() { if (!playerDead_) return; if (socket && state == WorldState::IN_WORLD) { @@ -2935,6 +3032,97 @@ void GameHandler::handleWho(network::Packet& packet) { } } +void GameHandler::handleFriendStatus(network::Packet& packet) { + FriendStatusData data; + if (!FriendStatusParser::parse(packet, data)) { + LOG_WARNING("Failed to parse SMSG_FRIEND_STATUS"); + return; + } + + // Look up player name from GUID + std::string playerName; + auto it = playerNameCache.find(data.guid); + if (it != playerNameCache.end()) { + playerName = it->second; + } else { + playerName = "Unknown"; + } + + // Update friends cache + if (data.status == 1 || data.status == 2) { // Added or online + friendsCache[playerName] = data.guid; + } else if (data.status == 0) { // Removed + friendsCache.erase(playerName); + } + + // Status messages + switch (data.status) { + case 0: + addSystemChatMessage(playerName + " has been removed from your friends list."); + break; + case 1: + addSystemChatMessage(playerName + " has been added to your friends list."); + break; + case 2: + addSystemChatMessage(playerName + " is now online."); + break; + case 3: + addSystemChatMessage(playerName + " is now offline."); + break; + case 4: + addSystemChatMessage("Player not found."); + break; + case 5: + addSystemChatMessage(playerName + " is already in your friends list."); + break; + case 6: + addSystemChatMessage("Your friends list is full."); + break; + case 7: + addSystemChatMessage(playerName + " is ignoring you."); + break; + default: + LOG_INFO("Friend status: ", (int)data.status, " for ", playerName); + break; + } + + LOG_INFO("Friend status update: ", playerName, " status=", (int)data.status); +} + +void GameHandler::handleRandomRoll(network::Packet& packet) { + RandomRollData data; + if (!RandomRollParser::parse(packet, data)) { + LOG_WARNING("Failed to parse SMSG_RANDOM_ROLL"); + return; + } + + // Get roller name + std::string rollerName; + if (data.rollerGuid == playerGuid) { + rollerName = "You"; + } else { + auto it = playerNameCache.find(data.rollerGuid); + if (it != playerNameCache.end()) { + rollerName = it->second; + } else { + rollerName = "Someone"; + } + } + + // Build message + std::string msg = rollerName; + if (data.rollerGuid == playerGuid) { + msg += " roll "; + } else { + msg += " rolls "; + } + msg += std::to_string(data.result); + msg += " (" + std::to_string(data.minRoll) + "-" + std::to_string(data.maxRoll) + ")"; + + addSystemChatMessage(msg); + LOG_INFO("Random roll: ", rollerName, " rolled ", data.result, " (", data.minRoll, "-", data.maxRoll, ")"); +} + 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 20bca0c3..7c1b1627 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1230,6 +1230,67 @@ network::Packet WhoPacket::build(uint32_t minLevel, uint32_t maxLevel, return packet; } +// ============================================================ +// Social Commands +// ============================================================ + +network::Packet AddFriendPacket::build(const std::string& playerName, const std::string& note) { + network::Packet packet(static_cast(Opcode::CMSG_ADD_FRIEND)); + packet.writeString(playerName); + packet.writeString(note); + LOG_DEBUG("Built CMSG_ADD_FRIEND: player=", playerName); + return packet; +} + +network::Packet DelFriendPacket::build(uint64_t friendGuid) { + network::Packet packet(static_cast(Opcode::CMSG_DEL_FRIEND)); + packet.writeUInt64(friendGuid); + LOG_DEBUG("Built CMSG_DEL_FRIEND: guid=0x", std::hex, friendGuid, std::dec); + return packet; +} + +network::Packet SetContactNotesPacket::build(uint64_t friendGuid, const std::string& note) { + network::Packet packet(static_cast(Opcode::CMSG_SET_CONTACT_NOTES)); + packet.writeUInt64(friendGuid); + packet.writeString(note); + LOG_DEBUG("Built CMSG_SET_CONTACT_NOTES: guid=0x", std::hex, friendGuid, std::dec); + return packet; +} + +bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data) { + data.status = packet.readUInt8(); + data.guid = packet.readUInt64(); + if (data.status == 1) { // Online + data.note = packet.readString(); + data.chatFlag = packet.readUInt8(); + } + LOG_DEBUG("Parsed SMSG_FRIEND_STATUS: status=", (int)data.status, " guid=0x", std::hex, data.guid, std::dec); + return true; +} + +// ============================================================ +// Random Roll +// ============================================================ + +network::Packet RandomRollPacket::build(uint32_t minRoll, uint32_t maxRoll) { + network::Packet packet(static_cast(Opcode::MSG_RANDOM_ROLL)); + packet.writeUInt32(minRoll); + packet.writeUInt32(maxRoll); + LOG_DEBUG("Built MSG_RANDOM_ROLL: ", minRoll, "-", maxRoll); + return packet; +} + +bool RandomRollParser::parse(network::Packet& packet, RandomRollData& data) { + data.rollerGuid = packet.readUInt64(); + data.targetGuid = packet.readUInt64(); + data.minRoll = packet.readUInt32(); + data.maxRoll = packet.readUInt32(); + data.result = packet.readUInt32(); + LOG_DEBUG("Parsed SMSG_RANDOM_ROLL: roller=0x", std::hex, data.rollerGuid, std::dec, + " result=", data.result, " (", data.minRoll, "-", data.maxRoll, ")"); + return true; +} + 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 e0b1c65b..9ee7e323 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -988,6 +988,97 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /roll command + if (cmdLower == "roll" || cmdLower == "random" || cmdLower == "rnd") { + uint32_t minRoll = 1; + uint32_t maxRoll = 100; + + if (spacePos != std::string::npos) { + std::string args = command.substr(spacePos + 1); + size_t dashPos = args.find('-'); + size_t spacePos2 = args.find(' '); + + if (dashPos != std::string::npos) { + // Format: /roll 1-100 + try { + minRoll = std::stoul(args.substr(0, dashPos)); + maxRoll = std::stoul(args.substr(dashPos + 1)); + } catch (...) {} + } else if (spacePos2 != std::string::npos) { + // Format: /roll 1 100 + try { + minRoll = std::stoul(args.substr(0, spacePos2)); + maxRoll = std::stoul(args.substr(spacePos2 + 1)); + } catch (...) {} + } else { + // Format: /roll 100 (means 1-100) + try { + maxRoll = std::stoul(args); + } catch (...) {} + } + } + + gameHandler.randomRoll(minRoll, maxRoll); + chatInputBuffer[0] = '\0'; + return; + } + + // /friend or /addfriend command + if (cmdLower == "friend" || cmdLower == "addfriend") { + if (spacePos != std::string::npos) { + std::string args = command.substr(spacePos + 1); + size_t subCmdSpace = args.find(' '); + + if (cmdLower == "friend" && subCmdSpace != std::string::npos) { + std::string subCmd = args.substr(0, subCmdSpace); + std::transform(subCmd.begin(), subCmd.end(), subCmd.begin(), ::tolower); + + if (subCmd == "add") { + std::string playerName = args.substr(subCmdSpace + 1); + gameHandler.addFriend(playerName); + chatInputBuffer[0] = '\0'; + return; + } else if (subCmd == "remove" || subCmd == "delete" || subCmd == "rem") { + std::string playerName = args.substr(subCmdSpace + 1); + gameHandler.removeFriend(playerName); + chatInputBuffer[0] = '\0'; + return; + } + } else { + // /addfriend name or /friend name (assume add) + gameHandler.addFriend(args); + chatInputBuffer[0] = '\0'; + return; + } + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /friend add or /friend remove "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /removefriend or /delfriend command + if (cmdLower == "removefriend" || cmdLower == "delfriend" || cmdLower == "remfriend") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.removeFriend(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /removefriend "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + // Chat channel slash commands bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") {