unusable item detection logics modified

This commit is contained in:
Salikh Gurgenidze
2026-01-24 19:28:20 +04:00
parent 84f6d93d00
commit 771590fc80
5 changed files with 278 additions and 38 deletions
+96 -22
View File
@@ -83,26 +83,37 @@ local function ScanTooltipLines(bagID, slotID, itemLink)
tooltip:SetOwner(WorldFrame, "ANCHOR_NONE") tooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
tooltip:ClearLines() 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 -- Set tooltip based on what we have
if bagID and slotID then if bagID and slotID then
if bagID == -1 then if bagID == -1 then
-- Bank main bag: use SetInventoryItem (slot 39 + slotID) -- Bank main bag: SetInventoryItem doesn't work for bank slots
if tooltip.SetInventoryItem then -- Use SetHyperlink with the item link instead
tooltip:SetInventoryItem("player", 39 + slotID) if not SafeSetHyperlink(tooltip, itemLink) then
else return {}
-- Fallback to hyperlink if SetInventoryItem not available
if itemLink then
tooltip:SetHyperlink(itemLink)
else
return {}
end
end end
elseif bagID >= 5 and bagID <= 10 then
-- Bank bags: Try SetBagItem first, it should work for bank bags
tooltip:SetBagItem(bagID, slotID)
else else
-- Regular bags and bank bags (5-11) -- Regular bags (0-4): SetBagItem works reliably
tooltip:SetBagItem(bagID, slotID) tooltip:SetBagItem(bagID, slotID)
end end
elseif itemLink then elseif itemLink then
tooltip:SetHyperlink(itemLink) if not SafeSetHyperlink(tooltip, itemLink) then
return {}
end
else else
return {} return {}
end end
@@ -301,28 +312,84 @@ local function IsRedColor(r, g, b)
return (dr < 0.15 and dg < 0.15 and db < 0.15) return (dr < 0.15 and dg < 0.15 and db < 0.15)
end 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) -- 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) local function DetectUnusable(lines)
for _, line in ipairs(lines) do local numLines = table.getn(lines)
if line.r and line.g and line.b then
-- 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) -- Check for red text (requirements not met)
local isRed = IsRedColor(line.r, line.g, line.b) or local isRed = IsRedColor(line.r, line.g, line.b) or
(line.r > 0.85 and line.g < 0.3 and line.b < 0.3) (line.r > 0.85 and line.g < 0.3 and line.b < 0.3)
if isRed then if isRed then
local text = line.left or "" local text = line.left or ""
-- Ignore durability lines (broken items) -- Ignore durability lines (broken items)
if durabilityPattern and string.find(text, durabilityPattern) then if durabilityPattern and string.find(text, durabilityPattern) then
-- skip durability -- skip durability
else -- Only count as unusable if text looks like a requirement
elseif IsRequirementText(text) then
return true return true
end end
-- If red but not a requirement pattern, ignore it (might be flavor text or loading state)
end end
end end
-- Also check right column (some requirements appear there) -- Also check right column (some requirements appear there, like "Warrior" class)
if line.rightR and line.rightG and line.rightB then if line and line.rightR and line.rightG and line.rightB then
local isRed = IsRedColor(line.rightR, line.rightG, line.rightB) or local isRed = IsRedColor(line.rightR, line.rightG, line.rightB) or
(line.rightR > 0.85 and line.rightG < 0.3 and line.rightB < 0.3) (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 "" local text = line.right or ""
if durabilityPattern and string.find(text, durabilityPattern) then if durabilityPattern and string.find(text, durabilityPattern) then
-- skip durability -- skip durability
else elseif IsRequirementText(text) then
return true return true
end end
end end
@@ -369,13 +436,19 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID)
-- Scan tooltip once -- Scan tooltip once
local lines = ScanTooltipLines(bagID, slotID, itemLink) local lines = ScanTooltipLines(bagID, slotID, itemLink)
local numLines = table.getn(lines)
-- Debug: log if tooltip scan failed -- 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)", addon:Debug("ItemDetection: No tooltip lines for %s (bag=%s, slot=%s)",
tostring(itemData.name or itemLink), tostring(bagID), tostring(slotID)) tostring(itemData.name or itemLink), tostring(bagID), tostring(slotID))
end 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 -- Detect all properties
local isPermanentEnchant = DetectPermanentEnchant(lines) local isPermanentEnchant = DetectPermanentEnchant(lines)
local isQuestItem, isQuestStarter = DetectQuestItem(lines, itemData) local isQuestItem, isQuestStarter = DetectQuestItem(lines, itemData)
@@ -409,8 +482,9 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID)
isUnusable = isUnusable, isUnusable = isUnusable,
} }
-- Cache result -- Only cache result if tooltip data appears complete
if cacheKey then -- This prevents caching incorrect results from partially-loaded item data
if cacheKey and tooltipLooksComplete then
detectionCache[cacheKey] = result detectionCache[cacheKey] = result
end end
+86 -5
View File
@@ -30,6 +30,68 @@ local isMerchantOpen = false -- Track whether a vendor window is currently open
-- Global click catcher for clearing search focus -- Global click catcher for clearing search focus
local clickCatcher = nil 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) -- Get player race icon path (using racial ability icons)
function Guda_GetPlayerRaceIcon() function Guda_GetPlayerRaceIcon()
local _, race = UnitRace("player") local _, race = UnitRace("player")
@@ -121,6 +183,10 @@ function Guda_BagFrame_OnShow(self)
end end
BagFrame:Update() 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 end
-- OnHide -- OnHide
@@ -143,6 +209,9 @@ function Guda_BagFrame_OnHide(self)
throttleFrame:Hide() throttleFrame:Hide()
end end
-- Cancel any pending deferred usability check (debounce safety)
CancelDeferredUsabilityCheck()
-- Clean up all buttons when frame is hidden (safe since we're not displaying) -- Clean up all buttons when frame is hidden (safe since we're not displaying)
-- Use itemButtons hash instead of GetChildren() to avoid table allocation -- Use itemButtons hash instead of GetChildren() to avoid table allocation
for _, bagParent in pairs(bagParents) do for _, bagParent in pairs(bagParents) do
@@ -1774,7 +1843,7 @@ function Guda_BagFrame_Sort()
return return
end 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") local sortBtn = getglobal("Guda_BagFrame_SortButton")
if sortBtn and sortBtn.isCategoryView then if sortBtn and sortBtn.isCategoryView then
Guda_BagFrame_MergeStacks() Guda_BagFrame_MergeStacks()
@@ -1793,10 +1862,11 @@ function Guda_BagFrame_Sort()
end end
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() function Guda_BagFrame_MergeStacks()
if currentViewChar then if currentViewChar then
addon:Print("Cannot merge stacks for another character!") addon:Print("Cannot restack for another character!")
return return
end end
@@ -1896,7 +1966,13 @@ function Guda_BagFrame_MergeStacks()
end end
if table.getn(moveQueue) == 0 then 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 return
end end
@@ -1911,10 +1987,15 @@ function Guda_BagFrame_MergeStacks()
local function ProcessNextMove() local function ProcessNextMove()
if queueIndex > table.getn(moveQueue) then if queueIndex > table.getn(moveQueue) then
addon:Print("Merged " .. totalMoves .. " stack(s)")
addon.Modules.SortEngine.sortingInProgress = false addon.Modules.SortEngine.sortingInProgress = false
addon.Modules.SortEngine:UpdateSortButtonState(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() BagFrame:Update()
addon:Print("Restacked " .. totalMoves .. " stack(s)")
return return
end end
+2 -2
View File
@@ -146,8 +146,8 @@
<OnEnter> <OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_TOP") GameTooltip:SetOwner(this, "ANCHOR_TOP")
if this.isCategoryView then if this.isCategoryView then
GameTooltip:SetText("Merge Stacks") GameTooltip:SetText("Restack and Clean")
GameTooltip:AddLine("Combines split stacks of the same item", 0.7, 0.7, 0.7) GameTooltip:AddLine("Merges split stacks and refreshes the view", 0.7, 0.7, 0.7)
else else
GameTooltip:SetText("Sort Bags") GameTooltip:SetText("Sort Bags")
end end
+92 -7
View File
@@ -21,6 +21,68 @@ local recentlyEmptiedSlots = {}
-- Global click catcher for clearing bank search focus -- Global click catcher for clearing bank search focus
local bankClickCatcher = nil 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) -- Clear recently emptied slots (called when view is reset or frame hidden)
function BankFrame:ClearRecentlyEmptiedSlots() function BankFrame:ClearRecentlyEmptiedSlots()
for k in pairs(recentlyEmptiedSlots) do for k in pairs(recentlyEmptiedSlots) do
@@ -125,7 +187,11 @@ function Guda_BankFrame_OnShow(self)
end end
BankFrame:Update() 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 -- OnHide
function Guda_BankFrame_OnHide(self) function Guda_BankFrame_OnHide(self)
@@ -141,6 +207,9 @@ function Guda_BankFrame_OnHide(self)
throttleFrame:Hide() throttleFrame:Hide()
end end
-- Cancel any pending deferred usability check (debounce safety)
CancelBankDeferredUsabilityCheck()
-- Clear recently emptied slots tracking (reset placeholders) -- Clear recently emptied slots tracking (reset placeholders)
BankFrame:ClearRecentlyEmptiedSlots() BankFrame:ClearRecentlyEmptiedSlots()
@@ -1412,7 +1481,7 @@ function Guda_BankFrame_Sort()
return return
end 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") local sortBtn = getglobal("Guda_BankFrame_SortButton")
if sortBtn and sortBtn.isCategoryView then if sortBtn and sortBtn.isCategoryView then
Guda_BankFrame_MergeStacks() Guda_BankFrame_MergeStacks()
@@ -1431,15 +1500,16 @@ function Guda_BankFrame_Sort()
end end
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() function Guda_BankFrame_MergeStacks()
if isReadOnlyMode or currentViewChar then if isReadOnlyMode or currentViewChar then
addon:Print("Cannot merge stacks in read-only mode!") addon:Print("Cannot restack in read-only mode!")
return return
end end
if not addon.Modules.BankScanner:IsBankOpen() then 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 return
end end
@@ -1539,7 +1609,15 @@ function Guda_BankFrame_MergeStacks()
end end
if table.getn(moveQueue) == 0 then 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 return
end end
@@ -1554,10 +1632,17 @@ function Guda_BankFrame_MergeStacks()
local function ProcessNextMove() local function ProcessNextMove()
if queueIndex > table.getn(moveQueue) then if queueIndex > table.getn(moveQueue) then
addon:Print("Merged " .. totalMoves .. " stack(s)")
addon.Modules.SortEngine.sortingInProgress = false addon.Modules.SortEngine.sortingInProgress = false
addon.Modules.SortEngine:UpdateSortButtonState(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() BankFrame:Update()
addon:Print("Restacked " .. totalMoves .. " stack(s)")
return return
end end
+2 -2
View File
@@ -99,8 +99,8 @@
<OnEnter> <OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_TOP") GameTooltip:SetOwner(this, "ANCHOR_TOP")
if this.isCategoryView then if this.isCategoryView then
GameTooltip:SetText("Merge Stacks") GameTooltip:SetText("Restack and Clean")
GameTooltip:AddLine("Combines split stacks of the same item", 0.7, 0.7, 0.7) GameTooltip:AddLine("Merges split stacks and refreshes the view", 0.7, 0.7, 0.7)
else else
GameTooltip:SetText("Sort Bank") GameTooltip:SetText("Sort Bank")
end end