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: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
+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
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
+2 -2
View File
@@ -146,8 +146,8 @@
<OnEnter>
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
+92 -7
View File
@@ -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
+2 -2
View File
@@ -99,8 +99,8 @@
<OnEnter>
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