feat: add GetSpellInfo, GetSpellTexture, GetItemInfo and more Lua API functions

Add spell icon path resolution via SpellIcon.dbc + Spell.dbc lazy loading,
wired through GameHandler callback. Fix UnitBuff/UnitDebuff to return icon
texture paths instead of nil. Add GetLocale, GetBuildInfo, GetCurrentMapAreaID.
This commit is contained in:
Kelsi 2026-03-20 13:58:54 -07:00
parent 3ff43a530f
commit 22b0cc8a3c
3 changed files with 202 additions and 1 deletions

View file

@ -287,6 +287,13 @@ public:
using AddonEventCallback = std::function<void(const std::string&, const std::vector<std::string>&)>;
void setAddonEventCallback(AddonEventCallback cb) { addonEventCallback_ = std::move(cb); }
// Spell icon path resolver: spellId -> texture path string (e.g., "Interface\\Icons\\Spell_Fire_Fireball01")
using SpellIconPathResolver = std::function<std::string(uint32_t)>;
void setSpellIconPathResolver(SpellIconPathResolver r) { spellIconPathResolver_ = std::move(r); }
std::string getSpellIconPath(uint32_t spellId) const {
return spellIconPathResolver_ ? spellIconPathResolver_(spellId) : std::string{};
}
// Emote animation callback: (entityGuid, animationId)
using EmoteAnimCallback = std::function<void(uint64_t, uint32_t)>;
void setEmoteAnimCallback(EmoteAnimCallback cb) { emoteAnimCallback_ = std::move(cb); }
@ -2644,6 +2651,7 @@ private:
ChatBubbleCallback chatBubbleCallback_;
AddonChatCallback addonChatCallback_;
AddonEventCallback addonEventCallback_;
SpellIconPathResolver spellIconPathResolver_;
EmoteAnimCallback emoteAnimCallback_;
// Targeting

View file

