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