Add cached inventory count index for tooltips
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
-- Guda ClassicAPI Inventory Count Index
|
||||
-- Replaces per-mouseover full inventory scans with a cached itemID index.
|
||||
-- Lua 5.0 compatible.
|
||||
|
||||
local addon = Guda
|
||||
|
||||
local InventoryIndex = {}
|
||||
addon.Modules.InventoryIndex = InventoryIndex
|
||||
|
||||
local staticIndex = nil -- other characters / shared characters
|
||||
local currentIndex = nil -- current character live bags + bank/mail/equipment
|
||||
local staticDirty = true
|
||||
local currentDirty = true
|
||||
local eventFrame = nil
|
||||
|
||||
-- Reused presentation tables to avoid per-mouseover garbage.
|
||||
local displayCharacters = {}
|
||||
local breakdownParts = {}
|
||||
local charParts = {}
|
||||
|
||||
local function GetItemID(itemData)
|
||||
if not itemData then return nil end
|
||||
|
||||
if type(itemData) == "number" then
|
||||
return itemData
|
||||
end
|
||||
|
||||
if type(itemData) == "table" then
|
||||
if itemData.itemID then
|
||||
return tonumber(itemData.itemID)
|
||||
end
|
||||
itemData = itemData.link
|
||||
end
|
||||
|
||||
if type(itemData) ~= "string" then return nil end
|
||||
local _, _, id = string.find(itemData, "item:(%d+)")
|
||||
return id and tonumber(id) or nil
|
||||
end
|
||||
|
||||
local function NewIndex()
|
||||
return {}
|
||||
end
|
||||
|
||||
local function EnsureItemEntry(index, itemID)
|
||||
local entry = index[itemID]
|
||||
if not entry then
|
||||
entry = {
|
||||
totalBags = 0,
|
||||
totalBank = 0,
|
||||
totalMail = 0,
|
||||
totalEquipped = 0,
|
||||
characters = {},
|
||||
characterMap = {},
|
||||
}
|
||||
index[itemID] = entry
|
||||
end
|
||||
return entry
|
||||
end
|
||||
|
||||
local function EnsureCharacterEntry(itemEntry, charKey, charName, charData, isCurrent, isShared)
|
||||
local entry = itemEntry.characterMap[charKey]
|
||||
if not entry then
|
||||
entry = {
|
||||
name = (charData and charData.name) or charName,
|
||||
classToken = charData and charData.classToken,
|
||||
bagCount = 0,
|
||||
bankCount = 0,
|
||||
mailCount = 0,
|
||||
equippedCount = 0,
|
||||
isCurrent = isCurrent and true or false,
|
||||
isShared = isShared and true or false,
|
||||
}
|
||||
itemEntry.characterMap[charKey] = entry
|
||||
table.insert(itemEntry.characters, entry)
|
||||
end
|
||||
return entry
|
||||
end
|
||||
|
||||
local function AddCount(index, itemID, count, source, charKey, charName, charData, isCurrent, isShared)
|
||||
itemID = tonumber(itemID)
|
||||
count = tonumber(count) or 1
|
||||
if not itemID or count <= 0 then return end
|
||||
|
||||
local itemEntry = EnsureItemEntry(index, itemID)
|
||||
local charEntry = EnsureCharacterEntry(itemEntry, charKey, charName, charData, isCurrent, isShared)
|
||||
|
||||
if source == "bags" then
|
||||
itemEntry.totalBags = itemEntry.totalBags + count
|
||||
charEntry.bagCount = charEntry.bagCount + count
|
||||
elseif source == "bank" then
|
||||
itemEntry.totalBank = itemEntry.totalBank + count
|
||||
charEntry.bankCount = charEntry.bankCount + count
|
||||
elseif source == "mail" then
|
||||
itemEntry.totalMail = itemEntry.totalMail + count
|
||||
charEntry.mailCount = charEntry.mailCount + count
|
||||
elseif source == "equipped" then
|
||||
itemEntry.totalEquipped = itemEntry.totalEquipped + count
|
||||
charEntry.equippedCount = charEntry.equippedCount + count
|
||||
end
|
||||
end
|
||||
|
||||
local function IndexContainers(index, containers, source, charKey, charName, charData, isCurrent, isShared)
|
||||
if type(containers) ~= "table" then return end
|
||||
|
||||
for _, bagData in pairs(containers) do
|
||||
if type(bagData) == "table" and type(bagData.slots) == "table" then
|
||||
for _, itemData in pairs(bagData.slots) do
|
||||
if itemData then
|
||||
local itemID = GetItemID(itemData)
|
||||
if itemID then
|
||||
AddCount(index, itemID, itemData.count or 1, source,
|
||||
charKey, charName, charData, isCurrent, isShared)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function IndexMailbox(index, mailboxData, charKey, charName, charData, isCurrent, isShared)
|
||||
if type(mailboxData) ~= "table" then return end
|
||||
|
||||
for _, mail in ipairs(mailboxData) do
|
||||
if type(mail) == "table" then
|
||||
if type(mail.items) == "table" then
|
||||
for _, item in ipairs(mail.items) do
|
||||
local itemID = GetItemID(item)
|
||||
if itemID then
|
||||
AddCount(index, itemID, item.count or 1, "mail",
|
||||
charKey, charName, charData, isCurrent, isShared)
|
||||
end
|
||||
end
|
||||
elseif mail.item then
|
||||
local itemID = GetItemID(mail.item)
|
||||
if itemID then
|
||||
AddCount(index, itemID, mail.item.count or 1, "mail",
|
||||
charKey, charName, charData, isCurrent, isShared)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function IndexSavedEquipped(index, equippedData, charKey, charName, charData, isCurrent, isShared)
|
||||
if type(equippedData) ~= "table" then return end
|
||||
|
||||
for _, itemData in pairs(equippedData) do
|
||||
local itemID = GetItemID(itemData)
|
||||
if itemID then
|
||||
AddCount(index, itemID, 1, "equipped",
|
||||
charKey, charName, charData, isCurrent, isShared)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function IndexLiveEquipped(index, charKey, charName, charData)
|
||||
for slotID = 1, 19 do
|
||||
local link = GetInventoryItemLink("player", slotID)
|
||||
if link then
|
||||
local itemID = GetItemID(link)
|
||||
if itemID then
|
||||
AddCount(index, itemID, 1, "equipped",
|
||||
charKey, charName, charData, true, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function InventoryIndex:InvalidateCurrent()
|
||||
currentDirty = true
|
||||
end
|
||||
|
||||
function InventoryIndex:InvalidateAll()
|
||||
staticDirty = true
|
||||
currentDirty = true
|
||||
end
|
||||
|
||||
local function IsIncludedCharacter(charName, charData, currentRealm)
|
||||
if type(charData) ~= "table" then return false end
|
||||
if charData.realm ~= currentRealm then return false end
|
||||
if addon.Modules.DB and addon.Modules.DB.IsGoldBlacklisted
|
||||
and addon.Modules.DB:IsGoldBlacklisted(charName) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function InventoryIndex:BuildStaticIndex()
|
||||
local index = NewIndex()
|
||||
local currentPlayerName = addon.Modules.DB:GetPlayerFullName()
|
||||
local currentRealm = GetRealmName()
|
||||
|
||||
local function AddSource(data, isShared)
|
||||
if type(data) ~= "table" then return end
|
||||
|
||||
for charName, charData in pairs(data) do
|
||||
if charName ~= currentPlayerName
|
||||
and IsIncludedCharacter(charName, charData, currentRealm) then
|
||||
local charKey = (isShared and "shared:" or "local:") .. charName
|
||||
IndexContainers(index, charData.bags, "bags", charKey, charName, charData, false, isShared)
|
||||
IndexContainers(index, charData.bank, "bank", charKey, charName, charData, false, isShared)
|
||||
IndexMailbox(index, charData.mailbox, charKey, charName, charData, false, isShared)
|
||||
IndexSavedEquipped(index, charData.equipped, charKey, charName, charData, false, isShared)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AddSource(Guda_DB and Guda_DB.characters, false)
|
||||
AddSource(addon.sharedCharacters, true)
|
||||
|
||||
staticIndex = index
|
||||
staticDirty = false
|
||||
end
|
||||
|
||||
function InventoryIndex:BuildCurrentIndex()
|
||||
local index = NewIndex()
|
||||
local currentPlayerName = addon.Modules.DB:GetPlayerFullName()
|
||||
local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[currentPlayerName]
|
||||
|
||||
if type(charData) ~= "table" then
|
||||
currentIndex = index
|
||||
currentDirty = false
|
||||
return
|
||||
end
|
||||
|
||||
local currentRealm = GetRealmName()
|
||||
if not IsIncludedCharacter(currentPlayerName, charData, currentRealm) then
|
||||
currentIndex = index
|
||||
currentDirty = false
|
||||
return
|
||||
end
|
||||
|
||||
local charKey = "current:" .. currentPlayerName
|
||||
|
||||
-- Bags: use BagScanner's already-refreshed runtime snapshot. This is the
|
||||
-- same state consumed by the bag UI, so tooltip counting performs no
|
||||
-- independent container scan.
|
||||
local bagData = nil
|
||||
if addon.Modules.BagScanner and addon.Modules.BagScanner.GetBagData then
|
||||
bagData = addon.Modules.BagScanner:GetBagData()
|
||||
else
|
||||
bagData = charData.bags
|
||||
end
|
||||
IndexContainers(index, bagData, "bags", charKey, currentPlayerName, charData, true, false)
|
||||
|
||||
-- Bank: use live cached bank data only while the NPC bank session is open;
|
||||
-- otherwise use the last persisted snapshot.
|
||||
local bankData = charData.bank
|
||||
if addon.Modules.BankScanner and addon.Modules.BankScanner.IsBankOpen
|
||||
and addon.Modules.BankScanner:IsBankOpen()
|
||||
and addon.Modules.BankScanner.GetBankData then
|
||||
bankData = addon.Modules.BankScanner:GetBankData()
|
||||
end
|
||||
IndexContainers(index, bankData, "bank", charKey, currentPlayerName, charData, true, false)
|
||||
|
||||
-- MailboxScanner persists changes with a short debounce. Reading the
|
||||
-- snapshot here avoids rescanning 100+ mails from every item tooltip.
|
||||
IndexMailbox(index, charData.mailbox, charKey, currentPlayerName, charData, true, false)
|
||||
|
||||
-- Equipment is only 19 slots and is indexed once per invalidation instead
|
||||
-- of once per mouseover.
|
||||
IndexLiveEquipped(index, charKey, currentPlayerName, charData)
|
||||
|
||||
currentIndex = index
|
||||
currentDirty = false
|
||||
end
|
||||
|
||||
function InventoryIndex:GetItem(itemID)
|
||||
itemID = tonumber(itemID)
|
||||
if not itemID then return nil, nil end
|
||||
|
||||
if staticDirty or not staticIndex then
|
||||
self:BuildStaticIndex()
|
||||
end
|
||||
if currentDirty or not currentIndex then
|
||||
self:BuildCurrentIndex()
|
||||
end
|
||||
|
||||
return currentIndex[itemID], staticIndex[itemID]
|
||||
end
|
||||
|
||||
function InventoryIndex:GetStats()
|
||||
local staticItems = 0
|
||||
local currentItems = 0
|
||||
if staticIndex then for _ in pairs(staticIndex) do staticItems = staticItems + 1 end end
|
||||
if currentIndex then for _ in pairs(currentIndex) do currentItems = currentItems + 1 end end
|
||||
return {
|
||||
staticItems = staticItems,
|
||||
currentItems = currentItems,
|
||||
staticDirty = staticDirty,
|
||||
currentDirty = currentDirty,
|
||||
}
|
||||
end
|
||||
|
||||
local function GetClassColor(classToken)
|
||||
if not classToken then return 1.0, 1.0, 1.0 end
|
||||
local color = RAID_CLASS_COLORS[classToken]
|
||||
if color then return color.r, color.g, color.b end
|
||||
return 1.0, 1.0, 1.0
|
||||
end
|
||||
|
||||
local function CollectCharacters(currentEntry, staticEntry)
|
||||
for i = 1, table.getn(displayCharacters) do
|
||||
displayCharacters[i] = nil
|
||||
end
|
||||
|
||||
local index = 0
|
||||
if currentEntry and currentEntry.characters then
|
||||
for _, entry in ipairs(currentEntry.characters) do
|
||||
index = index + 1
|
||||
displayCharacters[index] = entry
|
||||
end
|
||||
end
|
||||
if staticEntry and staticEntry.characters then
|
||||
for _, entry in ipairs(staticEntry.characters) do
|
||||
index = index + 1
|
||||
displayCharacters[index] = entry
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(displayCharacters, function(a, b)
|
||||
if a.isShared ~= b.isShared then return not a.isShared end
|
||||
if a.isCurrent and not b.isCurrent then return true end
|
||||
if not a.isCurrent and b.isCurrent then return false end
|
||||
return (a.name or "") < (b.name or "")
|
||||
end)
|
||||
|
||||
return displayCharacters
|
||||
end
|
||||
|
||||
-- Fast replacement for Tooltip:AddInventoryInfo. The expensive inventory walk
|
||||
-- happens only when an index is dirty; normal mouseovers are direct itemID
|
||||
-- lookups plus formatting.
|
||||
local Tooltip = addon.Modules.Tooltip
|
||||
if Tooltip then
|
||||
function Tooltip:AddInventoryInfo(tooltip, link)
|
||||
if not addon.Modules.DB:GetSetting("showTooltipCounts") then return end
|
||||
if not Guda_DB or type(Guda_DB) ~= "table" then return end
|
||||
if not Guda_DB.characters or type(Guda_DB.characters) ~= "table" then return end
|
||||
|
||||
local itemID = GetItemID(link)
|
||||
if not itemID then return end
|
||||
|
||||
local currentEntry, staticEntry = InventoryIndex:GetItem(itemID)
|
||||
if not currentEntry and not staticEntry then return end
|
||||
|
||||
local totalBags = (currentEntry and currentEntry.totalBags or 0)
|
||||
+ (staticEntry and staticEntry.totalBags or 0)
|
||||
local totalBank = (currentEntry and currentEntry.totalBank or 0)
|
||||
+ (staticEntry and staticEntry.totalBank or 0)
|
||||
local totalMail = (currentEntry and currentEntry.totalMail or 0)
|
||||
+ (staticEntry and staticEntry.totalMail or 0)
|
||||
local totalEquipped = (currentEntry and currentEntry.totalEquipped or 0)
|
||||
+ (staticEntry and staticEntry.totalEquipped or 0)
|
||||
local totalCount = totalBags + totalBank + totalMail + totalEquipped
|
||||
if totalCount <= 0 then return end
|
||||
|
||||
tooltip:AddLine(" ")
|
||||
tooltip:AddLine("|cFFFFD200" .. Guda_L["Inventory"] .. "|r")
|
||||
|
||||
local totalText = "|cFF00FFFF" .. Guda_L["Total"] .. "|r: |cFFFFFFFF" .. totalCount .. "|r"
|
||||
local bpIndex = 0
|
||||
if totalBags > 0 then bpIndex = bpIndex + 1; breakdownParts[bpIndex] = "|cFF00FFFF" .. Guda_L["Bags"] .. "|r: |cFFFFFFFF" .. totalBags .. "|r" end
|
||||
if totalBank > 0 then bpIndex = bpIndex + 1; breakdownParts[bpIndex] = "|cFF00FFFF" .. Guda_L["Bank"] .. "|r: |cFFFFFFFF" .. totalBank .. "|r" end
|
||||
if totalMail > 0 then bpIndex = bpIndex + 1; breakdownParts[bpIndex] = "|cFF00FFFF" .. Guda_L["Mail"] .. "|r: |cFFFFFFFF" .. totalMail .. "|r" end
|
||||
if totalEquipped > 0 then bpIndex = bpIndex + 1; breakdownParts[bpIndex] = "|cFF00FFFF" .. Guda_L["Equipped"] .. "|r: |cFFFFFFFF" .. totalEquipped .. "|r" end
|
||||
for i = bpIndex + 1, table.getn(breakdownParts) do breakdownParts[i] = nil end
|
||||
|
||||
local breakdownText = ""
|
||||
if bpIndex > 0 then
|
||||
breakdownText = "(" .. table.concat(breakdownParts, " | ") .. ")"
|
||||
end
|
||||
tooltip:AddDoubleLine(totalText, breakdownText, 1, 1, 1, 1, 1, 1)
|
||||
|
||||
local characters = CollectCharacters(currentEntry, staticEntry)
|
||||
local sharedSeparatorShown = false
|
||||
for _, charInfo in ipairs(characters) do
|
||||
if charInfo.isShared and not sharedSeparatorShown then
|
||||
tooltip:AddLine("|cFF80C0FF" .. Guda_L["Other Accounts"] .. "|r")
|
||||
sharedSeparatorShown = true
|
||||
end
|
||||
|
||||
local r, g, b = GetClassColor(charInfo.classToken)
|
||||
local pIndex = 0
|
||||
if charInfo.bagCount > 0 then pIndex = pIndex + 1; charParts[pIndex] = "|cFF00FFFF" .. Guda_L["Bags"] .. "|r: |cFFFFFFFF" .. charInfo.bagCount .. "|r" end
|
||||
if charInfo.bankCount > 0 then pIndex = pIndex + 1; charParts[pIndex] = "|cFF00FFFF" .. Guda_L["Bank"] .. "|r: |cFFFFFFFF" .. charInfo.bankCount .. "|r" end
|
||||
if charInfo.mailCount > 0 then pIndex = pIndex + 1; charParts[pIndex] = "|cFF00FFFF" .. Guda_L["Mail"] .. "|r: |cFFFFFFFF" .. charInfo.mailCount .. "|r" end
|
||||
if charInfo.equippedCount > 0 then pIndex = pIndex + 1; charParts[pIndex] = "|cFF00FFFF" .. Guda_L["Equipped"] .. "|r: |cFFFFFFFF" .. charInfo.equippedCount .. "|r" end
|
||||
for i = pIndex + 1, table.getn(charParts) do charParts[i] = nil end
|
||||
|
||||
local countText = pIndex > 0 and table.concat(charParts, " | ") or ""
|
||||
local displayName = charInfo.name or "?"
|
||||
if charInfo.isCurrent then
|
||||
displayName = displayName .. " |cFFFFFF00(*)|r"
|
||||
end
|
||||
tooltip:AddDoubleLine(displayName, countText, r, g, b, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- Wrap Tooltip initialization so its legacy ClearCache definition is
|
||||
-- replaced after the original hooks are installed.
|
||||
local OriginalTooltipInitialize = Tooltip.Initialize
|
||||
function Tooltip:Initialize()
|
||||
OriginalTooltipInitialize(self)
|
||||
|
||||
function Tooltip:ClearCache()
|
||||
InventoryIndex:InvalidateCurrent()
|
||||
addon:Debug("Tooltip inventory index invalidated")
|
||||
end
|
||||
|
||||
InventoryIndex:Initialize()
|
||||
end
|
||||
end
|
||||
|
||||
function InventoryIndex:Initialize()
|
||||
if eventFrame then return end
|
||||
|
||||
eventFrame = CreateFrame("Frame", "Guda_InventoryIndexEvents")
|
||||
eventFrame:RegisterEvent("BAG_UPDATE")
|
||||
eventFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
|
||||
eventFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
|
||||
eventFrame:RegisterEvent("BANKFRAME_OPENED")
|
||||
eventFrame:RegisterEvent("BANKFRAME_CLOSED")
|
||||
eventFrame:RegisterEvent("MAIL_INBOX_UPDATE")
|
||||
eventFrame:RegisterEvent("MAIL_SHOW")
|
||||
eventFrame:RegisterEvent("MAIL_CLOSED")
|
||||
eventFrame:RegisterEvent("MAIL_SEND_SUCCESS")
|
||||
eventFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
||||
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
|
||||
eventFrame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
InventoryIndex:InvalidateAll()
|
||||
elseif event == "MAIL_SEND_SUCCESS" then
|
||||
-- Sending mail may update another locally stored character too.
|
||||
InventoryIndex:InvalidateAll()
|
||||
elseif event == "UNIT_INVENTORY_CHANGED" then
|
||||
if not arg1 or arg1 == "player" then
|
||||
InventoryIndex:InvalidateCurrent()
|
||||
end
|
||||
else
|
||||
InventoryIndex:InvalidateCurrent()
|
||||
end
|
||||
end)
|
||||
|
||||
addon:Debug("ClassicAPI inventory count index initialized")
|
||||
end
|
||||
Reference in New Issue
Block a user