Kelsidavis-WoWee/src/ui/talent_screen.cpp
Kelsi 40a98f2436 Fix talent tab crash and missing talent points at level 10
Unique child window and button IDs per tab prevent ImGui state corruption
when switching talent tabs. Parse SMSG_INSPECT_TALENT type=0 properly to
populate unspentTalentPoints_ and learnedTalents_ from the server response.
2026-03-12 03:15:56 -07:00

667 lines
24 KiB
C++

#include "ui/talent_screen.hpp"
#include "ui/keybinding_manager.hpp"
#include "core/input.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include "rendering/vk_context.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include <algorithm>
#include <cmath>
namespace wowee { namespace ui {
// WoW class names indexed by class ID (1-11)
static const char* classNames[] = {
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue", "Priest",
"Death Knight", "Shaman", "Mage", "Warlock", "Unknown", "Druid"
};
static const char* getClassName(uint8_t classId) {
return (classId >= 1 && classId <= 11) ? classNames[classId] : "Unknown";
}
void TalentScreen::render(game::GameHandler& gameHandler) {
// Talents toggle via keybinding (edge-triggered)
// Customizable key (default: N) from KeybindingManager
bool talentsDown = KeybindingManager::getInstance().isActionPressed(
KeybindingManager::Action::TOGGLE_TALENTS, false);
if (talentsDown && !nKeyWasDown) {
open = !open;
}
nKeyWasDown = talentsDown;
if (!open) return;
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
float winW = 680.0f;
float winH = 600.0f;
float winX = (screenW - winW) * 0.5f;
float winY = (screenH - winH) * 0.5f;
ImGui::SetNextWindowPos(ImVec2(winX, winY), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_FirstUseEver);
// Build title with point distribution
uint8_t playerClass = gameHandler.getPlayerClass();
std::string title = "Talents";
if (playerClass > 0) {
title = std::string(getClassName(playerClass)) + " Talents";
}
bool windowOpen = open;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
if (ImGui::Begin(title.c_str(), &windowOpen)) {
renderTalentTrees(gameHandler);
}
ImGui::End();
ImGui::PopStyleVar();
if (!windowOpen) {
open = false;
}
}
void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) {
auto* assetManager = core::Application::getInstance().getAssetManager();
// Ensure talent DBCs are loaded once
static bool dbcLoadAttempted = false;
if (!dbcLoadAttempted) {
dbcLoadAttempted = true;
gameHandler.loadTalentDbc();
loadSpellDBC(assetManager);
loadSpellIconDBC(assetManager);
}
uint8_t playerClass = gameHandler.getPlayerClass();
if (playerClass == 0) {
ImGui::TextDisabled("Class information not available.");
return;
}
// Get talent tabs for this class, sorted by orderIndex
uint32_t classMask = 1u << (playerClass - 1);
std::vector<const game::GameHandler::TalentTabEntry*> classTabs;
for (const auto& [tabId, tab] : gameHandler.getAllTalentTabs()) {
if (tab.classMask & classMask) {
classTabs.push_back(&tab);
}
}
std::sort(classTabs.begin(), classTabs.end(),
[](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; });
if (classTabs.empty()) {
ImGui::TextDisabled("No talent trees available for your class.");
return;
}
// Compute points-per-tree for display
uint32_t treeTotals[3] = {0, 0, 0};
for (size_t ti = 0; ti < classTabs.size() && ti < 3; ti++) {
for (const auto& [tid, rank] : gameHandler.getLearnedTalents()) {
const auto* t = gameHandler.getTalentEntry(tid);
if (t && t->tabId == classTabs[ti]->tabId) {
treeTotals[ti] += rank;
}
}
}
// Header: spec switcher + unspent points + point distribution
uint8_t activeSpec = gameHandler.getActiveTalentSpec();
uint8_t unspent = gameHandler.getUnspentTalentPoints();
// Spec buttons
for (uint8_t s = 0; s < 2; s++) {
if (s > 0) ImGui::SameLine();
bool isActive = (s == activeSpec);
if (isActive) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.8f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.9f, 1.0f));
}
char specLabel[32];
snprintf(specLabel, sizeof(specLabel), "Spec %u", s + 1);
if (ImGui::Button(specLabel, ImVec2(70, 0))) {
if (!isActive) gameHandler.switchTalentSpec(s);
}
if (isActive) ImGui::PopStyleColor(2);
}
// Point distribution
ImGui::SameLine(0, 20);
if (classTabs.size() >= 3) {
ImGui::Text("(%u / %u / %u)", treeTotals[0], treeTotals[1], treeTotals[2]);
}
// Unspent points
ImGui::SameLine(0, 20);
if (unspent > 0) {
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%u point%s available",
unspent, unspent > 1 ? "s" : "");
} else {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "No points available");
}
ImGui::Separator();
// Render tabs with point counts in tab labels
if (ImGui::BeginTabBar("TalentTabs")) {
for (size_t ti = 0; ti < classTabs.size(); ti++) {
const auto* tab = classTabs[ti];
char tabLabel[128];
uint32_t pts = (ti < 3) ? treeTotals[ti] : 0;
snprintf(tabLabel, sizeof(tabLabel), "%s (%u)###tab%u", tab->name.c_str(), pts, tab->tabId);
if (ImGui::BeginTabItem(tabLabel)) {
renderTalentTree(gameHandler, tab->tabId, tab->backgroundFile);
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
}
void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId,
const std::string& bgFile) {
auto* assetManager = core::Application::getInstance().getAssetManager();
// Collect all talents for this tab
std::vector<const game::GameHandler::TalentEntry*> talents;
for (const auto& [talentId, talent] : gameHandler.getAllTalents()) {
if (talent.tabId == tabId) {
talents.push_back(&talent);
}
}
if (talents.empty()) {
ImGui::TextDisabled("No talents in this tree.");
return;
}
// Sort talents by row then column for consistent rendering
std::sort(talents.begin(), talents.end(), [](const auto* a, const auto* b) {
if (a->row != b->row) return a->row < b->row;
return a->column < b->column;
});
// Find grid dimensions
uint8_t maxRow = 0, maxCol = 0;
for (const auto* talent : talents) {
maxRow = std::max(maxRow, talent->row);
maxCol = std::max(maxCol, talent->column);
}
// WoW talent grids are always 4 columns wide
if (maxCol < 3) maxCol = 3;
const float iconSize = 40.0f;
const float spacing = 8.0f;
const float cellSize = iconSize + spacing;
const float gridWidth = (maxCol + 1) * cellSize + spacing;
const float gridHeight = (maxRow + 1) * cellSize + spacing;
// Points in this tree
uint32_t pointsInTree = 0;
for (const auto& [tid, rank] : gameHandler.getLearnedTalents()) {
const auto* t = gameHandler.getTalentEntry(tid);
if (t && t->tabId == tabId) {
pointsInTree += rank;
}
}
// Center the grid
float availW = ImGui::GetContentRegionAvail().x;
float offsetX = std::max(0.0f, (availW - gridWidth) * 0.5f);
char childId[32];
snprintf(childId, sizeof(childId), "TalentGrid_%u", tabId);
ImGui::BeginChild(childId, ImVec2(0, 0), false);
ImVec2 gridOrigin = ImGui::GetCursorScreenPos();
gridOrigin.x += offsetX;
// Draw background texture if available
if (!bgFile.empty() && assetManager) {
VkDescriptorSet bgTex = VK_NULL_HANDLE;
auto bgIt = bgTextureCache_.find(tabId);
if (bgIt != bgTextureCache_.end()) {
bgTex = bgIt->second;
} else {
// Only load the background if icon uploads aren't saturating this frame.
// Background is cosmetic; skip if we're already loading icons this frame.
std::string bgPath = bgFile;
for (auto& c : bgPath) { if (c == '\\') c = '/'; }
bgPath += ".blp";
auto blpData = assetManager->readFile(bgPath);
if (!blpData.empty()) {
auto image = pipeline::BLPLoader::load(blpData);
if (image.isValid()) {
auto* window = core::Application::getInstance().getWindow();
auto* vkCtx = window ? window->getVkContext() : nullptr;
if (vkCtx) {
bgTex = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
}
}
}
// Cache even if null to avoid retrying every frame on missing files
bgTextureCache_[tabId] = bgTex;
}
if (bgTex) {
auto* drawList = ImGui::GetWindowDrawList();
float bgW = gridWidth + spacing * 2;
float bgH = gridHeight + spacing * 2;
drawList->AddImage((ImTextureID)(uintptr_t)bgTex,
ImVec2(gridOrigin.x - spacing, gridOrigin.y - spacing),
ImVec2(gridOrigin.x + bgW - spacing, gridOrigin.y + bgH - spacing),
ImVec2(0, 0), ImVec2(1, 1),
IM_COL32(255, 255, 255, 60)); // Subtle background
}
}
// Build a position lookup for prerequisite arrows
struct TalentPos {
const game::GameHandler::TalentEntry* talent;
ImVec2 center;
};
std::unordered_map<uint32_t, TalentPos> talentPositions;
// First pass: compute positions
for (const auto* talent : talents) {
float x = gridOrigin.x + talent->column * cellSize + spacing;
float y = gridOrigin.y + talent->row * cellSize + spacing;
ImVec2 center(x + iconSize * 0.5f, y + iconSize * 0.5f);
talentPositions[talent->talentId] = {talent, center};
}
// Draw prerequisite arrows
auto* drawList = ImGui::GetWindowDrawList();
for (const auto* talent : talents) {
for (int i = 0; i < 3; ++i) {
if (talent->prereqTalent[i] == 0) continue;
auto fromIt = talentPositions.find(talent->prereqTalent[i]);
auto toIt = talentPositions.find(talent->talentId);
if (fromIt == talentPositions.end() || toIt == talentPositions.end()) continue;
uint8_t prereqRank = gameHandler.getTalentRank(talent->prereqTalent[i]);
bool met = prereqRank >= talent->prereqRank[i];
ImU32 lineCol = met ? IM_COL32(100, 220, 100, 200) : IM_COL32(120, 120, 120, 150);
ImVec2 from = fromIt->second.center;
ImVec2 to = toIt->second.center;
// Draw line from bottom of prerequisite to top of dependent
ImVec2 lineStart(from.x, from.y + iconSize * 0.5f);
ImVec2 lineEnd(to.x, to.y - iconSize * 0.5f);
drawList->AddLine(lineStart, lineEnd, lineCol, 2.0f);
// Arrow head
float arrowSize = 5.0f;
drawList->AddTriangleFilled(
ImVec2(lineEnd.x, lineEnd.y),
ImVec2(lineEnd.x - arrowSize, lineEnd.y - arrowSize * 1.5f),
ImVec2(lineEnd.x + arrowSize, lineEnd.y - arrowSize * 1.5f),
lineCol);
}
}
// Render talent icons
for (uint8_t row = 0; row <= maxRow; ++row) {
for (uint8_t col = 0; col <= maxCol; ++col) {
const game::GameHandler::TalentEntry* talent = nullptr;
for (const auto* t : talents) {
if (t->row == row && t->column == col) {
talent = t;
break;
}
}
float x = gridOrigin.x + col * cellSize + spacing;
float y = gridOrigin.y + row * cellSize + spacing;
ImGui::SetCursorScreenPos(ImVec2(x, y));
if (talent) {
renderTalent(gameHandler, *talent, pointsInTree);
} else {
// Empty cell — invisible placeholder
char emptyId[32];
snprintf(emptyId, sizeof(emptyId), "e_%u_%u_%u", tabId, row, col);
ImGui::InvisibleButton(emptyId, ImVec2(iconSize, iconSize));
}
}
}
// Reserve space for the full grid so scrolling works
ImGui::SetCursorScreenPos(ImVec2(gridOrigin.x, gridOrigin.y + gridHeight));
ImGui::Dummy(ImVec2(gridWidth, 0));
ImGui::EndChild();
}
void TalentScreen::renderTalent(game::GameHandler& gameHandler,
const game::GameHandler::TalentEntry& talent,
uint32_t pointsInTree) {
auto* assetManager = core::Application::getInstance().getAssetManager();
uint8_t currentRank = gameHandler.getTalentRank(talent.talentId);
// Check if can learn
bool canLearn = currentRank < talent.maxRank &&
gameHandler.getUnspentTalentPoints() > 0;
// Check prerequisites
bool prereqsMet = true;
for (int i = 0; i < 3; ++i) {
if (talent.prereqTalent[i] != 0) {
uint8_t prereqRank = gameHandler.getTalentRank(talent.prereqTalent[i]);
if (prereqRank < talent.prereqRank[i]) {
prereqsMet = false;
canLearn = false;
break;
}
}
}
// Check tier requirement (need row*5 points in tree)
if (talent.row > 0) {
uint32_t requiredPoints = talent.row * 5;
if (pointsInTree < requiredPoints) {
canLearn = false;
}
}
// Determine visual state
enum TalentState { MAXED, PARTIAL, AVAILABLE, LOCKED };
TalentState state;
if (currentRank >= talent.maxRank) {
state = MAXED;
} else if (currentRank > 0) {
state = PARTIAL;
} else if (canLearn && prereqsMet) {
state = AVAILABLE;
} else {
state = LOCKED;
}
// Colors per state
ImVec4 borderColor;
ImVec4 tint;
switch (state) {
case MAXED: borderColor = ImVec4(0.2f, 0.9f, 0.2f, 1.0f); tint = ImVec4(1,1,1,1); break;
case PARTIAL: borderColor = ImVec4(0.2f, 0.8f, 0.2f, 1.0f); tint = ImVec4(1,1,1,1); break;
case AVAILABLE:borderColor = ImVec4(1.0f, 1.0f, 1.0f, 0.8f); tint = ImVec4(1,1,1,1); break;
case LOCKED: borderColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); tint = ImVec4(0.4f,0.4f,0.4f,1); break;
}
const float iconSize = 40.0f;
ImGui::PushID(static_cast<int>(talent.talentId));
// Get spell icon
uint32_t spellId = talent.rankSpells[0];
VkDescriptorSet iconTex = VK_NULL_HANDLE;
if (spellId != 0) {
auto it = spellIconIds.find(spellId);
if (it != spellIconIds.end()) {
iconTex = getSpellIcon(it->second, assetManager);
}
}
// Click target
bool clicked = ImGui::InvisibleButton("##t", ImVec2(iconSize, iconSize));
bool hovered = ImGui::IsItemHovered();
ImVec2 pMin = ImGui::GetItemRectMin();
ImVec2 pMax = ImGui::GetItemRectMax();
auto* dl = ImGui::GetWindowDrawList();
// Background fill
ImU32 bgCol;
if (state == LOCKED) {
bgCol = IM_COL32(20, 20, 25, 200);
} else {
bgCol = IM_COL32(30, 30, 40, 200);
}
dl->AddRectFilled(pMin, pMax, bgCol, 3.0f);
// Icon
if (iconTex) {
ImU32 tintCol = IM_COL32(
static_cast<int>(tint.x * 255), static_cast<int>(tint.y * 255),
static_cast<int>(tint.z * 255), static_cast<int>(tint.w * 255));
dl->AddImage((ImTextureID)(uintptr_t)iconTex,
ImVec2(pMin.x + 2, pMin.y + 2),
ImVec2(pMax.x - 2, pMax.y - 2),
ImVec2(0, 0), ImVec2(1, 1), tintCol);
}
// Border
float borderThick = hovered ? 2.5f : 1.5f;
ImU32 borderCol = IM_COL32(
static_cast<int>(borderColor.x * 255), static_cast<int>(borderColor.y * 255),
static_cast<int>(borderColor.z * 255), static_cast<int>(borderColor.w * 255));
dl->AddRect(pMin, pMax, borderCol, 3.0f, 0, borderThick);
// Hover glow
if (hovered && state != LOCKED) {
dl->AddRect(ImVec2(pMin.x - 1, pMin.y - 1), ImVec2(pMax.x + 1, pMax.y + 1),
IM_COL32(255, 255, 255, 60), 3.0f, 0, 1.0f);
}
// Rank counter (bottom-right corner)
{
char rankText[16];
snprintf(rankText, sizeof(rankText), "%u/%u", currentRank, talent.maxRank);
ImVec2 textSize = ImGui::CalcTextSize(rankText);
ImVec2 textPos(pMax.x - textSize.x - 2, pMax.y - textSize.y - 1);
// Background pill for readability
dl->AddRectFilled(ImVec2(textPos.x - 2, textPos.y - 1),
ImVec2(pMax.x, pMax.y),
IM_COL32(0, 0, 0, 180), 2.0f);
// Text shadow
dl->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 255), rankText);
// Rank text color
ImU32 rankCol;
switch (state) {
case MAXED: rankCol = IM_COL32(80, 255, 80, 255); break;
case PARTIAL: rankCol = IM_COL32(80, 255, 80, 255); break;
default: rankCol = IM_COL32(200, 200, 200, 255); break;
}
dl->AddText(textPos, rankCol, rankText);
}
// Tooltip
if (hovered) {
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(320.0f);
// Spell name
const std::string& spellName = gameHandler.getSpellName(spellId);
if (!spellName.empty()) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", spellName.c_str());
} else {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Talent #%u", talent.talentId);
}
// Rank display
ImVec4 rankColor;
switch (state) {
case MAXED: rankColor = ImVec4(0.3f, 0.9f, 0.3f, 1); break;
case PARTIAL: rankColor = ImVec4(0.3f, 0.9f, 0.3f, 1); break;
default: rankColor = ImVec4(0.7f, 0.7f, 0.7f, 1); break;
}
ImGui::TextColored(rankColor, "Rank %u/%u", currentRank, talent.maxRank);
// Current rank description
if (currentRank > 0 && currentRank <= 5 && talent.rankSpells[currentRank - 1] != 0) {
auto tooltipIt = spellTooltips.find(talent.rankSpells[currentRank - 1]);
if (tooltipIt != spellTooltips.end() && !tooltipIt->second.empty()) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Current:");
ImGui::TextWrapped("%s", tooltipIt->second.c_str());
}
}
// Next rank description
if (currentRank < talent.maxRank && currentRank < 5 && talent.rankSpells[currentRank] != 0) {
auto tooltipIt = spellTooltips.find(talent.rankSpells[currentRank]);
if (tooltipIt != spellTooltips.end() && !tooltipIt->second.empty()) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Next Rank:");
ImGui::TextWrapped("%s", tooltipIt->second.c_str());
}
}
// Prerequisites
for (int i = 0; i < 3; ++i) {
if (talent.prereqTalent[i] == 0) continue;
const auto* prereq = gameHandler.getTalentEntry(talent.prereqTalent[i]);
if (!prereq || prereq->rankSpells[0] == 0) continue;
uint8_t prereqCurrentRank = gameHandler.getTalentRank(talent.prereqTalent[i]);
bool met = prereqCurrentRank >= talent.prereqRank[i];
ImVec4 pColor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1) : ImVec4(1.0f, 0.3f, 0.3f, 1);
const std::string& prereqName = gameHandler.getSpellName(prereq->rankSpells[0]);
ImGui::Spacing();
ImGui::TextColored(pColor, "Requires %u point%s in %s",
talent.prereqRank[i],
talent.prereqRank[i] > 1 ? "s" : "",
prereqName.empty() ? "prerequisite" : prereqName.c_str());
}
// Tier requirement
if (talent.row > 0 && currentRank == 0) {
uint32_t requiredPoints = talent.row * 5;
if (pointsInTree < requiredPoints) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Requires %u points in this tree (%u/%u)",
requiredPoints, pointsInTree, requiredPoints);
}
}
// Action hint
if (canLearn && prereqsMet) {
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Click to learn");
}
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
// Handle click
if (clicked && canLearn && prereqsMet) {
const auto& learned = gameHandler.getLearnedTalents();
uint8_t desiredRank;
if (learned.find(talent.talentId) == learned.end()) {
desiredRank = 0; // First rank (0-indexed on wire)
} else {
desiredRank = currentRank; // currentRank is already the next 0-indexed rank to learn
}
gameHandler.learnTalent(talent.talentId, desiredRank);
}
ImGui::PopID();
}
void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
if (spellDbcLoaded) return;
spellDbcLoaded = true;
if (!assetManager || !assetManager->isInitialized()) return;
auto dbc = assetManager->loadDBC("Spell.dbc");
if (!dbc || !dbc->isLoaded()) return;
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t count = dbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) {
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
if (spellId == 0) continue;
uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
spellIconIds[spellId] = iconId;
std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139);
if (!tooltip.empty()) {
spellTooltips[spellId] = tooltip;
}
}
}
void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
if (iconDbcLoaded) return;
iconDbcLoaded = true;
if (!assetManager || !assetManager->isInitialized()) return;
auto dbc = assetManager->loadDBC("SpellIcon.dbc");
if (!dbc || !dbc->isLoaded()) return;
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths[id] = path;
}
}
}
VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
if (iconId == 0 || !assetManager) return VK_NULL_HANDLE;
auto cit = spellIconCache.find(iconId);
if (cit != spellIconCache.end()) return cit->second;
// Rate-limit texture uploads to avoid multi-hundred-ms stalls when switching
// to a tab whose icons are not yet cached (each upload is a blocking GPU op).
// Allow at most 4 new icon loads per frame; the rest show a blank icon and
// load on the next frame, spreading the cost across ~5 frames.
static int loadsThisFrame = 0;
static int lastImGuiFrame = -1;
int curFrame = ImGui::GetFrameCount();
if (curFrame != lastImGuiFrame) { loadsThisFrame = 0; lastImGuiFrame = curFrame; }
if (loadsThisFrame >= 4) return VK_NULL_HANDLE; // defer, don't cache null
++loadsThisFrame;
auto pit = spellIconPaths.find(iconId);
if (pit == spellIconPaths.end()) {
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
std::string iconPath = pit->second + ".blp";
auto blpData = assetManager->readFile(iconPath);
if (blpData.empty()) {
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
auto image = pipeline::BLPLoader::load(blpData);
if (!image.isValid()) {
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
auto* window = core::Application::getInstance().getWindow();
auto* vkCtx = window ? window->getVkContext() : nullptr;
if (!vkCtx) {
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
spellIconCache[iconId] = ds;
return ds;
}
}} // namespace wowee::ui