From 367390a852643f7b8217e56beab560e68f9a887a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 10:41:18 -0700 Subject: [PATCH] feat: add /who results window with sortable player table Store structured WhoEntry data from SMSG_WHO responses and show them in a dedicated popup window with Name/Guild/Level/Class/Zone columns. Right-click on any row to Whisper, Invite, Add Friend, or Ignore. Window auto-opens when /who or /whois is typed; shows online count in the title bar. Results persist until the next /who query. --- include/game/game_handler.hpp | 17 ++++++ include/ui/game_screen.hpp | 4 ++ src/game/game_handler.cpp | 27 +++++---- src/ui/game_screen.cpp | 103 ++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 14 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index bf2c3915..3981f199 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -353,6 +353,19 @@ public: uint32_t getTotalTimePlayed() const { return totalTimePlayed_; } uint32_t getLevelTimePlayed() const { return levelTimePlayed_; } + // Who results (structured, from last SMSG_WHO response) + struct WhoEntry { + std::string name; + std::string guildName; + uint32_t level = 0; + uint32_t classId = 0; + uint32_t raceId = 0; + uint32_t zoneId = 0; + }; + const std::vector& getWhoResults() const { return whoResults_; } + uint32_t getWhoOnlineCount() const { return whoOnlineCount_; } + std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); } + // Social commands void addFriend(const std::string& playerName, const std::string& note = ""); void removeFriend(const std::string& playerName); @@ -2303,6 +2316,10 @@ private: uint32_t totalTimePlayed_ = 0; uint32_t levelTimePlayed_ = 0; + // Who results (last SMSG_WHO response) + std::vector whoResults_; + uint32_t whoOnlineCount_ = 0; + // Trade state TradeStatus tradeStatus_ = TradeStatus::None; uint64_t tradePeerGuid_= 0; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 17327216..492f224e 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -396,6 +396,10 @@ private: int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none) int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none) + // Who Results window + bool showWhoWindow_ = false; + void renderWhoWindow(game::GameHandler& gameHandler); + // Instance Lockouts window bool showInstanceLockouts_ = false; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3431e927..1b61649c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -18352,13 +18352,15 @@ void GameHandler::handleWho(network::Packet& packet) { LOG_INFO("WHO response: ", displayCount, " players displayed, ", onlineCount, " total online"); + // Store structured results for the who-results window + whoResults_.clear(); + whoOnlineCount_ = onlineCount; + if (displayCount == 0) { addSystemChatMessage("No players found."); return; } - addSystemChatMessage(std::to_string(onlineCount) + " player(s) online:"); - for (uint32_t i = 0; i < displayCount; ++i) { if (packet.getReadPos() >= packet.getSize()) break; std::string playerName = packet.readString(); @@ -18373,19 +18375,16 @@ void GameHandler::handleWho(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() >= 4) zoneId = packet.readUInt32(); - const char* className = getClassName(static_cast(classId)); + // Store structured entry + WhoEntry entry; + entry.name = playerName; + entry.guildName = guildName; + entry.level = level; + entry.classId = classId; + entry.raceId = raceId; + entry.zoneId = zoneId; + whoResults_.push_back(std::move(entry)); - std::string msg = " " + playerName; - if (!guildName.empty()) - msg += " <" + guildName + ">"; - msg += " - Level " + std::to_string(level) + " " + className; - if (zoneId != 0) { - std::string zoneName = getAreaName(zoneId); - if (!zoneName.empty()) - msg += " [" + zoneName + "]"; - } - - addSystemChatMessage(msg); LOG_INFO(" ", playerName, " (", guildName, ") Lv", level, " Class:", classId, " Race:", raceId, " Zone:", zoneId); } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 86ae4257..29838f3d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -31,6 +31,7 @@ #include "pipeline/dbc_layout.hpp" #include "game/expansion_profile.hpp" +#include "game/character.hpp" #include "core/logger.hpp" #include #include @@ -596,6 +597,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderAuctionHouseWindow(gameHandler); renderDungeonFinderWindow(gameHandler); renderInstanceLockouts(gameHandler); + renderWhoWindow(gameHandler); renderAchievementWindow(gameHandler); renderGmTicketWindow(gameHandler); renderInspectWindow(gameHandler); @@ -4040,6 +4042,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } gameHandler.queryWho(query); + showWhoWindow_ = true; chatInputBuffer[0] = '\0'; return; } @@ -16838,6 +16841,106 @@ void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) { ImGui::PopStyleVar(2); } +// ─── Who Results Window ─────────────────────────────────────────────────────── +void GameScreen::renderWhoWindow(game::GameHandler& gameHandler) { + if (!showWhoWindow_) return; + + const auto& results = gameHandler.getWhoResults(); + + ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(200, 180), ImGuiCond_FirstUseEver); + + char title[64]; + uint32_t onlineCount = gameHandler.getWhoOnlineCount(); + if (onlineCount > 0) + snprintf(title, sizeof(title), "Players Online: %u###WhoWindow", onlineCount); + else + snprintf(title, sizeof(title), "Who###WhoWindow"); + + if (!ImGui::Begin(title, &showWhoWindow_)) { + ImGui::End(); + return; + } + + if (results.empty()) { + ImGui::TextDisabled("No results. Use /who [filter] to search."); + ImGui::End(); + return; + } + + // Table: Name | Guild | Level | Class | Zone + if (ImGui::BeginTable("##WhoTable", 5, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, + ImVec2(0, 0))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.22f); + ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthStretch, 0.20f); + ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f); + ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 0.20f); + ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < results.size(); ++i) { + const auto& e = results[i]; + ImGui::TableNextRow(); + ImGui::PushID(static_cast(i)); + + // Name (class-colored if class is known) + ImGui::TableSetColumnIndex(0); + uint8_t cid = static_cast(e.classId); + ImVec4 nameCol = classColorVec4(cid); + ImGui::TextColored(nameCol, "%s", e.name.c_str()); + + // Right-click context menu on the name + if (ImGui::BeginPopupContextItem("##WhoCtx")) { + ImGui::TextDisabled("%s", e.name.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem("Whisper")) { + selectedChatType = 4; + strncpy(whisperTargetBuffer, e.name.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + refocusChatInput = true; + } + if (ImGui::MenuItem("Invite to Group")) + gameHandler.inviteToGroup(e.name); + if (ImGui::MenuItem("Add Friend")) + gameHandler.addFriend(e.name); + if (ImGui::MenuItem("Ignore")) + gameHandler.addIgnore(e.name); + ImGui::EndPopup(); + } + + // Guild + ImGui::TableSetColumnIndex(1); + if (!e.guildName.empty()) + ImGui::TextDisabled("<%s>", e.guildName.c_str()); + + // Level + ImGui::TableSetColumnIndex(2); + ImGui::Text("%u", e.level); + + // Class + ImGui::TableSetColumnIndex(3); + const char* className = game::getClassName(static_cast(e.classId)); + ImGui::TextColored(nameCol, "%s", className); + + // Zone + ImGui::TableSetColumnIndex(4); + if (e.zoneId != 0) { + std::string zoneName = gameHandler.getWhoAreaName(e.zoneId); + ImGui::TextUnformatted(zoneName.empty() ? "Unknown" : zoneName.c_str()); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::End(); +} + // ─── Achievement Window ─────────────────────────────────────────────────────── void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { if (!showAchievementWindow_) return;