Files
Guda-ClassicAPI/Core/ItemDetectionClassicAPI.lua
T

367 lines
12 KiB
Lua

-- Guda ClassicAPI item detection fast path
-- Replaces the allocation-heavy tooltip line-table pipeline with a single-pass
-- scanner while preserving Guda's public ItemDetection API.
local addon = Guda
local ItemDetection = addon.Modules.ItemDetection
if not ItemDetection then return end
local originalGetCharges = ItemDetection.GetCharges
local detectionCache = {}
local cacheHits = 0
local cacheMisses = 0
local EMPTY_PROPERTIES = {
isQuestItem = false,
isQuestStarter = false,
isQuestUsable = false,
isJunk = false,
isPermanentEnchant = false,
isUnusable = false,
}
local durabilityPattern = DURABILITY_TEMPLATE
and string.gsub(DURABILITY_TEMPLATE, "%%d", "%%d+") or nil
local function GetCacheKey(itemLink)
return itemLink
end
local function ExtractItemID(itemData)
if not itemData then return nil end
if itemData.itemID then return tonumber(itemData.itemID) end
if not itemData.link then return nil end
local _, _, id = string.find(itemData.link, "item:(%d+)")
return id and tonumber(id) or nil
end
local function IsExcludedQuestItem(itemData)
local exclusions = addon.Constants and addon.Constants.QUEST_CATEGORY_EXCLUSIONS
if not exclusions then return false end
local itemID = ExtractItemID(itemData)
return itemID and exclusions[itemID] and true or false
end
local function IsRedColor(r, g, b)
if not r or not g or not b then return false end
local dr = math.abs(r - 1.0)
local dg = math.abs(g - 0.125)
local db = math.abs(b - 0.125)
return (dr < 0.15 and dg < 0.15 and db < 0.15)
or (r > 0.85 and g < 0.3 and b < 0.3)
end
local function IsRequirementText(text)
if not text or text == "" then return false end
local lower = string.lower(text)
if string.find(lower, "requires") then return true end
if string.find(lower, "require") then return true end
if string.find(lower, "classes:") then return true end
if string.find(lower, "races:") then return true end
if string.find(lower, "level %d") then return true end
if string.find(lower, "skill:") then return true end
if string.find(lower, "reputation") then return true end
if string.find(lower, "riding") then return true end
if string.find(lower, "already known") then return true end
if string.find(lower, "warrior") then return true end
if string.find(lower, "paladin") then return true end
if string.find(lower, "hunter") then return true end
if string.find(lower, "rogue") then return true end
if string.find(lower, "priest") then return true end
if string.find(lower, "shaman") then return true end
if string.find(lower, "mage") then return true end
if string.find(lower, "warlock") then return true end
if string.find(lower, "druid") then return true end
if string.find(lower, "human") then return true end
if string.find(lower, "dwarf") then return true end
if string.find(lower, "night elf") then return true end
if string.find(lower, "gnome") then return true end
if string.find(lower, "orc") then return true end
if string.find(lower, "undead") then return true end
if string.find(lower, "tauren") then return true end
if string.find(lower, "troll") then return true end
if string.find(lower, "goblin") then return true end
if string.find(lower, "high elf") then return true end
return false
end
local function SafeSetHyperlink(tooltip, link)
if not link then return false end
local _, _, itemString = string.find(link, "|H(item:[^|]+)|h")
if not itemString and string.find(link, "^item:") then
itemString = link
end
if not itemString then return false end
local ok = pcall(tooltip.SetHyperlink, tooltip, itemString)
return ok
end
local function PrepareTooltip(bagID, slotID, itemLink)
local tooltip, tooltipName = addon.Modules.Utils:GetScanTooltip()
tooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
tooltip:ClearLines()
if bagID and slotID then
if bagID == -1 then
if not SafeSetHyperlink(tooltip, itemLink) then return nil, nil end
else
local ok = pcall(tooltip.SetBagItem, tooltip, bagID, slotID)
if not ok then
if not SafeSetHyperlink(tooltip, itemLink) then return nil, nil end
end
end
elseif itemLink then
if not SafeSetHyperlink(tooltip, itemLink) then return nil, nil end
else
return nil, nil
end
return tooltip, tooltipName
end
local function IsWhiteJunkCandidate(itemData)
if not itemData then return false end
local quality = tonumber(itemData.quality)
local itemLink = itemData.link or ""
if quality == nil and string.find(itemLink, "|cffffffff") then
quality = 1
end
if quality ~= 1 then return false end
if not (addon.Modules.DB and addon.Modules.DB:GetSetting("whiteItemsJunk")) then
return false
end
local itemClass = itemData.class or ""
if itemClass ~= "Weapon" and itemClass ~= "Armor" then return false end
local subLower = string.lower(itemData.subclass or "")
if string.find(subLower, "trinket")
or string.find(subLower, "ring")
or string.find(subLower, "neck")
or string.find(subLower, "tabard")
or string.find(subLower, "shirt")
or string.find(subLower, "fishing pole")
or string.find(subLower, "mining pick")
or string.find(subLower, "skinning knife") then
return false
end
local nameLower = string.lower(itemData.name or "")
if string.find(nameLower, "mining pick")
or string.find(nameLower, "skinning knife")
or string.find(nameLower, "blacksmith hammer")
or string.find(nameLower, "fishing pole")
or string.find(nameLower, "gnomish army knife") then
return false
end
return true
end
-- Single pass over the tooltip font strings. No `lines = {}` table and no
-- per-line `{left=..., r=...}` allocations are created.
local function ScanProperties(itemData, bagID, slotID)
local result = {
isQuestItem = false,
isQuestStarter = false,
isQuestUsable = false,
isJunk = false,
isPermanentEnchant = false,
isUnusable = false,
}
if not itemData then return result, false end
local quality = tonumber(itemData.quality)
local itemLink = itemData.link or ""
if quality == 0 or string.find(itemLink, "|cff9d9d9d") then
result.isJunk = true
end
local excludedQuest = IsExcludedQuestItem(itemData)
if not excludedQuest and itemData.class == "Quest" then
result.isQuestItem = true
end
local whiteJunkCandidate = IsWhiteJunkCandidate(itemData)
local whiteHasSpecialText = false
local tooltip, tooltipName = PrepareTooltip(bagID, slotID, itemData.link)
if not tooltip then return result, false end
local numLines = tooltip:NumLines() or 0
local complete = numLines >= 2
for i = 1, numLines do
local leftLine = getglobal(tooltipName .. "TextLeft" .. i)
local rightLine = getglobal(tooltipName .. "TextRight" .. i)
local leftText = leftLine and leftLine:GetText() or ""
local rightText = rightLine and rightLine:GetText() or ""
local leftLower = leftText ~= "" and string.lower(leftText) or ""
local lr, lg, lb = 1, 1, 1
if leftLine and leftLine.GetTextColor then
lr, lg, lb = leftLine:GetTextColor()
end
if string.find(leftLower, "permanently") then
result.isPermanentEnchant = true
end
if not excludedQuest then
if string.find(leftLower, "quest starter")
or string.find(leftLower, "this item begins a quest")
or string.find(leftLower, "begins a quest")
or string.find(leftLower, "starts a quest") then
result.isQuestItem = true
result.isQuestStarter = true
elseif string.find(leftLower, "quest item") then
result.isQuestItem = true
end
end
local isYellow = lr > 0.9 and lg > 0.75 and lb < 0.2
local isGreen = lr < 0.3 and lg > 0.9 and lb < 0.3
if (isYellow or isGreen) and string.find(leftLower, "use:") then
result.isQuestUsable = true
end
if whiteJunkCandidate then
if (isYellow and (string.find(leftLower, "use:") or string.find(leftLower, "equip:")))
or isGreen then
whiteHasSpecialText = true
end
end
if i >= 2 then
if IsRedColor(lr, lg, lb) then
if not (durabilityPattern and string.find(leftText, durabilityPattern))
and IsRequirementText(leftText) then
result.isUnusable = true
end
end
if rightLine and rightLine.GetTextColor and rightText ~= "" then
local rr, rg, rb = rightLine:GetTextColor()
if IsRedColor(rr, rg, rb) then
if not (durabilityPattern and string.find(rightText, durabilityPattern))
and IsRequirementText(rightText) then
result.isUnusable = true
end
end
end
end
end
if whiteJunkCandidate and not whiteHasSpecialText then
result.isJunk = true
end
if result.isPermanentEnchant then
result.isQuestItem = false
result.isQuestStarter = false
result.isQuestUsable = false
end
return result, complete
end
function ItemDetection:ClearCache()
detectionCache = {}
cacheHits = 0
cacheMisses = 0
end
function ItemDetection:InvalidateItem(itemLink)
if itemLink then detectionCache[itemLink] = nil end
end
function ItemDetection:InvalidateItems(itemLinks)
if not itemLinks then return end
for _, link in ipairs(itemLinks) do
if link then detectionCache[link] = nil end
end
end
function ItemDetection:IsCached(itemLink)
return itemLink and detectionCache[itemLink] ~= nil
end
function ItemDetection:GetCacheStats()
local total = cacheHits + cacheMisses
local size = 0
for _ in pairs(detectionCache) do size = size + 1 end
return {
hits = cacheHits,
misses = cacheMisses,
total = total,
hitRate = total > 0 and (cacheHits / total * 100) or 0,
size = size,
}
end
function ItemDetection:GetItemProperties(itemData, bagID, slotID)
if not itemData then return EMPTY_PROPERTIES end
local cacheKey = GetCacheKey(itemData.link)
if cacheKey and detectionCache[cacheKey] then
cacheHits = cacheHits + 1
return detectionCache[cacheKey]
end
cacheMisses = cacheMisses + 1
local result, complete = ScanProperties(itemData, bagID, slotID)
if cacheKey and complete then
detectionCache[cacheKey] = result
end
return result
end
function ItemDetection:IsUnusableCached(itemData)
if not itemData or not itemData.link then return nil end
local props = detectionCache[GetCacheKey(itemData.link)]
if not props then return nil end
return props.isUnusable
end
-- ClassicAPI can read charges directly from the live item descriptor, making
-- tooltip text parsing unnecessary for the common path.
function ItemDetection:GetCharges(itemData, bagID, slotID)
if not bagID or not slotID then return nil end
local api = addon.Modules.ClassicAPI
if api and api.HasContainerItemCharges and api:HasContainerItemCharges() then
return api:GetContainerItemCharges(bagID, slotID)
end
if originalGetCharges then
return originalGetCharges(self, itemData, bagID, slotID)
end
return nil
end
-- No per-slot tooltip charge cache is needed when ClassicAPI provides the
-- descriptor value. Keep this method for callers and for the Vanilla fallback.
local originalInvalidateCharges = ItemDetection.InvalidateCharges
function ItemDetection:InvalidateCharges(bagID)
local api = addon.Modules.ClassicAPI
if api and api.HasContainerItemCharges and api:HasContainerItemCharges() then
return
end
if originalInvalidateCharges then
return originalInvalidateCharges(self, bagID)
end
end
addon:Debug("ClassicAPI single-pass ItemDetection enabled")