366 lines
11 KiB
Lua
366 lines
11 KiB
Lua
-- Guda Bank Scanner
|
|
-- Scans and stores bank contents with caching and event pending tracking
|
|
|
|
local addon = Guda
|
|
|
|
local BankScanner = {}
|
|
addon.Modules.BankScanner = BankScanner
|
|
|
|
local bankOpen = false
|
|
|
|
local bankCache = nil
|
|
local cacheValid = false
|
|
local eventPending = false
|
|
local dirtySlots = {}
|
|
local dirtyBags = {}
|
|
|
|
local function IsBankAccessible()
|
|
if bankOpen then return true end
|
|
local testSlots = GetContainerNumSlots(-1)
|
|
return testSlots and testSlots > 0
|
|
end
|
|
|
|
local function ReleaseBankBagData(bagData)
|
|
if addon.Modules.BagScanner and addon.Modules.BagScanner.ReleaseContainerData then
|
|
addon.Modules.BagScanner:ReleaseContainerData(bagData)
|
|
end
|
|
end
|
|
|
|
local function ReleaseAllBankCacheData()
|
|
if not bankCache then return end
|
|
for _, bagData in pairs(bankCache) do
|
|
ReleaseBankBagData(bagData)
|
|
end
|
|
end
|
|
|
|
function BankScanner:ClearCache()
|
|
ReleaseAllBankCacheData()
|
|
bankCache = nil
|
|
cacheValid = false
|
|
dirtySlots = {}
|
|
dirtyBags = {}
|
|
eventPending = false
|
|
end
|
|
|
|
function BankScanner:IsEventPending()
|
|
return eventPending
|
|
end
|
|
|
|
function BankScanner:ClearEventPending()
|
|
eventPending = false
|
|
end
|
|
|
|
function BankScanner:MarkSlotDirty(bagID, slotID)
|
|
if not dirtySlots[bagID] then
|
|
dirtySlots[bagID] = {}
|
|
end
|
|
dirtySlots[bagID][slotID] = true
|
|
eventPending = true
|
|
end
|
|
|
|
local function GetLiveSlotInfo(bagID, slotID)
|
|
local api = addon.Modules.ClassicAPI
|
|
if api and api.GetContainerItemInfo then
|
|
return api:GetContainerItemInfo(bagID, slotID)
|
|
end
|
|
|
|
local texture, count, locked, quality, readable, lootable =
|
|
GetContainerItemInfo(bagID, slotID)
|
|
local link = GetContainerItemLink(bagID, slotID)
|
|
if not texture and not link then return nil end
|
|
|
|
local itemID = nil
|
|
if link then
|
|
local _, _, idStr = string.find(link, "item:(%d+)")
|
|
if idStr then itemID = tonumber(idStr) end
|
|
end
|
|
|
|
return {
|
|
iconFileID = texture,
|
|
stackCount = count or 1,
|
|
isLocked = locked and true or false,
|
|
quality = quality,
|
|
isReadable = readable and true or false,
|
|
hasLoot = lootable and true or false,
|
|
hyperlink = link,
|
|
itemID = itemID,
|
|
}
|
|
end
|
|
|
|
-- Refresh only one affected bank container. Unlike the old cache-hit path,
|
|
-- this does not walk every bank bag just to prove that an already-valid cache
|
|
-- is valid. ClassicAPI gives us cheap per-slot identity/count state for the one
|
|
-- bag the UI/event layer marked dirty.
|
|
function BankScanner:RefreshBankBagInPlace(bagID)
|
|
if not bankCache then return end
|
|
|
|
local oldBag = bankCache[bagID]
|
|
if not oldBag then
|
|
bankCache[bagID] = self:ScanBankBag(bagID)
|
|
return
|
|
end
|
|
|
|
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
|
|
if not addon.Modules.Utils:IsBagValid(bagID) or oldBag.numSlots ~= numSlots then
|
|
ReleaseBankBagData(oldBag)
|
|
bankCache[bagID] = self:ScanBankBag(bagID)
|
|
return
|
|
end
|
|
|
|
local freeSlots = 0
|
|
for slotID = 1, numSlots do
|
|
local liveInfo = GetLiveSlotInfo(bagID, slotID)
|
|
local oldData = oldBag.slots[slotID]
|
|
|
|
if not liveInfo then
|
|
if oldData then
|
|
addon.Modules.BagScanner:ReleaseItemData(oldData)
|
|
oldBag.slots[slotID] = nil
|
|
end
|
|
freeSlots = freeSlots + 1
|
|
else
|
|
local liveItemID = tonumber(liveInfo.itemID)
|
|
local liveLink = liveInfo.hyperlink
|
|
local liveCount = liveInfo.stackCount or 1
|
|
local liveLocked = liveInfo.isLocked and true or false
|
|
|
|
local changed = false
|
|
if not oldData then
|
|
changed = true
|
|
elseif oldData.itemID ~= liveItemID then
|
|
changed = true
|
|
elseif oldData.link ~= liveLink then
|
|
changed = true
|
|
elseif oldData.count ~= liveCount then
|
|
changed = true
|
|
elseif (oldData.locked and true or false) ~= liveLocked then
|
|
changed = true
|
|
elseif liveInfo.quality ~= nil and oldData.quality ~= liveInfo.quality then
|
|
changed = true
|
|
elseif liveInfo.iconFileID and oldData.texture ~= liveInfo.iconFileID then
|
|
changed = true
|
|
end
|
|
|
|
if changed then
|
|
if oldData then addon.Modules.BagScanner:ReleaseItemData(oldData) end
|
|
oldBag.slots[slotID] = addon.Modules.BagScanner:ScanSlot(bagID, slotID)
|
|
end
|
|
|
|
if not oldBag.slots[slotID] then
|
|
freeSlots = freeSlots + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
oldBag.numSlots = numSlots
|
|
oldBag.freeSlots = freeSlots
|
|
end
|
|
|
|
function BankScanner:GetBankData()
|
|
if not IsBankAccessible() then
|
|
addon:DebugCategory("GetBankData: bank not accessible")
|
|
return {}
|
|
end
|
|
|
|
if cacheValid and bankCache then
|
|
local bagsToRefresh = dirtyBags
|
|
dirtyBags = {}
|
|
|
|
for bagID in pairs(bagsToRefresh) do
|
|
self:RefreshBankBagInPlace(bagID)
|
|
end
|
|
|
|
for bagID, slots in pairs(dirtySlots) do
|
|
if not bagsToRefresh[bagID]
|
|
and bagID ~= nil and type(slots) == "table" then
|
|
if bankCache[bagID] then
|
|
for slotID in pairs(slots) do
|
|
if type(slotID) == "number" and slotID >= 1 then
|
|
local oldData = bankCache[bagID].slots[slotID]
|
|
local newData = addon.Modules.BagScanner:ScanSlot(bagID, slotID)
|
|
local wasEmpty = oldData == nil
|
|
local isEmpty = newData == nil
|
|
|
|
if oldData then addon.Modules.BagScanner:ReleaseItemData(oldData) end
|
|
bankCache[bagID].slots[slotID] = newData
|
|
|
|
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
|
|
end
|
|
else
|
|
bankCache[bagID] = self:ScanBankBag(bagID)
|
|
end
|
|
end
|
|
end
|
|
|
|
dirtySlots = {}
|
|
eventPending = false
|
|
return bankCache
|
|
end
|
|
|
|
-- Full rescan is now reserved for first open / explicit full invalidation.
|
|
ReleaseAllBankCacheData()
|
|
bankCache = self:ScanBank()
|
|
cacheValid = true
|
|
dirtySlots = {}
|
|
dirtyBags = {}
|
|
eventPending = false
|
|
return bankCache
|
|
end
|
|
|
|
function BankScanner:UpdateSlot(bagID, slotID)
|
|
if not bankOpen then return end
|
|
self:MarkSlotDirty(bagID, slotID)
|
|
end
|
|
|
|
function BankScanner:InvalidateCache()
|
|
cacheValid = false
|
|
eventPending = true
|
|
end
|
|
|
|
function BankScanner:GetCachedItemCount(bagID)
|
|
if not bankCache or not bankCache[bagID] or not bankCache[bagID].slots then
|
|
return 0
|
|
end
|
|
local count = 0
|
|
for _, item in pairs(bankCache[bagID].slots) do
|
|
if item then count = count + 1 end
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- Keep the existing API name used by BankFrame, but change its semantics from
|
|
-- "destroy this cache entry" to "refresh this one bank container on demand".
|
|
function BankScanner:InvalidateBag(bagID)
|
|
if not IsBankAccessible() then return end
|
|
dirtyBags[bagID] = true
|
|
eventPending = true
|
|
end
|
|
|
|
function BankScanner:ScanBank()
|
|
if not IsBankAccessible() then
|
|
addon:Debug("Cannot scan bank - not accessible")
|
|
return {}
|
|
end
|
|
|
|
local bankData = {}
|
|
for _, bagID in ipairs(addon.Constants.BANK_BAGS) do
|
|
bankData[bagID] = self:ScanBankBag(bagID)
|
|
end
|
|
return bankData
|
|
end
|
|
|
|
function BankScanner:ScanBankBag(bagID)
|
|
local bagType = "regular"
|
|
if addon.Modules.Utils:IsSoulBag(bagID) then
|
|
bagType = "soul"
|
|
elseif addon.Modules.Utils:IsHerbBag(bagID) then
|
|
bagType = "herb"
|
|
elseif addon.Modules.Utils:IsEnchantBag(bagID) then
|
|
bagType = "enchant"
|
|
elseif addon.Modules.Utils:IsAmmoQuiverBag(bagID) then
|
|
bagType = "ammo"
|
|
end
|
|
|
|
local bag = {
|
|
slots = {},
|
|
numSlots = addon.Modules.Utils:GetBagSlotCount(bagID),
|
|
freeSlots = 0,
|
|
bagType = bagType,
|
|
}
|
|
|
|
if not addon.Modules.Utils:IsBagValid(bagID) then
|
|
return bag
|
|
end
|
|
|
|
for slot = 1, bag.numSlots do
|
|
local itemData = addon.Modules.BagScanner:ScanSlot(bagID, slot)
|
|
bag.slots[slot] = itemData
|
|
if not itemData then
|
|
bag.freeSlots = bag.freeSlots + 1
|
|
end
|
|
end
|
|
|
|
return bag
|
|
end
|
|
|
|
local function CloneItemData(itemData)
|
|
if not itemData then return nil end
|
|
return {
|
|
link = itemData.link,
|
|
itemID = itemData.itemID,
|
|
texture = itemData.texture,
|
|
count = itemData.count,
|
|
quality = itemData.quality,
|
|
name = itemData.name,
|
|
iLevel = itemData.iLevel,
|
|
type = itemData.type,
|
|
class = itemData.class,
|
|
subclass = itemData.subclass,
|
|
equipSlot = itemData.equipSlot,
|
|
locked = itemData.locked,
|
|
readable = itemData.readable,
|
|
lootable = itemData.lootable,
|
|
isBound = itemData.isBound,
|
|
}
|
|
end
|
|
|
|
local function CloneBankDataForDatabase(runtimeData)
|
|
local snapshot = {}
|
|
if not runtimeData then return snapshot end
|
|
|
|
for bagID, bag in pairs(runtimeData) do
|
|
local bagCopy = {
|
|
slots = {},
|
|
numSlots = bag.numSlots,
|
|
freeSlots = bag.freeSlots,
|
|
bagType = bag.bagType,
|
|
}
|
|
if bag.slots then
|
|
for slotID, itemData in pairs(bag.slots) do
|
|
bagCopy.slots[slotID] = CloneItemData(itemData)
|
|
end
|
|
end
|
|
snapshot[bagID] = bagCopy
|
|
end
|
|
|
|
return snapshot
|
|
end
|
|
|
|
function BankScanner:SaveToDatabase()
|
|
if not bankOpen then return end
|
|
|
|
local bankData = self:GetBankData()
|
|
addon.Modules.DB:SaveBank(CloneBankDataForDatabase(bankData))
|
|
addon:Debug("Bank data saved")
|
|
end
|
|
|
|
function BankScanner:Initialize()
|
|
addon.Modules.Events:OnBankOpen(function()
|
|
bankOpen = true
|
|
BankScanner:ClearCache()
|
|
addon:Debug("Bank opened")
|
|
|
|
Guda_ScheduleTimer(0.5, function()
|
|
BankScanner:SaveToDatabase()
|
|
end)
|
|
end, "BankScanner")
|
|
|
|
addon.Modules.Events:OnBankClose(function()
|
|
addon:Debug("Bank closing - performing final save")
|
|
BankScanner:SaveToDatabase()
|
|
|
|
bankOpen = false
|
|
BankScanner:ClearCache()
|
|
addon:Debug("Bank closed")
|
|
end, "BankScanner")
|
|
end
|
|
|
|
function BankScanner:IsBankOpen()
|
|
return bankOpen
|
|
end
|