Files
coa-altoholic/DataStore_Containers/DataStore_Containers.lua
T
florian.berthold 0a56cbe560
release / release (push) Successful in 5s
coa.17: comprehensive partial-data hardening + DataStore_Characters login scan + Skills strip cap
- Hardening sweep across DataStore_* (softened crash-asserts in Talents/Containers/Quests
  to graceful nil) + Altoholic frames (guarded remaining getter results).
- DataStore_Characters: scan on login (was ghost-gated -> name/level/class never populated;
  the core 'no character data' cause).
- Skills tab: cap inline professions at 6 (+N) so the strip stops overflowing into Cooking.
2026-05-29 19:53:03 +02:00

1100 lines
34 KiB
Lua

--[[ *** DataStore_Containers ***
Written by : Thaoky, EU-Marécages de Zangar
June 21st, 2009
This modules takes care of scanning & storing player bags, bank, & guild banks
Extended services:
- guild communication: at logon, sends guild bank tab info (last visit) to guildmates
- triggers events to manage transfers of guild bank tabs
--]]
if not DataStore then return end
local addonName = "DataStore_Containers"
_G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceComm-3.0", "AceSerializer-3.0", "AceTimer-3.0")
local addon = _G[addonName]
local THIS_ACCOUNT = "Default"
local commPrefix = "DS_Cont" -- let's keep it a bit shorter than the addon name, this goes on a comm channel, a byte is a byte ffs :p
local BI = LibStub("LibBabble-Inventory-3.0"):GetLookupTable()
local guildMembers = {} -- hash table containing guild member info (tab timestamps)
-- Func Call Spam Protection
local FCSP_timer_table_OnBagUpdate = {}
-- Message types
local MSG_SEND_BANK_TIMESTAMPS = 1 -- broacast at login
local MSG_BANK_TIMESTAMPS_REPLY = 2 -- reply to someone else's login
local MSG_BANKTAB_REQUEST = 3 -- request bank tab data ..
local MSG_BANKTAB_REQUEST_ACK = 4 -- .. ack the request, tell the requester to wait
local MSG_BANKTAB_REQUEST_REJECTED = 5 -- .. refuse the request
local MSG_BANKTAB_TRANSFER = 6 -- .. or send the data
local AddonDB_Defaults = {
global = {
-- CoA (Ascension) Realm Bank: shared per-realm storage, keyed by "Account.Realm"
-- (NOT per-character). Mirrors the guild bank's Tabs layout so the same
-- scan/read helpers can be reused.
RealmBanks = {
['*'] = { -- ["Account.Realm"]
lastUpdate = nil,
Tabs = {
['*'] = { -- tabID = table index [1] to [6]
name = nil,
icon = nil,
size = 0,
ids = {},
links = {},
counts = {}
}
},
}
},
Guilds = {
['*'] = { -- ["Account.Realm.Name"]
money = nil,
faction = nil,
Tabs = {
['*'] = { -- tabID = table index [1] to [6]
name = nil,
icon = nil,
visitedBy = "",
ClientTime = 0, -- since epoch
ClientDate = nil,
ClientHour = nil,
ClientMinute = nil,
ServerHour = nil,
ServerMinute = nil,
ids = {},
links = {},
counts = {}
}
},
}
},
Characters = {
['*'] = { -- ["Account.Realm.Name"]
lastUpdate = nil,
numBagSlots = 0,
numFreeBagSlots = 0,
numBankSlots = 0,
numFreeBankSlots = 0,
Containers = {
['*'] = { -- Containers["Bag0"]
icon = nil, -- Containers's texture
link = nil, -- Containers's itemlink
size = 0,
freeslots = 0,
bagtype = 0,
ids = {},
links = {},
counts = {},
cooldowns = {}
}
}
}
}
}
}
local function GetDBVersion()
return addon.db.global.Version or 0
end
local function SetDBVersion(version)
addon.db.global.Version = version
end
local DBUpdaters = {
-- Table of functions, each one updates to its index's version
-- ex: [3] = the function that upgrades from v2 to v3
[1] = function(self)
local function CopyTable(src, dest)
for k, v in pairs (src) do
if type(v) == "table" then
dest[k] = {}
CopyTable(v, dest[k])
else
dest[k] = v
end
end
end
-- This function moves guild bank tabs from the "Guilds/Guildkey" level to the "Guilds/Guildkey/Tabs" sub-table
for guildKey, guildTable in pairs(addon.db.global.Guilds) do
for tabID = 1, 6 do -- convert the 6 tabs
if type(guildTable[tabID]) == "table" then
CopyTable(guildTable[tabID], guildTable.Tabs[tabID])
wipe(guildTable[tabID])
guildTable[tabID] = nil
end
end
guildTable.money = 0
end
end,
}
local function UpdateDB()
local version = GetDBVersion()
for i = (version+1), #DBUpdaters do -- start from latest version +1 to the very last
DBUpdaters[i]()
SetDBVersion(i)
end
DBUpdaters = nil
GetDBVersion = nil
SetDBVersion = nil
end
-- *** Utility functions ***
local function GetThisGuild()
local guild = GetGuildInfo("player")
if guild then
local key = format("%s.%s.%s", THIS_ACCOUNT, GetRealmName(), guild)
return addon.db.global.Guilds[key]
end
end
local function GetBankTimestamps(guild)
-- returns a | delimited string containing the list of alts in the same guild
guild = guild or GetGuildInfo("player")
if not guild then return end
local thisGuild = GetThisGuild()
if not thisGuild then return end
local out = {}
for tabID, tab in pairs(thisGuild.Tabs) do
if tab.name then
table.insert(out, format("%d:%s:%d:%d:%d", tabID, tab.name, tab.ClientTime, tab.ServerHour, tab.ServerMinute))
end
end
return table.concat(out, "|")
end
local function SaveBankTimestamps(sender, timestamps)
if strlen(timestamps) == 0 then return end -- sender has no tabs
guildMembers[sender] = guildMembers[sender] or {}
wipe(guildMembers[sender])
for _, v in pairs( { strsplit("|", timestamps) }) do
local id, name, clientTime, serverHour, serverMinute = strsplit(":", v)
-- ex: guildMembers["Thaoky"]["RaidFood"] = { clientTime = 123, serverHour = ... }
guildMembers[sender][name] = {}
local tab = guildMembers[sender][name]
tab.id = tonumber(id)
tab.clientTime = tonumber(clientTime)
tab.serverHour = tonumber(serverHour)
tab.serverMinute = tonumber(serverMinute)
end
addon:SendMessage("DATASTORE_GUILD_BANKTABS_UPDATED", sender)
end
local function GuildBroadcast(messageType, ...)
local serializedData = addon:Serialize(messageType, ...)
addon:SendCommMessage(commPrefix, serializedData, "GUILD")
end
local function GuildWhisper(player, messageType, ...)
if DataStore:IsGuildMemberOnline(player) then
local serializedData = addon:Serialize(messageType, ...)
addon:SendCommMessage(commPrefix, serializedData, "WHISPER", player)
end
end
local function IsEnchanted(link)
if not link then return end
if not string.find(link, "0:0:0:0:0:0:0") then
-- enchants/jewels store values instead of zeroes in the link, if this string can't be found, there's at least one enchant/jewel
return true
end
end
local BAGS = 1 -- All bags, 0 to 11, and keyring ( id -2 )
local BANK = 2 -- 28 main slots
local GUILDBANK = 3 -- 98 main slots
local PERSONALBANK = 4 -- CoA personal bank (per-character), read via guild bank API
local REALMBANK = 5 -- CoA realm bank (per-realm), read via guild bank API
-- CoA reuses the Guild Bank UI for the Personal Bank and the Realm Bank.
-- These container key prefixes keep their scanned tabs out of the regular
-- "Bag0".."Bag11" / "Bag100" / "Bag-2" namespace so item-count loops can tell
-- them apart. Personal bank tabs live under the character; realm bank tabs live
-- in the realm-keyed global table.
local PERSONALBANK_PREFIX = "PBank" -- char.Containers["PBank1".."PBank6"]
local MAX_BANK_TABS = 6
local ContainerTypes = {
[BAGS] = {
GetSize = function(self, bagID)
return GetContainerNumSlots(bagID)
end,
GetFreeSlots = function(self, bagID)
local freeSlots, bagType = GetContainerNumFreeSlots(bagID)
return freeSlots, bagType
end,
GetLink = function(self, slotID, bagID)
return GetContainerItemLink(bagID, slotID)
end,
GetCount = function(self, slotID, bagID)
local _, count = GetContainerItemInfo(bagID, slotID)
return count
end,
GetCooldown = function(self, slotID, bagID)
local startTime, duration, isEnabled = GetContainerItemCooldown(bagID, slotID)
return startTime, duration, isEnabled
end,
},
[BANK] = {
GetSize = function(self)
return NUM_BANKGENERIC_SLOTS or 28 -- hardcoded in case the constant is not set
end,
GetFreeSlots = function(self)
local freeSlots, bagType = GetContainerNumFreeSlots(-1) -- -1 = player bank
return freeSlots, bagType
end,
GetLink = function(self, slotID)
return GetInventoryItemLink("player", slotID)
end,
GetCount = function(self, slotID)
return GetInventoryItemCount("player", slotID)
end,
GetCooldown = function(self, slotID)
local startTime, duration, isEnabled = GetInventoryItemCooldown("player", slotID)
return startTime, duration, isEnabled
end,
},
[GUILDBANK] = {
GetSize = function(self)
return MAX_GUILDBANK_SLOTS_PER_TAB or 98 -- hardcoded in case the constant is not set
end,
GetFreeSlots = function(self)
return nil, nil
end,
GetLink = function(self, slotID, tabID)
return GetGuildBankItemLink(tabID, slotID)
end,
GetCount = function(self, slotID, tabID)
local _, count = GetGuildBankItemInfo(tabID, slotID)
return count
end,
GetCooldown = function(self, slotID)
return nil
end,
},
-- Personal & Realm banks are read through the guild bank API (CoA reuses that UI).
[PERSONALBANK] = {
GetSize = function(self)
return MAX_GUILDBANK_SLOTS_PER_TAB or 98
end,
GetFreeSlots = function(self)
return nil, nil
end,
GetLink = function(self, slotID, tabID)
return GetGuildBankItemLink(tabID, slotID)
end,
GetCount = function(self, slotID, tabID)
local _, count = GetGuildBankItemInfo(tabID, slotID)
return count
end,
GetCooldown = function(self, slotID)
return nil
end,
},
[REALMBANK] = {
GetSize = function(self)
return MAX_GUILDBANK_SLOTS_PER_TAB or 98
end,
GetFreeSlots = function(self)
return nil, nil
end,
GetLink = function(self, slotID, tabID)
return GetGuildBankItemLink(tabID, slotID)
end,
GetCount = function(self, slotID, tabID)
local _, count = GetGuildBankItemInfo(tabID, slotID)
return count
end,
GetCooldown = function(self, slotID)
return nil
end,
}
}
-- *** CoA bank-type detection ***
-- CoA exposes BANK_PERMISSIONS_PAYLOAD via the client-only HasJsonCacheData /
-- GetJsonCacheData / C_Serialize APIs. Guard every call so this is harmless on
-- non-CoA clients (where the personal/realm bank simply never triggers).
local function GetCoABankType()
if not (HasJsonCacheData and GetJsonCacheData and C_Serialize and C_Serialize.FromJSON) then
return "guild"
end
if not HasJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0) then
return "guild"
end
local json = GetJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0)
if not json then
return "guild"
end
local jsonObject = C_Serialize:FromJSON(json)
if not jsonObject then
return "guild"
end
if jsonObject.IsPersonalBank then
return "personal"
elseif jsonObject.IsRealmBank then
return "realm"
end
return "guild"
end
local function GetRealmBankKey()
return format("%s.%s", THIS_ACCOUNT, GetRealmName())
end
local function GetThisRealmBank()
return addon.db.global.RealmBanks[GetRealmBankKey()]
end
-- *** Scanning functions ***
local function ScanContainer(bagID, containerType)
local Container = ContainerTypes[containerType]
local bag
if containerType == GUILDBANK then
local thisGuild = GetThisGuild()
if not thisGuild then return end
bag = thisGuild.Tabs[bagID] -- bag is actually the current tab
elseif containerType == PERSONALBANK then
-- per-character storage, keyed by a tab-specific prefix so it never
-- collides with the normal bag/bank containers
bag = addon.ThisCharacter.Containers[PERSONALBANK_PREFIX .. bagID]
elseif containerType == REALMBANK then
local realmBank = GetThisRealmBank()
bag = realmBank.Tabs[bagID] -- bag is the current tab in the realm-keyed table
else
bag = addon.ThisCharacter.Containers["Bag" .. bagID]
wipe(bag.cooldowns) -- does not exist for a guild bank
end
wipe(bag.ids) -- clean existing bag data
wipe(bag.counts)
wipe(bag.links)
local link, count
local startTime, duration, isEnabled
bag.size = Container:GetSize(bagID)
bag.freeslots, bag.bagtype = Container:GetFreeSlots(bagID)
-- Scan from 1 to bagsize for normal bags or guild bank tabs, but from 40 to 67 for main bank slots
local baseIndex = (containerType == BANK) and 39 or 0
local index
for slotID = baseIndex + 1, baseIndex + bag.size do
index = slotID - baseIndex
link = Container:GetLink(slotID, bagID)
if link then
bag.ids[index] = tonumber(link:match("item:(%d+)"))
if IsEnchanted(link) then
bag.links[index] = link
end
count = Container:GetCount(slotID, bagID)
if count and count > 1 then
bag.counts[index] = count -- only save the count if it's > 1 (to save some space since a count of 1 is extremely redundant)
end
end
startTime, duration, isEnabled = Container:GetCooldown(slotID, bagID)
if startTime and startTime > 0 then
bag.cooldowns[index] = startTime .."|".. duration .. "|" .. 1
end
end
if containerType == REALMBANK then
GetThisRealmBank().lastUpdate = time()
else
-- personal bank, bags, bank and guild bank all stamp the character;
-- this is what gates DataStore's "no value" guard for char-based getters
addon.ThisCharacter.lastUpdate = time()
end
end
local function ScanBagSlotsInfo()
local char = addon.ThisCharacter
local numBagSlots = 0
local numFreeBagSlots = 0
for bagID = 0, NUM_BAG_SLOTS do
local bag = char.Containers["Bag" .. bagID]
numBagSlots = numBagSlots + bag.size
numFreeBagSlots = numFreeBagSlots + bag.freeslots
end
char.numBagSlots = numBagSlots
char.numFreeBagSlots = numFreeBagSlots
end
local function ScanBankSlotsInfo()
local char = addon.ThisCharacter
local numBankSlots = NUM_BANKGENERIC_SLOTS
local numFreeBankSlots = char.Containers["Bag100"].freeslots
for bagID = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do -- 5 to 11
local bag = char.Containers["Bag" .. bagID]
numBankSlots = numBankSlots + bag.size
numFreeBankSlots = numFreeBankSlots + bag.freeslots
end
char.numBankSlots = numBankSlots
char.numFreeBankSlots = numFreeBankSlots
end
local function ScanGuildBankInfo()
-- only the current tab can be updated
local thisGuild = GetThisGuild()
local tabID = GetCurrentGuildBankTab()
local t = thisGuild.Tabs[tabID] -- t = current tab
t.name, t.icon = GetGuildBankTabInfo(tabID)
t.visitedBy = UnitName("player")
t.ClientTime = time()
if GetLocale() == "enUS" then -- adjust this test if there's demand
t.ClientDate = date("%m/%d/%Y")
else
t.ClientDate = date("%d/%m/%Y")
end
t.ClientHour = tonumber(date("%H"))
t.ClientMinute = tonumber(date("%M"))
t.ServerHour, t.ServerMinute = GetGameTime()
end
local function ScanBag(bagID)
if bagID < 0 then return end
local char = addon.ThisCharacter
local bag = char.Containers["Bag" .. bagID]
if bagID == 0 then -- Bag 0
bag.icon = "Interface\\Buttons\\Button-Backpack-Up";
bag.link = nil;
else -- Bags 1 through 11
bag.icon = GetInventoryItemTexture("player", ContainerIDToInventoryID(bagID))
bag.link = GetInventoryItemLink("player", ContainerIDToInventoryID(bagID))
end
ScanContainer(bagID, BAGS)
ScanBagSlotsInfo()
end
local function ScanKeyRing()
local char = addon.ThisCharacter
local bag = char.Containers["Bag" .. KEYRING_CONTAINER]
bag.icon = "Interface\\Icons\\INV_Misc_Key_14";
bag.link = nil
ScanContainer(KEYRING_CONTAINER, BAGS)
end
-- *** Event Handlers ***
-- local function OnBagUpdate(event, bag)
local function OnBagUpdate(arg)
local bag = arg
FCSP_timer_table_OnBagUpdate[bag] = nil -- manual reset (safety redundancy)
-- if bag < 0 then
-- return
-- end
-- if (bag >= 5) and (bag <= 11) and not addon.isBankOpen then
-- return
-- end
-- print("DataStore_Containers.lua -- OnBagUpdate(event, ",bag,")", DEBUG_CNT, format("%.3f",GetTime()%100)) -- DEBUG 2025 07 21 - 2
if bag == 0 then -- bag is 0 for both the keyring and the original backpack
ScanKeyRing()
end
ScanBag(bag)
end
local function FCSP_OnBagUpdate(event, bag)
-- this function limits calls to "OnBagUpdate" to max 1 every second
-- old limitations from "OnBagUpdate"
if bag < 0 then
return
end
if (bag >= 5) and (bag <= 11) and not addon.isBankOpen then
return
end
if FCSP_timer_table_OnBagUpdate[bag] then return end
-- FCSP_timer_table_OnBagUpdate[bag] = addon:ScheduleTimer(OnBagUpdate, 1, event, bag)
FCSP_timer_table_OnBagUpdate[bag] = addon:ScheduleTimer(OnBagUpdate, 1, bag)
end
local function OnBankFrameClosed()
addon.isBankOpen = nil
addon:UnregisterEvent("BANKFRAME_CLOSED")
addon:UnregisterEvent("PLAYERBANKSLOTS_CHANGED")
end
local function OnPlayerBankSlotsChanged(event, slotID)
-- from top left to bottom right, slotID = 1 to 28for main slots, and 29 to 35 for the additional bags
if (slotID >= 29) and (slotID <= 35) then
ScanBag(slotID - 24) -- bagID for bank bags goes from 5 to 11, so slotID - 24
else
ScanContainer(100, BANK)
ScanBankSlotsInfo()
end
end
local function OnBankFrameOpened()
addon.isBankOpen = true
for bagID = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do -- 5 to 11
ScanBag(bagID)
end
ScanContainer(100, BANK)
ScanBankSlotsInfo()
addon:RegisterEvent("BANKFRAME_CLOSED", OnBankFrameClosed)
addon:RegisterEvent("PLAYERBANKSLOTS_CHANGED", OnPlayerBankSlotsChanged)
end
-- Records the name/icon of a personal-bank tab. Reuses the per-character
-- container created by ScanContainer (PERSONALBANK_PREFIX .. tabID).
local function ScanPersonalBankInfo(tabID)
local bag = addon.ThisCharacter.Containers[PERSONALBANK_PREFIX .. tabID]
bag.name, bag.icon = GetGuildBankTabInfo(tabID)
end
local function ScanRealmBankInfo(tabID)
local t = GetThisRealmBank().Tabs[tabID]
t.name, t.icon = GetGuildBankTabInfo(tabID)
end
local function OnGuildBankFrameClosed()
addon:UnregisterEvent("GUILDBANKFRAME_CLOSED")
addon:UnregisterEvent("GUILDBANKBAGSLOTS_CHANGED")
-- only broadcast guild bank timestamps for the actual guild bank
if addon.coaBankType == "guild" then
local guildName = GetGuildInfo("player")
if guildName then
GuildBroadcast(MSG_SEND_BANK_TIMESTAMPS, GetBankTimestamps(guildName))
end
end
addon.coaBankType = nil
addon.coaBankAvailableTabs = nil
end
local function OnGuildBankBagSlotsChanged()
local currentTab = GetCurrentGuildBankTab()
if addon.coaBankType == "personal" then
ScanContainer(currentTab, PERSONALBANK)
ScanPersonalBankInfo(currentTab)
elseif addon.coaBankType == "realm" then
ScanContainer(currentTab, REALMBANK)
ScanRealmBankInfo(currentTab)
else
-- regular guild bank, unchanged behaviour
ScanContainer(currentTab, GUILDBANK)
ScanGuildBankInfo()
end
end
local function OnGuildBankFrameOpened()
-- CoA reuses the guild bank UI for the personal & realm banks; detect which
-- one this is BEFORE doing anything guild-specific. Harmless ("guild") when
-- the CoA JSON APIs are absent.
addon.coaBankType = GetCoABankType()
addon:RegisterEvent("GUILDBANKFRAME_CLOSED", OnGuildBankFrameClosed)
addon:RegisterEvent("GUILDBANKBAGSLOTS_CHANGED", OnGuildBankBagSlotsChanged)
if addon.coaBankType == "personal" or addon.coaBankType == "realm" then
-- Pre-query the other tabs so we snapshot the whole bank without the
-- player having to click each tab. Each QueryGuildBankTab triggers a
-- GUILDBANKBAGSLOTS_CHANGED for that tab, handled above.
if QueryGuildBankTab then
local currentTab = GetCurrentGuildBankTab and GetCurrentGuildBankTab() or 0
for tabID = 1, MAX_BANK_TABS do
local avail = GetGuildBankTabInfo(tabID)
if type(avail) == "string" and tabID ~= currentTab then
QueryGuildBankTab(tabID)
end
end
end
return
end
-- regular guild bank
local thisGuild = GetThisGuild()
if thisGuild then
thisGuild.money = GetGuildBankMoney()
thisGuild.faction = UnitFactionGroup("player")
end
end
-- ** Mixins **
local function _GetContainer(character, containerID)
-- containerID can be number or string
return character.Containers["Bag" .. containerID]
end
local function _GetContainers(character)
return character.Containers
end
local BagTypeStrings = {
[1] = BI["Quiver"],
[2] = BI["Ammo Pouch"],
[4] = BI["Soul Bag"],
[8] = BI["Leatherworking Bag"],
[16] = BI["Inscription Bag"],
[32] = BI["Herb Bag"],
[64] = BI["Enchanting Bag"],
[128] = BI["Engineering Bag"],
[512] = BI["Gem Bag"],
[1024] = BI["Mining Bag"],
}
local function _GetContainerInfo(character, containerID)
local bag = _GetContainer(character, containerID)
if type(bag) ~= "table" then return end -- CoA: unscanned bag on partial-data alt; was an index-nil crash
return bag.icon, bag.link, bag.size, bag.freeslots, BagTypeStrings[bag.bagtype]
end
local function _GetContainerSize(character, containerID)
-- containerID can be number or string
local bag = character.Containers["Bag" .. containerID] -- CoA: nil for unscanned bag on partial-data alt
return bag and bag.size
end
local function _GetSlotInfo(bag, slotID)
-- CoA: partial-data alts can have an unscanned/nil bag pointer (GetContainer returns nil
-- for a "BagN" the Containers module never scanned); return empties instead of asserting.
if type(bag) ~= "table" or type(slotID) ~= "number" then return end
-- return itemID, itemLink, itemCount
return bag.ids and bag.ids[slotID], bag.links and bag.links[slotID], (bag.counts and bag.counts[slotID]) or 1
end
local function _GetContainerCooldownInfo(bag, slotID)
-- CoA: partial-data alts can have an unscanned/nil bag pointer; degrade to nil gracefully.
if type(bag) ~= "table" or type(slotID) ~= "number" or type(bag.cooldowns) ~= "table" then return end
local cd = bag.cooldowns[slotID]
if cd then
local startTime, duration, isEnabled = strsplit("|", bag.cooldowns[slotID])
local remaining = duration - (GetTime() - startTime)
if remaining > 0 then -- valid cd ? return it
return tonumber(startTime), tonumber(duration), tonumber(isEnabled)
end
-- cooldown expired ? clean it from the db
bag.cooldowns[slotID] = nil
end
end
local function _GetContainerItemCount(character, searchedID)
local bagCount = 0
local bankCount = 0
local personalBankCount = 0
local id
for containerName, container in pairs(character.Containers) do
for slotID=1, container.size do
id = container.ids[slotID]
if (id) and (id == searchedID) then
local itemCount = container.counts[slotID] or 1
if (containerName == "Bag100") then
bankCount = bankCount + itemCount
elseif (containerName == "Bag-2") then
bagCount = bagCount + itemCount
elseif string.sub(containerName, 1, #PERSONALBANK_PREFIX) == PERSONALBANK_PREFIX then
personalBankCount = personalBankCount + itemCount -- CoA personal bank tab
else
local bagNum = tonumber(string.sub(containerName, 4))
if (bagNum >= 0) and (bagNum <= 4) then
bagCount = bagCount + itemCount
else
bankCount = bankCount + itemCount
end
end
end
end
end
-- 3rd return value (personal bank) is additive and backward-compatible:
-- existing callers that expect (bags, bank) just ignore it.
return bagCount, bankCount, personalBankCount
end
-- *** CoA personal bank (per-character) ***
local function _GetPersonalBankItemCount(character, searchedID)
local count = 0
for containerName, container in pairs(character.Containers) do
if string.sub(containerName, 1, #PERSONALBANK_PREFIX) == PERSONALBANK_PREFIX then
for slotID = 1, container.size do
if container.ids[slotID] == searchedID then
count = count + (container.counts[slotID] or 1)
end
end
end
end
return count
end
local function _GetPersonalBankTabCount(character)
-- number of personal bank tabs that have been scanned (have a size)
local n = 0
for tabID = 1, MAX_BANK_TABS do
local container = character.Containers[PERSONALBANK_PREFIX .. tabID]
if container and (container.size or 0) > 0 then
n = n + 1
end
end
return n
end
-- *** CoA realm bank (per-realm, NOT char-based) ***
-- These take a realm key string ("Account.Realm") directly and read the global
-- RealmBanks table, so they bypass DataStore's char/guild "no value" wrapper.
local function _GetRealmBankKey(realm, account)
realm = realm or GetRealmName()
account = account or THIS_ACCOUNT
return format("%s.%s", account, realm)
end
local function _GetRealmBank(realm, account)
return addon.db.global.RealmBanks[_GetRealmBankKey(realm, account)]
end
local function _GetRealmBankItemCount(realm, account, searchedID)
local realmBank = _GetRealmBank(realm, account)
if not realmBank then return 0 end
local count = 0
for _, tab in pairs(realmBank.Tabs) do
for slotID, id in pairs(tab.ids) do
if id == searchedID then
count = count + (tab.counts[slotID] or 1)
end
end
end
return count
end
local function _GetRealmBankTab(realm, account, tabID)
local realmBank = _GetRealmBank(realm, account)
return realmBank and realmBank.Tabs[tabID]
end
local function _GetRealmBankTabName(realm, account, tabID)
local tab = _GetRealmBankTab(realm, account, tabID)
return tab and tab.name
end
local function _GetRealmBankLastUpdate(realm, account)
local realmBank = _GetRealmBank(realm, account)
return realmBank and realmBank.lastUpdate
end
local function _GetNumBagSlots(character)
return character.numBagSlots
end
local function _GetNumFreeBagSlots(character)
return character.numFreeBagSlots
end
local function _GetNumBankSlots(character)
return character.numBankSlots
end
local function _GetNumFreeBankSlots(character)
return character.numFreeBankSlots
end
local function _DeleteGuild(name, realm, account)
realm = realm or GetRealmName()
account = account or THIS_ACCOUNT
local key = format("%s.%s.%s", account, realm, name)
addon.db.global.Guilds[key] = nil
end
local function _GetGuildBankItemCount(guild, searchedID)
local count = 0
for _, container in pairs(guild.Tabs) do
for slotID, id in pairs(container.ids) do
if (id == searchedID) then
count = count + (container.counts[slotID] or 1)
end
end
end
return count
end
local function _GetGuildBankTab(guild, tabID)
return guild.Tabs[tabID]
end
local function _GetGuildBankTabName(guild, tabID)
return guild.Tabs[tabID].name
end
local function _GetGuildBankTabIcon(guild, tabID)
return guild.Tabs[tabID].icon
end
local function _GetGuildBankTabItemCount(guild, tabID, searchedID)
local count = 0
local container = guild.Tabs[tabID]
if type(container) ~= "table" or type(container.ids) ~= "table" then return count end -- CoA: unscanned guild bank tab; was a pairs(nil) crash on item tooltips
for slotID, id in pairs(container.ids) do
if (id == searchedID) then
count = count + (container.counts[slotID] or 1)
end
end
return count
end
local function _GetGuildBankTabLastUpdate(guild, tabID)
return guild.Tabs[tabID].ClientTime
end
local function _GetGuildBankMoney(guild)
return guild.money
end
local function _GetGuildBankFaction(guild)
return guild.faction
end
local function _ImportGuildBankTab(guild, tabID, data)
wipe(guild.Tabs[tabID]) -- clear existing data
guild.Tabs[tabID] = data
end
local function _GetGuildBankTabSuppliers()
return guildMembers
end
local function _GetGuildMemberBankTabInfo(member, tabName)
-- for the current guild, return the guild member's data about a given tab
if guildMembers[member] then
if guildMembers[member][tabName] then
local tab = guildMembers[member][tabName]
return tab.clientTime, tab.serverHour, tab.serverMinute
end
end
end
local function _RequestGuildMemberBankTab(member, tabName)
GuildWhisper(member, MSG_BANKTAB_REQUEST, tabName)
end
local function _RejectBankTabRequest(member)
GuildWhisper(member, MSG_BANKTAB_REQUEST_REJECTED)
end
local function _SendBankTabToGuildMember(member, tabName)
-- send the actual content of a bank tab to a guild member
local thisGuild = GetThisGuild()
if thisGuild then
local tabID
if guildMembers[member] then
if guildMembers[member][tabName] then
tabID = guildMembers[member][tabName].id
end
end
if tabID then
GuildWhisper(member, MSG_BANKTAB_TRANSFER, thisGuild.Tabs[tabID])
end
end
end
local PublicMethods = {
GetContainer = _GetContainer,
GetContainers = _GetContainers,
GetContainerInfo = _GetContainerInfo,
GetContainerSize = _GetContainerSize,
GetSlotInfo = _GetSlotInfo,
GetContainerCooldownInfo = _GetContainerCooldownInfo,
GetContainerItemCount = _GetContainerItemCount,
GetNumBagSlots = _GetNumBagSlots,
GetNumFreeBagSlots = _GetNumFreeBagSlots,
GetNumBankSlots = _GetNumBankSlots,
GetNumFreeBankSlots = _GetNumFreeBankSlots,
DeleteGuild = _DeleteGuild,
GetGuildBankItemCount = _GetGuildBankItemCount,
GetGuildBankTab = _GetGuildBankTab,
GetGuildBankTabName = _GetGuildBankTabName,
GetGuildBankTabIcon = _GetGuildBankTabIcon,
GetGuildBankTabItemCount = _GetGuildBankTabItemCount,
GetGuildBankTabLastUpdate = _GetGuildBankTabLastUpdate,
GetGuildBankMoney = _GetGuildBankMoney,
GetGuildBankFaction = _GetGuildBankFaction,
ImportGuildBankTab = _ImportGuildBankTab,
GetGuildMemberBankTabInfo = _GetGuildMemberBankTabInfo,
RequestGuildMemberBankTab = _RequestGuildMemberBankTab,
RejectBankTabRequest = _RejectBankTabRequest,
SendBankTabToGuildMember = _SendBankTabToGuildMember,
GetGuildBankTabSuppliers = _GetGuildBankTabSuppliers,
-- CoA personal bank (per-character)
GetPersonalBankItemCount = _GetPersonalBankItemCount,
GetPersonalBankTabCount = _GetPersonalBankTabCount,
-- CoA realm bank (per-realm; takes realm/account, not a character/guild key)
GetRealmBankItemCount = _GetRealmBankItemCount,
GetRealmBankTab = _GetRealmBankTab,
GetRealmBankTabName = _GetRealmBankTabName,
GetRealmBankLastUpdate = _GetRealmBankLastUpdate,
}
-- *** Guild Comm ***
--[[ *** Protocol ***
At login:
Broadcast of guild bank timers on the guild channel
After the guild bank frame is closed:
Broadcast of guild bank timers on the guild channel
Client addon calls: DataStore:RequestGuildMemberBankTab()
Client Server
==> MSG_BANKTAB_REQUEST
<== MSG_BANKTAB_REQUEST_ACK (immediate ack)
<== MSG_BANKTAB_REQUEST_REJECTED (stop)
or
<== MSG_BANKTAB_TRANSFER (actual data transfer)
--]]
local function OnAnnounceLogin(self, guildName)
-- when the main DataStore module sends its login info, share the guild bank last visit time across guild members
local timestamps = GetBankTimestamps(guildName)
if timestamps then -- nil if guild bank hasn't been visited yet, so don't broadcast anything
GuildBroadcast(MSG_SEND_BANK_TIMESTAMPS, timestamps)
end
end
local function OnGuildMemberOffline(self, member)
guildMembers[member] = nil
addon:SendMessage("DATASTORE_GUILD_BANKTABS_UPDATED", member)
end
local GuildCommCallbacks = {
[MSG_SEND_BANK_TIMESTAMPS] = function(sender, timestamps)
if sender ~= UnitName("player") then -- don't send back to self
local timestamps = GetBankTimestamps()
if timestamps then
GuildWhisper(sender, MSG_BANK_TIMESTAMPS_REPLY, timestamps) -- reply by sending my own data..
end
end
SaveBankTimestamps(sender, timestamps)
end,
[MSG_BANK_TIMESTAMPS_REPLY] = function(sender, timestamps)
SaveBankTimestamps(sender, timestamps)
end,
[MSG_BANKTAB_REQUEST] = function(sender, tabName)
-- trigger the event only, actual response (ack or not) must be handled by client addons
GuildWhisper(sender, MSG_BANKTAB_REQUEST_ACK) -- confirm that the request has been received
addon:SendMessage("DATASTORE_BANKTAB_REQUESTED", sender, tabName)
end,
[MSG_BANKTAB_REQUEST_ACK] = function(sender)
addon:SendMessage("DATASTORE_BANKTAB_REQUEST_ACK", sender)
end,
[MSG_BANKTAB_REQUEST_REJECTED] = function(sender)
addon:SendMessage("DATASTORE_BANKTAB_REQUEST_REJECTED", sender)
end,
[MSG_BANKTAB_TRANSFER] = function(sender, data)
local guildName = GetGuildInfo("player")
local guild = GetThisGuild()
for tabID, tab in pairs(guild.Tabs) do
if tab.name == data.name then -- this is the tab being updated
_ImportGuildBankTab(guild, tabID, data)
addon:SendMessage("DATASTORE_BANKTAB_UPDATE_SUCCESS", sender, guildName, data.name, tabID)
GuildBroadcast(MSG_SEND_BANK_TIMESTAMPS, GetBankTimestamps(guildName))
end
end
end,
}
function addon:OnInitialize()
addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults)
UpdateDB()
DataStore:RegisterModule(addonName, addon, PublicMethods)
DataStore:SetGuildCommCallbacks(commPrefix, GuildCommCallbacks)
DataStore:SetCharacterBasedMethod("GetContainer")
DataStore:SetCharacterBasedMethod("GetContainers")
DataStore:SetCharacterBasedMethod("GetContainerInfo")
DataStore:SetCharacterBasedMethod("GetContainerSize")
DataStore:SetCharacterBasedMethod("GetContainerItemCount")
DataStore:SetCharacterBasedMethod("GetNumBagSlots")
DataStore:SetCharacterBasedMethod("GetNumFreeBagSlots")
DataStore:SetCharacterBasedMethod("GetNumBankSlots")
DataStore:SetCharacterBasedMethod("GetNumFreeBankSlots")
DataStore:SetCharacterBasedMethod("GetPersonalBankItemCount")
DataStore:SetCharacterBasedMethod("GetPersonalBankTabCount")
-- Realm bank methods are intentionally NOT char/guild based: they take a
-- realm/account string pair directly and read the realm-keyed global table.
DataStore:SetGuildBasedMethod("GetGuildBankItemCount")
DataStore:SetGuildBasedMethod("GetGuildBankTab")
DataStore:SetGuildBasedMethod("GetGuildBankTabName")
DataStore:SetGuildBasedMethod("GetGuildBankTabIcon")
DataStore:SetGuildBasedMethod("GetGuildBankTabItemCount")
DataStore:SetGuildBasedMethod("GetGuildBankTabLastUpdate")
DataStore:SetGuildBasedMethod("GetGuildBankMoney")
DataStore:SetGuildBasedMethod("GetGuildBankFaction")
DataStore:SetGuildBasedMethod("ImportGuildBankTab")
addon:RegisterMessage("DATASTORE_ANNOUNCELOGIN", OnAnnounceLogin)
addon:RegisterMessage("DATASTORE_GUILD_MEMBER_OFFLINE", OnGuildMemberOffline)
addon:RegisterComm(commPrefix, DataStore:GetGuildCommHandler())
end
function addon:OnEnable()
-- manually update bags 0 to 4, then register the event, this avoids reacting to the flood of BAG_UPDATE events at login
for bagID = 0, NUM_BAG_SLOTS do
ScanBag(bagID)
end
ScanKeyRing()
-- addon:RegisterEvent("BAG_UPDATE", OnBagUpdate)
addon:RegisterEvent("BAG_UPDATE", FCSP_OnBagUpdate)
addon:RegisterEvent("BANKFRAME_OPENED", OnBankFrameOpened)
addon:RegisterEvent("GUILDBANKFRAME_OPENED", OnGuildBankFrameOpened)
end
function addon:OnDisable()
addon:UnregisterEvent("BAG_UPDATE")
addon:UnregisterEvent("BANKFRAME_OPENED")
addon:UnregisterEvent("GUILDBANKFRAME_OPENED")
end