diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 909e4cc3..9f2cb352 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -261,6 +261,15 @@ public: std::string getLastWhisperSender() const { return lastWhisperSender_; } void setLastWhisperSender(const std::string& name) { lastWhisperSender_ = name; } + // Party/Raid management + void uninvitePlayer(const std::string& playerName); + void leaveParty(); + void setMainTank(uint64_t targetGuid); + void setMainAssist(uint64_t targetGuid); + void clearMainTank(); + void clearMainAssist(); + void requestRaidInfo(); + // ---- 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 e18480bf..b292f338 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -175,6 +175,8 @@ enum class Opcode : uint16_t { SMSG_GROUP_LIST = 0x07D, SMSG_PARTY_COMMAND_RESULT = 0x07E, MSG_RAID_TARGET_UPDATE = 0x321, + CMSG_REQUEST_RAID_INFO = 0x2CD, + SMSG_RAID_INSTANCE_INFO = 0x2CC, // ---- Phase 5: Loot ---- CMSG_AUTOSTORE_LOOT_ITEM = 0x108, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c8be1cc3..09587147 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -884,6 +884,39 @@ public: static network::Packet build(); }; +// ============================================================ +// Party/Raid Management +// ============================================================ + +/** CMSG_GROUP_UNINVITE_GUID packet builder */ +class GroupUninvitePacket { +public: + static network::Packet build(const std::string& playerName); +}; + +/** CMSG_GROUP_DISBAND packet builder */ +class GroupDisbandPacket { +public: + static network::Packet build(); +}; + +/** MSG_RAID_TARGET_UPDATE packet builder */ +class RaidTargetUpdatePacket { +public: + /** + * Build raid target marker update packet + * @param targetIndex 0-7 for raid icons, 0 = MainTank, 1 = MainAssist + * @param targetGuid GUID to mark, or 0 to clear + */ + static network::Packet build(uint8_t targetIndex, uint64_t targetGuid); +}; + +/** CMSG_REQUEST_RAID_INFO packet builder */ +class RequestRaidInfoPacket { +public: + static network::Packet build(); +}; + // ============================================================ // Random Roll // ============================================================ @@ -1315,12 +1348,6 @@ public: static network::Packet build(); }; -/** CMSG_GROUP_DISBAND (leave party) packet builder */ -class GroupDisbandPacket { -public: - static network::Packet build(); -}; - /** SMSG_GROUP_LIST parser */ class GroupListParser { public: diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index bcede487..f5f9c2b8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2116,6 +2116,109 @@ void GameHandler::replyToLastWhisper(const std::string& message) { LOG_INFO("Replied to ", lastWhisperSender_, ": ", message); } +void GameHandler::uninvitePlayer(const std::string& playerName) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot uninvite player: not in world or not connected"); + return; + } + + if (playerName.empty()) { + addSystemChatMessage("You must specify a player name to uninvite."); + return; + } + + auto packet = GroupUninvitePacket::build(playerName); + socket->send(packet); + addSystemChatMessage("Removed " + playerName + " from the group."); + LOG_INFO("Uninvited player: ", playerName); +} + +void GameHandler::leaveParty() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot leave party: not in world or not connected"); + return; + } + + auto packet = GroupDisbandPacket::build(); + socket->send(packet); + addSystemChatMessage("You have left the group."); + LOG_INFO("Left party/raid"); +} + +void GameHandler::setMainTank(uint64_t targetGuid) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot set main tank: not in world or not connected"); + return; + } + + if (targetGuid == 0) { + addSystemChatMessage("You must have a target selected."); + return; + } + + // Main tank uses index 0 + auto packet = RaidTargetUpdatePacket::build(0, targetGuid); + socket->send(packet); + addSystemChatMessage("Main tank set."); + LOG_INFO("Set main tank: 0x", std::hex, targetGuid, std::dec); +} + +void GameHandler::setMainAssist(uint64_t targetGuid) { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot set main assist: not in world or not connected"); + return; + } + + if (targetGuid == 0) { + addSystemChatMessage("You must have a target selected."); + return; + } + + // Main assist uses index 1 + auto packet = RaidTargetUpdatePacket::build(1, targetGuid); + socket->send(packet); + addSystemChatMessage("Main assist set."); + LOG_INFO("Set main assist: 0x", std::hex, targetGuid, std::dec); +} + +void GameHandler::clearMainTank() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot clear main tank: not in world or not connected"); + return; + } + + // Clear main tank by setting GUID to 0 + auto packet = RaidTargetUpdatePacket::build(0, 0); + socket->send(packet); + addSystemChatMessage("Main tank cleared."); + LOG_INFO("Cleared main tank"); +} + +void GameHandler::clearMainAssist() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot clear main assist: not in world or not connected"); + return; + } + + // Clear main assist by setting GUID to 0 + auto packet = RaidTargetUpdatePacket::build(1, 0); + socket->send(packet); + addSystemChatMessage("Main assist cleared."); + LOG_INFO("Cleared main assist"); +} + +void GameHandler::requestRaidInfo() { + if (state != WorldState::IN_WORLD || !socket) { + LOG_WARNING("Cannot request raid info: not in world or not connected"); + return; + } + + auto packet = RequestRaidInfoPacket::build(); + socket->send(packet); + addSystemChatMessage("Requesting raid lockout information..."); + LOG_INFO("Requested raid info"); +} + 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 993e6dca..b6a2aa83 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1421,6 +1421,37 @@ network::Packet DuelCancelPacket::build() { return packet; } +// ============================================================ +// Party/Raid Management +// ============================================================ + +network::Packet GroupUninvitePacket::build(const std::string& playerName) { + network::Packet packet(static_cast(Opcode::CMSG_GROUP_UNINVITE_GUID)); + packet.writeString(playerName); + LOG_DEBUG("Built CMSG_GROUP_UNINVITE_GUID for player: ", playerName); + return packet; +} + +network::Packet GroupDisbandPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_GROUP_DISBAND)); + LOG_DEBUG("Built CMSG_GROUP_DISBAND"); + return packet; +} + +network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targetGuid) { + network::Packet packet(static_cast(Opcode::MSG_RAID_TARGET_UPDATE)); + packet.writeUInt8(targetIndex); + packet.writeUInt64(targetGuid); + LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", (uint32_t)targetIndex, ", guid: 0x", std::hex, targetGuid, std::dec); + return packet; +} + +network::Packet RequestRaidInfoPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_REQUEST_RAID_INFO)); + LOG_DEBUG("Built CMSG_REQUEST_RAID_INFO"); + return packet; +} + // ============================================================ // Random Roll // ============================================================ @@ -2092,11 +2123,6 @@ network::Packet GroupDeclinePacket::build() { return packet; } -network::Packet GroupDisbandPacket::build() { - network::Packet packet(static_cast(Opcode::CMSG_GROUP_DISBAND)); - return packet; -} - bool GroupListParser::parse(network::Packet& packet, GroupListData& data) { data.groupType = packet.readUInt8(); data.subGroup = packet.readUInt8(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 8260161f..cdf697fc 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1330,6 +1330,74 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // Party/Raid management commands + if (cmdLower == "uninvite" || cmdLower == "kick") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.uninvitePlayer(playerName); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /uninvite "; + gameHandler.addLocalChatMessage(msg); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "leave" || cmdLower == "leaveparty") { + gameHandler.leaveParty(); + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "maintank" || cmdLower == "mt") { + if (gameHandler.hasTarget()) { + gameHandler.setMainTank(gameHandler.getTargetGuid()); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You must target a player to set as main tank."; + gameHandler.addLocalChatMessage(msg); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "mainassist" || cmdLower == "ma") { + if (gameHandler.hasTarget()) { + gameHandler.setMainAssist(gameHandler.getTargetGuid()); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You must target a player to set as main assist."; + gameHandler.addLocalChatMessage(msg); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "clearmaintank") { + gameHandler.clearMainTank(); + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "clearmainassist") { + gameHandler.clearMainAssist(); + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "raidinfo") { + gameHandler.requestRaidInfo(); + chatInputBuffer[0] = '\0'; + return; + } + // Chat channel slash commands bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") {