From e572cdfb4af91c594120854dcb87f2c3bfaef7f4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 09:44:43 -0700 Subject: [PATCH] feat: show guild names on player nameplates Read PLAYER_GUILDID from entity update fields (UNIT_END + 3) and query guild names via CMSG_GUILD_QUERY. Cache results in guildNameCache_ so each guild ID is queried only once. Display in grey below the player name on nameplates. Fix handleGuildQueryResponse to not overwrite the local player's guild data when querying other guilds. --- include/game/game_handler.hpp | 9 ++++++ src/game/game_handler.cpp | 58 ++++++++++++++++++++++++++++------- src/ui/game_screen.cpp | 20 ++++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 47997040..02bd74cb 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -608,6 +608,13 @@ public: uint32_t getPetitionCost() const { return petitionCost_; } uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; } + // Guild name lookup for other players' nameplates + // Returns the guild name for a given guildId, or empty if unknown. + // Automatically queries the server for unknown guild IDs. + const std::string& lookupGuildName(uint32_t guildId); + // Returns the guildId for a player entity (from PLAYER_GUILDID update field). + uint32_t getEntityGuildId(uint64_t guid) const; + // Ready check struct ReadyCheckResult { std::string name; @@ -2952,6 +2959,8 @@ private: GuildInfoData guildInfoData_; GuildQueryResponseData guildQueryData_; bool hasGuildRoster_ = false; + std::unordered_map guildNameCache_; // guildId → guild name + std::unordered_set pendingGuildNameQueries_; // in-flight guild queries bool pendingGuildInvite_ = false; std::string pendingGuildInviterName_; std::string pendingGuildInviteGuildName_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0cdfe948..15e55819 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -759,6 +759,8 @@ void GameHandler::disconnect() { activeCharacterGuid_ = 0; playerNameCache.clear(); pendingNameQueries.clear(); + guildNameCache_.clear(); + pendingGuildNameQueries_.clear(); friendGuids_.clear(); contacts_.clear(); transportAttachments_.clear(); @@ -19577,6 +19579,28 @@ void GameHandler::queryGuildInfo(uint32_t guildId) { LOG_INFO("Querying guild info: guildId=", guildId); } +static const std::string kEmptyString; + +const std::string& GameHandler::lookupGuildName(uint32_t guildId) { + if (guildId == 0) return kEmptyString; + auto it = guildNameCache_.find(guildId); + if (it != guildNameCache_.end()) return it->second; + // Query the server if we haven't already + if (pendingGuildNameQueries_.insert(guildId).second) { + queryGuildInfo(guildId); + } + return kEmptyString; +} + +uint32_t GameHandler::getEntityGuildId(uint64_t guid) const { + auto entity = entityManager.getEntity(guid); + if (!entity || entity->getType() != ObjectType::PLAYER) return 0; + // PLAYER_GUILDID = UNIT_END + 3 across all expansions + const uint16_t ufUnitEnd = fieldIndex(UF::UNIT_END); + if (ufUnitEnd == 0xFFFF) return 0; + return entity->getField(ufUnitEnd + 3); +} + void GameHandler::createGuild(const std::string& guildName) { if (state != WorldState::IN_WORLD || !socket) return; auto packet = GuildCreatePacket::build(guildName); @@ -19661,18 +19685,30 @@ void GameHandler::handleGuildQueryResponse(network::Packet& packet) { GuildQueryResponseData data; if (!packetParsers_->parseGuildQueryResponse(packet, data)) return; - const bool wasUnknown = guildName_.empty(); - guildName_ = data.guildName; - guildQueryData_ = data; - guildRankNames_.clear(); - for (uint32_t i = 0; i < 10; ++i) { - guildRankNames_.push_back(data.rankNames[i]); + // Always cache the guild name for nameplate lookups + if (data.guildId != 0 && !data.guildName.empty()) { + guildNameCache_[data.guildId] = data.guildName; + pendingGuildNameQueries_.erase(data.guildId); + } + + // Check if this is the local player's guild + const Character* ch = getActiveCharacter(); + bool isLocalGuild = (ch && ch->hasGuild() && ch->guildId == data.guildId); + + if (isLocalGuild) { + const bool wasUnknown = guildName_.empty(); + guildName_ = data.guildName; + guildQueryData_ = data; + guildRankNames_.clear(); + for (uint32_t i = 0; i < 10; ++i) { + guildRankNames_.push_back(data.rankNames[i]); + } + LOG_INFO("Guild name set to: ", guildName_); + if (wasUnknown && !guildName_.empty()) + addSystemChatMessage("Guild: <" + guildName_ + ">"); + } else { + LOG_INFO("Cached guild name: id=", data.guildId, " name=", data.guildName); } - LOG_INFO("Guild name set to: ", guildName_); - // Only announce once — when we first learn our own guild name at login. - // Subsequent queries (e.g. querying other players' guilds) are silent. - if (wasUnknown && !guildName_.empty()) - addSystemChatMessage("Guild: <" + guildName_ + ">"); } void GameHandler::handleGuildEvent(network::Packet& packet) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 88130e6d..902940e8 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -11125,9 +11125,29 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { ? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC : IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC } + // Guild name for player nameplates: shift name up to make room + std::string guildTag; + if (isPlayer) { + uint32_t guildId = gameHandler.getEntityGuildId(guid); + if (guildId != 0) { + const std::string& gn = gameHandler.lookupGuildName(guildId); + if (!gn.empty()) guildTag = "<" + gn + ">"; + } + } + if (!guildTag.empty()) nameY -= 10.0f; // shift name up for guild line + drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf); drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf); + // Guild tag below the name (WoW-style in lighter color) + if (!guildTag.empty()) { + ImVec2 guildSz = ImGui::CalcTextSize(guildTag.c_str()); + float guildX = sx - guildSz.x * 0.5f; + float guildY = nameY + textSize.y + 1.0f; + drawList->AddText(ImVec2(guildX + 1.0f, guildY + 1.0f), IM_COL32(0, 0, 0, A(120)), guildTag.c_str()); + drawList->AddText(ImVec2(guildX, guildY), IM_COL32(180, 180, 180, A(200)), guildTag.c_str()); + } + // Group leader crown to the right of the name on player nameplates if (isPlayer && gameHandler.isInGroup() && gameHandler.getPartyData().leaderGuid == guid) {