if not WeakAuras.IsLibsOK() then return end ---@type string local AddonName = ... ---@class OptionsPrivate local OptionsPrivate = select(2, ...) -- Lua APIs local pairs, error, coroutine = pairs, error, coroutine -- WoW APIs local IsSpellKnown = IsSpellKnown ---@class WeakAuras local WeakAuras = WeakAuras local spellCache = {} WeakAuras.spellCache = spellCache local cache local metaData local bestIcon = {} -- Builds a cache of name/icon pairs from existing spell data -- This is a rather slow operation, so it's only done once, and the result is subsequently saved function spellCache.Build() if not cache then error("spellCache has not been loaded. Call WeakAuras.spellCache.Load(...) first.") end if not metaData.needsRebuild then return end local holes if WeakAuras.IsClassicEra() then holes = {} holes[63707] = 81743 holes[81748] = 219002 holes[219004] = 285223 holes[285224] = 301088 holes[301101] = 324269 elseif WeakAuras.IsCataClassic() then holes = {} holes[121820] = 158262 holes[158263] = 186402 holes[186403] = 219002 holes[219004] = 243805 holes[243806] = 261127 holes[262591] = 281624 holes[301101] = 324269 end wipe(cache) local co = coroutine.create(function() local id = 0 local misses = 0 while misses < 80000 do id = id + 1 local name = OptionsPrivate.Private.ExecEnv.GetSpellName(id) local icon = OptionsPrivate.Private.ExecEnv.GetSpellIcon(id) if(icon == 136243) then -- 136243 is the a gear icon, we can ignore those spells misses = 0; elseif name and name ~= "" and icon then cache[name] = cache[name] or {} if not cache[name].spells or cache[name].spells == "" then cache[name].spells = id .. "=" .. icon else cache[name].spells = cache[name].spells .. "," .. id .. "=" .. icon end misses = 0 if holes and holes[id] then id = holes[id] end else misses = misses + 1 end coroutine.yield() end if WeakAuras.IsCataOrRetail() then for _, category in pairs(GetCategoryList()) do local total = GetCategoryNumAchievements(category, true) for i = 1, total do local id,name,_,_,_,_,_,_,_,iconID = GetAchievementInfo(category, i) if name and iconID then cache[name] = cache[name] or {} if not cache[name].achievements or cache[name].achievements == "" then cache[name].achievements = id .. "=" .. iconID else cache[name].achievements = cache[name].achievements .. "," .. id .. "=" .. iconID end end end coroutine.yield() end end metaData.needsRebuild = false end) OptionsPrivate.Private.dynFrame:AddAction("spellCache", co) end --[[ function to help find big holes in spellIds to help speedup Build() local id = 0 local misses = 0 local lastId print("####") while misses < 400000 do id = id + 1 local name = GetSpellInfo(id) local icon = GetSpellTexture(id) if icon == 136243 then -- 136243 is the a gear icon, we can ignore those spells misses = 0 elseif name and name ~= "" and icon then if misses > 10000 then print(("holes[%s] = %s"):format(lastId, id - 1)) end lastId = id misses = 0 else misses = misses + 1 end end print("lastId", lastId) ]] function spellCache.GetIcon(name) if (name == nil) then return nil; end if cache then if (bestIcon[name]) then return bestIcon[name] end local icons = cache[name] local bestMatch = nil if (icons) then if (icons.spells) then for spell, icon in icons.spells:gmatch("(%d+)=(%d+)") do local spellId = tonumber(spell) if not bestMatch or (spellId and IsSpellKnown(spellId)) then bestMatch = tonumber(icon) end end end end bestIcon[name] = bestMatch return bestIcon[name] else error("spellCache has not been loaded. Call WeakAuras.spellCache.Load(...) first.") end end function spellCache.GetSpellsMatching(name) if cache[name] then if cache[name].spells then local result = {} for spell, icon in cache[name].spells:gmatch("(%d+)=(%d+)") do local spellId = tonumber(spell) local iconId = tonumber(icon) result[spellId] = icon end return result end end end function spellCache.AddIcon(name, id, icon) if not cache then error("spellCache has not been loaded. Call WeakAuras.spellCache.Load(...) first.") return end if name and id and icon then cache[name] = cache[name] or {} if not cache[name].spells or cache[name].spells == "" then cache[name].spells = id .. "=" .. icon else cache[name].spells = cache[name].spells .. "," .. id .. "=" .. icon end end end function spellCache.Get() if cache then return cache else error("spellCache has not been loaded. Call WeakAuras.spellCache.Load(...) first.") end end function spellCache.Load(data) metaData = data cache = metaData.spellCache local _, build = GetBuildInfo(); local locale = GetLocale(); local version = WeakAuras.versionString local num = 0; for i,v in pairs(cache) do num = num + 1; end if(num < 39000 or metaData.locale ~= locale or metaData.build ~= build or metaData.version ~= version or not metaData.spellCacheStrings) then metaData.build = build; metaData.locale = locale; metaData.version = version; metaData.spellCacheAchievements = true metaData.spellCacheStrings = true metaData.needsRebuild = true wipe(cache) end end -- This function computes the Levenshtein distance between two strings -- It is used in this program to match spell icon textures with "good" spell names; i.e., -- spell names that are very similar to the name of the texture local function Lev(str1, str2) local matrix = {}; for i=0, str1:len() do matrix[i] = {[0] = i}; end for j=0, str2:len() do matrix[0][j] = j; end for j=1, str2:len() do for i =1, str1:len() do if(str1:sub(i, i) == str2:sub(j, j)) then matrix[i][j] = matrix[i-1][j-1]; else matrix[i][j] = math.min(matrix[i-1][j], matrix[i][j-1], matrix[i-1][j-1]) + 1; end end end return matrix[str1:len()][str2:len()]; end function spellCache.BestKeyMatch(nearkey) local bestKey = ""; local bestDistance = math.huge; local partialMatches = {}; if cache[nearkey] then return nearkey end for key, value in pairs(cache) do if key:lower() == nearkey:lower() then return key end if(key:lower():find(nearkey:lower(), 1, true)) then partialMatches[key] = value; end end for key, value in pairs(partialMatches) do local distance = Lev(nearkey, key); if(distance < bestDistance) then bestKey = key; bestDistance = distance; end end return bestKey; end function spellCache.CorrectAuraName(input) if (not cache) then error("spellCache has not been loaded. Call WeakAuras.spellCache.Load(...) first.") end local spellId = WeakAuras.SafeToNumber(input); if(spellId) then local name, _, icon = OptionsPrivate.Private.ExecEnv.GetSpellInfo(spellId); if(name) then spellCache.AddIcon(name, spellId, icon) return name, spellId; else return "Invalid Spell ID"; end else local ret = spellCache.BestKeyMatch(input); if(ret == "") then return "No Match Found", nil; else return ret, nil; end end end