Add /roll and friend management commands

Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode

Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
  - Friend added/removed confirmations
  - Friend online/offline notifications
  - Error messages (not found, already friends, list full, ignoring)

Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)

Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
This commit is contained in:
kelsi davis 2026-02-07 12:51:30 -08:00
parent f9c4cbddee
commit 6f45c6ab69
6 changed files with 427 additions and 0 deletions

View file

@ -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;