From 882cb1bae3ce71aa5eba72bc394844eb3e8b26b0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 17:39:35 -0700 Subject: [PATCH] feat: implement WotLK glyph display in talent screen Store glyph IDs from SMSG_TALENTS_INFO (previously discarded) in learnedGlyphs_[2][6] per talent spec. Load GlyphProperties.dbc to map glyphId to spellId and major/minor type. Add a Glyphs tab to the talent screen showing all 6 slots with spell icons and names. Also clear vehicleId_ on SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA. --- include/game/game_handler.hpp | 9 +++ include/ui/talent_screen.hpp | 7 +++ src/game/game_handler.cpp | 9 ++- src/ui/talent_screen.cpp | 106 ++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 47f1e2b3..ae4ed3fd 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -706,6 +706,14 @@ public: static std::unordered_map empty; return spec < 2 ? learnedTalents_[spec] : empty; } + + // Glyphs (WotLK): up to 6 glyph slots per spec (3 major + 3 minor) + static constexpr uint8_t MAX_GLYPH_SLOTS = 6; + const std::array& getGlyphs() const { return learnedGlyphs_[activeTalentSpec_]; } + const std::array& getGlyphs(uint8_t spec) const { + static std::array empty{}; + return spec < 2 ? learnedGlyphs_[spec] : empty; + } uint8_t getTalentRank(uint32_t talentId) const { auto it = learnedTalents_[activeTalentSpec_].find(talentId); return (it != learnedTalents_[activeTalentSpec_].end()) ? it->second : 0; @@ -2308,6 +2316,7 @@ private: uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1) uint8_t unspentTalentPoints_[2] = {0, 0}; // Unspent points per spec std::unordered_map learnedTalents_[2]; // Learned talents per spec + std::array, 2> learnedGlyphs_{}; // Glyphs per spec std::unordered_map talentCache_; // talentId -> entry std::unordered_map talentTabCache_; // tabId -> entry bool talentDbcLoaded_ = false; diff --git a/include/ui/talent_screen.hpp b/include/ui/talent_screen.hpp index 18bbe152..72eafc2a 100644 --- a/include/ui/talent_screen.hpp +++ b/include/ui/talent_screen.hpp @@ -28,6 +28,8 @@ private: void loadSpellDBC(pipeline::AssetManager* assetManager); void loadSpellIconDBC(pipeline::AssetManager* assetManager); + void loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager); + void renderGlyphs(game::GameHandler& gameHandler); VkDescriptorSet getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager); bool open = false; @@ -36,11 +38,16 @@ private: // DBC caches bool spellDbcLoaded = false; bool iconDbcLoaded = false; + bool glyphDbcLoaded = false; std::unordered_map spellIconIds; // spellId -> iconId std::unordered_map spellIconPaths; // iconId -> path std::unordered_map spellIconCache; // iconId -> texture std::unordered_map spellTooltips; // spellId -> description std::unordered_map bgTextureCache_; // tabId -> bg texture + + // GlyphProperties.dbc cache: glyphId -> { spellId, isMajor } + struct GlyphInfo { uint32_t spellId = 0; bool isMajor = false; }; + std::unordered_map glyphProperties_; // glyphId -> info }; } // namespace ui diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d7bf2db0..c7396f8f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6252,6 +6252,9 @@ void GameHandler::handlePacket(network::Packet& packet) { handleQuestPoiQueryResponse(packet); break; case Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA: + vehicleId_ = 0; // Vehicle ride cancelled; clear UI + packet.setReadPos(packet.getSize()); + break; case Opcode::SMSG_RESET_RANGED_COMBAT_TIMER: case Opcode::SMSG_PROFILEDATA_RESPONSE: packet.setReadPos(packet.getSize()); @@ -6891,6 +6894,8 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { talentsInitialized_ = false; learnedTalents_[0].clear(); learnedTalents_[1].clear(); + learnedGlyphs_[0].fill(0); + learnedGlyphs_[1].fill(0); unspentTalentPoints_[0] = 0; unspentTalentPoints_[1] = 0; activeTalentSpec_ = 0; @@ -11338,10 +11343,12 @@ void GameHandler::handleInspectResults(network::Packet& packet) { learnedTalents_[g][talentId] = rank; } if (packet.getSize() - packet.getReadPos() < 1) break; + learnedGlyphs_[g].fill(0); uint8_t glyphCount = packet.readUInt8(); for (uint8_t gl = 0; gl < glyphCount; ++gl) { if (packet.getSize() - packet.getReadPos() < 2) break; - packet.readUInt16(); // glyphId (skip) + uint16_t glyphId = packet.readUInt16(); + if (gl < MAX_GLYPH_SLOTS) learnedGlyphs_[g][gl] = glyphId; } } diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index 5c6bdaf9..b1231f24 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -76,6 +76,7 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { gameHandler.loadTalentDbc(); loadSpellDBC(assetManager); loadSpellIconDBC(assetManager); + loadGlyphPropertiesDBC(assetManager); } uint8_t playerClass = gameHandler.getPlayerClass(); @@ -161,6 +162,18 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } } + + // Glyphs tab (WotLK only — visible when any glyph slot is populated or DBC data loaded) + if (!glyphProperties_.empty() || [&]() { + const auto& g = gameHandler.getGlyphs(); + for (auto id : g) if (id != 0) return true; + return false; }()) { + if (ImGui::BeginTabItem("Glyphs")) { + renderGlyphs(gameHandler); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); } } @@ -616,6 +629,99 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { } } +void TalentScreen::loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager) { + if (glyphDbcLoaded) return; + glyphDbcLoaded = true; + + if (!assetManager || !assetManager->isInitialized()) return; + + auto dbc = assetManager->loadDBC("GlyphProperties.dbc"); + if (!dbc || !dbc->isLoaded()) return; + + // GlyphProperties.dbc: field 0=ID, field 1=SpellID, field 2=GlyphSlotFlags (1=minor), field 3=SpellIconID + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + uint32_t id = dbc->getUInt32(i, 0); + uint32_t spellId = dbc->getUInt32(i, 1); + uint32_t flags = dbc->getUInt32(i, 2); + if (id == 0) continue; + GlyphInfo info; + info.spellId = spellId; + info.isMajor = (flags == 0); // flag 0 = major, flag 1 = minor + glyphProperties_[id] = info; + } +} + +void TalentScreen::renderGlyphs(game::GameHandler& gameHandler) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + const auto& glyphs = gameHandler.getGlyphs(); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Major Glyphs"); + ImGui::Separator(); + + // WotLK: 6 glyph slots total. Slots 0,2,4 are major by convention from the server, + // but we check GlyphProperties.dbc flags when available. + // Display all 6 slots grouped: show major (non-minor) first, then minor. + std::vector> majorSlots, minorSlots; + for (int i = 0; i < game::GameHandler::MAX_GLYPH_SLOTS; i++) { + uint16_t glyphId = glyphs[i]; + bool isMajor = true; + if (glyphId != 0) { + auto git = glyphProperties_.find(glyphId); + if (git != glyphProperties_.end()) isMajor = git->second.isMajor; + else isMajor = (i % 2 == 0); // fallback: even slots = major + } else { + isMajor = (i % 2 == 0); // empty slots follow same pattern + } + if (isMajor) majorSlots.push_back({i, true}); + else minorSlots.push_back({i, false}); + } + + auto renderGlyphSlot = [&](int slotIdx) { + uint16_t glyphId = glyphs[slotIdx]; + char label[64]; + if (glyphId == 0) { + snprintf(label, sizeof(label), "Slot %d [Empty]", slotIdx + 1); + ImGui::TextDisabled("%s", label); + return; + } + + uint32_t spellId = 0; + uint32_t iconId = 0; + auto git = glyphProperties_.find(glyphId); + if (git != glyphProperties_.end()) { + spellId = git->second.spellId; + auto iit = spellIconIds.find(spellId); + if (iit != spellIconIds.end()) iconId = iit->second; + } + + // Icon (24x24) + VkDescriptorSet icon = getSpellIcon(iconId, assetManager); + if (icon != VK_NULL_HANDLE) { + ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(24, 24)); + ImGui::SameLine(0, 6); + } else { + ImGui::Dummy(ImVec2(24, 24)); + ImGui::SameLine(0, 6); + } + + // Spell name + const std::string& name = spellId ? gameHandler.getSpellName(spellId) : ""; + if (!name.empty()) { + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", name.c_str()); + } else { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Glyph #%u", (uint32_t)glyphId); + } + }; + + for (auto& [idx, major] : majorSlots) renderGlyphSlot(idx); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Minor Glyphs"); + ImGui::Separator(); + for (auto& [idx, major] : minorSlots) renderGlyphSlot(idx); +} + VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) { if (iconId == 0 || !assetManager) return VK_NULL_HANDLE;