From 85a7d66c4efb30b077be0a199e0f777d1a3ed2ae Mon Sep 17 00:00:00 2001 From: kelsi davis Date: Sat, 7 Feb 2026 13:09:12 -0800 Subject: [PATCH] Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit - Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite - PvP toggle: /pvp to toggle PvP flag - Ready check system: /readycheck, /ready, /notready for group coordination - Duel forfeit: /yield, /forfeit, /surrender to cancel active duels --- include/game/game_handler.hpp | 19 +++++ include/game/opcodes.hpp | 25 ++++++ include/game/world_packets.hpp | 82 ++++++++++++++++++ src/game/game_handler.cpp | 151 +++++++++++++++++++++++++++++++++ src/game/world_packets.cpp | 87 +++++++++++++++++++ src/ui/game_screen.cpp | 128 ++++++++++++++++++++++++++++ 6 files changed, 492 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2cf88bd1..3fc4cf36 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -235,6 +235,25 @@ public: void followTarget(); void assistTarget(); + // PvP + void togglePvp(); + + // Guild commands + void requestGuildInfo(); + void requestGuildRoster(); + void setGuildMotd(const std::string& motd); + void promoteGuildMember(const std::string& playerName); + void demoteGuildMember(const std::string& playerName); + void leaveGuild(); + void inviteToGuild(const std::string& playerName); + + // Ready check + void initiateReadyCheck(); + void respondToReadyCheck(bool ready); + + // Duel + void forfeitDuel(); + // ---- Phase 1: Name queries ---- void queryPlayerName(uint64_t guid); void queryCreatureInfo(uint32_t entry, uint64_t guid); diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 4b001794..e18480bf 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -85,6 +85,31 @@ enum class Opcode : uint16_t { CMSG_SHOWING_HELM = 0x2B9, CMSG_SHOWING_CLOAK = 0x2BA, + // ---- PvP ---- + CMSG_TOGGLE_PVP = 0x253, + + // ---- Guild ---- + CMSG_GUILD_INVITE = 0x082, + CMSG_GUILD_ACCEPT = 0x084, + CMSG_GUILD_DECLINE_INVITATION = 0x085, + CMSG_GUILD_INFO = 0x087, + CMSG_GUILD_GET_ROSTER = 0x089, + CMSG_GUILD_PROMOTE_MEMBER = 0x08B, + CMSG_GUILD_DEMOTE_MEMBER = 0x08C, + CMSG_GUILD_LEAVE = 0x08D, + CMSG_GUILD_MOTD = 0x091, + SMSG_GUILD_INFO = 0x088, + SMSG_GUILD_ROSTER = 0x08A, + + // ---- Ready Check ---- + MSG_RAID_READY_CHECK = 0x322, + MSG_RAID_READY_CHECK_CONFIRM = 0x3AE, + + // ---- Duel ---- + CMSG_DUEL_ACCEPTED = 0x16C, + CMSG_DUEL_CANCELLED = 0x16D, + SMSG_DUEL_REQUESTED = 0x167, + // ---- Random Roll ---- MSG_RANDOM_ROLL = 0x1FB, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index a9660740..c8be1cc3 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -802,6 +802,88 @@ public: static network::Packet build(bool show); }; +// ============================================================ +// PvP +// ============================================================ + +/** CMSG_TOGGLE_PVP packet builder */ +class TogglePvpPacket { +public: + static network::Packet build(); +}; + +// ============================================================ +// Guild Commands +// ============================================================ + +/** CMSG_GUILD_INFO packet builder */ +class GuildInfoPacket { +public: + static network::Packet build(); +}; + +/** CMSG_GUILD_GET_ROSTER packet builder */ +class GuildRosterPacket { +public: + static network::Packet build(); +}; + +/** CMSG_GUILD_MOTD packet builder */ +class GuildMotdPacket { +public: + static network::Packet build(const std::string& motd); +}; + +/** CMSG_GUILD_PROMOTE_MEMBER packet builder */ +class GuildPromotePacket { +public: + static network::Packet build(const std::string& playerName); +}; + +/** CMSG_GUILD_DEMOTE_MEMBER packet builder */ +class GuildDemotePacket { +public: + static network::Packet build(const std::string& playerName); +}; + +/** CMSG_GUILD_LEAVE packet builder */ +class GuildLeavePacket { +public: + static network::Packet build(); +}; + +/** CMSG_GUILD_INVITE packet builder */ +class GuildInvitePacket { +public: + static network::Packet build(const std::string& playerName); +}; + +// ============================================================ +// Ready Check +// ============================================================ + +/** MSG_RAID_READY_CHECK packet builder */ +class ReadyCheckPacket { +public: + static network::Packet build(); +}; + +/** MSG_RAID_READY_CHECK_CONFIRM packet builder */ +class ReadyCheckConfirmPacket { +public: + static network::Packet build(bool ready); +}; + +// ============================================================ +// Duel +// ============================================================ + +/** CMSG_DUEL_CANCELLED packet builder */ +class DuelCancelPacket { +public: + static network::Packet build(); +}; + // ============================================================ // Random Roll // ============================================================ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 15088025..c9c9ed07 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1893,6 +1893,157 @@ void GameHandler::assistTarget() { LOG_INFO("Assisting ", targetName, ", now targeting GUID: 0x", std::hex, assistTargetGuid, std::dec); } +void GameHandler::togglePvp() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot toggle PvP: not in world or not connected"); + return; + } + + auto packet = TogglePvpPacket::build(); + socket->send(packet); + addSystemChatMessage("PvP flag toggled."); + LOG_INFO("Toggled PvP flag"); +} + +void GameHandler::requestGuildInfo() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot request guild info: not in world or not connected"); + return; + } + + auto packet = GuildInfoPacket::build(); + socket->send(packet); + LOG_INFO("Requested guild info"); +} + +void GameHandler::requestGuildRoster() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot request guild roster: not in world or not connected"); + return; + } + + auto packet = GuildRosterPacket::build(); + socket->send(packet); + addSystemChatMessage("Requesting guild roster..."); + LOG_INFO("Requested guild roster"); +} + +void GameHandler::setGuildMotd(const std::string& motd) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot set guild MOTD: not in world or not connected"); + return; + } + + auto packet = GuildMotdPacket::build(motd); + socket->send(packet); + addSystemChatMessage("Guild MOTD updated."); + LOG_INFO("Set guild MOTD: ", motd); +} + +void GameHandler::promoteGuildMember(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot promote guild member: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name."); + return; + } + + auto packet = GuildPromotePacket::build(playerName); + socket->send(packet); + addSystemChatMessage("Promoting " + playerName + "..."); + LOG_INFO("Promoting guild member: ", playerName); +} + +void GameHandler::demoteGuildMember(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot demote guild member: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name."); + return; + } + + auto packet = GuildDemotePacket::build(playerName); + socket->send(packet); + addSystemChatMessage("Demoting " + playerName + "..."); + LOG_INFO("Demoting guild member: ", playerName); +} + +void GameHandler::leaveGuild() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot leave guild: not in world or not connected"); + return; + } + + auto packet = GuildLeavePacket::build(); + socket->send(packet); + addSystemChatMessage("Leaving guild..."); + LOG_INFO("Leaving guild"); +} + +void GameHandler::inviteToGuild(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot invite to guild: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name."); + return; + } + + auto packet = GuildInvitePacket::build(playerName); + socket->send(packet); + addSystemChatMessage("Inviting " + playerName + " to guild..."); + LOG_INFO("Inviting to guild: ", playerName); +} + +void GameHandler::initiateReadyCheck() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot initiate ready check: not in world or not connected"); + return; + } + + if (!isInGroup()) { + addSystemChatMessage("You must be in a group to initiate a ready check."); + return; + } + + auto packet = ReadyCheckPacket::build(); + socket->send(packet); + addSystemChatMessage("Ready check initiated."); + LOG_INFO("Initiated ready check"); +} + +void GameHandler::respondToReadyCheck(bool ready) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot respond to ready check: not in world or not connected"); + return; + } + + auto packet = ReadyCheckConfirmPacket::build(ready); + socket->send(packet); + addSystemChatMessage(ready ? "You are ready." : "You are not ready."); + LOG_INFO("Responded to ready check: ", ready ? "ready" : "not ready"); +} + +void GameHandler::forfeitDuel() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot forfeit duel: not in world or not connected"); + return; + } + + auto packet = DuelCancelPacket::build(); + socket->send(packet); + addSystemChatMessage("You have forfeited the duel."); + LOG_INFO("Forfeited duel"); +} + void GameHandler::releaseSpirit() { if (!playerDead_) return; if (socket && state == WorldState::IN_WORLD) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 50c677fa..993e6dca 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1334,6 +1334,93 @@ network::Packet ShowingCloakPacket::build(bool show) { return packet; } +// ============================================================ +// PvP +// ============================================================ + +network::Packet TogglePvpPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_TOGGLE_PVP)); + LOG_DEBUG("Built CMSG_TOGGLE_PVP"); + return packet; +} + +// ============================================================ +// Guild Commands +// ============================================================ + +network::Packet GuildInfoPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_INFO)); + LOG_DEBUG("Built CMSG_GUILD_INFO"); + return packet; +} + +network::Packet GuildRosterPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_GET_ROSTER)); + LOG_DEBUG("Built CMSG_GUILD_GET_ROSTER"); + return packet; +} + +network::Packet GuildMotdPacket::build(const std::string& motd) { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_MOTD)); + packet.writeString(motd); + LOG_DEBUG("Built CMSG_GUILD_MOTD: ", motd); + return packet; +} + +network::Packet GuildPromotePacket::build(const std::string& playerName) { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_PROMOTE_MEMBER)); + packet.writeString(playerName); + LOG_DEBUG("Built CMSG_GUILD_PROMOTE_MEMBER: ", playerName); + return packet; +} + +network::Packet GuildDemotePacket::build(const std::string& playerName) { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_DEMOTE_MEMBER)); + packet.writeString(playerName); + LOG_DEBUG("Built CMSG_GUILD_DEMOTE_MEMBER: ", playerName); + return packet; +} + +network::Packet GuildLeavePacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_LEAVE)); + LOG_DEBUG("Built CMSG_GUILD_LEAVE"); + return packet; +} + +network::Packet GuildInvitePacket::build(const std::string& playerName) { + network::Packet packet(static_cast(Opcode::CMSG_GUILD_INVITE)); + packet.writeString(playerName); + LOG_DEBUG("Built CMSG_GUILD_INVITE: ", playerName); + return packet; +} + +// ============================================================ +// Ready Check +// ============================================================ + +network::Packet ReadyCheckPacket::build() { + network::Packet packet(static_cast(Opcode::MSG_RAID_READY_CHECK)); + LOG_DEBUG("Built MSG_RAID_READY_CHECK"); + return packet; +} + +network::Packet ReadyCheckConfirmPacket::build(bool ready) { + network::Packet packet(static_cast(Opcode::MSG_RAID_READY_CHECK_CONFIRM)); + packet.writeUInt8(ready ? 1 : 0); + LOG_DEBUG("Built MSG_RAID_READY_CHECK_CONFIRM: ready=", ready); + return packet; +} + +// ============================================================ +// Duel +// ============================================================ + +network::Packet DuelCancelPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_DUEL_CANCELLED)); + LOG_DEBUG("Built CMSG_DUEL_CANCELLED"); + return packet; +} + // ============================================================ // Random Roll // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 9ff46442..f24cabeb 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1178,6 +1178,134 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /pvp command + if (cmdLower == "pvp") { + gameHandler.togglePvp(); + chatInputBuffer[0] = '\0'; + return; + } + + // /ginfo command + if (cmdLower == "ginfo" || cmdLower == "guildinfo") { + gameHandler.requestGuildInfo(); + chatInputBuffer[0] = '\0'; + return; + } + + // /groster command + if (cmdLower == "groster" || cmdLower == "guildroster") { + gameHandler.requestGuildRoster(); + chatInputBuffer[0] = '\0'; + return; + } + + // /gmotd command + if (cmdLower == "gmotd" || cmdLower == "guildmotd") { + if (spacePos != std::string::npos) { + std::string motd = command.substr(spacePos + 1); + gameHandler.setGuildMotd(motd); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /gmotd "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /gpromote command + if (cmdLower == "gpromote" || cmdLower == "guildpromote") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.promoteGuildMember(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /gpromote "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /gdemote command + if (cmdLower == "gdemote" || cmdLower == "guilddemote") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.demoteGuildMember(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /gdemote "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /gquit command + if (cmdLower == "gquit" || cmdLower == "guildquit" || cmdLower == "leaveguild") { + gameHandler.leaveGuild(); + chatInputBuffer[0] = '\0'; + return; + } + + // /ginvite command + if (cmdLower == "ginvite" || cmdLower == "guildinvite") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.inviteToGuild(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /ginvite "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + + // /readycheck command + if (cmdLower == "readycheck" || cmdLower == "rc") { + gameHandler.initiateReadyCheck(); + chatInputBuffer[0] = '\0'; + return; + } + + // /ready command (respond yes to ready check) + if (cmdLower == "ready") { + gameHandler.respondToReadyCheck(true); + chatInputBuffer[0] = '\0'; + return; + } + + // /notready command (respond no to ready check) + if (cmdLower == "notready" || cmdLower == "nr") { + gameHandler.respondToReadyCheck(false); + chatInputBuffer[0] = '\0'; + return; + } + + // /yield or /forfeit command + if (cmdLower == "yield" || cmdLower == "forfeit" || cmdLower == "surrender") { + gameHandler.forfeitDuel(); + chatInputBuffer[0] = '\0'; + return; + } + // Chat channel slash commands bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") {