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.
This commit is contained in:
Kelsi 2026-03-12 17:39:35 -07:00
parent b7c1aa39a9
commit 882cb1bae3
4 changed files with 130 additions and 1 deletions

View file

@ -706,6 +706,14 @@ public:
static std::unordered_map<uint32_t, uint8_t> 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<uint16_t, MAX_GLYPH_SLOTS>& getGlyphs() const { return learnedGlyphs_[activeTalentSpec_]; }
const std::array<uint16_t, MAX_GLYPH_SLOTS>& getGlyphs(uint8_t spec) const {
static std::array<uint16_t, MAX_GLYPH_SLOTS> 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<uint32_t, uint8_t> learnedTalents_[2]; // Learned talents per spec
std::array<std::array<uint16_t, MAX_GLYPH_SLOTS>, 2> learnedGlyphs_{}; // Glyphs per spec
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
bool talentDbcLoaded_ = false;

View file

@ -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<uint32_t, uint32_t> spellIconIds; // spellId -> iconId
std::unordered_map<uint32_t, std::string> spellIconPaths; // iconId -> path
std::unordered_map<uint32_t, VkDescriptorSet> spellIconCache; // iconId -> texture
std::unordered_map<uint32_t, std::string> spellTooltips; // spellId -> description
std::unordered_map<uint32_t, VkDescriptorSet> bgTextureCache_; // tabId -> bg texture
// GlyphProperties.dbc cache: glyphId -> { spellId, isMajor }
struct GlyphInfo { uint32_t spellId = 0; bool isMajor = false; };
std::unordered_map<uint32_t, GlyphInfo> glyphProperties_; // glyphId -> info
};
} // namespace ui

View file

@ -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;
}
}

View file

@ -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<std::pair<int, bool>> 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;