Files
coa-bagnon/Bagnon/utility/itemEvents.lua
T
florian.berthold 60e3cea9fd fix(items): keep retrying nil slot reads instead of accepting after one pass
Single-retry was still flashing slots empty on the inventory when lag
exceeded 0.25s — the retry pass would read nil and broadcast the empty.
Track a per-slot retry counter (capped at RETRY_MAX=12, ~3s) so we keep
deferring until the link comes back or we hit the ceiling. Bank was
never affected — it goes through PLAYERBANKSLOTS_CHANGED, not BAG_UPDATE.
2026-05-14 02:12:38 +02:00

329 lines
7.5 KiB
Lua

--[[
BagEvents
A library of functions for accessing and updating bag slot information
Based on SpecialEvents-Bags by Tekkub Stoutwrithe (tekkub@gmail.com)
ITEM_SLOT_ADD
args: bag, slot, link, count, locked, coolingDown
called when a new slot becomes available to the player
ITEM_SLOT_REMOVE
args: bag, slot
called when an item slot is removed from being in use
ITEM_SLOT_UPDATE
args: bag, slot, link, count, locked, coolingDown
called when an item slot's item or item count changes
ITEM_SLOT_UPDATE_COOLDOWN
args: bag, slot, coolingDown
called when an item's cooldown starts/ends
BANK_OPENED
args: none
called when the bank has opened and all of the bagnon events have SendMessaged
BANK_CLOSED
args: none
called when the bank is closed and all of the bagnon events have SendMessaged
BAG_UPDATE_TYPE
args: bag, type
called when the type of a bag changes (aka, what items you can put in it changes)
--]]
local Bagnon = LibStub('AceAddon-3.0'):GetAddon('Bagnon')
local Bucket = LibStub("AceBucket-3.0")
local BagEvents = Bagnon.Ears:New()
Bucket:Embed(BagEvents)
Bagnon.BagEvents = BagEvents
--[[ privates? ]]--
local slots = {}
local bagTypes = {}
local pendingBags = {}
local retryCounts = {}
-- Retry knobs. Server lag spikes on Ascension can easily exceed a second;
-- keep retrying so we don't broadcast an empty slot while item data is still
-- in flight. Accept whatever we read after RETRY_MAX attempts so a genuinely
-- emptied slot eventually resolves.
local RETRY_INTERVAL = 0.25
local RETRY_MAX = 12 -- ~3s worst case
local function ToIndex(bag, slot)
return (bag < 0 and bag*100 - slot) or bag*100 + slot
end
local function GetBagSize(bag)
return (bag == KEYRING_CONTAINER and GetKeyRingSize()) or GetContainerNumSlots(bag)
end
local retryFrame = CreateFrame('Frame')
retryFrame.elapsed = 0
retryFrame:Hide()
retryFrame:SetScript('OnUpdate', function(self, elapsed)
self.elapsed = self.elapsed + elapsed
if self.elapsed < RETRY_INTERVAL then return end
self.elapsed = 0
local bags = pendingBags
pendingBags = {}
for bag in pairs(bags) do
Bagnon.BagEvents:UpdateItems(bag)
end
if not next(pendingBags) then
self:Hide()
end
end)
local function scheduleRetry(bag)
pendingBags[bag] = true
retryFrame.elapsed = 0
retryFrame:Show()
end
--[[ Startup ]]--
function BagEvents:Load()
self.atBank = false
self.firstVisit = true
self.frame = CreateFrame('Frame')
self.RegisterEvent = function(self, event)
self.frame:RegisterEvent(event)
end
self.OnEvent = function(f, event, ...)
if self[event] then
self[event](self, event, ...)
end
end
self.frame:SetScript('OnEvent', self.OnEvent)
self:RegisterEvent('PLAYER_LOGIN')
end
--[[ Update Functions ]]--
--all info
function BagEvents:AddItem(bag, slot)
local index = ToIndex(bag,slot)
if not slots[index] then slots[index] = {} end
local data = slots[index]
local texture, count, locked, quality, readable, lootable, link = GetContainerItemInfo(bag, slot)
local start, duration, enable = GetContainerItemCooldown(bag, slot)
local onCooldown = (start > 0 and duration > 0 and enable > 0)
data[1] = link
data[2] = count
data[3] = locked
data[4] = onCooldown
self:SendMessage('ITEM_SLOT_ADD', bag, slot, link, count, locked, onCooldown)
end
function BagEvents:RemoveItem(bag, slot)
local data = slots[ToIndex(bag, slot)]
if data and next(data) then
local prevLink = data[1]
for i in pairs(data) do
data[i] = nil
end
self:SendMessage('ITEM_SLOT_REMOVE', bag, slot, prevLink)
end
end
function BagEvents:UpdateItem(bag, slot)
local index = ToIndex(bag, slot)
local data = slots[index]
if data then
local prevLink = data[1]
local prevCount = data[2]
local texture, count, locked, quality, readable, lootable, link = GetContainerItemInfo(bag, slot)
local start, duration, enable = GetContainerItemCooldown(bag, slot)
local onCooldown = (start > 0 and duration > 0 and enable > 0)
-- A previously occupied slot reading back nil is usually a lag race between
-- BAG_UPDATE and item data arriving from the server. Keep deferring up to
-- RETRY_MAX times so a multi-second lag spike doesn't flash items empty.
if prevLink and (not link) then
local n = (retryCounts[index] or 0) + 1
if n < RETRY_MAX then
retryCounts[index] = n
scheduleRetry(bag)
return
end
end
retryCounts[index] = nil
if not(prevLink == link and prevCount == count) then
data[1] = link
data[2] = count
data[3] = locked
data[4] = onCooldown
self:SendMessage('ITEM_SLOT_UPDATE', bag, slot, link, count, locked, onCooldown)
end
end
end
function BagEvents:UpdateItems(bag)
for slot = 1, GetBagSize(bag) do
self:UpdateItem(bag, slot)
end
end
--cooldowns
function BagEvents:UpdateCooldown(bag, slot)
local data = slots[ToIndex(bag,slot)]
if data and data[1] then
local start, duration, enable = GetContainerItemCooldown(bag, slot)
local onCooldown = (start > 0 and duration > 0 and enable > 0)
if data[4] ~= onCooldown then
data[4] = onCooldown
self:SendMessage('ITEM_SLOT_UPDATE_COOLDOWN', bag, slot, onCooldown)
end
end
end
function BagEvents:UpdateCooldowns(bag)
for slot = 1, GetBagSize(bag) do
self:UpdateCooldown(bag, slot)
end
end
--bag sizes
function BagEvents:UpdateBagSize(bag)
local prevSize = slots[bag*100] or 0
local newSize = GetBagSize(bag) or 0
slots[bag*100] = newSize
if prevSize > newSize then
for slot = newSize+1, prevSize do
self:RemoveItem(bag, slot)
end
elseif prevSize < newSize then
for slot = prevSize+1, newSize do
self:AddItem(bag, slot)
end
end
end
function BagEvents:UpdateBagType(bag)
local _, newType = GetContainerNumFreeSlots(bag)
local prevType = bagTypes[bag]
if newType ~= prevType then
bagTypes[bag] = newType
self:SendMessage('BAG_UPDATE_TYPE', bag, newType)
end
end
function BagEvents:UpdateBagSizes()
if self:AtBank() then
for bag = 1, NUM_BAG_SLOTS + GetNumBankSlots() do
self:UpdateBagSize(bag)
end
else
for bag = 1, NUM_BAG_SLOTS do
self:UpdateBagSize(bag)
end
end
self:UpdateBagSize(KEYRING_CONTAINER)
end
function BagEvents:UpdateBagTypes()
if self:AtBank() then
for bag = 1, NUM_BAG_SLOTS + GetNumBankSlots() do
self:UpdateBagType(bag)
end
else
for bag = 1, NUM_BAG_SLOTS do
self:UpdateBagType(bag)
end
end
end
--[[ Events ]]--
function BagEvents:PLAYER_LOGIN(...)
self:RegisterBucketEvent('BAG_UPDATE', 0.2, 'BAG_UPDATE')
self:RegisterEvent('BAG_UPDATE_COOLDOWN')
self:RegisterEvent('PLAYERBANKSLOTS_CHANGED')
self:RegisterEvent('BANKFRAME_OPENED')
self:RegisterEvent('BANKFRAME_CLOSED')
self:UpdateBagSize(KEYRING_CONTAINER)
self:UpdateItems(KEYRING_CONTAINER)
self:UpdateBagSize(BACKPACK_CONTAINER)
self:UpdateItems(BACKPACK_CONTAINER)
end
function BagEvents:BAG_UPDATE(bagIDs)
self:UpdateBagTypes()
self:UpdateBagSizes()
for bagID in pairs(bagIDs) do
self:UpdateItems(bagID)
end
end
function BagEvents:PLAYERBANKSLOTS_CHANGED(...)
self:UpdateBagTypes()
self:UpdateBagSizes()
self:UpdateItems(BANK_CONTAINER)
end
function BagEvents:BANKFRAME_OPENED(...)
self.atBank = true
if self.firstVisit then
self.firstVisit = nil
self:UpdateBagSize(BANK_CONTAINER)
self:UpdateBagTypes()
self:UpdateBagSizes()
end
self:SendMessage('BANK_OPENED')
end
function BagEvents:BANKFRAME_CLOSED(...)
self.atBank = false
self:SendMessage('BANK_CLOSED')
end
function BagEvents:BAG_UPDATE_COOLDOWN(...)
self:UpdateCooldowns(BACKPACK_CONTAINER)
for bag = 1, NUM_BAG_SLOTS do
self:UpdateCooldowns(bag)
end
end
--[[ Accessor Methods ]]--
function BagEvents:AtBank()
return self.atBank
end
--load the thing
BagEvents:Load()