From c7e16646fc59085f9051d7afa2d30ac3be1da8c1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 21 Mar 2026 04:16:12 -0700 Subject: [PATCH] feat: resolve spell cast time and range from DBC for GetSpellInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SpellDataResolver that lazily loads Spell.dbc, SpellCastTimes.dbc, and SpellRange.dbc to provide cast time and range data. GetSpellInfo() now returns real castTime (ms), minRange, and maxRange instead of hardcoded 0 values. This enables spell tooltip addons, cast bar addons (Quartz), and range check addons to display accurate spell information. The DBC chain is: Spell.dbc[CastingTimeIndex] → SpellCastTimes.dbc[Base ms] Spell.dbc[RangeIndex] → SpellRange.dbc[MinRange, MaxRange] Follows the same lazy-loading pattern as SpellIconPathResolver and ItemIconPathResolver. --- include/game/game_handler.hpp | 9 +++++ src/addons/lua_engine.cpp | 8 ++-- src/core/application.cpp | 70 +++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 145f2f48..f98c47aa 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -294,6 +294,14 @@ public: return spellIconPathResolver_ ? spellIconPathResolver_(spellId) : std::string{}; } + // Spell data resolver: spellId -> {castTimeMs, minRange, maxRange} + struct SpellDataInfo { uint32_t castTimeMs = 0; float minRange = 0; float maxRange = 0; }; + using SpellDataResolver = std::function; + void setSpellDataResolver(SpellDataResolver r) { spellDataResolver_ = std::move(r); } + SpellDataInfo getSpellData(uint32_t spellId) const { + return spellDataResolver_ ? spellDataResolver_(spellId) : SpellDataInfo{}; + } + // Item icon path resolver: displayInfoId -> texture path (e.g., "Interface\\Icons\\INV_Sword_04") using ItemIconPathResolver = std::function; void setItemIconPathResolver(ItemIconPathResolver r) { itemIconPathResolver_ = std::move(r); } @@ -2680,6 +2688,7 @@ private: AddonEventCallback addonEventCallback_; SpellIconPathResolver spellIconPathResolver_; ItemIconPathResolver itemIconPathResolver_; + SpellDataResolver spellDataResolver_; RandomPropertyNameResolver randomPropertyNameResolver_; EmoteAnimCallback emoteAnimCallback_; diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index b3ad636e..e43f5dd9 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -990,9 +990,11 @@ static int lua_GetSpellInfo(lua_State* L) { std::string iconPath = gh->getSpellIconPath(spellId); if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); else lua_pushnil(L); // 3: icon texture path - lua_pushnumber(L, 0); // 4: castTime (ms) — not tracked - lua_pushnumber(L, 0); // 5: minRange - lua_pushnumber(L, 0); // 6: maxRange + // Resolve cast time and range from Spell.dbc → SpellCastTimes.dbc / SpellRange.dbc + auto spellData = gh->getSpellData(spellId); + lua_pushnumber(L, spellData.castTimeMs); // 4: castTime (ms) + lua_pushnumber(L, spellData.minRange); // 5: minRange (yards) + lua_pushnumber(L, spellData.maxRange); // 6: maxRange (yards) lua_pushnumber(L, spellId); // 7: spellId return 7; } diff --git a/src/core/application.cpp b/src/core/application.cpp index c007c09c..c8ee0903 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -439,6 +439,76 @@ bool Application::initialize() { return "Interface\\Icons\\" + it->second; }); } + // Wire spell data resolver: spellId -> {castTimeMs, minRange, maxRange} + { + auto castTimeMap = std::make_shared>(); + auto rangeMap = std::make_shared>>(); + auto spellCastIdx = std::make_shared>(); // spellId→castTimeIdx + auto spellRangeIdx = std::make_shared>(); // spellId→rangeIdx + auto loaded = std::make_shared(false); + auto* am = assetManager.get(); + gameHandler->setSpellDataResolver([castTimeMap, rangeMap, spellCastIdx, spellRangeIdx, loaded, am](uint32_t spellId) -> game::GameHandler::SpellDataInfo { + if (!am) return {}; + if (!*loaded) { + *loaded = true; + // Load SpellCastTimes.dbc + auto ctDbc = am->loadDBC("SpellCastTimes.dbc"); + if (ctDbc && ctDbc->isLoaded()) { + for (uint32_t i = 0; i < ctDbc->getRecordCount(); ++i) { + uint32_t id = ctDbc->getUInt32(i, 0); + int32_t base = static_cast(ctDbc->getUInt32(i, 1)); + if (id > 0 && base > 0) (*castTimeMap)[id] = static_cast(base); + } + } + // Load SpellRange.dbc + const auto* srL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellRange") : nullptr; + uint32_t minRField = srL ? (*srL)["MinRange"] : 1; + uint32_t maxRField = srL ? (*srL)["MaxRange"] : 4; + auto rDbc = am->loadDBC("SpellRange.dbc"); + if (rDbc && rDbc->isLoaded()) { + for (uint32_t i = 0; i < rDbc->getRecordCount(); ++i) { + uint32_t id = rDbc->getUInt32(i, 0); + float minR = rDbc->getFloat(i, minRField); + float maxR = rDbc->getFloat(i, maxRField); + if (id > 0) (*rangeMap)[id] = {minR, maxR}; + } + } + // Load Spell.dbc: extract castTimeIndex and rangeIndex per spell + auto sDbc = am->loadDBC("Spell.dbc"); + const auto* spL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; + if (sDbc && sDbc->isLoaded()) { + uint32_t idF = spL ? (*spL)["ID"] : 0; + uint32_t ctF = spL ? (*spL)["CastingTimeIndex"] : 134; // WotLK default + uint32_t rF = spL ? (*spL)["RangeIndex"] : 132; + for (uint32_t i = 0; i < sDbc->getRecordCount(); ++i) { + uint32_t id = sDbc->getUInt32(i, idF); + if (id == 0) continue; + uint32_t ct = sDbc->getUInt32(i, ctF); + uint32_t ri = sDbc->getUInt32(i, rF); + if (ct > 0) (*spellCastIdx)[id] = ct; + if (ri > 0) (*spellRangeIdx)[id] = ri; + } + } + LOG_INFO("SpellDataResolver: loaded ", spellCastIdx->size(), " cast indices, ", + spellRangeIdx->size(), " range indices"); + } + game::GameHandler::SpellDataInfo info; + auto ciIt = spellCastIdx->find(spellId); + if (ciIt != spellCastIdx->end()) { + auto ctIt = castTimeMap->find(ciIt->second); + if (ctIt != castTimeMap->end()) info.castTimeMs = ctIt->second; + } + auto riIt = spellRangeIdx->find(spellId); + if (riIt != spellRangeIdx->end()) { + auto rIt = rangeMap->find(riIt->second); + if (rIt != rangeMap->end()) { + info.minRange = rIt->second.first; + info.maxRange = rIt->second.second; + } + } + return info; + }); + } // Wire random property/suffix name resolver for item display { auto propNames = std::make_shared>();