Files
Nanami-UI/Bags/Offline.lua
2026-03-16 13:48:46 +08:00

338 lines
10 KiB
Lua

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