diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d458ed7f..c04edcb4 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -340,6 +340,10 @@ public: void leaveGuild(); void inviteToGuild(const std::string& playerName); void kickGuildMember(const std::string& playerName); + void disbandGuild(); + void setGuildLeader(const std::string& name); + void setGuildPublicNote(const std::string& name, const std::string& note); + void setGuildOfficerNote(const std::string& name, const std::string& note); void acceptGuildInvite(); void declineGuildInvite(); void queryGuildInfo(uint32_t guildId); @@ -353,6 +357,7 @@ public: const std::string& getGuildName() const { return guildName_; } const GuildRosterData& getGuildRoster() const { return guildRoster_; } bool hasGuildRoster() const { return hasGuildRoster_; } + const std::vector& getGuildRankNames() const { return guildRankNames_; } bool hasPendingGuildInvite() const { return pendingGuildInvite_; } const std::string& getPendingGuildInviterName() const { return pendingGuildInviterName_; } const std::string& getPendingGuildInviteGuildName() const { return pendingGuildInviteGuildName_; } diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index f5b96796..74632a37 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -117,6 +117,10 @@ enum class LogicalOpcode : uint16_t { SMSG_GUILD_QUERY_RESPONSE, SMSG_GUILD_INVITE, CMSG_GUILD_REMOVE, + CMSG_GUILD_DISBAND, + CMSG_GUILD_LEADER, + CMSG_GUILD_SET_PUBLIC_NOTE, + CMSG_GUILD_SET_OFFICER_NOTE, SMSG_GUILD_EVENT, SMSG_GUILD_COMMAND_RESULT, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c92fbea7..52a44efd 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1003,6 +1003,30 @@ public: static network::Packet build(const std::string& playerName); }; +/** CMSG_GUILD_DISBAND packet builder (empty body) */ +class GuildDisbandPacket { +public: + static network::Packet build(); +}; + +/** CMSG_GUILD_LEADER packet builder */ +class GuildLeaderPacket { +public: + static network::Packet build(const std::string& playerName); +}; + +/** CMSG_GUILD_SET_PUBLIC_NOTE packet builder */ +class GuildSetPublicNotePacket { +public: + static network::Packet build(const std::string& playerName, const std::string& note); +}; + +/** CMSG_GUILD_SET_OFFICER_NOTE packet builder */ +class GuildSetOfficerNotePacket { +public: + static network::Packet build(const std::string& playerName, const std::string& note); +}; + /** CMSG_GUILD_ACCEPT packet builder (empty body) */ class GuildAcceptPacket { public: diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index abf53255..5c8f5e41 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -64,6 +64,10 @@ private: bool showChatWindow = true; bool showPlayerInfo = false; bool showGuildRoster_ = false; + std::string selectedGuildMember_; + bool showGuildNoteEdit_ = false; + bool editingOfficerNote_ = false; + char guildNoteEditBuffer_[256] = {0}; bool refocusChatInput = false; bool chatWindowLocked = true; ImVec2 chatWindowPos_ = ImVec2(0.0f, 0.0f); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 199e59e1..6397f30f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -772,7 +772,8 @@ void GameHandler::handlePacket(network::Packet& packet) { break; case Opcode::SMSG_CHANNEL_NOTIFY: - if (state == WorldState::IN_WORLD) { + // Accept during ENTERING_WORLD too — server auto-joins channels before VERIFY_WORLD + if (state == WorldState::IN_WORLD || state == WorldState::ENTERING_WORLD) { handleChannelNotify(packet); } break; @@ -4331,6 +4332,23 @@ void GameHandler::handleChannelNotify(network::Packet& packet) { LOG_INFO("Left channel: ", data.channelName); break; } + case ChannelNotifyType::PLAYER_ALREADY_MEMBER: { + // Server says we're already in this channel (e.g. server auto-joined us) + // Still track it in our channel list + bool found = false; + for (const auto& ch : joinedChannels_) { + if (ch == data.channelName) { found = true; break; } + } + if (!found) { + joinedChannels_.push_back(data.channelName); + LOG_INFO("Already in channel: ", data.channelName); + } + break; + } + case ChannelNotifyType::NOT_IN_AREA: { + LOG_DEBUG("Cannot join channel ", data.channelName, " (not in area)"); + break; + } default: LOG_DEBUG("Channel notify type ", static_cast(data.notifyType), " for channel ", data.channelName); @@ -7172,6 +7190,34 @@ void GameHandler::kickGuildMember(const std::string& playerName) { LOG_INFO("Kicking guild member: ", playerName); } +void GameHandler::disbandGuild() { + if (state != WorldState::IN_WORLD || !socket) return; + auto packet = GuildDisbandPacket::build(); + socket->send(packet); + LOG_INFO("Disbanding guild"); +} + +void GameHandler::setGuildLeader(const std::string& name) { + if (state != WorldState::IN_WORLD || !socket) return; + auto packet = GuildLeaderPacket::build(name); + socket->send(packet); + LOG_INFO("Setting guild leader: ", name); +} + +void GameHandler::setGuildPublicNote(const std::string& name, const std::string& note) { + if (state != WorldState::IN_WORLD || !socket) return; + auto packet = GuildSetPublicNotePacket::build(name, note); + socket->send(packet); + LOG_INFO("Setting public note for ", name, ": ", note); +} + +void GameHandler::setGuildOfficerNote(const std::string& name, const std::string& note) { + if (state != WorldState::IN_WORLD || !socket) return; + auto packet = GuildSetOfficerNotePacket::build(name, note); + socket->send(packet); + LOG_INFO("Setting officer note for ", name, ": ", note); +} + void GameHandler::acceptGuildInvite() { if (state != WorldState::IN_WORLD || !socket) return; pendingGuildInvite_ = false; @@ -7291,6 +7337,20 @@ void GameHandler::handleGuildEvent(network::Packet& packet) { chatMsg.message = msg; addLocalChatMessage(chatMsg); } + + // Auto-refresh roster after membership/rank changes + switch (data.eventType) { + case GuildEvent::PROMOTION: + case GuildEvent::DEMOTION: + case GuildEvent::JOINED: + case GuildEvent::LEFT: + case GuildEvent::REMOVED: + case GuildEvent::LEADER_CHANGED: + if (hasGuildRoster_) requestGuildRoster(); + break; + default: + break; + } } void GameHandler::handleGuildInvite(network::Packet& packet) { diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index ad90ea12..3dfe1bc6 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -100,6 +100,10 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"SMSG_GUILD_QUERY_RESPONSE", LogicalOpcode::SMSG_GUILD_QUERY_RESPONSE}, {"SMSG_GUILD_INVITE", LogicalOpcode::SMSG_GUILD_INVITE}, {"CMSG_GUILD_REMOVE", LogicalOpcode::CMSG_GUILD_REMOVE}, + {"CMSG_GUILD_DISBAND", LogicalOpcode::CMSG_GUILD_DISBAND}, + {"CMSG_GUILD_LEADER", LogicalOpcode::CMSG_GUILD_LEADER}, + {"CMSG_GUILD_SET_PUBLIC_NOTE", LogicalOpcode::CMSG_GUILD_SET_PUBLIC_NOTE}, + {"CMSG_GUILD_SET_OFFICER_NOTE", LogicalOpcode::CMSG_GUILD_SET_OFFICER_NOTE}, {"SMSG_GUILD_EVENT", LogicalOpcode::SMSG_GUILD_EVENT}, {"SMSG_GUILD_COMMAND_RESULT", LogicalOpcode::SMSG_GUILD_COMMAND_RESULT}, {"MSG_RAID_READY_CHECK", LogicalOpcode::MSG_RAID_READY_CHECK}, @@ -404,6 +408,10 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_GUILD_QUERY_RESPONSE, 0x052}, {LogicalOpcode::SMSG_GUILD_INVITE, 0x083}, {LogicalOpcode::CMSG_GUILD_REMOVE, 0x08E}, + {LogicalOpcode::CMSG_GUILD_DISBAND, 0x08F}, + {LogicalOpcode::CMSG_GUILD_LEADER, 0x090}, + {LogicalOpcode::CMSG_GUILD_SET_PUBLIC_NOTE, 0x234}, + {LogicalOpcode::CMSG_GUILD_SET_OFFICER_NOTE, 0x235}, {LogicalOpcode::SMSG_GUILD_EVENT, 0x092}, {LogicalOpcode::SMSG_GUILD_COMMAND_RESULT, 0x093}, {LogicalOpcode::MSG_RAID_READY_CHECK, 0x322}, diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 9905980e..2e89dedd 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1622,6 +1622,35 @@ network::Packet GuildRemovePacket::build(const std::string& playerName) { return packet; } +network::Packet GuildDisbandPacket::build() { + network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_DISBAND)); + LOG_DEBUG("Built CMSG_GUILD_DISBAND"); + return packet; +} + +network::Packet GuildLeaderPacket::build(const std::string& playerName) { + network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_LEADER)); + packet.writeString(playerName); + LOG_DEBUG("Built CMSG_GUILD_LEADER: ", playerName); + return packet; +} + +network::Packet GuildSetPublicNotePacket::build(const std::string& playerName, const std::string& note) { + network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_SET_PUBLIC_NOTE)); + packet.writeString(playerName); + packet.writeString(note); + LOG_DEBUG("Built CMSG_GUILD_SET_PUBLIC_NOTE: ", playerName, " -> ", note); + return packet; +} + +network::Packet GuildSetOfficerNotePacket::build(const std::string& playerName, const std::string& note) { + network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_SET_OFFICER_NOTE)); + packet.writeString(playerName); + packet.writeString(note); + LOG_DEBUG("Built CMSG_GUILD_SET_OFFICER_NOTE: ", playerName, " -> ", note); + return packet; +} + network::Packet GuildAcceptPacket::build() { network::Packet packet(wireOpcode(Opcode::CMSG_GUILD_ACCEPT)); LOG_DEBUG("Built CMSG_GUILD_ACCEPT"); diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 203b9e8b..e084c67a 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -955,8 +955,8 @@ void CameraController::update(float deltaTime) { // Find max safe distance using raycast + sphere radius collisionDistance = currentDistance; - // WMO raycast collision: zoom in when camera would clip through walls - if (wmoRenderer && cachedInsideWMO && currentDistance > MIN_DISTANCE) { + // WMO raycast collision: zoom in when camera would clip through walls/floors + if (wmoRenderer && currentDistance > MIN_DISTANCE) { glm::vec3 camRayOrigin = pivot; glm::vec3 camRayDir = camDir; float wmoHitDist = wmoRenderer->raycastBoundingBoxes(camRayOrigin, camRayDir, currentDistance); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 7f04c816..7c54c04f 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1973,6 +1973,31 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /gdisband command + if (cmdLower == "gdisband" || cmdLower == "guilddisband") { + gameHandler.disbandGuild(); + chatInputBuffer[0] = '\0'; + return; + } + + // /gleader command + if (cmdLower == "gleader" || cmdLower == "guildleader") { + if (spacePos != std::string::npos) { + std::string playerName = command.substr(spacePos + 1); + gameHandler.setGuildLeader(playerName); + chatInputBuffer[0] = '\0'; + return; + } + + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "Usage: /gleader "; + gameHandler.addLocalChatMessage(msg); + chatInputBuffer[0] = '\0'; + return; + } + // /readycheck command if (cmdLower == "readycheck" || cmdLower == "rc") { gameHandler.initiateReadyCheck(); @@ -2019,8 +2044,26 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { // Reply command if (cmdLower == "r" || cmdLower == "reply") { - std::string replyMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; - gameHandler.replyToLastWhisper(replyMsg); + std::string lastSender = gameHandler.getLastWhisperSender(); + if (lastSender.empty()) { + game::MessageChatData errMsg; + errMsg.type = game::ChatType::SYSTEM; + errMsg.language = game::ChatLanguage::UNIVERSAL; + errMsg.message = "No one has whispered you yet."; + gameHandler.addLocalChatMessage(errMsg); + chatInputBuffer[0] = '\0'; + return; + } + // Set whisper target to last whisper sender + strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + if (spacePos != std::string::npos) { + // /r message — send reply immediately + std::string replyMsg = command.substr(spacePos + 1); + gameHandler.sendChatMessage(game::ChatType::WHISPER, replyMsg, lastSender); + } + // Switch to whisper tab + selectedChatType = 4; chatInputBuffer[0] = '\0'; return; } @@ -3710,8 +3753,8 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; - ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 300, screenH / 2 - 250), ImGuiCond_Once); - ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Once); + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 375, screenH / 2 - 250), ImGuiCond_Once); + ImGui::SetNextWindowSize(ImVec2(750, 500), ImGuiCond_Once); std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Roster") : "Guild Roster"; bool open = showGuildRoster_; @@ -3735,8 +3778,10 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount); ImGui::Separator(); + const auto& rankNames = gameHandler.getGuildRankNames(); + // Table - if (ImGui::BeginTable("GuildRoster", 6, + if (ImGui::BeginTable("GuildRoster", 7, ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Sortable)) { ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort); @@ -3745,6 +3790,7 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f); ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableSetupColumn("Note"); + ImGui::TableSetupColumn("Officer Note"); ImGui::TableHeadersRow(); // Online members first, then offline @@ -3768,8 +3814,19 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%s", m.name.c_str()); + // Right-click context menu + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + selectedGuildMember_ = m.name; + ImGui::OpenPopup("GuildMemberContext"); + } + ImGui::TableNextColumn(); - ImGui::TextColored(textColor, "%u", m.rankIndex); + // Show rank name instead of index + if (m.rankIndex < rankNames.size()) { + ImGui::TextColored(textColor, "%s", rankNames[m.rankIndex].c_str()); + } else { + ImGui::TextColored(textColor, "Rank %u", m.rankIndex); + } ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%u", m.level); @@ -3783,9 +3840,80 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%s", m.publicNote.c_str()); + + ImGui::TableNextColumn(); + ImGui::TextColored(textColor, "%s", m.officerNote.c_str()); } ImGui::EndTable(); } + + // Context menu popup + if (ImGui::BeginPopup("GuildMemberContext")) { + ImGui::Text("%s", selectedGuildMember_.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem("Promote")) { + gameHandler.promoteGuildMember(selectedGuildMember_); + } + if (ImGui::MenuItem("Demote")) { + gameHandler.demoteGuildMember(selectedGuildMember_); + } + if (ImGui::MenuItem("Kick")) { + gameHandler.kickGuildMember(selectedGuildMember_); + } + ImGui::Separator(); + if (ImGui::MenuItem("Set Public Note...")) { + showGuildNoteEdit_ = true; + editingOfficerNote_ = false; + guildNoteEditBuffer_[0] = '\0'; + // Pre-fill with existing note + for (const auto& mem : roster.members) { + if (mem.name == selectedGuildMember_) { + snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str()); + break; + } + } + } + if (ImGui::MenuItem("Set Officer Note...")) { + showGuildNoteEdit_ = true; + editingOfficerNote_ = true; + guildNoteEditBuffer_[0] = '\0'; + for (const auto& mem : roster.members) { + if (mem.name == selectedGuildMember_) { + snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str()); + break; + } + } + } + ImGui::Separator(); + if (ImGui::MenuItem("Set as Leader")) { + gameHandler.setGuildLeader(selectedGuildMember_); + } + ImGui::EndPopup(); + } + + // Note edit modal + if (showGuildNoteEdit_) { + ImGui::OpenPopup("EditGuildNote"); + showGuildNoteEdit_ = false; + } + if (ImGui::BeginPopupModal("EditGuildNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("%s %s for %s:", + editingOfficerNote_ ? "Officer" : "Public", "Note", selectedGuildMember_.c_str()); + ImGui::InputText("##guildnote", guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_)); + if (ImGui::Button("Save")) { + if (editingOfficerNote_) { + gameHandler.setGuildOfficerNote(selectedGuildMember_, guildNoteEditBuffer_); + } else { + gameHandler.setGuildPublicNote(selectedGuildMember_, guildNoteEditBuffer_); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } } ImGui::End();