-------------------------------------------------------------------------------- -- S-Frames: Offline DB for Bag Module (Bags/Offline.lua) -- Tracks inventory across characters and realms -- NOTE: This file defines SFrames.Bags first - must be loaded before other Bag files -------------------------------------------------------------------------------- SFrames.Bags = SFrames.Bags or {} SFrames.Bags.Offline = {} local offlineFrame = CreateFrame("Frame") offlineFrame:RegisterEvent("PLAYER_LOGIN") offlineFrame:RegisterEvent("BAG_UPDATE") offlineFrame:RegisterEvent("BANKFRAME_OPENED") offlineFrame:RegisterEvent("BANKFRAME_CLOSED") offlineFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED") offlineFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED") local realmName = "" local playerName = "" local isBankOpen = false local function QueueBankRefreshScans() local elapsed = 0 local index = 1 local points = { 0.08, 0.25, 0.55, 0.95 } local t = CreateFrame("Frame") t:SetScript("OnUpdate", function() elapsed = elapsed + (arg1 or 0) if index <= table.getn(points) and elapsed >= points[index] then if isBankOpen then SFrames.Bags.Offline:ScanBags() end index = index + 1 end if index > table.getn(points) then this:SetScript("OnUpdate", nil) end end) end local function InitDB() if not SFramesGlobalDB then SFramesGlobalDB = {} end if not SFramesGlobalDB[realmName] then SFramesGlobalDB[realmName] = {} end if not SFramesGlobalDB[realmName][playerName] then SFramesGlobalDB[realmName][playerName] = { bags = {}, bank = {}, bankSlots = 0, bankBags = {}, equippedBags = {}, money = 0 } end end local function IsBankBagInvSlot(slotID) return type(slotID) == "number" and slotID > 23 end local function IsBagItemLink(link) if type(link) ~= "string" or link == "" then return false end local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) return equipLoc == "INVTYPE_BAG" end local function GetBagLinkState(link) if type(link) ~= "string" or link == "" then return "invalid" end local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link) if equipLoc == "INVTYPE_BAG" then return "bag" end if equipLoc == nil or equipLoc == "" then return "unknown" end return "invalid" end local function SafeGetInventorySlotByName(slotName) if not GetInventorySlotInfo then return nil end local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) if ok and IsBankBagInvSlot(slotID) then return slotID end return nil end local function ResolvePlayerBagInvSlot(index) if type(index) ~= "number" or index < 1 or index > 4 then return nil end local liveBtn = _G["CharacterBag" .. (index - 1) .. "Slot"] if liveBtn and liveBtn.GetID then local id = liveBtn:GetID() if type(id) == "number" and id > 0 then return id end end if ContainerIDToInventoryID then local ok, slotID = pcall(function() return ContainerIDToInventoryID(index) end) if ok and type(slotID) == "number" and slotID > 0 then return slotID end end local slotNames = { "Bag" .. (index - 1) .. "Slot", "Bag" .. index .. "Slot", "CharacterBag" .. (index - 1) .. "Slot", } for _, slotName in ipairs(slotNames) do local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) if ok and type(slotID) == "number" and slotID > 0 then return slotID end end -- Vanilla fallback. local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 } return fallback[index] end local function CaptureEquippedBagMeta(db) if not db then return end db.equippedBags = {} for index = 1, 4 do local invSlot = ResolvePlayerBagInvSlot(index) local link = nil local texture = nil if invSlot then link = GetInventoryItemLink("player", invSlot) texture = GetInventoryItemTexture("player", invSlot) end local size = GetContainerNumSlots(index) or 0 db.equippedBags[index] = { link = link, texture = texture, size = tonumber(size) or 0, } end end local function ResolveBankBagInvSlot(index) if type(index) ~= "number" or index <= 0 then return nil end local bagID = index + 4 local function TryBankButtonID(buttonID, isBank) if not BankButtonIDToInvSlotID then return nil end local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end) if ok and IsBankBagInvSlot(slotID) then return slotID end return nil end local function AcceptIfBag(slotID) if not IsBankBagInvSlot(slotID) then return nil end local link = GetInventoryItemLink("player", slotID) if link and IsBagItemLink(link) then return slotID end return nil end -- Guda-style primary mapping local slot = TryBankButtonID(bagID, 1) if slot then return slot end slot = TryBankButtonID(index, 1) if slot then return slot end -- Fallbacks: only when they are confirmed bag items slot = TryBankButtonID(bagID, nil) slot = AcceptIfBag(slot) if slot then return slot end slot = TryBankButtonID(index, nil) slot = AcceptIfBag(slot) if slot then return slot end if ContainerIDToInventoryID then local ok, s = pcall(function() return ContainerIDToInventoryID(bagID) end) if ok then slot = AcceptIfBag(s) if slot then return slot end end end slot = AcceptIfBag(SafeGetInventorySlotByName("BankBagSlot" .. index)) if slot then return slot end slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index)) if slot then return slot end local liveBtn = _G["BankFrameBag" .. index] or _G["BankFrameBag" .. index .. "Slot"] if liveBtn and liveBtn.GetID then slot = AcceptIfBag(liveBtn:GetID()) if slot then return slot end end return nil end local function CaptureBankBagMeta(db) if not db then return end local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0 db.bankSlots = tonumber(purchased) or 0 db.bankBags = {} for index = 1, 7 do local bagID = index + 4 local size = GetContainerNumSlots(bagID) or 0 local info = { unlocked = (index <= db.bankSlots), size = tonumber(size) or 0, link = nil, texture = nil, } if info.unlocked then local invSlot = ResolveBankBagInvSlot(index) if invSlot then local link = GetInventoryItemLink("player", invSlot) local state = GetBagLinkState(link) if (state == "bag") or (state == "unknown" and info.size > 0) then info.link = link info.texture = GetInventoryItemTexture("player", invSlot) end end end db.bankBags[index] = info end end local function ScanContainer(containerID, isBank) if not SFramesGlobalDB or realmName == "" or playerName == "" then return end local db = SFramesGlobalDB[realmName][playerName] local targetDB = isBank and db.bank or db.bags local size = GetContainerNumSlots(containerID) targetDB[containerID] = { size = size, items = {} } for slot = 1, size do local link = GetContainerItemLink(containerID, slot) if link then local texture, itemCount, locked, quality = GetContainerItemInfo(containerID, slot) local _, _, idStr = string.find(link, "item:(%d+):") local itemID = idStr and tonumber(idStr) or nil targetDB[containerID].items[slot] = { id = itemID, link = link, count = itemCount, texture = texture, quality = quality } end end end function SFrames.Bags.Offline:ScanBags() if realmName == "" or playerName == "" then return end InitDB() local db = SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][playerName] if not db then return end for bag = 0, 4 do ScanContainer(bag, false) end CaptureEquippedBagMeta(db) if isBankOpen then ScanContainer(-1, true) for bag = 5, 11 do ScanContainer(bag, true) end CaptureBankBagMeta(db) end db.money = GetMoney() end local scanPending = false local scanTimer = CreateFrame("Frame") scanTimer:Hide() local function RequestBagScan() if scanPending then return end scanPending = true scanTimer.elapsed = 0 scanTimer:Show() scanTimer:SetScript("OnUpdate", function() this.elapsed = this.elapsed + arg1 if this.elapsed > 0.5 then this:SetScript("OnUpdate", nil) this:Hide() scanPending = false SFrames.Bags.Offline:ScanBags() end end) end offlineFrame:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then realmName = GetRealmName() or "" playerName = UnitName("player") or "" InitDB() SFrames.Bags.Offline:ScanBags() elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" then RequestBagScan() elseif event == "BANKFRAME_OPENED" then isBankOpen = true RequestBagScan() QueueBankRefreshScans() elseif event == "BANKFRAME_CLOSED" then isBankOpen = false end end) function SFrames.Bags.Offline:GetCharacterList() if not SFramesGlobalDB or realmName == "" then return {} end local chars = {} if SFramesGlobalDB[realmName] then for name in pairs(SFramesGlobalDB[realmName]) do table.insert(chars, name) end end return chars end function SFrames.Bags.Offline:GetCharacterData(name) if not SFramesGlobalDB or realmName == "" then return nil end return SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][name] end function SFrames.Bags.Offline:GetCurrentPlayerName() return playerName end