0a56cbe560
release / release (push) Successful in 5s
- 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.
1100 lines
34 KiB
Lua
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
|