From bfbf590ee208e8c9979eb51a066c52da2a18858f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Mar 2026 08:11:13 -0700 Subject: [PATCH] refactor: cache macro primary spell ID to avoid per-frame name search The macro cooldown display from the previous commit iterated all known spells (400+) every frame for each macro on the action bar, doing lowercase string comparisons. Moved the spell name resolution into a cached lookup (macroPrimarySpellCache_) that only runs once per macro and is invalidated when macro text is edited. The per-frame path now just does a single hash map lookup + spellCooldowns check. --- include/ui/game_screen.hpp | 4 ++ src/ui/game_screen.cpp | 89 +++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 4121b974..ecdeab68 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -435,6 +435,10 @@ private: void loadExtendedCostDBC(); std::string formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler); + // Macro cooldown cache: maps macro slot index → resolved primary spell ID (0 = no spell found) + std::unordered_map macroPrimarySpellCache_; + uint32_t resolveMacroPrimarySpellId(int slotIndex, game::GameHandler& gameHandler); + // Death Knight rune bar: client-predicted fill (0.0=depleted, 1.0=ready) for smooth animation float runeClientFill_[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index c83bcc4b..bceb3372 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8642,6 +8642,46 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage return ds; } +uint32_t GameScreen::resolveMacroPrimarySpellId(int slotIndex, game::GameHandler& gameHandler) { + auto cacheIt = macroPrimarySpellCache_.find(slotIndex); + if (cacheIt != macroPrimarySpellCache_.end()) return cacheIt->second; + + uint32_t macroId = gameHandler.getActionBar()[slotIndex].id; + const std::string& macroText = gameHandler.getMacroText(macroId); + uint32_t result = 0; + if (!macroText.empty()) { + for (const auto& cmdLine : allMacroCommands(macroText)) { + std::string cl = cmdLine; + for (char& c : cl) c = static_cast(std::tolower(static_cast(c))); + if (cl.rfind("/cast ", 0) != 0) continue; + size_t sp2 = cmdLine.find(' '); + if (sp2 == std::string::npos) continue; + std::string spellArg = cmdLine.substr(sp2 + 1); + if (!spellArg.empty() && spellArg.front() == '[') { + size_t ce = spellArg.find(']'); + if (ce != std::string::npos) spellArg = spellArg.substr(ce + 1); + } + size_t semi = spellArg.find(';'); + if (semi != std::string::npos) spellArg = spellArg.substr(0, semi); + size_t ss = spellArg.find_first_not_of(" \t!"); + if (ss != std::string::npos) spellArg = spellArg.substr(ss); + size_t se = spellArg.find_last_not_of(" \t"); + if (se != std::string::npos) spellArg.resize(se + 1); + if (spellArg.empty()) continue; + std::string spLow = spellArg; + for (char& c : spLow) c = static_cast(std::tolower(static_cast(c))); + for (uint32_t sid : gameHandler.getKnownSpells()) { + std::string sn = gameHandler.getSpellName(sid); + for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); + if (sn == spLow) { result = sid; break; } + } + break; + } + } + macroPrimarySpellCache_[slotIndex] = result; + return result; +} + void GameScreen::renderActionBar(game::GameHandler& gameHandler) { // Use ImGui's display size — always in sync with the current swap-chain/frame, // whereas window->getWidth/Height() can lag by one frame on resize events. @@ -8689,49 +8729,17 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const auto& slot = bar[absSlot]; bool onCooldown = !slot.isReady(); - // Macro cooldown: resolve the macro's primary spell and check its cooldown. - // In WoW, a macro like "/cast Fireball" shows Fireball's cooldown on the button. + // Macro cooldown: check the cached primary spell's cooldown. float macroCooldownRemaining = 0.0f; float macroCooldownTotal = 0.0f; if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0 && !onCooldown) { - const std::string& macroText = gameHandler.getMacroText(slot.id); - if (!macroText.empty()) { - // Find first /cast spell ID (same logic as icon resolution) - for (const auto& cmdLine : allMacroCommands(macroText)) { - std::string cl = cmdLine; - for (char& c : cl) c = static_cast(std::tolower(static_cast(c))); - if (cl.rfind("/cast ", 0) != 0) continue; - size_t sp2 = cmdLine.find(' '); - if (sp2 == std::string::npos) continue; - std::string spellArg = cmdLine.substr(sp2 + 1); - if (!spellArg.empty() && spellArg.front() == '[') { - size_t ce = spellArg.find(']'); - if (ce != std::string::npos) spellArg = spellArg.substr(ce + 1); - } - size_t semi = spellArg.find(';'); - if (semi != std::string::npos) spellArg = spellArg.substr(0, semi); - size_t ss = spellArg.find_first_not_of(" \t!"); - if (ss != std::string::npos) spellArg = spellArg.substr(ss); - size_t se = spellArg.find_last_not_of(" \t"); - if (se != std::string::npos) spellArg.resize(se + 1); - if (spellArg.empty()) continue; - // Find spell ID by name - std::string spLow = spellArg; - for (char& c : spLow) c = static_cast(std::tolower(static_cast(c))); - for (uint32_t sid : gameHandler.getKnownSpells()) { - std::string sn = gameHandler.getSpellName(sid); - for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); - if (sn == spLow) { - float cd = gameHandler.getSpellCooldown(sid); - if (cd > 0.0f) { - macroCooldownRemaining = cd; - macroCooldownTotal = cd; - onCooldown = true; - } - break; - } - } - break; + uint32_t macroSpellId = resolveMacroPrimarySpellId(absSlot, gameHandler); + if (macroSpellId != 0) { + float cd = gameHandler.getSpellCooldown(macroSpellId); + if (cd > 0.0f) { + macroCooldownRemaining = cd; + macroCooldownTotal = cd; + onCooldown = true; } } } @@ -9336,6 +9344,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImVec2(320.0f, 80.0f)); if (ImGui::Button("Save")) { gameHandler.setMacroText(macroEditorId_, std::string(macroEditorBuf_)); + macroPrimarySpellCache_.clear(); // invalidate resolved spell IDs ImGui::CloseCurrentPopup(); } ImGui::SameLine();