diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index 1649e3e..e434ac4 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -600,7 +600,12 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar return isBoE == ruleValue elseif ruleType == "isQuestItem" then - -- Use consolidated quest detection from Utils + -- Use centralized ItemDetection for quest item detection + if addon.Modules.ItemDetection then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isQuestItem == ruleValue + end + -- Fallback to Utils if ItemDetection not available local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false) return isQuestItem == ruleValue @@ -662,86 +667,13 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar return isProfessionTool == ruleValue elseif ruleType == "isJunk" then - -- Junk items: gray items (quality 0) OR white equippable items (quality 1 + Weapon/Armor) - -- EXCLUDES: - -- 1. Trinkets, Rings, Necklaces (these typically have special effects) - -- 2. Profession tools (skinning knife, mining pick, fishing poles, etc.) - -- 3. Items with yellow description text (Use:, Equip:, Chance on hit: effects) - -- 4. Items with green description text (set bonuses, special properties) - local quality = itemData.quality - local isGray = false - local isWhiteEquip = false - - -- Check for gray items (quality 0) - if quality == 0 then - isGray = true - elseif not isOtherChar and addon.Modules.Utils and addon.Modules.Utils.IsItemGrayTooltip then - -- Tooltip fallback for gray detection - isGray = addon.Modules.Utils:IsItemGrayTooltip(bagID, slotID, itemData.link) + -- Use centralized ItemDetection for junk detection + if addon.Modules.ItemDetection then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isJunk == ruleValue end - - -- Check for white equippable items (quality 1 + Weapon/Armor) - if quality == 1 then - local itemClass = itemData.class or "" - local itemSubclass = itemData.subclass or "" - - if itemClass == "Weapon" or itemClass == "Armor" then - -- In Turtle WoW, equip slot info is in itemSubType, not itemEquipLoc - local itemSubclass = itemData.subclass or "" - addon:Debug("isJunk check: name='%s', subclass='%s', class='%s'", - tostring(itemData.name), tostring(itemSubclass), tostring(itemClass)) - - -- EXCLUDE: Trinkets, Rings, Necklaces, Tabards, Shirts - these typically have special effects or are cosmetic - -- Check by itemSubType which contains INVTYPE_* values in Turtle WoW - local isSpecialSlot = (itemSubclass == "INVTYPE_TRINKET" or - itemSubclass == "INVTYPE_FINGER" or - itemSubclass == "INVTYPE_NECK" or - itemSubclass == "INVTYPE_TABARD" or - itemSubclass == "INVTYPE_BODY") - if isSpecialSlot then - addon:Debug("isJunk: EXCLUDED (trinket/ring/neck/tabard/shirt) - %s (subclass=%s)", tostring(itemData.name), tostring(itemSubclass)) - return false == ruleValue - end - - -- Check if this is a profession tool (should NOT be junk) - local isProfessionTool = false - - -- Check by item ID - if itemData.link then - local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) - if itemID and addon.Constants.PROFESSION_TOOL_IDS and addon.Constants.PROFESSION_TOOL_IDS[itemID] then - isProfessionTool = true - addon:Debug("isJunk: PROFESSION TOOL (by ID) - %s", tostring(itemData.name)) - end - end - - -- Check by subtype (e.g., Fishing Pole) - if not isProfessionTool and addon.Constants.PROFESSION_TOOL_SUBTYPES and addon.Constants.PROFESSION_TOOL_SUBTYPES[itemSubclass] then - isProfessionTool = true - addon:Debug("isJunk: PROFESSION TOOL (by subtype) - %s", tostring(itemData.name)) - end - - -- Check for special tooltip text (yellow/green descriptions) - -- These items have Use:, Equip:, or special effects and should NOT be junk - local hasSpecialText = false - if not isProfessionTool and not isOtherChar then - if addon.Modules.Utils and addon.Modules.Utils.HasSpecialTooltipText then - hasSpecialText = addon.Modules.Utils:HasSpecialTooltipText(bagID, slotID, itemData.link) - if hasSpecialText then - addon:Debug("isJunk: HAS SPECIAL TEXT (Use:/Equip:/green) - %s", tostring(itemData.name)) - end - end - end - - if not isProfessionTool and not hasSpecialText then - isWhiteEquip = true - addon:Debug("isJunk: WHITE EQUIP DETECTED - %s", tostring(itemData.name)) - end - end - end - - local isJunk = isGray or isWhiteEquip - return isJunk == ruleValue + -- Fallback: gray items are always junk + return (itemData.quality == 0) == ruleValue end return false diff --git a/Core/ItemDetection.lua b/Core/ItemDetection.lua new file mode 100644 index 0000000..d42fb0e --- /dev/null +++ b/Core/ItemDetection.lua @@ -0,0 +1,379 @@ +-- 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 detection cache +function ItemDetection:ClearCache() + detectionCache = {} + cacheHits = 0 + cacheMisses = 0 +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 +--===================================================== + +-- Get or create the scanning tooltip +local scanTooltip = nil +local function GetScanTooltip() + if not scanTooltip then + scanTooltip = CreateFrame("GameTooltip", "GudaItemDetectionTooltip", nil, "GameTooltipTemplate") + scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") + end + return scanTooltip +end + +-- Scan tooltip and return all text lines +local function ScanTooltipLines(bagID, slotID, itemLink) + local tooltip = GetScanTooltip() + local tooltipName = "GudaItemDetectionTooltip" + + -- 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 text colors (for detecting yellow/green text) + local lr, lg, lb, la = 1, 1, 1, 1 + if leftLine and leftLine.GetTextColor then + lr, lg, lb, la = leftLine: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, + }) + 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 + +--===================================================== +-- Public API - Cached Detection +--===================================================== + +-- Get all item properties at once (cached) +-- Returns: { isQuestItem, isQuestStarter, isQuestUsable, isJunk, isPermanentEnchant } +function ItemDetection:GetItemProperties(itemData, bagID, slotID) + if not itemData then + return { + isQuestItem = false, + isQuestStarter = false, + isQuestUsable = false, + isJunk = false, + isPermanentEnchant = 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) + + -- 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, + } + + -- 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 + +--===================================================== +-- 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 diff --git a/Core/Main.lua b/Core/Main.lua index 320b8a9..21a50c2 100644 --- a/Core/Main.lua +++ b/Core/Main.lua @@ -15,6 +15,11 @@ function Main:Initialize() -- Initialize database addon.Modules.DB:Initialize() + -- Initialize item detection (before scanners, as they may use it) + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:Initialize() + end + -- Initialize scanners addon.Modules.BagScanner:Initialize() addon.Modules.BankScanner:Initialize() @@ -178,6 +183,12 @@ function Main:SetupSlashCommands() addon:Print("Tooltip Cache: %d hits, %d misses (%.1f%% hit rate)", stats.hits, stats.misses, stats.hitRate) end + -- Show item detection cache stats + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.GetCacheStats then + local stats = addon.Modules.ItemDetection:GetCacheStats() + addon:Print("ItemDetection Cache: %d hits, %d misses (%.1f%% hit rate, %d items)", + stats.hits, stats.misses, stats.hitRate, stats.size) + end elseif msg == "perfreset" then -- Reset performance statistics @@ -193,6 +204,10 @@ function Main:SetupSlashCommands() if addon.Modules.Utils and addon.Modules.Utils.ClearTooltipCache then addon.Modules.Utils:ClearTooltipCache() end + -- Clear item detection cache + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end elseif msg == "help" then -- Show help diff --git a/Data/BankScanner.lua b/Data/BankScanner.lua index 8f8f706..a919a5f 100644 --- a/Data/BankScanner.lua +++ b/Data/BankScanner.lua @@ -78,6 +78,14 @@ function BankScanner:GetBankData() end end dirtySlots = {} + + -- Check for invalidated bags (nil entries) and rescan them + for _, bagID in ipairs(addon.Constants.BANK_BAGS) do + if bankCache[bagID] == nil then + bankCache[bagID] = self:ScanBankBag(bagID) + end + end + return bankCache end diff --git a/Guda.toc b/Guda.toc index dc9c2bf..12dfe0e 100644 --- a/Guda.toc +++ b/Guda.toc @@ -14,6 +14,7 @@ Core\Database.lua DB\QuestItems.lua Core\Events.lua Core\Utils.lua +Core\ItemDetection.lua Core\CategoryManager.lua Core\Tooltip.lua diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 7314eda..a109938 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -236,24 +236,45 @@ local function GetItemProperties(bagID, slotID, itemLink) end -- Check if an item is a quest item by scanning its tooltip -local function IsQuestItemTooltip(bagID, slotID) +-- Uses centralized ItemDetection when available +local function IsQuestItemTooltip(bagID, slotID, itemData) if not bagID or not slotID then return false end + -- Use ItemDetection if available + if addon.Modules.ItemDetection and itemData then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isQuestItem + end + -- Fallback to local GetItemProperties local link = GetContainerItemLink(bagID, slotID) local props = GetItemProperties(bagID, slotID, link) return props and props.isQuest or false end -- Check if a quest item is usable (has 'Use:' or click to text) -local function IsQuestItemUsable(bagID, slotID) +-- Uses centralized ItemDetection when available +local function IsQuestItemUsable(bagID, slotID, itemData) if not bagID or not slotID then return false end + -- Use ItemDetection if available + if addon.Modules.ItemDetection and itemData then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isQuestUsable + end + -- Fallback to local GetItemProperties local link = GetContainerItemLink(bagID, slotID) local props = GetItemProperties(bagID, slotID, link) return props and props.isQuestUsable or false end -- Check if item is a quest starter (explicit 'Starts a Quest' or 'This Item Begins a Quest') -local function IsQuestItemStarter(bagID, slotID) +-- Uses centralized ItemDetection when available +local function IsQuestItemStarter(bagID, slotID, itemData) if not bagID or not slotID then return false end + -- Use ItemDetection if available + if addon.Modules.ItemDetection and itemData then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isQuestStarter + end + -- Fallback to local GetItemProperties local link = GetContainerItemLink(bagID, slotID) local props = GetItemProperties(bagID, slotID, link) return props and props.isQuestStarter or false @@ -635,52 +656,42 @@ local function AddSortKeys(items) item.isMount = isMount -- Class and slot ordering - -- Check for items that should be treated as junk: - -- 1. Gray items (quality 0) - -- 2. Items with gray tooltip - -- 3. White equippable items (quality 1 Weapon/Armor) - vendor trash - -- EXCLUDES from junk: - -- - Trinkets, Rings, Necklaces (these typically have special effects) - -- - Profession tools (skinning knife, mining pick, fishing poles, etc.) - -- - Items with yellow description text (Use:, Equip:, Chance on hit: effects) - -- - Items with green description text (set bonuses, special properties) - local isGrayItem = itemRarity == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) - local isWhiteEquip = false + -- Check for items that should be treated as junk + -- Use centralized ItemDetection when available + local isJunkItem = false + if addon.Modules.ItemDetection then + local props = addon.Modules.ItemDetection:GetItemProperties(item.data, item.bagID, item.slot) + isJunkItem = props.isJunk + else + -- Fallback: manual junk detection + local isGrayItem = itemRarity == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) + local isWhiteEquip = false - if itemRarity == 1 and (itemCategory == "Weapon" or itemCategory == "Armor") then - -- In Turtle WoW, equip slot info is in itemSubType, not itemEquipLoc - addon:Debug("SortEngine isJunk: name='%s', subclass='%s', class='%s'", - tostring(itemName), tostring(itemSubType), tostring(itemCategory)) + if itemRarity == 1 and (itemCategory == "Weapon" or itemCategory == "Armor") then + addon:Debug("SortEngine isJunk: name='%s', subclass='%s', class='%s'", + tostring(itemName), tostring(itemSubType), tostring(itemCategory)) - -- EXCLUDE: Trinkets, Rings, Necklaces, Tabards, Shirts - these typically have special effects or are cosmetic - -- Check by itemSubType which contains INVTYPE_* values in Turtle WoW - local isSpecialSlot = (itemSubType == "INVTYPE_TRINKET" or - itemSubType == "INVTYPE_FINGER" or - itemSubType == "INVTYPE_NECK" or - itemSubType == "INVTYPE_TABARD" or - itemSubType == "INVTYPE_BODY") + local isSpecialSlot = (itemSubType == "INVTYPE_TRINKET" or + itemSubType == "INVTYPE_FINGER" or + itemSubType == "INVTYPE_NECK" or + itemSubType == "INVTYPE_TABARD" or + itemSubType == "INVTYPE_BODY") - if isSpecialSlot then - addon:Debug("SortEngine: EXCLUDED (special slot) - %s (subclass=%s)", tostring(itemName), tostring(itemSubType)) - end - - if not isSpecialSlot then - -- Check exclusions for white equippable items - local isProfTool = IsProfessionTool(item.data.link, itemSubType) - local hasSpecialText = false - - if not isProfTool then - hasSpecialText = HasSpecialTooltipText(item.bagID, item.slot, item.data.link) - end - - -- Only mark as junk if NOT a profession tool AND NOT has special text - if not isProfTool and not hasSpecialText then - isWhiteEquip = true + if not isSpecialSlot then + local isProfTool = IsProfessionTool(item.data.link, itemSubType) + local hasSpecialText = false + if not isProfTool then + hasSpecialText = HasSpecialTooltipText(item.bagID, item.slot, item.data.link) + end + if not isProfTool and not hasSpecialText then + isWhiteEquip = true + end end end + isJunkItem = isGrayItem or isWhiteEquip end - if isGrayItem or isWhiteEquip then + if isJunkItem then item.sortedClass = CATEGORY_ORDER["Junk"] or 99 item.equipSlotOrder = 999 item.isEquippable = false -- Treat junk gear as junk, not gear @@ -704,7 +715,7 @@ local function AddSortKeys(items) local nameLower = string.lower(item.itemName) if string.find(nameLower, "manual") or string.find(nameLower, "quest") then item.sortedClass = CATEGORY_ORDER["Quest"] or 7 - elseif IsQuestItemTooltip(item.bagID, item.slot) then + elseif IsQuestItemTooltip(item.bagID, item.slot, item.data) then item.sortedClass = CATEGORY_ORDER["Quest"] or 7 elseif addon.IsQuestItemByID then -- Check QuestItemsDB for faction-specific quest items @@ -739,10 +750,10 @@ local function AddSortKeys(items) item.isPermanentEnchant = isPermanentEnchant -- Store for potential use in sorting local nameLower = string.lower(item.itemName) if not isPermanentEnchant then - if itemCategory == "Quest" or string.find(nameLower, "quest") or item.data.class == "Quest" or IsQuestItemTooltip(item.bagID, item.slot) then + if itemCategory == "Quest" or string.find(nameLower, "quest") or item.data.class == "Quest" or IsQuestItemTooltip(item.bagID, item.slot, item.data) then item.isQuest = true - if IsQuestItemStarter(item.bagID, item.slot) then item.isQuestStarter = true end - if IsQuestItemUsable(item.bagID, item.slot) then item.isQuestUsable = true end + if IsQuestItemStarter(item.bagID, item.slot, item.data) then item.isQuestStarter = true end + if IsQuestItemUsable(item.bagID, item.slot, item.data) then item.isQuestUsable = true end elseif addon.IsQuestItemByID then -- Check QuestItemsDB for faction-specific quest items local playerFaction = UnitFactionGroup("player") @@ -1167,57 +1178,47 @@ end -- Split a list of collected items into non-junk and junk items -- Junk includes: gray items (quality 0), gray tooltip items, white equippable items (quality 1 Weapon/Armor) --- EXCLUDES from junk: --- 1. Trinkets, Rings, Necklaces (these typically have special effects) --- 2. Profession tools (skinning knife, mining pick, fishing poles, etc.) --- 3. Items with yellow description text (Use:, Equip:, Chance on hit: effects) --- 4. Items with green description text (set bonuses, special properties) +-- Uses centralized ItemDetection when available local function SplitGreyItems(items) local nonGreys, greys = {}, {} for _, item in ipairs(items) do - -- Use same logic as AddSortKeys for determining Junk status (stability) - local quality = tonumber(item.quality or 0) - local isGray = quality == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) - -- White equippable items (Weapon/Armor) are also treated as junk - local itemClass = item.class or "" - local itemSubclass = item.data and item.data.subclass or "" - local itemLink = item.data and item.data.link - local isWhiteEquip = false + local isJunk = false - if quality == 1 and (itemClass == "Weapon" or itemClass == "Armor") then - -- In Turtle WoW, equip slot info is in itemSubType (stored as subclass), not equipLoc - addon:Debug("SplitGreyItems isJunk: name='%s', subclass='%s', class='%s'", - tostring(item.data and item.data.name), tostring(itemSubclass), tostring(itemClass)) + -- Use centralized ItemDetection when available + if addon.Modules.ItemDetection and item.data then + local props = addon.Modules.ItemDetection:GetItemProperties(item.data, item.bagID, item.slot) + isJunk = props.isJunk + else + -- Fallback: manual junk detection + local quality = tonumber(item.quality or 0) + local isGray = quality == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data and item.data.link) + local itemClass = item.class or "" + local itemSubclass = item.data and item.data.subclass or "" + local itemLink = item.data and item.data.link + local isWhiteEquip = false - -- EXCLUDE: Trinkets, Rings, Necklaces, Tabards, Shirts - these typically have special effects or are cosmetic - -- Check by subclass which contains INVTYPE_* values in Turtle WoW - local isSpecialSlot = (itemSubclass == "INVTYPE_TRINKET" or - itemSubclass == "INVTYPE_FINGER" or - itemSubclass == "INVTYPE_NECK" or - itemSubclass == "INVTYPE_TABARD" or - itemSubclass == "INVTYPE_BODY") + if quality == 1 and (itemClass == "Weapon" or itemClass == "Armor") then + local isSpecialSlot = (itemSubclass == "INVTYPE_TRINKET" or + itemSubclass == "INVTYPE_FINGER" or + itemSubclass == "INVTYPE_NECK" or + itemSubclass == "INVTYPE_TABARD" or + itemSubclass == "INVTYPE_BODY") - if isSpecialSlot then - addon:Debug("SplitGreyItems: EXCLUDED (special slot) - %s (subclass='%s')", tostring(item.data and item.data.name), tostring(itemSubclass)) - end - - if not isSpecialSlot then - -- Check exclusions for white equippable items - local isProfTool = IsProfessionTool(itemLink, itemSubclass) - local hasSpecialText = false - - if not isProfTool then - hasSpecialText = HasSpecialTooltipText(item.bagID, item.slot, itemLink) - end - - -- Only mark as junk if NOT a profession tool AND NOT has special text - if not isProfTool and not hasSpecialText then - isWhiteEquip = true + if not isSpecialSlot then + local isProfTool = IsProfessionTool(itemLink, itemSubclass) + local hasSpecialText = false + if not isProfTool then + hasSpecialText = HasSpecialTooltipText(item.bagID, item.slot, itemLink) + end + if not isProfTool and not hasSpecialText then + isWhiteEquip = true + end end end + isJunk = isGray or isWhiteEquip end - if isGray or isWhiteEquip then + if isJunk then table.insert(greys, item) else table.insert(nonGreys, item) @@ -1464,6 +1465,12 @@ function SortEngine:SortBags() end end + -- Clear caches after sorting to ensure fresh detection on next UI update + addon.Modules.BagScanner:ClearCache() + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end + -- Return total moves made return routeCount + consolidateCount + specializedMoves + regularMoves end @@ -1595,6 +1602,12 @@ function SortEngine:SortBank() end end + -- Clear caches after sorting to ensure fresh detection on next UI update + addon.Modules.BankScanner:ClearCache() + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end + -- Return total moves made return routeCount + consolidateCount + specializedMoves + regularMoves end @@ -1651,7 +1664,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort -- Sorting is complete! addon:DebugSort("%s sort complete! (%d passes, %d total moves)", sortType, passCount, totalMoves) - -- Final update + -- Final update (cache clearing happens after delay) local frame = CreateFrame("Frame") local startTime = GetTime() frame:SetScript("OnUpdate", function() @@ -1659,6 +1672,15 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort frame:SetScript("OnUpdate", nil) SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) + -- Clear caches after sorting to ensure fresh detection + if sortType == "bank" then + addon.Modules.BankScanner:ClearCache() + else + addon.Modules.BagScanner:ClearCache() + end + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end updateFrame() end end) @@ -1667,7 +1689,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort addon:DebugSort("%s sort stopped at safety limit! (%d/%d items still need sorting after %d passes)", sortType, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, passCount) - -- Final update + -- Final update (cache clearing happens after delay) local frame = CreateFrame("Frame") local startTime = GetTime() frame:SetScript("OnUpdate", function() @@ -1675,6 +1697,15 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort frame:SetScript("OnUpdate", nil) SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) + -- Clear caches after sorting to ensure fresh detection + if sortType == "bank" then + addon.Modules.BankScanner:ClearCache() + else + addon.Modules.BagScanner:ClearCache() + end + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end updateFrame() end end) @@ -1690,7 +1721,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort addon:DebugSort("%s sort stopped due to no progress after %d passes (items remaining: %d/%d)", sortType, passCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems) - -- Final update + -- Final update (cache clearing happens after delay) local frame = CreateFrame("Frame") local startTime = GetTime() frame:SetScript("OnUpdate", function() @@ -1698,6 +1729,15 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort frame:SetScript("OnUpdate", nil) SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) + -- Clear caches after sorting to ensure fresh detection + if sortType == "bank" then + addon.Modules.BankScanner:ClearCache() + else + addon.Modules.BagScanner:ClearCache() + end + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end updateFrame() end end) diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index 2005058..fc9b482 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -1630,13 +1630,22 @@ function BankFrame:Initialize() if currentViewChar then return end if event == "PLAYERBANKSLOTS_CHANGED" and arg1 then - -- Mark specific slot as dirty in main bank (bagID = -1) - addon.Modules.BankScanner:MarkSlotDirty(-1, arg1) + -- Invalidate the entire main bank bag to ensure fresh data + -- (MarkSlotDirty was causing timing issues with item data) + addon.Modules.BankScanner:InvalidateBag(-1) + -- Clear ItemDetection cache to ensure fresh detection after item swap + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end elseif event == "BAG_UPDATE" and arg1 then -- Check if this is a bank bag (5-10) if arg1 >= 5 and arg1 <= 10 then - -- Invalidate the specific bank bag (we don't know which slot) + -- Invalidate the specific bank bag addon.Modules.BankScanner:InvalidateBag(arg1) + -- Clear ItemDetection cache to ensure fresh detection after item swap + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end else -- Not a bank bag, ignore for bank frame return @@ -1645,9 +1654,13 @@ function BankFrame:Initialize() -- Bank container slot changed (bag added/removed) -- Must clear entire cache since bag structure changed addon.Modules.BankScanner:ClearCache() + if addon.Modules.ItemDetection then + addon.Modules.ItemDetection:ClearCache() + end end - ScheduleBankFrameUpdate(0.1) + -- Slightly longer delay to ensure WoW API has updated + ScheduleBankFrameUpdate(0.15) end) end diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index e149176..7dc2ec6 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -9,8 +9,14 @@ local scanTooltip = CreateFrame("GameTooltip", "Guda_QuestScanTooltip", nil, "Ga scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") -- Helper function to check if an item is a quest item --- Delegates to consolidated Utils:IsQuestItem() function -local function IsQuestItem(bagID, slotID, isBank) +-- Uses centralized ItemDetection module +local function IsQuestItem(bagID, slotID, isBank, itemData) + -- Use ItemDetection if available + if addon and addon.Modules and addon.Modules.ItemDetection then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isQuestItem, props.isQuestStarter + end + -- Fallback to Utils if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then return addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, isBank) end @@ -363,7 +369,7 @@ function Guda_ItemButton_OnLoad(self) if link and addon and addon.Modules and addon.Modules.Utils then local itemID = addon.Modules.Utils:ExtractItemID(link) if itemID then - local isQuest = IsQuestItem(this.bagID, this.slotID, this.isBank) + local isQuest = IsQuestItem(this.bagID, this.slotID, this.isBank, this.itemData) local isUnique = addon.Modules.Utils:IsUniqueItem(this.bagID, this.slotID, link) -- Only pin to QuestItemBar if it's a unique quest item @@ -958,6 +964,33 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha itemLink = GetContainerItemLink(bagID, slotID) itemQuality = quality isLocked = locked + + -- Ensure itemData is populated for live items (needed for ItemDetection) + if itemLink then + local itemName, _, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(itemLink) + if not itemData then + -- Create new itemData + if itemName then + itemData = { + link = itemLink, + name = itemName, + quality = itemRarity or quality or 0, + class = itemType, + subclass = itemSubType, + texture = itemTexture, + count = 1, + } + self.itemData = itemData + end + else + -- Update existing itemData with missing fields + if not itemData.link then itemData.link = itemLink end + if itemData.quality == nil then itemData.quality = itemRarity or quality or 0 end + if not itemData.class and itemType then itemData.class = itemType end + if not itemData.subclass and itemSubType then itemData.subclass = itemSubType end + if not itemData.name and itemName then itemData.name = itemName end + end + end elseif itemData then -- Use cached metadata from database itemQuality = itemData.quality @@ -1071,7 +1104,7 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha end if not self.otherChar and not self.isReadOnly then - local isQuest, isQuestStarter = IsQuestItem(bagID, slotID) + local isQuest, isQuestStarter = IsQuestItem(bagID, slotID, self.isBank, itemData) -- Update quest icon with the appropriate texture Guda_ItemButton_UpdateQuestIcon(self, isQuest, isQuestStarter) if self.questBorder then diff --git a/UI/QuestItemBar.lua b/UI/QuestItemBar.lua index 13740d9..74f429b 100644 --- a/UI/QuestItemBar.lua +++ b/UI/QuestItemBar.lua @@ -14,103 +14,35 @@ local questItems = {} local flyoutButtons = {} local flyoutFrame --- Create a hidden tooltip for scanning -local scanTooltip -local function GetScanTooltip() - if not scanTooltip then - scanTooltip = CreateFrame("GameTooltip", "Guda_QuestBarScanTooltip", nil, "GameTooltipTemplate") - scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - end - return scanTooltip -end +--===================================================== +-- Quest Item Detection (using centralized ItemDetection) +--===================================================== --- Combined function to check if an item is a quest item AND usable in ONE tooltip scan --- This avoids the expensive double-scan that was causing lag +-- Combined function to check if an item is a quest item AND usable +-- Uses centralized ItemDetection module for consistent detection function QuestItemBar:CheckQuestItemUsable(bagID, slotID) if not bagID or not slotID then return false, false, false end - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetBagItem(bagID, slotID) - - local isQuestItem = false - local isQuestStarter = false - local isUsable = false - local isPermanentEnchant = false -- Track enchanting scrolls/vellums - - for i = 1, tooltip:NumLines() do - local line = getglobal("Guda_QuestBarScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - local tl = string.lower(text) - -- Check for quest item indicators - if string.find(text, "Quest Starter") or - string.find(text, "This Item Begins a Quest") or - string.find(text, "Use: Starts a Quest") then - isQuestItem = true - isQuestStarter = true - isUsable = true -- Quest starters are always usable - elseif string.find(text, "Quest Item") then - isQuestItem = true - end - -- Check for usability (case-insensitive) - if string.find(tl, "use:") or string.find(tl, "begins a quest") or string.find(tl, "starts a quest") then - isUsable = true - end - -- Check for permanent enchant items (should NOT be quest items) - -- Just check for "permanently" anywhere (green text doesn't have "Use:" prefix) - if string.find(tl, "permanently") then - isPermanentEnchant = true - end - end - end + -- Get item data for ItemDetection + local itemData = nil + if addon.Modules.BagScanner then + itemData = addon.Modules.BagScanner:ScanSlot(bagID, slotID) end - -- Permanent enchant items are NOT quest items, even if categorized as Quest - if isPermanentEnchant then - return false, false, false + -- Use centralized ItemDetection + if addon.Modules.ItemDetection and itemData then + local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID) + return props.isQuestItem, props.isQuestStarter, props.isQuestUsable end - -- Fallback check for quest category if not detected from tooltip - local link = GetContainerItemLink(bagID, slotID) - local itemID - local itemCategory, itemType - - if link and addon.Modules.Utils and addon.Modules.Utils.ExtractItemID and addon.Modules.Utils.GetItemInfoSafe then - itemID = addon.Modules.Utils:ExtractItemID(link) - if itemID then - _, _, _, _, itemCategory, itemType = addon.Modules.Utils:GetItemInfoSafe(itemID) - end + -- Fallback to Utils if ItemDetection not available + if addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then + local isQuestItem, isQuestStarter = addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, false) + -- For usability fallback, check if it's a quest item (assume usable) + return isQuestItem, isQuestStarter, isQuestItem end - -- If it's a Weapon or Armor, it shouldn't be a QuestItem unless it's specifically categorized as Quest - -- This avoids "Use:" equipment showing up in the quest bar - if itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor" then - if itemCategory ~= "Quest" and itemType ~= "Quest" then - isQuestItem = false - isQuestStarter = false - end - end - - if not isQuestItem then - if itemCategory == "Quest" or itemType == "Quest" then - isQuestItem = true - end - end - - -- Check the QuestItemsDB for known faction-specific quest items - if not isQuestItem then - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - local isDBQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - if isDBQuestItem then - isQuestItem = true - end - end - end - - return isQuestItem, isQuestStarter, isUsable + return false, false, false end -- Scan bags for quest items (optimized: single tooltip scan per item)