-- 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