From 06db5f15faa74252ef9f543537bc7c66682b0fb3 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 12:18:20 +0400 Subject: [PATCH 1/5] fix: Duplicate BAG_UPDATE handlers --- Sorting/SortEngine.lua | 40 +++++++++++++++++++++++++++------------- UI/BankFrame.lua | 8 +++----- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 80d0702..63a418f 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -12,8 +12,9 @@ SortEngine.sortingInProgress = false -- Performance: Max items to move per cycle -- Baganator uses 5 for manual transfers, but sorting needs more for smooth operation local MAX_MOVES_PER_CYCLE = 20 --- Bank uses fewer moves per cycle to avoid lock conflicts (bank ops are slower) -local MAX_BANK_MOVES_PER_CYCLE = 15 +-- Bank needs more moves per cycle due to larger capacity (240 vs 160 slots) +-- But not too many to avoid server-side lock conflicts +local MAX_BANK_MOVES_PER_CYCLE = 25 -- Current sort context (set by ExecuteSort, used by ApplySort) local currentSortType = "bags" @@ -1117,22 +1118,35 @@ local function ApplySort(bagIDs, items, targetPositions) end -- Execute swaps with occupied slots (if we haven't hit the limit) + -- Be conservative with swaps - they can cause chain reactions + -- Limit swaps to half of max moves to leave room for next pass corrections + local maxSwaps = math.floor(maxMoves / 2) + local swapCount = 0 + for _, move in ipairs(swapOccupied) do - -- Limit moves per cycle - if moveCount >= maxMoves then + -- Limit total moves and swaps separately + if moveCount >= maxMoves or swapCount >= maxSwaps then break end - local _, _, sourceLocked = GetContainerItemInfo(move.sourceBag, move.sourceSlot) - local _, _, targetLocked = GetContainerItemInfo(move.targetBag, move.targetSlot) - - if not sourceLocked and not targetLocked then - PickupContainerItem(move.sourceBag, move.sourceSlot) - PickupContainerItem(move.targetBag, move.targetSlot) - ClearCursor() - moveCount = moveCount + 1 + -- Verify source item is still there (previous swaps may have moved it) + local sourceLink = GetContainerItemLink(move.sourceBag, move.sourceSlot) + if not sourceLink then + -- Source slot is now empty, skip this swap (will be handled next pass) + addon:DebugSort("Swap skipped: source slot now empty (%d:%d)", move.sourceBag, move.sourceSlot) else - lockedCount = lockedCount + 1 + local _, _, sourceLocked = GetContainerItemInfo(move.sourceBag, move.sourceSlot) + local _, _, targetLocked = GetContainerItemInfo(move.targetBag, move.targetSlot) + + if not sourceLocked and not targetLocked then + PickupContainerItem(move.sourceBag, move.sourceSlot) + PickupContainerItem(move.targetBag, move.targetSlot) + ClearCursor() + moveCount = moveCount + 1 + swapCount = swapCount + 1 + else + lockedCount = lockedCount + 1 + end end end diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index e43f4b0..cb206ad 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -1714,11 +1714,9 @@ function BankFrame:Initialize() end end - addon.Modules.Events:OnBagUpdate(function() - if addon.Modules.BankScanner:IsBankOpen() and not currentViewChar then - ScheduleBankFrameUpdate(0.1) - end - end, "BankFrameUI") + -- NOTE: BAG_UPDATE for bank bags (5-10) is handled by the updateFrame below + -- which provides incremental update logic. We don't need a separate OnBagUpdate + -- handler here as it would cause duplicate processing. -- Update when items get locked/unlocked (debounced for trading, mailing, etc.) addon.Modules.Events:Register("ITEM_LOCK_CHANGED", function() From 52af32daaaca53c0bb78820a08efa63ad5cbaf5c Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 12:36:48 +0400 Subject: [PATCH 2/5] fix: Added isUnusable detection with caching --- Core/ItemDetection.lua | 70 +++++++++++++++++++++++- Core/Tooltip.lua | 14 ++--- Core/Utils.lua | 33 ++++++++++++ Data/BankScanner.lua | 12 ++--- Data/EquipmentScanner.lua | 14 ++--- Data/MoneyTracker.lua | 14 ++--- Sorting/SortEngine.lua | 110 +++++++++++++++----------------------- UI/BagFrame.lua | 16 +----- UI/BankFrame.lua | 50 +++++++---------- UI/ItemButton.lua | 95 ++++---------------------------- UI/QuestItemBar.lua | 14 ++--- UI/TrackedItemBar.lua | 14 ++--- 12 files changed, 198 insertions(+), 258 deletions(-) diff --git a/Core/ItemDetection.lua b/Core/ItemDetection.lua index 4d90cee..dcda5c8 100644 --- a/Core/ItemDetection.lua +++ b/Core/ItemDetection.lua @@ -91,18 +91,25 @@ local function ScanTooltipLines(bagID, slotID, itemLink) local leftText = leftLine and leftLine:GetText() or "" local rightText = rightLine and rightLine:GetText() or "" - -- Get text colors (for detecting yellow/green text) + -- Get left text colors (for detecting yellow/green/red text) local lr, lg, lb, la = 1, 1, 1, 1 if leftLine and leftLine.GetTextColor then lr, lg, lb, la = leftLine:GetTextColor() end + -- Get right text colors (for detecting red requirements) + local rr, rg, rb, ra = 1, 1, 1, 1 + if rightLine and rightLine.GetTextColor then + rr, rg, rb, ra = rightLine:GetTextColor() + end + table.insert(lines, { left = leftText, right = rightText, leftLower = leftText and string.lower(leftText) or "", rightLower = rightText and string.lower(rightText) or "", r = lr, g = lg, b = lb, + rightR = rr, rightG = rg, rightB = rb, }) end @@ -255,12 +262,63 @@ local function DetectQuestUsable(lines) return false end +-- Durability pattern for filtering out broken item red text +local durabilityPattern = DURABILITY_TEMPLATE and string.gsub(DURABILITY_TEMPLATE, "%%d", "%%d+") or nil + +-- Check if text color is red (unusable requirement) +local function IsRedColor(r, g, b) + if not r or not g or not b then return false end + -- RED_FONT_COLOR is typically (1.0, 0.1, 0.1) + local dr = math.abs(r - 1.0) + local dg = math.abs(g - 0.125) + local db = math.abs(b - 0.125) + return (dr < 0.15 and dg < 0.15 and db < 0.15) +end + +-- Check if item is unusable (has red text indicating unmet requirements) +-- Excludes durability lines (broken items show red durability) +local function DetectUnusable(lines) + for _, line in ipairs(lines) do + if line.r and line.g and line.b then + -- Check for red text (requirements not met) + local isRed = IsRedColor(line.r, line.g, line.b) or + (line.r > 0.85 and line.g < 0.3 and line.b < 0.3) + + if isRed then + local text = line.left or "" + -- Ignore durability lines (broken items) + if durabilityPattern and string.find(text, durabilityPattern) then + -- skip durability + else + return true + end + end + end + + -- Also check right column (some requirements appear there) + if line.rightR and line.rightG and line.rightB then + local isRed = IsRedColor(line.rightR, line.rightG, line.rightB) or + (line.rightR > 0.85 and line.rightG < 0.3 and line.rightB < 0.3) + + if isRed then + local text = line.right or "" + if durabilityPattern and string.find(text, durabilityPattern) then + -- skip durability + else + return true + end + end + end + end + return false +end + --===================================================== -- Public API - Cached Detection --===================================================== -- Get all item properties at once (cached) --- Returns: { isQuestItem, isQuestStarter, isQuestUsable, isJunk, isPermanentEnchant } +-- Returns: { isQuestItem, isQuestStarter, isQuestUsable, isJunk, isPermanentEnchant, isUnusable } function ItemDetection:GetItemProperties(itemData, bagID, slotID) if not itemData then return { @@ -269,6 +327,7 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID) isQuestUsable = false, isJunk = false, isPermanentEnchant = false, + isUnusable = false, } end @@ -296,6 +355,7 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID) local isQuestItem, isQuestStarter = DetectQuestItem(lines, itemData) local isQuestUsable = DetectQuestUsable(lines) local isJunk = DetectJunk(lines, itemData) + local isUnusable = DetectUnusable(lines) -- Debug: log junk detection for gray items if addon.DEBUG then @@ -320,6 +380,7 @@ function ItemDetection:GetItemProperties(itemData, bagID, slotID) isQuestUsable = isQuestUsable, isJunk = isJunk, isPermanentEnchant = isPermanentEnchant, + isUnusable = isUnusable, } -- Cache result @@ -356,6 +417,11 @@ function ItemDetection:IsPermanentEnchant(itemData, bagID, slotID) return props.isPermanentEnchant end +function ItemDetection:IsUnusable(itemData, bagID, slotID) + local props = self:GetItemProperties(itemData, bagID, slotID) + return props.isUnusable +end + --===================================================== -- Initialization --===================================================== diff --git a/Core/Tooltip.lua b/Core/Tooltip.lua index 7c34881..bd790ab 100644 --- a/Core/Tooltip.lua +++ b/Core/Tooltip.lua @@ -615,16 +615,10 @@ function Tooltip:Initialize() if event == "BAG_UPDATE" then if cacheClearPending then return end cacheClearPending = true - -- Debounce: batch rapid BAG_UPDATE events - local debounceFrame = CreateFrame("Frame") - debounceFrame.elapsed = 0 - debounceFrame:SetScript("OnUpdate", function() - this.elapsed = this.elapsed + arg1 - if this.elapsed >= 0.2 then - this:SetScript("OnUpdate", nil) - cacheClearPending = false - Tooltip:ClearCache() - end + -- Debounce: batch rapid BAG_UPDATE events (uses pooled timer) + Guda_ScheduleTimer(0.2, function() + cacheClearPending = false + Tooltip:ClearCache() end) end end) diff --git a/Core/Utils.lua b/Core/Utils.lua index 6a9980b..88d9bff 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -5,6 +5,39 @@ local addon = Guda local Utils = {} addon.Modules.Utils = Utils +--============================================================================= +-- Timer Frame Pool (prevents memory leaks from temporary timers) +-- Global function so all modules can use it +--============================================================================= +local timerPool = {} +local TIMER_POOL_MAX = 10 -- Limit pool size to prevent unbounded growth + +function Guda_ScheduleTimer(delay, callback) + -- Try to get a frame from the pool + local frame = table.remove(timerPool) + if not frame then + frame = CreateFrame("Frame") + end + + frame.elapsed = 0 + frame.delay = delay + frame.callback = callback + frame:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed >= this.delay then + this:SetScript("OnUpdate", nil) + this:Hide() + -- Return frame to pool if not full + if table.getn(timerPool) < TIMER_POOL_MAX then + table.insert(timerPool, this) + end + -- Execute callback + this.callback() + end + end) + frame:Show() +end + --============================================================================= -- Tooltip Scan Caching -- Caches results of expensive tooltip scanning operations diff --git a/Data/BankScanner.lua b/Data/BankScanner.lua index a919a5f..d88b9b3 100644 --- a/Data/BankScanner.lua +++ b/Data/BankScanner.lua @@ -188,15 +188,9 @@ function BankScanner:Initialize() BankScanner:ClearCache() -- Clear cache on open addon:Debug("Bank opened") - -- Delay scan to ensure bank is fully loaded - local frame = CreateFrame("Frame") - local elapsed = 0 - frame:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed >= 0.5 then - frame:SetScript("OnUpdate", nil) - BankScanner:SaveToDatabase() - end + -- Delay scan to ensure bank is fully loaded (uses pooled timer) + Guda_ScheduleTimer(0.5, function() + BankScanner:SaveToDatabase() end) end, "BankScanner") diff --git a/Data/EquipmentScanner.lua b/Data/EquipmentScanner.lua index 1de3f7c..d145ec8 100644 --- a/Data/EquipmentScanner.lua +++ b/Data/EquipmentScanner.lua @@ -91,16 +91,10 @@ function EquipmentScanner:Initialize() playerLoggedIn = true addon:Print("Scanning equipped items...") - -- Delay scan to ensure character is fully loaded - local frame = CreateFrame("Frame") - local elapsed = 0 - frame:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed >= 2.0 then - frame:SetScript("OnUpdate", nil) - EquipmentScanner:SaveToDatabase() - addon:Print("Equipped items scanned and saved!") - end + -- Delay scan to ensure character is fully loaded (uses pooled timer) + Guda_ScheduleTimer(2.0, function() + EquipmentScanner:SaveToDatabase() + addon:Print("Equipped items scanned and saved!") end) end, "EquipmentScanner") diff --git a/Data/MoneyTracker.lua b/Data/MoneyTracker.lua index 0ecc772..2165bd7 100644 --- a/Data/MoneyTracker.lua +++ b/Data/MoneyTracker.lua @@ -36,17 +36,11 @@ function MoneyTracker:Initialize() MoneyTracker:Update() end, "MoneyTracker") - -- Initial update and save on login + -- Initial update and save on login (uses pooled timer) addon.Modules.Events:OnPlayerLogin(function() - local frame = CreateFrame("Frame") - local elapsed = 0 - frame:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed >= 1 then - frame:SetScript("OnUpdate", nil) - MoneyTracker:Update() - addon:Debug("Initial money saved") - end + Guda_ScheduleTimer(1, function() + MoneyTracker:Update() + addon:Debug("Initial money saved") end) end, "MoneyTracker") end diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 63a418f..5c0baaf 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -1722,52 +1722,42 @@ 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 (cache clearing happens after delay) - local frame = CreateFrame("Frame") - local startTime = GetTime() - frame:SetScript("OnUpdate", function() - if GetTime() - startTime >= 0.3 then - frame:SetScript("OnUpdate", nil) - SortEngine.sortingInProgress = false - SortEngine:UpdateSortButtonState(false) - currentSortType = "bags" -- Reset sort context - -- 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() + -- Final update (cache clearing happens after delay, uses pooled timer) + Guda_ScheduleTimer(0.3, function() + SortEngine.sortingInProgress = false + SortEngine:UpdateSortButtonState(false) + currentSortType = "bags" -- Reset sort context + -- 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) elseif passCount >= safetyLimit then -- Hit safety limit but not fully sorted 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 (cache clearing happens after delay) - local frame = CreateFrame("Frame") - local startTime = GetTime() - frame:SetScript("OnUpdate", function() - if GetTime() - startTime >= 0.3 then - frame:SetScript("OnUpdate", nil) - SortEngine.sortingInProgress = false - SortEngine:UpdateSortButtonState(false) - currentSortType = "bags" -- Reset sort context - -- 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() + -- Final update (cache clearing happens after delay, uses pooled timer) + Guda_ScheduleTimer(0.3, function() + SortEngine.sortingInProgress = false + SortEngine:UpdateSortButtonState(false) + currentSortType = "bags" -- Reset sort context + -- 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) else -- No progress guard: stop if we make no moves repeatedly @@ -1781,26 +1771,21 @@ 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 (cache clearing happens after delay) - local frame = CreateFrame("Frame") - local startTime = GetTime() - frame:SetScript("OnUpdate", function() - if GetTime() - startTime >= 0.3 then - frame:SetScript("OnUpdate", nil) - SortEngine.sortingInProgress = false - SortEngine:UpdateSortButtonState(false) - currentSortType = "bags" -- Reset sort context - -- 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() + -- Final update (cache clearing happens after delay, uses pooled timer) + Guda_ScheduleTimer(0.3, function() + SortEngine.sortingInProgress = false + SortEngine:UpdateSortButtonState(false) + currentSortType = "bags" -- Reset sort context + -- 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) return end @@ -1819,15 +1804,8 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort addon:DebugSort("Waiting %.1f seconds before next pass...", totalDelay) - -- Wait with progressive delay, then sort again - local frame = CreateFrame("Frame") - local startTime = GetTime() - frame:SetScript("OnUpdate", function() - if GetTime() - startTime >= totalDelay then - frame:SetScript("OnUpdate", nil) - DoSortPass() -- Recursive call for next pass - end - end) + -- Wait with progressive delay, then sort again (uses pooled timer) + Guda_ScheduleTimer(totalDelay, DoSortPass) end end diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 7c97a5b..90778af 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -1849,20 +1849,8 @@ function Guda_BagFrame_MergeStacks() ProcessNextMove() end --- Simple timer helper -function Guda_ScheduleTimer(delay, callback) - local frame = CreateFrame("Frame") - frame.elapsed = 0 - frame.delay = delay - frame.callback = callback - frame:SetScript("OnUpdate", function() - this.elapsed = this.elapsed + arg1 - if this.elapsed >= this.delay then - this:SetScript("OnUpdate", nil) - this.callback() - end - end) -end +-- NOTE: Guda_ScheduleTimer is defined in Core/Utils.lua (loaded earlier) +-- to ensure it's available for all modules that need timers -- Hook bag container buttons to open Guda Bag View local function HookBagContainers() diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index cb206ad..2da25fa 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -1611,40 +1611,28 @@ function BankFrame:Initialize() -- Update when bank is opened addon.Modules.Events:OnBankOpen(function() - -- Delay showing custom bank to let TransmogUI finish processing - local frame = CreateFrame("Frame") - local elapsed = 0 - frame:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed >= 0.2 then - frame:SetScript("OnUpdate", nil) + -- Delay showing custom bank to let TransmogUI finish processing (uses pooled timer) + Guda_ScheduleTimer(0.2, function() + -- Show current character's bank in interactive mode + currentViewChar = nil - -- Show current character's bank in interactive mode - currentViewChar = nil + -- Do not auto-hide BagFrame when opening BankFrame + -- Users may want both frames visible simultaneously; layout issues, if any, + -- should be addressed via positioning rather than auto-hiding. - -- Do not auto-hide BagFrame when opening BankFrame - -- Previously, we hid BagFrame to prevent button overlap: - -- local bagFrame = getglobal("Guda_BagFrame") - -- if bagFrame and bagFrame:IsShown() then - -- bagFrame:Hide() - -- end - -- Users may want both frames visible simultaneously; layout issues, if any, - -- should be addressed via positioning rather than auto-hiding. - - -- Show and update custom bank frame - local customBankFrame = getglobal("Guda_BankFrame") - if customBankFrame then - customBankFrame:Show() - end - - -- Force disable pfUI banks if enabled (pfUI uses pfBank for its bank frame) - if pfUI and pfUI.bag and pfUI.bag.left and pfUI.bag.left.Hide then - pfUI.bag.left:Hide() - end - - addon.Modules.BankFrame:EnsureBagButtonsInitialized() - addon.Modules.BankFrame:Update() + -- Show and update custom bank frame + local customBankFrame = getglobal("Guda_BankFrame") + if customBankFrame then + customBankFrame:Show() end + + -- Force disable pfUI banks if enabled (pfUI uses pfBank for its bank frame) + if pfUI and pfUI.bag and pfUI.bag.left and pfUI.bag.left.Hide then + pfUI.bag.left:Hide() + end + + addon.Modules.BankFrame:EnsureBagButtonsInitialized() + addon.Modules.BankFrame:Update() end) end, "BankFrameUI") diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index cfba5db..6b4874f 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -110,91 +110,10 @@ local function Guda_GetUnusableColor() return 0.9, 0.2, 0.2, 1.0 end --- Build durability match pattern based on the client template -local durabilityPattern -if DURABILITY_TEMPLATE then - -- e.g. "Durability %d / %d" -> "Durability (.+)" - durabilityPattern = string.gsub(DURABILITY_TEMPLATE, "%%[^%s]+", "(.+)") -end - --- Tiny helper to compare font color to Blizzard's RED_FONT_COLOR -local function IsRedColor(r, g, b) - if not r or not g or not b or not RED_FONT_COLOR then return false end - local dr = math.abs(r - RED_FONT_COLOR.r) - local dg = math.abs(g - RED_FONT_COLOR.g) - local db = math.abs(b - RED_FONT_COLOR.b) - return (dr < 0.08 and dg < 0.08 and db < 0.08) -end - --- Scan tooltip for red text that is NOT a durability line --- Uses shared tooltip from Utils module -local function IsItemUnusable(bagID, slotID, isBank) - bagID = tonumber(bagID) - slotID = tonumber(slotID) - if not bagID or not slotID then return false end - - -- Get shared tooltip from Utils - local scanTooltip, tooltipName = addon.Modules.Utils:GetScanTooltip() - - -- Some clients require SetOwner before every SetBagItem/SetInventoryItem to populate lines - if scanTooltip.SetOwner then - scanTooltip:SetOwner(UIParent or WorldFrame, "ANCHOR_NONE") - end - scanTooltip:ClearLines() - - if isBank and bagID == -1 then - -- Bank frame item buttons map slots 1.. to inventory slots 40.. (39 + slot) - if scanTooltip.SetInventoryItem then - scanTooltip:SetInventoryItem("player", 39 + slotID) - else - -- Fallback to bag scan if API missing - scanTooltip:SetBagItem(bagID, slotID) - end - else - scanTooltip:SetBagItem(bagID, slotID) - end - - if scanTooltip.Show then scanTooltip:Show() end - - local num = scanTooltip:NumLines() or 0 - for i = 1, num do - -- Scan LEFT column - local left = getglobal(tooltipName .. "TextLeft" .. i) - if left and left:IsShown() then - local text = left:GetText() - local r, g, b = left:GetTextColor() - -- Be tolerant with red detection in case client colors differ slightly - local isRed = IsRedColor(r, g, b) or (r and g and b and r > 0.85 and g < 0.3 and b < 0.3) - if text and isRed then - -- Ignore red durability (broken) lines - if durabilityPattern and string.find(text, durabilityPattern, 1) then - -- skip durability - else - if scanTooltip.Hide then scanTooltip:Hide() end - return true - end - end - end - - -- Scan RIGHT column as well (required level etc can appear here on some clients) - local right = getglobal(tooltipName .. "TextRight" .. i) - if right and right:IsShown() then - local text = right:GetText() - local r, g, b = right:GetTextColor() - local isRed = IsRedColor(r, g, b) or (r and g and b and r > 0.85 and g < 0.3 and b < 0.3) - if text and isRed then - if durabilityPattern and string.find(text, durabilityPattern, 1) then - -- skip durability - else - if scanTooltip.Hide then scanTooltip:Hide() end - return true - end - end - end - end - - return false -end +-- NOTE: IsItemUnusable detection is now handled by ItemDetection:IsUnusable() +-- which uses cached tooltip scanning to avoid duplicate scans per item. +-- The old IsItemUnusable, IsRedColor, and durabilityPattern have been removed +-- to prevent redundant tooltip scanning - all detection is now centralized. -- Apply/remove red tint on item texture for unusable items local function Guda_ItemButton_UpdateUsableTint(self) @@ -225,7 +144,11 @@ local function Guda_ItemButton_UpdateUsableTint(self) return end - local unusable = IsItemUnusable(self.bagID, self.slotID, self.isBank) + -- Use cached detection from ItemDetection module (avoids duplicate tooltip scans) + local unusable = false + if self.itemData and addon.Modules.ItemDetection then + unusable = addon.Modules.ItemDetection:IsUnusable(self.itemData, self.bagID, self.slotID) + end -- Ensure overlay exists (created in OnLoad, but be defensive) if not self.unusableOverlay then diff --git a/UI/QuestItemBar.lua b/UI/QuestItemBar.lua index 152982c..e8de95c 100644 --- a/UI/QuestItemBar.lua +++ b/UI/QuestItemBar.lua @@ -649,16 +649,10 @@ function QuestItemBar:Initialize() addon.Modules.Events:Register("BAG_UPDATE", function() if bagUpdatePending then return end bagUpdatePending = true - -- Debounce: wait 0.15 seconds before updating to batch rapid events - local debounceFrame = CreateFrame("Frame") - debounceFrame.elapsed = 0 - debounceFrame:SetScript("OnUpdate", function() - this.elapsed = this.elapsed + arg1 - if this.elapsed >= 0.15 then - this:SetScript("OnUpdate", nil) - bagUpdatePending = false - QuestItemBar:Update() - end + -- Debounce: wait 0.15 seconds before updating (uses pooled timer) + Guda_ScheduleTimer(0.15, function() + bagUpdatePending = false + QuestItemBar:Update() end) end, "QuestItemBar") diff --git a/UI/TrackedItemBar.lua b/UI/TrackedItemBar.lua index 0244f19..304bf9e 100644 --- a/UI/TrackedItemBar.lua +++ b/UI/TrackedItemBar.lua @@ -312,16 +312,10 @@ function TrackedItemBar:Initialize() addon.Modules.Events:Register("BAG_UPDATE", function() if bagUpdatePending then return end bagUpdatePending = true - -- Debounce: wait 0.15 seconds before updating to batch rapid events - local debounceFrame = CreateFrame("Frame") - debounceFrame.elapsed = 0 - debounceFrame:SetScript("OnUpdate", function() - this.elapsed = this.elapsed + arg1 - if this.elapsed >= 0.15 then - this:SetScript("OnUpdate", nil) - bagUpdatePending = false - TrackedItemBar:Update() - end + -- Debounce: wait 0.15 seconds before updating (uses pooled timer) + Guda_ScheduleTimer(0.15, function() + bagUpdatePending = false + TrackedItemBar:Update() end) end, "TrackedItemBar") From f53d566534bccc2d1339a2fc07ab41757c472fab Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 13:18:19 +0400 Subject: [PATCH 3/5] stash --- UI/FrameHelpers.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 912fb6a..2676727 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -210,7 +210,13 @@ end -- Sort items within a category function Guda_SortCategoryItems(items) + if not items then return end table.sort(items, function(a, b) + -- Nil guards for sort stability + if not a then return false end + if not b then return true end + if not a.itemData then return false end + if not b.itemData then return true end -- Rank Trade Goods: meat (name ends with 'meat') = 2, egg (contains 'egg') = 1, others = 0 local function tgRank(d) if not d or not d.name then return 0 end From 8019e19e3736313688cf9cd46472b23b42de77c3 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 13:19:51 +0400 Subject: [PATCH 4/5] category view issues --- Core/ItemDetection.lua | 28 +++++++++++++- Sorting/SortEngine.lua | 67 ++++++++++++++++---------------- UI/BagFrame.lua | 43 +++++++++++++-------- UI/BankFrame.lua | 86 +++++++++++++++++++----------------------- UI/FrameHelpers.lua | 11 +++--- 5 files changed, 132 insertions(+), 103 deletions(-) diff --git a/Core/ItemDetection.lua b/Core/ItemDetection.lua index dcda5c8..6379308 100644 --- a/Core/ItemDetection.lua +++ b/Core/ItemDetection.lua @@ -15,13 +15,39 @@ local detectionCache = {} local cacheHits = 0 local cacheMisses = 0 --- Clear the detection cache +-- Clear the entire detection cache (use sparingly - only for major events) +-- For simple item moves, use InvalidateItem() or don't invalidate at all function ItemDetection:ClearCache() detectionCache = {} cacheHits = 0 cacheMisses = 0 end +-- Invalidate a specific item from cache (by itemLink) +-- Use this when a specific item's properties might have changed +function ItemDetection:InvalidateItem(itemLink) + if itemLink then + detectionCache[itemLink] = nil + end +end + +-- Invalidate multiple items from cache +-- Use this for batch operations +function ItemDetection:InvalidateItems(itemLinks) + if itemLinks then + for _, link in ipairs(itemLinks) do + if link then + detectionCache[link] = nil + end + end + end +end + +-- Check if we have cached data for an item (useful for debugging) +function ItemDetection:IsCached(itemLink) + return itemLink and detectionCache[itemLink] ~= nil +end + -- Get cache statistics function ItemDetection:GetCacheStats() local total = cacheHits + cacheMisses diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 5c0baaf..e979ba3 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -578,10 +578,12 @@ local function ConsolidateStacks(bagIDs) if maxStack > 1 then -- Sort stacks: higher priority bags first, then larger stacks table.sort(group.stacks, function(a, b) - if a.priority ~= b.priority then - return a.priority > b.priority + if not a then return false end + if not b then return true end + if (a.priority or 0) ~= (b.priority or 0) then + return (a.priority or 0) > (b.priority or 0) end - return a.count > b.count + return (a.count or 0) > (b.count or 0) end) -- Greedy consolidation: fill stacks from left to right @@ -826,9 +828,13 @@ local function SortItems(items) end table.sort(items, function(a, b) + -- Guard against nil entries + if not a then return false end + if not b then return true end + -- 1. Priority items first (Hearthstone, etc.) - if a.priority ~= b.priority then - return a.priority < b.priority + if (a.priority or 0) ~= (b.priority or 0) then + return (a.priority or 0) < (b.priority or 0) end -- 2. Equippable items always come before non-equippable items @@ -1018,10 +1024,12 @@ local function BuildTargetPositions(bagIDs, itemCount) -- Only sort if we have more than one element if table.getn(sortedBags) > 1 then table.sort(sortedBags, function(a, b) - if a.priority ~= b.priority then - return a.priority > b.priority + if not a then return false end + if not b then return true end + if (a.priority or 0) ~= (b.priority or 0) then + return (a.priority or 0) > (b.priority or 0) end - return a.bagID < b.bagID + return (a.bagID or 0) < (b.bagID or 0) end) end @@ -1189,10 +1197,12 @@ local function BuildGreyTailPositions(bagIDs, greyCount) -- Only sort if we have more than one element if table.getn(ordered) > 1 then table.sort(ordered, function(a, b) - if a.priority ~= b.priority then - return a.priority < b.priority -- lowest first + if not a then return false end + if not b then return true end + if (a.priority or 0) ~= (b.priority or 0) then + return (a.priority or 0) < (b.priority or 0) -- lowest first end - return a.bagID > b.bagID -- higher bagID later (treated as further to the right) + return (a.bagID or 0) > (b.bagID or 0) -- higher bagID later (treated as further to the right) end) end @@ -1214,15 +1224,17 @@ local function BuildGreyTailPositions(bagIDs, greyCount) -- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions) if table.getn(tailSlots) > 1 then table.sort(tailSlots, function(a, b) + if not a then return false end + if not b then return true end local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0 local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0 if aPrio ~= bPrio then return aPrio > bPrio end - if a.bag ~= b.bag then - return a.bag < b.bag + if (a.bag or 0) ~= (b.bag or 0) then + return (a.bag or 0) < (b.bag or 0) end - return a.slot < b.slot + return (a.slot or 0) < (b.slot or 0) end) end @@ -1518,11 +1530,9 @@ function SortEngine:SortBags() end end - -- Clear caches after sorting to ensure fresh detection on next UI update + -- Clear bag position cache after sorting to ensure fresh slot data on next UI update + -- NOTE: Don't clear ItemDetection cache - item properties don't change when items move addon.Modules.BagScanner:ClearCache() - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end -- Return total moves made return routeCount + consolidateCount + specializedMoves + regularMoves @@ -1655,11 +1665,9 @@ function SortEngine:SortBank() end end - -- Clear caches after sorting to ensure fresh detection on next UI update + -- Clear bag position cache after sorting to ensure fresh slot data on next UI update + -- NOTE: Don't clear ItemDetection cache - item properties don't change when items move addon.Modules.BankScanner:ClearCache() - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end -- Return total moves made return routeCount + consolidateCount + specializedMoves + regularMoves @@ -1727,15 +1735,12 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) currentSortType = "bags" -- Reset sort context - -- Clear caches after sorting to ensure fresh detection + -- Clear bag position cache - item properties don't change on move 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) elseif passCount >= safetyLimit then @@ -1748,15 +1753,12 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) currentSortType = "bags" -- Reset sort context - -- Clear caches after sorting to ensure fresh detection + -- Clear bag position cache - item properties don't change on move 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) else @@ -1776,15 +1778,12 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) currentSortType = "bags" -- Reset sort context - -- Clear caches after sorting to ensure fresh detection + -- Clear bag position cache - item properties don't change on move 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) return diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 90778af..8622731 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -21,6 +21,7 @@ function BagFrame:GetCurrentViewChar() end local searchText = "" local itemButtons = {} +local slotToButton = {} -- Fast O(1) lookup: slotToButton[bagID][slotID] = button local showKeyring = false -- Toggle for keyring display local hiddenBags = {} -- Track which bags are hidden (bagID -> true/false) local bagParents = {} -- Per-bag parent frames to carry bagID for Blizzard item button templates @@ -243,16 +244,13 @@ function BagFrame:UpdateChangedSlots(bagID) local numSlots = GetContainerNumSlots(bagID) if not numSlots or numSlots == 0 then return -1 end + -- Check if we have the slot lookup table for this bag + if not slotToButton[bagID] then return -1 end + local updatedCount = 0 for slotID = 1, numSlots do - -- Find button for this slot - local targetButton = nil - for _, button in ipairs(itemButtons) do - if button.bagID == bagID and button.slotID == slotID then - targetButton = button - break - end - end + -- O(1) button lookup using hash table + local targetButton = slotToButton[bagID][slotID] if not targetButton then -- Button not found, need full redraw @@ -498,10 +496,13 @@ function BagFrame:Update() -- Display items local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single" - -- Clear itemButtons table before rebuilding (prevents stale references) + -- Clear itemButtons table and slot lookup before rebuilding (prevents stale references) for k in pairs(itemButtons) do itemButtons[k] = nil end + for k in pairs(slotToButton) do + slotToButton[k] = nil + end -- Reset all section headers before displaying items local i = 1 @@ -704,6 +705,9 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName) Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar) button.inUse = true table.insert(itemButtons, button) + -- Populate slot lookup for O(1) access + if not slotToButton[bagID] then slotToButton[bagID] = {} end + slotToButton[bagID][slot] = button col = col + 1 if col >= blockCols then @@ -766,8 +770,11 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName) if numItems > 0 then if sec.name == "Tools" then table.sort(items, function(a, b) - if a.itemData.quality ~= b.itemData.quality then - return a.itemData.quality > b.itemData.quality + -- Guard against nil entries + if not a or not a.itemData then return false end + if not b or not b.itemData then return true end + if (a.itemData.quality or 0) ~= (b.itemData.quality or 0) then + return (a.itemData.quality or 0) > (b.itemData.quality or 0) end return (a.itemData.name or "") < (b.itemData.name or "") end) @@ -830,6 +837,9 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName) Guda_ItemButton_SetItem(button, item.bagID, item.slotID, item.itemData, false, isOtherChar and charName or nil, self:PassesSearchFilter(item.itemData), isOtherChar) button.inUse = true table.insert(itemButtons, button) + -- Populate slot lookup for O(1) access + if not slotToButton[item.bagID] then slotToButton[item.bagID] = {} end + slotToButton[item.bagID][item.slotID] = button sCol = sCol + 1 if sCol >= blockCols then @@ -1005,6 +1015,9 @@ function BagFrame:DisplayItems(bagData, isOtherChar, charName) Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar) table.insert(itemButtons, button) + -- Populate slot lookup for O(1) access + if not slotToButton[bagID] then slotToButton[bagID] = {} end + slotToButton[bagID][slot] = button -- Advance position col = col + 1 @@ -1746,7 +1759,9 @@ function Guda_BagFrame_MergeStacks() if table.getn(group.stacks) > 1 then -- Sort stacks: larger stacks first (targets), smaller stacks last (sources) table.sort(group.stacks, function(a, b) - return a.count > b.count + if not a then return false end + if not b then return true end + return (a.count or 0) > (b.count or 0) end) local sourceLoopStart = table.getn(group.stacks) @@ -2688,10 +2703,8 @@ function BagFrame:Initialize() if not isSorting then -- Try incremental update for manual item moves + -- NOTE: Don't clear ItemDetection cache - item properties don't change on move addon.Modules.BagScanner:InvalidateBag(arg1) - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end -- Try to update only changed slots in this bag local result = BagFrame:UpdateChangedSlots(arg1) diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index 2da25fa..cc98f5b 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -14,6 +14,7 @@ local searchText = "" local isReadOnlyMode = false -- Track if viewing saved bank (read-only) or live bank (interactive) local hiddenBankBags = {} -- Track which bank bags are hidden (bagID -> true/false) local bankBagParents = {} -- Parent frames per bank bag (same approach as BagFrame) +local bankSlotToButton = {} -- Fast O(1) lookup: bankSlotToButton[bagID][slotID] = button -- Global click catcher for clearing bank search focus local bankClickCatcher = nil @@ -135,18 +136,9 @@ function BankFrame:UpdateSingleSlot(bagID, slotID) if not Guda_BankFrame:IsShown() then return false end if currentViewChar then return false end -- Can't do single-slot for other characters - -- Find the button for this slot - local bankBagParent = bankBagParents[bagID] - if not bankBagParent or not bankBagParent.itemButtons then return false end - - local targetButton = nil - for button in pairs(bankBagParent.itemButtons) do - if button.bagID == bagID and button.slotID == slotID then - targetButton = button - break - end - end - + -- O(1) button lookup using hash table + if not bankSlotToButton[bagID] then return false end + local targetButton = bankSlotToButton[bagID][slotID] if not targetButton then return false end -- Get fresh item data for this slot @@ -190,22 +182,16 @@ function BankFrame:UpdateChangedSlots(bagID) if not Guda_BankFrame:IsShown() then return -1 end if currentViewChar then return -1 end - local bankBagParent = bankBagParents[bagID] - if not bankBagParent or not bankBagParent.itemButtons then return -1 end + -- Check if we have the slot lookup table for this bag + if not bankSlotToButton[bagID] then return -1 end local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) if not numSlots or numSlots == 0 then return -1 end local updatedCount = 0 for slotID = 1, numSlots do - -- Find button for this slot - local targetButton = nil - for button in pairs(bankBagParent.itemButtons) do - if button.bagID == bagID and button.slotID == slotID then - targetButton = button - break - end - end + -- O(1) button lookup using hash table + local targetButton = bankSlotToButton[bagID][slotID] if not targetButton then -- Button not found, need full redraw @@ -302,7 +288,7 @@ function BankFrame:Update() end -- Mark all existing buttons as not in use (we'll mark active ones during display) - -- Mark all existing buttons as not in use (we'll mark active ones during display) + -- Also clear the slot lookup table for fresh rebuild for _, bankBagParent in pairs(bankBagParents) do if bankBagParent and bankBagParent.itemButtons then for button in pairs(bankBagParent.itemButtons) do @@ -312,6 +298,9 @@ function BankFrame:Update() end end end + for k in pairs(bankSlotToButton) do + bankSlotToButton[k] = nil + end -- Determine if we're in read-only mode: -- - If viewing another character → read-only @@ -522,6 +511,9 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName) local matchesFilter = self:PassesSearchFilter(itemData) Guda_ItemButton_SetItem(button, bagID, slot, itemData, true, isOtherChar and charName or nil, matchesFilter, isOtherChar or isReadOnlyMode) button.inUse = true + -- Populate slot lookup for O(1) access + if not bankSlotToButton[bagID] then bankSlotToButton[bagID] = {} end + bankSlotToButton[bagID][slot] = button col = col + 1 if col >= blockCols then @@ -580,8 +572,11 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName) if numItems > 0 then if sec.name == "Tools" then table.sort(items, function(a, b) - if a.itemData.quality ~= b.itemData.quality then - return a.itemData.quality > b.itemData.quality + -- Guard against nil entries + if not a or not a.itemData then return false end + if not b or not b.itemData then return true end + if (a.itemData.quality or 0) ~= (b.itemData.quality or 0) then + return (a.itemData.quality or 0) > (b.itemData.quality or 0) end return (a.itemData.name or "") < (b.itemData.name or "") end) @@ -642,7 +637,10 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName) button:Show() Guda_ItemButton_SetItem(button, item.bagID, item.slotID, item.itemData, true, isOtherChar and charName or nil, self:PassesSearchFilter(item.itemData), isOtherChar or isReadOnlyMode) button.inUse = true - + -- Populate slot lookup for O(1) access + if not bankSlotToButton[item.bagID] then bankSlotToButton[item.bagID] = {} end + bankSlotToButton[item.bagID][item.slotID] = button + sCol = sCol + 1 if sCol >= blockCols then sCol = 0 @@ -795,6 +793,9 @@ function BankFrame:DisplayItems(bankData, isOtherChar, charName) button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", xPos, yPos) Guda_ItemButton_SetItem(button, bagID, slot, itemData, true, isOtherChar and charName or nil, matchesFilter, isReadOnlyMode) + -- Populate slot lookup for O(1) access + if not bankSlotToButton[bagID] then bankSlotToButton[bagID] = {} end + bankSlotToButton[bagID][slot] = button col = col + 1 if col >= perRow then @@ -1229,7 +1230,9 @@ function Guda_BankFrame_MergeStacks() if table.getn(group.stacks) > 1 then -- Sort stacks: larger stacks first (targets), smaller stacks last (sources) table.sort(group.stacks, function(a, b) - return a.count > b.count + if not a then return false end + if not b then return true end + return (a.count or 0) > (b.count or 0) end) local sourceLoopStart = table.getn(group.stacks) @@ -1730,11 +1733,9 @@ function BankFrame:Initialize() -- arg1 is the slot number (1-28 for main bank) -- Try single-slot update if not sorting if not isSorting then - -- Invalidate cache for fresh data + -- Invalidate bag scanner cache for fresh slot data + -- NOTE: Don't clear ItemDetection cache - item properties don't change on move addon.Modules.BankScanner:InvalidateBag(-1) - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end -- Try single-slot update if BankFrame:UpdateSingleSlot(-1, arg1) then return -- Success, no full redraw needed @@ -1742,30 +1743,21 @@ function BankFrame:Initialize() end -- Fallback: full redraw (sorting or single-slot failed) addon.Modules.BankScanner:InvalidateBag(-1) - 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 bag scanner cache for fresh slot data + -- NOTE: Don't clear ItemDetection cache - item properties don't change on move + addon.Modules.BankScanner:InvalidateBag(arg1) + -- Try incremental update if not sorting if not isSorting then - addon.Modules.BankScanner:InvalidateBag(arg1) - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end -- Try to update only changed slots local result = BankFrame:UpdateChangedSlots(arg1) if result >= 0 then return -- Success, no full redraw needed end -- Fall through to full redraw - else - -- Sorting in progress - invalidate for full redraw - addon.Modules.BankScanner:InvalidateBag(arg1) - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end end else -- Not a bank bag, ignore for bank frame @@ -1773,11 +1765,9 @@ function BankFrame:Initialize() end elseif event == "PLAYERBANKBAGSLOTS_CHANGED" then -- Bank container slot changed (bag added/removed) - -- Must clear entire cache since bag structure changed + -- Clear bag scanner cache since structure changed + -- NOTE: Don't clear ItemDetection cache - item properties don't change addon.Modules.BankScanner:ClearCache() - if addon.Modules.ItemDetection then - addon.Modules.ItemDetection:ClearCache() - end end -- Slightly longer delay to ensure WoW API has updated diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 2676727..145a020 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -212,11 +212,12 @@ end function Guda_SortCategoryItems(items) if not items then return end table.sort(items, function(a, b) - -- Nil guards for sort stability + -- Guard against nil entries if not a then return false end if not b then return true end if not a.itemData then return false end if not b.itemData then return true end + -- Rank Trade Goods: meat (name ends with 'meat') = 2, egg (contains 'egg') = 1, others = 0 local function tgRank(d) if not d or not d.name then return 0 end @@ -233,8 +234,8 @@ function Guda_SortCategoryItems(items) return ra > rb end -- Priority: consumable restore tags (eat > drink > restore > nil) - local pa = a.itemData and a.itemData.restoreTag or nil - local pb = b.itemData and b.itemData.restoreTag or nil + local pa = a.itemData.restoreTag + local pb = b.itemData.restoreTag local function pr(t) if t == "eat" then return 3 end if t == "drink" then return 2 end @@ -248,8 +249,8 @@ function Guda_SortCategoryItems(items) if a.itemData.subclass ~= b.itemData.subclass then return (a.itemData.subclass or "") < (b.itemData.subclass or "") end - if a.itemData.quality ~= b.itemData.quality then - return a.itemData.quality > b.itemData.quality + if (a.itemData.quality or 0) ~= (b.itemData.quality or 0) then + return (a.itemData.quality or 0) > (b.itemData.quality or 0) end return (a.itemData.name or "") < (b.itemData.name or "") end) From 5587a60280bd470b618e49e6d1e37d7976bbf400 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 14:28:25 +0400 Subject: [PATCH 5/5] version: 1.6.9 --- Guda.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guda.toc b/Guda.toc index 16b5278..567fbe1 100644 --- a/Guda.toc +++ b/Guda.toc @@ -2,7 +2,7 @@ ## Title: Guda ## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW) ## Author: Vati -## Version: 1.6.8 +## Version: 1.6.9 ## SavedVariables: Guda_DB ## SavedVariablesPerCharacter: Guda_CharDB