diff --git a/Data/BagScanner.lua b/Data/BagScanner.lua index a92b566..0928d71 100644 --- a/Data/BagScanner.lua +++ b/Data/BagScanner.lua @@ -1,12 +1,100 @@ -- Guda Bag Scanner --- Scans and stores bag contents +-- Scans and stores bag contents with caching and event pending tracking local addon = Guda local BagScanner = {} addon.Modules.BagScanner = BagScanner --- Scan all bags and return data +-- Cache for bag data to avoid re-scanning all slots on every update +local bagCache = nil +local cacheValid = false + +-- Event pending tracking (like Baganator's IsBagEventPending) +local eventPending = false +local dirtySlots = {} -- Track specific slots that changed: dirtySlots[bagID][slotID] = true + +-- Clear the bag cache +function BagScanner:ClearCache() + bagCache = nil + cacheValid = false + dirtySlots = {} +end + +-- Check if a bag event is pending (use before transfers) +function BagScanner:IsEventPending() + return eventPending +end + +-- Clear the pending flag (call after processing) +function BagScanner:ClearEventPending() + eventPending = false +end + +-- Mark a specific slot as dirty (incremental tracking) +function BagScanner:MarkSlotDirty(bagID, slotID) + if not dirtySlots[bagID] then + dirtySlots[bagID] = {} + end + dirtySlots[bagID][slotID] = true + eventPending = true +end + +-- Get dirty slots and clear them +function BagScanner:GetAndClearDirtySlots() + local dirty = dirtySlots + dirtySlots = {} + return dirty +end + +-- Get cached bag data, or scan if cache is invalid +function BagScanner:GetBagData() + if cacheValid and bagCache then + -- Process any dirty slots incrementally + for bagID, slots in pairs(dirtySlots) do + if bagCache[bagID] then + for slotID in pairs(slots) do + local oldData = bagCache[bagID].slots[slotID] + local newData = self:ScanSlot(bagID, slotID) + bagCache[bagID].slots[slotID] = newData + + -- Update free slot count + local wasEmpty = (oldData == nil) + local isEmpty = (newData == nil) + if wasEmpty and not isEmpty then + bagCache[bagID].freeSlots = bagCache[bagID].freeSlots - 1 + elseif not wasEmpty and isEmpty then + bagCache[bagID].freeSlots = bagCache[bagID].freeSlots + 1 + end + end + else + -- Bag not in cache, scan it + bagCache[bagID] = self:ScanBag(bagID) + end + end + dirtySlots = {} + return bagCache + end + + -- Cache miss - do full scan + bagCache = self:ScanBags() + cacheValid = true + dirtySlots = {} + return bagCache +end + +-- Invalidate cache (force full re-scan on next update) +function BagScanner:InvalidateCache() + cacheValid = false +end + +-- Invalidate a specific bag in the cache +function BagScanner:InvalidateBag(bagID) + if not bagCache then return end + bagCache[bagID] = nil +end + +-- Scan all bags and return data (full scan) function BagScanner:ScanBags() local bagData = {} @@ -61,8 +149,6 @@ function BagScanner:ScanSlot(bagID, slot) local itemLink = GetContainerItemLink(bagID, slot) -- Get item info - -- Correct order: itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, - -- itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice local name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice if itemLink then name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink) @@ -76,9 +162,9 @@ function BagScanner:ScanSlot(bagID, slot) name = name, iLevel = iLevel, type = itemType, - class = itemCategory, -- Category (e.g., "Consumable", "Armor") - subclass = itemSubType, -- SubType (e.g., "Potion", "Cloth") - equipSlot = itemEquipLoc, -- Equipment slot (e.g., "INVTYPE_HEAD") - correct now! + class = itemCategory, + subclass = itemSubType, + equipSlot = itemEquipLoc, locked = locked, } end @@ -88,29 +174,35 @@ function BagScanner:SaveToDatabase() local bagData = self:ScanBags() addon.Modules.DB:SaveBags(bagData) addon:Debug("Bag data saved to database") - + -- Clear tooltip cache so counts update immediately if addon.Modules.Tooltip and addon.Modules.Tooltip.ClearCache then addon.Modules.Tooltip:ClearCache() end end --- Initialize with auto-save on bag changes +-- Initialize with event pending tracking function BagScanner:Initialize() - -- Create event frame for bag updates local eventFrame = CreateFrame("Frame") self.eventFrame = eventFrame - -- Register bag update events + -- Register bag update events for pending tracking eventFrame:RegisterEvent("BAG_UPDATE") eventFrame:RegisterEvent("BAG_UPDATE_DELAYED") - -- - --eventFrame:SetScript("OnEvent", function() - -- if event == "BAG_UPDATE" or event == "BAG_UPDATE_DELAYED" then - -- addon:Debug("Bag update detected, saving data...") - -- self:SaveToDatabase() - -- end - --end) + eventFrame:SetScript("OnEvent", function() + eventPending = true + -- Mark the specific bag as having changes + if arg1 then + if not dirtySlots[arg1] then + dirtySlots[arg1] = {} + end + -- We don't know which slot, so mark entire bag for rescan + -- by invalidating it + if bagCache and bagCache[arg1] then + bagCache[arg1] = nil + end + end + end) - addon:Debug("Bag scanner initialized with auto-save") -end \ No newline at end of file + addon:Debug("Bag scanner initialized with event pending tracking") +end diff --git a/Data/BankScanner.lua b/Data/BankScanner.lua index 9eb1a39..f63d8f0 100644 --- a/Data/BankScanner.lua +++ b/Data/BankScanner.lua @@ -1,5 +1,5 @@ -- Guda Bank Scanner --- Scans and stores bank contents +-- Scans and stores bank contents with caching and event pending tracking local addon = Guda @@ -8,7 +8,102 @@ addon.Modules.BankScanner = BankScanner local bankOpen = false --- Scan all bank bags and return data +-- Cache for bank data to avoid re-scanning all slots on every update +local bankCache = nil +local cacheValid = false + +-- Event pending tracking (like Baganator's IsBagEventPending) +local eventPending = false +local dirtySlots = {} -- Track specific slots that changed + +-- Clear the bank cache (called when bank opens or significant changes occur) +function BankScanner:ClearCache() + bankCache = nil + cacheValid = false + dirtySlots = {} + eventPending = false +end + +-- Check if a bank event is pending (use before transfers) +function BankScanner:IsEventPending() + return eventPending +end + +-- Clear the pending flag (call after processing) +function BankScanner:ClearEventPending() + eventPending = false +end + +-- Mark a specific slot as dirty (incremental tracking) +function BankScanner:MarkSlotDirty(bagID, slotID) + if not dirtySlots[bagID] then + dirtySlots[bagID] = {} + end + dirtySlots[bagID][slotID] = true + eventPending = true +end + +-- Get cached bank data, or scan if cache is invalid +function BankScanner:GetBankData() + if not bankOpen then + return {} + end + + if cacheValid and bankCache then + -- Process any dirty slots incrementally + for bagID, slots in pairs(dirtySlots) do + if bankCache[bagID] then + for slotID in pairs(slots) do + local oldData = bankCache[bagID].slots[slotID] + local newData = addon.Modules.BagScanner:ScanSlot(bagID, slotID) + bankCache[bagID].slots[slotID] = newData + + -- Update free slot count + local wasEmpty = (oldData == nil) + local isEmpty = (newData == nil) + if wasEmpty and not isEmpty then + bankCache[bagID].freeSlots = bankCache[bagID].freeSlots - 1 + elseif not wasEmpty and isEmpty then + bankCache[bagID].freeSlots = bankCache[bagID].freeSlots + 1 + end + end + else + -- Bag not in cache, scan it + bankCache[bagID] = self:ScanBankBag(bagID) + end + end + dirtySlots = {} + return bankCache + end + + -- Cache miss - do full scan + bankCache = self:ScanBank() + cacheValid = true + dirtySlots = {} + return bankCache +end + +-- Update a single slot in the cache (incremental update) +function BankScanner:UpdateSlot(bagID, slotID) + if not bankOpen then return end + + -- Mark as dirty for next GetBankData call + self:MarkSlotDirty(bagID, slotID) +end + +-- Invalidate cache (force full re-scan on next update) +function BankScanner:InvalidateCache() + cacheValid = false +end + +-- Invalidate a specific bag in the cache (force re-scan of just that bag) +function BankScanner:InvalidateBag(bagID) + if not bankOpen then return end + if not bankCache then return end + bankCache[bagID] = nil +end + +-- Scan all bank bags and return data (full scan) function BankScanner:ScanBank() if not bankOpen then addon:Debug("Cannot scan bank - not open") @@ -67,16 +162,17 @@ function BankScanner:SaveToDatabase() return end - local bankData = self:ScanBank() + local bankData = self:GetBankData() -- Use cached data addon.Modules.DB:SaveBank(bankData) addon:Debug("Bank data saved") end -- Initialize bank scanner function BankScanner:Initialize() - -- Bank opened - save bank data + -- Bank opened - do initial scan addon.Modules.Events:OnBankOpen(function() bankOpen = true + BankScanner:ClearCache() -- Clear cache on open addon:Debug("Bank opened") -- Delay scan to ensure bank is fully loaded @@ -94,11 +190,11 @@ function BankScanner:Initialize() -- Bank closed addon.Modules.Events:OnBankClose(function() -- Do a final save on close before marking bank as closed - -- so that any last changes are captured. addon:Debug("Bank closing - performing final save") BankScanner:SaveToDatabase() bankOpen = false + BankScanner:ClearCache() -- Clear cache on close addon:Debug("Bank closed") end, "BankScanner") end diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index c93325a..0449dc5 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -9,6 +9,17 @@ addon.Modules.SortEngine = SortEngine -- Flag to track if sorting is currently in progress SortEngine.sortingInProgress = false +-- Performance: Max items to move per cycle (like Baganator's 5-item limit) +local MAX_MOVES_PER_CYCLE = 5 + +-- Transfer status constants (like Baganator's SortStatus) +local TransferStatus = { + Complete = 0, -- All moves done + WaitingMove = 1, -- Waiting for item move to complete + WaitingUnlock = 2, -- Item is locked, waiting + Partial = 3, -- Some moves done, more pending +} + -- Update sort button appearance based on sorting state function SortEngine:UpdateSortButtonState(isDisabled) local buttons = { @@ -1002,6 +1013,18 @@ end local function ApplySort(bagIDs, items, targetPositions) ClearCursor() + -- Check if events are pending (like Baganator's IsBagEventPending) + -- If so, wait for them to complete before moving more items + if addon.Modules.BagScanner:IsEventPending() or + (addon.Modules.BankScanner:IsBankOpen() and addon.Modules.BankScanner:IsEventPending()) then + -- Clear pending flags and return 0 - will retry on next pass + addon.Modules.BagScanner:ClearEventPending() + if addon.Modules.BankScanner:IsBankOpen() then + addon.Modules.BankScanner:ClearEventPending() + end + return 0 + end + local moveToEmpty = {} local swapOccupied = {} @@ -1036,9 +1059,16 @@ local function ApplySort(bagIDs, items, targetPositions) end end - -- Execute moves to empty slots first + -- Execute moves to empty slots first (with limit per cycle) local moveCount = 0 + local lockedCount = 0 + for _, move in ipairs(moveToEmpty) do + -- Limit moves per cycle to prevent slot locking (like Baganator) + if moveCount >= MAX_MOVES_PER_CYCLE then + break + end + local _, _, locked = GetContainerItemInfo(move.sourceBag, move.sourceSlot) if not locked then PickupContainerItem(move.sourceBag, move.sourceSlot) @@ -1046,13 +1076,17 @@ local function ApplySort(bagIDs, items, targetPositions) ClearCursor() moveCount = moveCount + 1 else - -- If item is locked, it might be due to server lag or another process. - -- We don't increment moveCount but the item will be picked up in next pass. + lockedCount = lockedCount + 1 end end - -- Execute swaps with occupied slots + -- Execute swaps with occupied slots (if we haven't hit the limit) for _, move in ipairs(swapOccupied) do + -- Limit moves per cycle + if moveCount >= MAX_MOVES_PER_CYCLE then + break + end + local _, _, sourceLocked = GetContainerItemInfo(move.sourceBag, move.sourceSlot) local _, _, targetLocked = GetContainerItemInfo(move.targetBag, move.targetSlot) @@ -1061,6 +1095,8 @@ local function ApplySort(bagIDs, items, targetPositions) PickupContainerItem(move.targetBag, move.targetSlot) ClearCursor() moveCount = moveCount + 1 + else + lockedCount = lockedCount + 1 end end diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 2818a4c..e89eb33 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -381,8 +381,8 @@ function BagFrame:Update() displayName = currentViewChar end else - -- Viewing current character - bagData = addon.Modules.BagScanner:ScanBags() + -- Viewing current character - use cached data for performance + bagData = addon.Modules.BagScanner:GetBagData() displayName = UnitName("player") or "Character" end @@ -2580,12 +2580,21 @@ function BagFrame:Initialize() end -- Update on bag changes (debounced to prevent lag on rapid bag updates) - addon.Modules.Events:OnBagUpdate(function() + -- Register directly to access arg1 (bagID that changed) for incremental cache updates + local bagUpdateFrame = CreateFrame("Frame") + bagUpdateFrame:RegisterEvent("BAG_UPDATE") + bagUpdateFrame:SetScript("OnEvent", function() if currentViewChar then return end if not Guda_BagFrame:IsShown() then return end + + -- Invalidate only the specific bag that changed (arg1 = bagID) + if arg1 and arg1 >= 0 and arg1 <= 4 then + addon.Modules.BagScanner:InvalidateBag(arg1) + end + -- Use standard delay, will auto-extend if sorting is in progress ScheduleBagFrameUpdate(0.1) - end, "BagFrame") + end) -- Update item cooldown overlays when item cooldowns change addon.Modules.Events:Register("BAG_UPDATE_COOLDOWN", function() diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index eab8eb8..2005058 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -224,8 +224,8 @@ function BankFrame:Update() else -- Viewing current character's bank if bankIsOpen then - -- Bank is actually open - use live data (interactive mode) - bankData = addon.Modules.BankScanner:ScanBank() + -- Bank is actually open - use cached data for performance + bankData = addon.Modules.BankScanner:GetBankData() -- Use current character's name for the title local playerName = addon.Modules.DB:GetPlayerFullName() getglobal("Guda_BankFrame_Title"):SetText(playerName .. "'s Bank") @@ -1620,14 +1620,34 @@ function BankFrame:Initialize() ScheduleBankFrameUpdate(0.15) end, "BankFrameUI") - -- Register bank-specific update events (pfUI style, debounced) + -- Register bank-specific update events with incremental slot tracking local updateFrame = CreateFrame("Frame") updateFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED") updateFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED") + updateFrame:RegisterEvent("BAG_UPDATE") updateFrame:SetScript("OnEvent", function() - if addon.Modules.BankScanner:IsBankOpen() and not currentViewChar then - ScheduleBankFrameUpdate(0.1) + if not addon.Modules.BankScanner:IsBankOpen() then return end + if currentViewChar then return end + + if event == "PLAYERBANKSLOTS_CHANGED" and arg1 then + -- Mark specific slot as dirty in main bank (bagID = -1) + addon.Modules.BankScanner:MarkSlotDirty(-1, arg1) + elseif event == "BAG_UPDATE" and arg1 then + -- Check if this is a bank bag (5-10) + if arg1 >= 5 and arg1 <= 10 then + -- Invalidate the specific bank bag (we don't know which slot) + addon.Modules.BankScanner:InvalidateBag(arg1) + else + -- Not a bank bag, ignore for bank frame + return + end + elseif event == "PLAYERBANKBAGSLOTS_CHANGED" then + -- Bank container slot changed (bag added/removed) + -- Must clear entire cache since bag structure changed + addon.Modules.BankScanner:ClearCache() end + + ScheduleBankFrameUpdate(0.1) end) end