Files
Guda-ClassicAPI/Core/ItemDetection.lua
T
Salikh Gurgenidze 8019e19e37 category view issues
2026-01-24 13:19:51 +04:00

463 lines
15 KiB
Lua

-- Guda Item Detection
-- Centralized item property detection with caching
-- Used by: CategoryManager, SortEngine, QuestItemBar, ItemButton, BagFrame, BankFrame
local addon = Guda
local ItemDetection = {}
addon.Modules.ItemDetection = ItemDetection
--=====================================================
-- Detection Result Caching
-- Caches tooltip scan results to avoid repeated scans
--=====================================================
local detectionCache = {}
local cacheHits = 0
local cacheMisses = 0
-- Clear the entire detection cache (use sparingly - only for major events)
-- For simple item moves, use InvalidateItem() or don't invalidate at all
function ItemDetection:ClearCache()
detectionCache = {}
cacheHits = 0
cacheMisses = 0
end
-- Invalidate a specific item from cache (by itemLink)
-- Use this when a specific item's properties might have changed
function ItemDetection:InvalidateItem(itemLink)
if itemLink then
detectionCache[itemLink] = nil
end
end
-- Invalidate multiple items from cache
-- Use this for batch operations
function ItemDetection:InvalidateItems(itemLinks)
if itemLinks then
for _, link in ipairs(itemLinks) do
if link then
detectionCache[link] = nil
end
end
end
end
-- Check if we have cached data for an item (useful for debugging)
function ItemDetection:IsCached(itemLink)
return itemLink and detectionCache[itemLink] ~= nil
end
-- Get cache statistics
function ItemDetection:GetCacheStats()
local total = cacheHits + cacheMisses
local hitRate = total > 0 and (cacheHits / total * 100) or 0
local size = 0
for _ in pairs(detectionCache) do size = size + 1 end
return {
hits = cacheHits,
misses = cacheMisses,
total = total,
hitRate = hitRate,
size = size,
}
end
-- Generate cache key for an item
local function GetCacheKey(itemLink)
if not itemLink then return nil end
return itemLink
end
--=====================================================
-- Tooltip Scanning Helpers
-- Uses shared tooltip from Utils module
--=====================================================
-- Scan tooltip and return all text lines
local function ScanTooltipLines(bagID, slotID, itemLink)
-- Use shared tooltip from Utils module
local tooltip, tooltipName = addon.Modules.Utils:GetScanTooltip()
-- Ensure tooltip owner is set before each scan
tooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
tooltip:ClearLines()
-- Set tooltip based on what we have
if bagID and slotID then
if bagID == -1 then
-- Bank main bag: use SetInventoryItem (slot 39 + slotID)
if tooltip.SetInventoryItem then
tooltip:SetInventoryItem("player", 39 + slotID)
else
-- Fallback to hyperlink if SetInventoryItem not available
if itemLink then
tooltip:SetHyperlink(itemLink)
else
return {}
end
end
else
-- Regular bags and bank bags (5-11)
tooltip:SetBagItem(bagID, slotID)
end
elseif itemLink then
tooltip:SetHyperlink(itemLink)
else
return {}
end
local lines = {}
local numLines = tooltip:NumLines() or 0
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 ""
-- Get left text colors (for detecting yellow/green/red text)
local lr, lg, lb, la = 1, 1, 1, 1
if leftLine and leftLine.GetTextColor then
lr, lg, lb, la = leftLine:GetTextColor()
end
-- Get right text colors (for detecting red requirements)
local rr, rg, rb, ra = 1, 1, 1, 1
if rightLine and rightLine.GetTextColor then
rr, rg, rb, ra = rightLine:GetTextColor()
end
table.insert(lines, {
left = leftText,
right = rightText,
leftLower = leftText and string.lower(leftText) or "",
rightLower = rightText and string.lower(rightText) or "",
r = lr, g = lg, b = lb,
rightR = rr, rightG = rg, rightB = rb,
})
end
return lines
end
--=====================================================
-- Core Detection Functions
--=====================================================
-- Check if item is a permanent enchant (enchanting scroll/vellum)
local function DetectPermanentEnchant(lines)
for _, line in ipairs(lines) do
if string.find(line.leftLower, "permanently") then
return true
end
end
return false
end
-- Check if item is a quest item and/or quest starter
local function DetectQuestItem(lines, itemData)
local isQuestItem = false
local isQuestStarter = false
-- Check item category first
if itemData and itemData.class == "Quest" then
isQuestItem = true
end
-- Scan tooltip for quest-related text
for _, line in ipairs(lines) do
local text = line.leftLower
-- Quest starter patterns (highest priority)
if string.find(text, "quest starter") or
string.find(text, "this item begins a quest") or
string.find(text, "begins a quest") or
string.find(text, "starts a quest") then
isQuestItem = true
isQuestStarter = true
end
-- Quest item patterns
if string.find(text, "quest item") then
isQuestItem = true
end
end
return isQuestItem, isQuestStarter
end
-- Check if item is junk (gray or white equippable without special properties)
local function DetectJunk(lines, itemData)
if not itemData then return false end
-- Ensure quality is a number
local quality = tonumber(itemData.quality)
local itemClass = itemData.class or ""
local itemSubclass = itemData.subclass or ""
local itemName = itemData.name or ""
local itemLink = itemData.link or ""
-- Gray items are always junk
-- Check quality value OR link color (gray = |cff9d9d9d)
if quality == 0 then
return true
end
-- Fallback: check link color for gray items
if itemLink and string.find(itemLink, "|cff9d9d9d") then
return true
end
-- If quality is still nil, try to determine from link color
if quality == nil then
if string.find(itemLink, "|cffffffff") then
quality = 1 -- White
else
return false -- Unknown quality, don't mark as junk
end
end
-- White equippable items might be junk
if quality == 1 and (itemClass == "Weapon" or itemClass == "Armor") then
-- Exclusions: trinkets, rings, necklaces, tabards, shirts
local subLower = string.lower(itemSubclass)
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") then
return false
end
-- Check for profession tools by common names
local nameLower = string.lower(itemName)
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
-- Check for profession tool subtype
if string.find(subLower, "fishing pole") or
string.find(subLower, "mining pick") or
string.find(subLower, "skinning knife") then
return false
end
-- Check tooltip for special text (Use:, Equip:, green text)
for _, line in ipairs(lines) do
local text = line.leftLower
-- Yellow text (Use:, Equip:)
if line.r and line.g and line.b then
local isYellow = (line.r > 0.9 and line.g > 0.75 and line.b < 0.2)
local isGreen = (line.r < 0.2 and line.g > 0.9 and line.b < 0.2)
if isYellow and (string.find(text, "use:") or string.find(text, "equip:")) then
return false
end
if isGreen then
return false
end
end
end
-- White equippable without special properties = junk
return true
end
return false
end
-- Check if item is usable for a quest (has yellow "Use:" text related to quest)
local function DetectQuestUsable(lines)
for _, line in ipairs(lines) do
-- Check for yellow "Use:" text
if line.r and line.g and line.b then
local isYellow = (line.r > 0.9 and line.g > 0.75 and line.b < 0.2)
if isYellow and string.find(line.leftLower, "use:") then
return true
end
end
end
return false
end
-- Durability pattern for filtering out broken item red text
local durabilityPattern = DURABILITY_TEMPLATE and string.gsub(DURABILITY_TEMPLATE, "%%d", "%%d+") or nil
-- Check if text color is red (unusable requirement)
local function IsRedColor(r, g, b)
if not r or not g or not b then return false end
-- RED_FONT_COLOR is typically (1.0, 0.1, 0.1)
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)
end
-- Check if item is unusable (has red text indicating unmet requirements)
-- Excludes durability lines (broken items show red durability)
local function DetectUnusable(lines)
for _, line in ipairs(lines) do
if line.r and line.g and line.b then
-- Check for red text (requirements not met)
local isRed = IsRedColor(line.r, line.g, line.b) or
(line.r > 0.85 and line.g < 0.3 and line.b < 0.3)
if isRed then
local text = line.left or ""
-- Ignore durability lines (broken items)
if durabilityPattern and string.find(text, durabilityPattern) then
-- skip durability
else
return true
end
end
end
-- Also check right column (some requirements appear there)
if line.rightR and line.rightG and line.rightB then
local isRed = IsRedColor(line.rightR, line.rightG, line.rightB) or
(line.rightR > 0.85 and line.rightG < 0.3 and line.rightB < 0.3)
if isRed then
local text = line.right or ""
if durabilityPattern and string.find(text, durabilityPattern) then
-- skip durability
else
return true
end
end
end
end
return false
end
--=====================================================
-- Public API - Cached Detection
--=====================================================
-- Get all item properties at once (cached)
-- Returns: { isQuestItem, isQuestStarter, isQuestUsable, isJunk, isPermanentEnchant, isUnusable }
function ItemDetection:GetItemProperties(itemData, bagID, slotID)
if not itemData then
return {
isQuestItem = false,
isQuestStarter = false,
isQuestUsable = false,
isJunk = false,
isPermanentEnchant = false,
isUnusable = false,
}
end
local itemLink = itemData.link
local cacheKey = GetCacheKey(itemLink)
-- Check cache
if cacheKey and detectionCache[cacheKey] then
cacheHits = cacheHits + 1
return detectionCache[cacheKey]
end
cacheMisses = cacheMisses + 1
-- Scan tooltip once
local lines = ScanTooltipLines(bagID, slotID, itemLink)
-- Debug: log if tooltip scan failed
if table.getn(lines) == 0 and addon.DEBUG then
addon:Debug("ItemDetection: No tooltip lines for %s (bag=%s, slot=%s)",
tostring(itemData.name or itemLink), tostring(bagID), tostring(slotID))
end
-- Detect all properties
local isPermanentEnchant = DetectPermanentEnchant(lines)
local isQuestItem, isQuestStarter = DetectQuestItem(lines, itemData)
local isQuestUsable = DetectQuestUsable(lines)
local isJunk = DetectJunk(lines, itemData)
local isUnusable = DetectUnusable(lines)
-- Debug: log junk detection for gray items
if addon.DEBUG then
local quality = tonumber(itemData.quality)
local linkHasGray = itemLink and string.find(itemLink, "|cff9d9d9d")
if quality == 0 or linkHasGray then
addon:Debug("ItemDetection JUNK: %s quality=%s linkGray=%s isJunk=%s",
tostring(itemData.name), tostring(quality), tostring(linkHasGray), tostring(isJunk))
end
end
-- Permanent enchants are NOT quest items (even if categorized as Quest)
if isPermanentEnchant then
isQuestItem = false
isQuestStarter = false
isQuestUsable = false
end
local result = {
isQuestItem = isQuestItem,
isQuestStarter = isQuestStarter,
isQuestUsable = isQuestUsable,
isJunk = isJunk,
isPermanentEnchant = isPermanentEnchant,
isUnusable = isUnusable,
}
-- Cache result
if cacheKey then
detectionCache[cacheKey] = result
end
return result
end
-- Convenience functions for single property checks
function ItemDetection:IsQuestItem(itemData, bagID, slotID)
local props = self:GetItemProperties(itemData, bagID, slotID)
return props.isQuestItem, props.isQuestStarter
end
function ItemDetection:IsQuestStarter(itemData, bagID, slotID)
local props = self:GetItemProperties(itemData, bagID, slotID)
return props.isQuestStarter
end
function ItemDetection:IsQuestUsable(itemData, bagID, slotID)
local props = self:GetItemProperties(itemData, bagID, slotID)
return props.isQuestUsable
end
function ItemDetection:IsJunk(itemData, bagID, slotID)
local props = self:GetItemProperties(itemData, bagID, slotID)
return props.isJunk
end
function ItemDetection:IsPermanentEnchant(itemData, bagID, slotID)
local props = self:GetItemProperties(itemData, bagID, slotID)
return props.isPermanentEnchant
end
function ItemDetection:IsUnusable(itemData, bagID, slotID)
local props = self:GetItemProperties(itemData, bagID, slotID)
return props.isUnusable
end
--=====================================================
-- Initialization
--=====================================================
function ItemDetection:Initialize()
-- Clear cache when entering world (character switch)
addon.Modules.Events:Register("PLAYER_ENTERING_WORLD", function()
self:ClearCache()
end, "ItemDetection")
addon:Debug("ItemDetection module initialized")
end