@ -360,7 +360,9 @@ static int lua_UnitAura(lua_State* L, bool wantBuff) {
std::string name = gh->getSpellName(aura.spellId);
lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name
lua_pushstring(L, ""); // rank
lua_pushnil(L); // icon (texture path — not implemented)
std::string iconPath = gh->getSpellIconPath(aura.spellId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L); // icon texture path
lua_pushnumber(L, aura.charges); // count
lua_pushnil(L); // debuffType
lua_pushnumber(L, aura.maxDurationMs > 0 ? aura.maxDurationMs / 1000.0 : 0); // duration
@ -477,6 +479,144 @@ static int lua_HasTarget(lua_State* L) {
return 1;
}
// --- GetSpellInfo / GetSpellTexture ---
// GetSpellInfo(spellIdOrName) -> name, rank, icon, castTime, minRange, maxRange, spellId
static int lua_GetSpellInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnil(L); return 1; }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { lua_pushnil(L); return 1; }
std::string nameLow(name);
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
int bestRank = -1;
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sn != nameLow) continue;
int rank = 0;
const std::string& rk = gh->getSpellRank(sid);
if (!rk.empty()) {
std::string rkl = rk;
for (char& c : rkl) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (rkl.rfind("rank ", 0) == 0) {
try { rank = std::stoi(rkl.substr(5)); } catch (...) {}
}
}
if (rank > bestRank) { bestRank = rank; spellId = sid; }
}
}
if (spellId == 0) { lua_pushnil(L); return 1; }
std::string name = gh->getSpellName(spellId);
if (name.empty()) { lua_pushnil(L); return 1; }
lua_pushstring(L, name.c_str()); // 1: name
const std::string& rank = gh->getSpellRank(spellId);
lua_pushstring(L, rank.c_str()); // 2: rank
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
lua_pushnumber(L, spellId); // 7: spellId
return 7;
}
// GetSpellTexture(spellIdOrName) -> icon texture path string
static int lua_GetSpellTexture(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnil(L); return 1; }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { lua_pushnil(L); return 1; }
std::string nameLow(name);
for (char& c : nameLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0) { lua_pushnil(L); return 1; }
std::string iconPath = gh->getSpellIconPath(spellId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L);
return 1;
}
// GetItemInfo(itemId) -> name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, vendorPrice
static int lua_GetItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnil(L); return 1; }
uint32_t itemId = 0;
if (lua_isnumber(L, 1)) {
itemId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
// Try to parse "item:12345" link format
const char* s = lua_tostring(L, 1);
std::string str(s ? s : "");
auto pos = str.find("item:");
if (pos != std::string::npos) {
try { itemId = static_cast<uint32_t>(std::stoul(str.substr(pos + 5))); } catch (...) {}
}
}
if (itemId == 0) { lua_pushnil(L); return 1; }
const auto* info = gh->getItemInfo(itemId);
if (!info) { lua_pushnil(L); return 1; }
lua_pushstring(L, info->name.c_str()); // 1: name
// Build item link string: |cFFFFFFFF|Hitem:ID:0:0:0:0:0:0:0|h[Name]|h|r
char link[256];
snprintf(link, sizeof(link), "|cFFFFFFFF|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
itemId, info->name.c_str());
lua_pushstring(L, link); // 2: link
lua_pushnumber(L, info->quality); // 3: quality
lua_pushnumber(L, info->itemLevel); // 4: iLevel
lua_pushnumber(L, info->requiredLevel); // 5: requiredLevel
lua_pushstring(L, ""); // 6: class (type string)
lua_pushstring(L, ""); // 7: subclass
lua_pushnumber(L, info->maxStack > 0 ? info->maxStack : 1); // 8: maxStack
lua_pushstring(L, ""); // 9: equipSlot
lua_pushnil(L); // 10: texture (icon path — no ItemDisplayInfo icon resolver yet)
lua_pushnumber(L, info->sellPrice); // 11: vendorPrice
return 11;
}
// --- Locale/Build/Realm info ---
static int lua_GetLocale(lua_State* L) {
lua_pushstring(L, "enUS");
return 1;
}
static int lua_GetBuildInfo(lua_State* L) {
// Return WotLK defaults; expansion-specific version detection would need
// access to the expansion registry which isn't available here.
lua_pushstring(L, "3.3.5a"); // 1: version
lua_pushnumber(L, 12340); // 2: buildNumber
lua_pushstring(L, "Jan 1 2025");// 3: date
lua_pushnumber(L, 30300); // 4: tocVersion
return 4;
}
static int lua_GetCurrentMapAreaID(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getCurrentMapId() : 0);
return 1;
}
// --- Frame System ---
// Minimal WoW-compatible frame objects with RegisterEvent/SetScript/GetScript.
// Frames are Lua tables with a metatable that provides methods.
@ -812,6 +952,12 @@ void LuaEngine::registerCoreAPI() {
{"UnitDebuff", lua_UnitDebuff},
{"GetNumAddOns", lua_GetNumAddOns},
{"GetAddOnInfo", lua_GetAddOnInfo},
{"GetSpellInfo", lua_GetSpellInfo},
{"GetSpellTexture", lua_GetSpellTexture},
{"GetItemInfo", lua_GetItemInfo},
{"GetLocale", lua_GetLocale},
{"GetBuildInfo", lua_GetBuildInfo},
{"GetCurrentMapAreaID", lua_GetCurrentMapAreaID},
// Utilities
{"strsplit", lua_strsplit},
{"strtrim", lua_strtrim},

View file

@ -366,6 +366,53 @@ bool Application::initialize() {
addonManager_->fireEvent(event, args);
}
});
// Wire spell icon path resolver for Lua API (GetSpellInfo, UnitBuff icon, etc.)
{
auto spellIconPaths = std::make_shared<std::unordered_map<uint32_t, std::string>>();
auto spellIconIds = std::make_shared<std::unordered_map<uint32_t, uint32_t>>();
auto loaded = std::make_shared<bool>(false);
auto* am = assetManager.get();
gameHandler->setSpellIconPathResolver([spellIconPaths, spellIconIds, loaded, am](uint32_t spellId) -> std::string {
if (!am) return {};
// Lazy-load SpellIcon.dbc + Spell.dbc icon IDs on first call
if (!*loaded) {
*loaded = true;
auto iconDbc = am->loadDBC("SpellIcon.dbc");
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
if (iconDbc && iconDbc->isLoaded()) {
for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) {
uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) (*spellIconPaths)[id] = path;
}
}
auto spellDbc = am->loadDBC("Spell.dbc");
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
if (spellDbc && spellDbc->isLoaded()) {
uint32_t fieldCount = spellDbc->getFieldCount();
uint32_t iconField = 133; // WotLK default
uint32_t idField = 0;
if (spellL) {
uint32_t layoutIcon = (*spellL)["IconID"];
if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) {
iconField = layoutIcon;
idField = (*spellL)["ID"];
}
}
for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) {
uint32_t id = spellDbc->getUInt32(i, idField);
uint32_t iconId = spellDbc->getUInt32(i, iconField);
if (id > 0 && iconId > 0) (*spellIconIds)[id] = iconId;
}
}
}
auto iit = spellIconIds->find(spellId);
if (iit == spellIconIds->end()) return {};
auto pit = spellIconPaths->find(iit->second);
if (pit == spellIconPaths->end()) return {};
return pit->second;
});
}
LOG_INFO("Addon system initialized, found ", addonManager_->getAddons().size(), " addon(s)");
} else {
LOG_WARNING("Failed to initialize addon system");