448 lines
17 KiB
Lua
448 lines
17 KiB
Lua
-- 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
|