perf: add caching and transfer limiting for bag/bank operations

This commit is contained in:
Salikh Gurgenidze
2026-01-24 00:32:30 +04:00
parent 9305b46a85
commit 21efe4c1cc
5 changed files with 291 additions and 38 deletions
+110 -18
View File
@@ -1,12 +1,100 @@
-- Guda Bag Scanner -- Guda Bag Scanner
-- Scans and stores bag contents -- Scans and stores bag contents with caching and event pending tracking
local addon = Guda local addon = Guda
local BagScanner = {} local BagScanner = {}
addon.Modules.BagScanner = 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() function BagScanner:ScanBags()
local bagData = {} local bagData = {}
@@ -61,8 +149,6 @@ function BagScanner:ScanSlot(bagID, slot)
local itemLink = GetContainerItemLink(bagID, slot) local itemLink = GetContainerItemLink(bagID, slot)
-- Get item info -- 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 local name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice
if itemLink then if itemLink then
name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink) 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, name = name,
iLevel = iLevel, iLevel = iLevel,
type = itemType, type = itemType,
class = itemCategory, -- Category (e.g., "Consumable", "Armor") class = itemCategory,
subclass = itemSubType, -- SubType (e.g., "Potion", "Cloth") subclass = itemSubType,
equipSlot = itemEquipLoc, -- Equipment slot (e.g., "INVTYPE_HEAD") - correct now! equipSlot = itemEquipLoc,
locked = locked, locked = locked,
} }
end end
@@ -95,22 +181,28 @@ function BagScanner:SaveToDatabase()
end end
end end
-- Initialize with auto-save on bag changes -- Initialize with event pending tracking
function BagScanner:Initialize() function BagScanner:Initialize()
-- Create event frame for bag updates
local eventFrame = CreateFrame("Frame") local eventFrame = CreateFrame("Frame")
self.eventFrame = eventFrame self.eventFrame = eventFrame
-- Register bag update events -- Register bag update events for pending tracking
eventFrame:RegisterEvent("BAG_UPDATE") eventFrame:RegisterEvent("BAG_UPDATE")
eventFrame:RegisterEvent("BAG_UPDATE_DELAYED") eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
-- eventFrame:SetScript("OnEvent", function()
--eventFrame:SetScript("OnEvent", function() eventPending = true
-- if event == "BAG_UPDATE" or event == "BAG_UPDATE_DELAYED" then -- Mark the specific bag as having changes
-- addon:Debug("Bag update detected, saving data...") if arg1 then
-- self:SaveToDatabase() if not dirtySlots[arg1] then
-- end dirtySlots[arg1] = {}
--end) 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") addon:Debug("Bag scanner initialized with event pending tracking")
end end
+101 -5
View File
@@ -1,5 +1,5 @@
-- Guda Bank Scanner -- Guda Bank Scanner
-- Scans and stores bank contents -- Scans and stores bank contents with caching and event pending tracking
local addon = Guda local addon = Guda
@@ -8,7 +8,102 @@ addon.Modules.BankScanner = BankScanner
local bankOpen = false 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() function BankScanner:ScanBank()
if not bankOpen then if not bankOpen then
addon:Debug("Cannot scan bank - not open") addon:Debug("Cannot scan bank - not open")
@@ -67,16 +162,17 @@ function BankScanner:SaveToDatabase()
return return
end end
local bankData = self:ScanBank() local bankData = self:GetBankData() -- Use cached data
addon.Modules.DB:SaveBank(bankData) addon.Modules.DB:SaveBank(bankData)
addon:Debug("Bank data saved") addon:Debug("Bank data saved")
end end
-- Initialize bank scanner -- Initialize bank scanner
function BankScanner:Initialize() function BankScanner:Initialize()
-- Bank opened - save bank data -- Bank opened - do initial scan
addon.Modules.Events:OnBankOpen(function() addon.Modules.Events:OnBankOpen(function()
bankOpen = true bankOpen = true
BankScanner:ClearCache() -- Clear cache on open
addon:Debug("Bank opened") addon:Debug("Bank opened")
-- Delay scan to ensure bank is fully loaded -- Delay scan to ensure bank is fully loaded
@@ -94,11 +190,11 @@ function BankScanner:Initialize()
-- Bank closed -- Bank closed
addon.Modules.Events:OnBankClose(function() addon.Modules.Events:OnBankClose(function()
-- Do a final save on close before marking bank as closed -- 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") addon:Debug("Bank closing - performing final save")
BankScanner:SaveToDatabase() BankScanner:SaveToDatabase()
bankOpen = false bankOpen = false
BankScanner:ClearCache() -- Clear cache on close
addon:Debug("Bank closed") addon:Debug("Bank closed")
end, "BankScanner") end, "BankScanner")
end end
+40 -4
View File
@@ -9,6 +9,17 @@ addon.Modules.SortEngine = SortEngine
-- Flag to track if sorting is currently in progress -- Flag to track if sorting is currently in progress
SortEngine.sortingInProgress = false 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 -- Update sort button appearance based on sorting state
function SortEngine:UpdateSortButtonState(isDisabled) function SortEngine:UpdateSortButtonState(isDisabled)
local buttons = { local buttons = {
@@ -1002,6 +1013,18 @@ end
local function ApplySort(bagIDs, items, targetPositions) local function ApplySort(bagIDs, items, targetPositions)
ClearCursor() 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 moveToEmpty = {}
local swapOccupied = {} local swapOccupied = {}
@@ -1036,9 +1059,16 @@ local function ApplySort(bagIDs, items, targetPositions)
end end
end end
-- Execute moves to empty slots first -- Execute moves to empty slots first (with limit per cycle)
local moveCount = 0 local moveCount = 0
local lockedCount = 0
for _, move in ipairs(moveToEmpty) do 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) local _, _, locked = GetContainerItemInfo(move.sourceBag, move.sourceSlot)
if not locked then if not locked then
PickupContainerItem(move.sourceBag, move.sourceSlot) PickupContainerItem(move.sourceBag, move.sourceSlot)
@@ -1046,13 +1076,17 @@ local function ApplySort(bagIDs, items, targetPositions)
ClearCursor() ClearCursor()
moveCount = moveCount + 1 moveCount = moveCount + 1
else else
-- If item is locked, it might be due to server lag or another process. lockedCount = lockedCount + 1
-- We don't increment moveCount but the item will be picked up in next pass.
end end
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 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 _, _, sourceLocked = GetContainerItemInfo(move.sourceBag, move.sourceSlot)
local _, _, targetLocked = GetContainerItemInfo(move.targetBag, move.targetSlot) local _, _, targetLocked = GetContainerItemInfo(move.targetBag, move.targetSlot)
@@ -1061,6 +1095,8 @@ local function ApplySort(bagIDs, items, targetPositions)
PickupContainerItem(move.targetBag, move.targetSlot) PickupContainerItem(move.targetBag, move.targetSlot)
ClearCursor() ClearCursor()
moveCount = moveCount + 1 moveCount = moveCount + 1
else
lockedCount = lockedCount + 1
end end
end end
+13 -4
View File
@@ -381,8 +381,8 @@ function BagFrame:Update()
displayName = currentViewChar displayName = currentViewChar
end end
else else
-- Viewing current character -- Viewing current character - use cached data for performance
bagData = addon.Modules.BagScanner:ScanBags() bagData = addon.Modules.BagScanner:GetBagData()
displayName = UnitName("player") or "Character" displayName = UnitName("player") or "Character"
end end
@@ -2580,12 +2580,21 @@ function BagFrame:Initialize()
end end
-- Update on bag changes (debounced to prevent lag on rapid bag updates) -- 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 currentViewChar then return end
if not Guda_BagFrame:IsShown() 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 -- Use standard delay, will auto-extend if sorting is in progress
ScheduleBagFrameUpdate(0.1) ScheduleBagFrameUpdate(0.1)
end, "BagFrame") end)
-- Update item cooldown overlays when item cooldowns change -- Update item cooldown overlays when item cooldowns change
addon.Modules.Events:Register("BAG_UPDATE_COOLDOWN", function() addon.Modules.Events:Register("BAG_UPDATE_COOLDOWN", function()
+25 -5
View File
@@ -224,8 +224,8 @@ function BankFrame:Update()
else else
-- Viewing current character's bank -- Viewing current character's bank
if bankIsOpen then if bankIsOpen then
-- Bank is actually open - use live data (interactive mode) -- Bank is actually open - use cached data for performance
bankData = addon.Modules.BankScanner:ScanBank() bankData = addon.Modules.BankScanner:GetBankData()
-- Use current character's name for the title -- Use current character's name for the title
local playerName = addon.Modules.DB:GetPlayerFullName() local playerName = addon.Modules.DB:GetPlayerFullName()
getglobal("Guda_BankFrame_Title"):SetText(playerName .. "'s Bank") getglobal("Guda_BankFrame_Title"):SetText(playerName .. "'s Bank")
@@ -1620,14 +1620,34 @@ function BankFrame:Initialize()
ScheduleBankFrameUpdate(0.15) ScheduleBankFrameUpdate(0.15)
end, "BankFrameUI") end, "BankFrameUI")
-- Register bank-specific update events (pfUI style, debounced) -- Register bank-specific update events with incremental slot tracking
local updateFrame = CreateFrame("Frame") local updateFrame = CreateFrame("Frame")
updateFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED") updateFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
updateFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED") updateFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
updateFrame:RegisterEvent("BAG_UPDATE")
updateFrame:SetScript("OnEvent", function() updateFrame:SetScript("OnEvent", function()
if addon.Modules.BankScanner:IsBankOpen() and not currentViewChar then if not addon.Modules.BankScanner:IsBankOpen() then return end
ScheduleBankFrameUpdate(0.1) 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 end
ScheduleBankFrameUpdate(0.1)
end) end)
end end