diff --git a/Core/ItemDetection.lua b/Core/ItemDetection.lua index 6379308..5714d0d 100644 --- a/Core/ItemDetection.lua +++ b/Core/ItemDetection.lua @@ -83,26 +83,37 @@ local function ScanTooltipLines(bagID, slotID, itemLink) tooltip:SetOwner(WorldFrame, "ANCHOR_NONE") tooltip:ClearLines() + -- Helper to safely call SetHyperlink (can fail with "Unknown link type") + local function SafeSetHyperlink(tip, link) + if not link then return false end + -- Extract just the item:XXXX portion for SetHyperlink + local _, _, itemString = string.find(link, "|H(item:[^|]+)|h") + if itemString then + local success = pcall(function() tip:SetHyperlink(itemString) end) + return success + end + return false + end + -- 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 + -- Bank main bag: SetInventoryItem doesn't work for bank slots + -- Use SetHyperlink with the item link instead + if not SafeSetHyperlink(tooltip, itemLink) then + return {} end + elseif bagID >= 5 and bagID <= 10 then + -- Bank bags: Try SetBagItem first, it should work for bank bags + tooltip:SetBagItem(bagID, slotID) else - -- Regular bags and bank bags (5-11) + -- Regular bags (0-4): SetBagItem works reliably tooltip:SetBagItem(bagID, slotID) end elseif itemLink then - tooltip:SetHyperlink(itemLink) + if not SafeSetHyperlink(tooltip, itemLink) then + return {} + end else return {} end @@ -301,28 +312,84 @@ local function IsRedColor(r, g, b) return (dr < 0.15 and dg < 0.15 and db < 0.15) end +-- Check if red text is a legitimate requirement (not just loading state or flavor text) +-- Returns true if text appears to be an actual unmet requirement +local function IsRequirementText(text) + if not text or text == "" then return false end + local lower = string.lower(text) + + -- Known requirement patterns in vanilla WoW + if string.find(lower, "requires") then return true end -- "Requires Level 60", "Requires Class:", etc. + if string.find(lower, "require") then return true end -- Alternate forms + if string.find(lower, "classes:") then return true end -- "Classes: Warrior, Paladin" + if string.find(lower, "races:") then return true end -- "Races: Dwarf, Gnome" + if string.find(lower, "level %d") then return true end -- "Level 60" (without Requires) + if string.find(lower, "skill:") then return true end -- Profession requirements + if string.find(lower, "reputation") then return true end -- Reputation requirements + if string.find(lower, "riding") then return true end -- Riding skill + if string.find(lower, "already known") then return true end -- Recipe already known + + -- Class names (when shown in red, indicates class restriction) + 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 + + -- Race names (when shown in red, indicates race restriction) + 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 -- Turtle WoW + if string.find(lower, "high elf") then return true end -- Turtle WoW + + return false +end + -- Check if item is unusable (has red text indicating unmet requirements) --- Excludes durability lines (broken items show red durability) +-- Excludes: durability lines, item name (line 1), non-requirement text local function DetectUnusable(lines) - for _, line in ipairs(lines) do - if line.r and line.g and line.b then + local numLines = table.getn(lines) + + -- If tooltip has very few lines, data might not be loaded - be conservative + if numLines < 2 then + return false + end + + -- Start from line 2 to skip the item name (line 1) + -- Item names can appear red when data isn't fully loaded + for i = 2, numLines do + local line = lines[i] + if line and 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 + -- Only count as unusable if text looks like a requirement + elseif IsRequirementText(text) then return true end + -- If red but not a requirement pattern, ignore it (might be flavor text or loading state) end end - -- Also check right column (some requirements appear there) - if line.rightR and line.rightG and line.rightB then + -- Also check right column (some requirements appear there, like "Warrior" class) + if line and 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) @@ -330,7 +397,7 @@ local function DetectUnusable(lines) local text = line.right or "" if durabilityPattern and string.find(text, durabilityPattern) then -- skip durability - else + elseif IsRequirementText(text) then return true end end @@ -369,13 +436,19 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID) -- Scan tooltip once local lines = ScanTooltipLines(bagID, slotID, itemLink) + local numLines = table.getn(lines) -- Debug: log if tooltip scan failed - if table.getn(lines) == 0 and addon.DEBUG then + if numLines == 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 + -- Check if tooltip data appears complete + -- A proper item tooltip should have at least 2 lines (name + something) + -- If tooltip is too short, data may not be fully loaded - don't cache + local tooltipLooksComplete = (numLines >= 2) + -- Detect all properties local isPermanentEnchant = DetectPermanentEnchant(lines) local isQuestItem, isQuestStarter = DetectQuestItem(lines, itemData) @@ -409,8 +482,9 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID) isUnusable = isUnusable, } - -- Cache result - if cacheKey then + -- Only cache result if tooltip data appears complete + -- This prevents caching incorrect results from partially-loaded item data + if cacheKey and tooltipLooksComplete then detectionCache[cacheKey] = result end diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 5696d16..0163886 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -30,6 +30,68 @@ local isMerchantOpen = false -- Track whether a vendor window is currently open -- Global click catcher for clearing search focus local clickCatcher = nil +--===================================================== +-- Deferred Usability Tint System +-- Prevents false positives when item data isn't fully loaded on bag open +-- Uses debouncing to handle rapid open/close safely +--===================================================== +local usabilityCheckFrame = nil +local USABILITY_CHECK_DELAY = 0.25 -- Delay before re-checking usability (seconds) + +-- Cancel any pending deferred usability check +local function CancelDeferredUsabilityCheck() + if usabilityCheckFrame then + usabilityCheckFrame:Hide() + usabilityCheckFrame.pending = false + end +end + +-- Update usability tints on all visible item buttons +local function UpdateAllUsabilityTints() + if not Guda_BagFrame or not Guda_BagFrame:IsShown() then return end + + for _, bagParent in pairs(bagParents) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do + if button.hasItem and button:IsShown() and Guda_ItemButton_UpdateUsableTint then + Guda_ItemButton_UpdateUsableTint(button) + end + end + end + end +end + +-- Schedule a deferred usability check with debouncing +local function ScheduleDeferredUsabilityCheck() + -- Create frame on first use + if not usabilityCheckFrame then + usabilityCheckFrame = CreateFrame("Frame") + usabilityCheckFrame:Hide() + usabilityCheckFrame.elapsed = 0 + usabilityCheckFrame.pending = false + usabilityCheckFrame:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed >= USABILITY_CHECK_DELAY then + this:Hide() + this.pending = false + -- Only run if bag is still open + if Guda_BagFrame and Guda_BagFrame:IsShown() then + -- Clear detection cache and re-check all items + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end + UpdateAllUsabilityTints() + end + end + end) + end + + -- Reset timer (debounce behavior) + usabilityCheckFrame.elapsed = 0 + usabilityCheckFrame.pending = true + usabilityCheckFrame:Show() +end + -- Get player race icon path (using racial ability icons) function Guda_GetPlayerRaceIcon() local _, race = UnitRace("player") @@ -121,6 +183,10 @@ function Guda_BagFrame_OnShow(self) end BagFrame:Update() + + -- Schedule deferred usability check to fix false positives from uncached item data + -- This runs after a short delay when item info is fully loaded by the WoW client + ScheduleDeferredUsabilityCheck() end -- OnHide @@ -143,6 +209,9 @@ function Guda_BagFrame_OnHide(self) throttleFrame:Hide() end + -- Cancel any pending deferred usability check (debounce safety) + CancelDeferredUsabilityCheck() + -- Clean up all buttons when frame is hidden (safe since we're not displaying) -- Use itemButtons hash instead of GetChildren() to avoid table allocation for _, bagParent in pairs(bagParents) do @@ -1774,7 +1843,7 @@ function Guda_BagFrame_Sort() return end - -- Check if we're in category view - only merge stacks + -- Check if we're in category view - restack and clean local sortBtn = getglobal("Guda_BagFrame_SortButton") if sortBtn and sortBtn.isCategoryView then Guda_BagFrame_MergeStacks() @@ -1793,10 +1862,11 @@ function Guda_BagFrame_Sort() end end --- Merge stacks only (for category view) - queue-based approach like BagShui +-- Restack and Clean (for category view) - merges stacks and refreshes view +-- Queue-based approach like BagShui function Guda_BagFrame_MergeStacks() if currentViewChar then - addon:Print("Cannot merge stacks for another character!") + addon:Print("Cannot restack for another character!") return end @@ -1896,7 +1966,13 @@ function Guda_BagFrame_MergeStacks() end if table.getn(moveQueue) == 0 then - addon:Print("No stacks to merge") + -- No stacks to merge, just do a clean refresh + -- Clear item detection cache to force fresh tooltip scans + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end + BagFrame:Update() + addon:Print("View refreshed (no stacks to merge)") return end @@ -1911,10 +1987,15 @@ function Guda_BagFrame_MergeStacks() local function ProcessNextMove() if queueIndex > table.getn(moveQueue) then - addon:Print("Merged " .. totalMoves .. " stack(s)") addon.Modules.SortEngine.sortingInProgress = false addon.Modules.SortEngine:UpdateSortButtonState(false) + -- Clear item detection cache to force fresh tooltip scans + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end + -- Refresh the view BagFrame:Update() + addon:Print("Restacked " .. totalMoves .. " stack(s)") return end diff --git a/UI/BagFrame.xml b/UI/BagFrame.xml index ec71d30..82d179f 100644 --- a/UI/BagFrame.xml +++ b/UI/BagFrame.xml @@ -146,8 +146,8 @@ GameTooltip:SetOwner(this, "ANCHOR_TOP") if this.isCategoryView then - GameTooltip:SetText("Merge Stacks") - GameTooltip:AddLine("Combines split stacks of the same item", 0.7, 0.7, 0.7) + GameTooltip:SetText("Restack and Clean") + GameTooltip:AddLine("Merges split stacks and refreshes the view", 0.7, 0.7, 0.7) else GameTooltip:SetText("Sort Bags") end diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index f77fc73..7365caf 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -21,6 +21,68 @@ local recentlyEmptiedSlots = {} -- Global click catcher for clearing bank search focus local bankClickCatcher = nil +--===================================================== +-- Deferred Usability Tint System +-- Prevents false positives when item data isn't fully loaded on bank open +-- Uses debouncing to handle rapid open/close safely +--===================================================== +local bankUsabilityCheckFrame = nil +local BANK_USABILITY_CHECK_DELAY = 0.25 -- Delay before re-checking usability (seconds) + +-- Cancel any pending deferred usability check +local function CancelBankDeferredUsabilityCheck() + if bankUsabilityCheckFrame then + bankUsabilityCheckFrame:Hide() + bankUsabilityCheckFrame.pending = false + end +end + +-- Update usability tints on all visible bank item buttons +local function UpdateAllBankUsabilityTints() + if not Guda_BankFrame or not Guda_BankFrame:IsShown() then return end + + for _, bagParent in pairs(bankBagParents) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do + if button.hasItem and button:IsShown() and Guda_ItemButton_UpdateUsableTint then + Guda_ItemButton_UpdateUsableTint(button) + end + end + end + end +end + +-- Schedule a deferred usability check with debouncing +local function ScheduleBankDeferredUsabilityCheck() + -- Create frame on first use + if not bankUsabilityCheckFrame then + bankUsabilityCheckFrame = CreateFrame("Frame") + bankUsabilityCheckFrame:Hide() + bankUsabilityCheckFrame.elapsed = 0 + bankUsabilityCheckFrame.pending = false + bankUsabilityCheckFrame:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed >= BANK_USABILITY_CHECK_DELAY then + this:Hide() + this.pending = false + -- Only run if bank is still open + if Guda_BankFrame and Guda_BankFrame:IsShown() then + -- Clear detection cache and re-check all items + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end + UpdateAllBankUsabilityTints() + end + end + end) + end + + -- Reset timer (debounce behavior) + bankUsabilityCheckFrame.elapsed = 0 + bankUsabilityCheckFrame.pending = true + bankUsabilityCheckFrame:Show() +end + -- Clear recently emptied slots (called when view is reset or frame hidden) function BankFrame:ClearRecentlyEmptiedSlots() for k in pairs(recentlyEmptiedSlots) do @@ -125,7 +187,11 @@ function Guda_BankFrame_OnShow(self) end BankFrame:Update() - end + + -- Schedule deferred usability check to fix false positives from uncached item data + -- This runs after a short delay when item info is fully loaded by the WoW client + ScheduleBankDeferredUsabilityCheck() +end -- OnHide function Guda_BankFrame_OnHide(self) @@ -141,6 +207,9 @@ function Guda_BankFrame_OnHide(self) throttleFrame:Hide() end + -- Cancel any pending deferred usability check (debounce safety) + CancelBankDeferredUsabilityCheck() + -- Clear recently emptied slots tracking (reset placeholders) BankFrame:ClearRecentlyEmptiedSlots() @@ -1412,7 +1481,7 @@ function Guda_BankFrame_Sort() return end - -- Check if we're in category view - only merge stacks + -- Check if we're in category view - restack and clean local sortBtn = getglobal("Guda_BankFrame_SortButton") if sortBtn and sortBtn.isCategoryView then Guda_BankFrame_MergeStacks() @@ -1431,15 +1500,16 @@ function Guda_BankFrame_Sort() end end --- Merge stacks only (for category view) - queue-based approach like BagShui +-- Restack and Clean (for category view) - merges stacks and refreshes view +-- Queue-based approach like BagShui function Guda_BankFrame_MergeStacks() if isReadOnlyMode or currentViewChar then - addon:Print("Cannot merge stacks in read-only mode!") + addon:Print("Cannot restack in read-only mode!") return end if not addon.Modules.BankScanner:IsBankOpen() then - addon:Print("Bank must be open to merge stacks!") + addon:Print("Bank must be open to restack!") return end @@ -1539,7 +1609,15 @@ function Guda_BankFrame_MergeStacks() end if table.getn(moveQueue) == 0 then - addon:Print("No stacks to merge") + -- No stacks to merge, just do a clean refresh + BankFrame:ClearRecentlyEmptiedSlots() + addon.Modules.BankScanner:InvalidateCache() + -- Clear item detection cache to force fresh tooltip scans + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end + BankFrame:Update() + addon:Print("View refreshed (no stacks to merge)") return end @@ -1554,10 +1632,17 @@ function Guda_BankFrame_MergeStacks() local function ProcessNextMove() if queueIndex > table.getn(moveQueue) then - addon:Print("Merged " .. totalMoves .. " stack(s)") addon.Modules.SortEngine.sortingInProgress = false addon.Modules.SortEngine:UpdateSortButtonState(false) + -- Clear all caches for a clean view after restacking + BankFrame:ClearRecentlyEmptiedSlots() + addon.Modules.BankScanner:InvalidateCache() + -- Clear item detection cache to force fresh tooltip scans + if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then + addon.Modules.ItemDetection:ClearCache() + end BankFrame:Update() + addon:Print("Restacked " .. totalMoves .. " stack(s)") return end diff --git a/UI/BankFrame.xml b/UI/BankFrame.xml index 0784841..959a02f 100644 --- a/UI/BankFrame.xml +++ b/UI/BankFrame.xml @@ -99,8 +99,8 @@ GameTooltip:SetOwner(this, "ANCHOR_TOP") if this.isCategoryView then - GameTooltip:SetText("Merge Stacks") - GameTooltip:AddLine("Combines split stacks of the same item", 0.7, 0.7, 0.7) + GameTooltip:SetText("Restack and Clean") + GameTooltip:AddLine("Merges split stacks and refreshes the view", 0.7, 0.7, 0.7) else GameTooltip:SetText("Sort Bank") end