Files
florian.berthold 0aac38f254 feat: v2.14.0 - personal/realm bank, stat search, faster sort
- Bagnon_Forever: silence "Unknown Bank Type" for regular guild banks; mirror
  IsPersonalBank/IsRealmBank from BANK_PERMISSIONS_PAYLOAD onto GuildBankFrame
  and broadcast GUILDBANK_TYPE_DETECTED so other modules pick up the bank type
- TitleFrame: show "Personal Bank" / "Realm Bank" instead of always "Guild Bank";
  re-render on GUILDBANK_TYPE_DETECTED to handle event-ordering races
- LibItemSearch: full-tooltip text scan (substring across every line, cached);
  bare-text search now falls back to it at 3+ chars so typing "Agility" or
  "Strength" finds matching gear. Explicit prefix tt:<text> also works
- SortBtn: rewrite DoContainerMoves to use the 3-pickup swap pattern with
  client-side cursor verification and an explicit 50ms inter-swap delay;
  much faster than the old link-verify loop without outpacing the realm
  server and desyncing after ~10 moves
2026-05-13 08:02:49 +02:00

637 lines
16 KiB
Lua

--[[
Database.lua
BagnonForever's implementation of BagnonDB
--]]
BagnonDB = CreateFrame('GameTooltip', 'BagnonDB', nil, 'BasicTooltipTemplate')
LibStub("AceBucket-3.0"):Embed(BagnonDB)
BagnonDB:SetScript('OnEvent', function(self, event, arg1)
if arg1 == 'Bagnon_Forever' then
self:UnregisterEvent('ADDON_LOADED')
self:Initialize()
end
end)
BagnonDB:RegisterEvent('ADDON_LOADED')
ASC_PERSONAL_BANK_OFFSET = 1000;
ASC_REALM_BANK_OFFSET = 2000;
GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET = 2; -- Offset used to identify when guild bank tabs are loaded
--constants
local L = BAGNON_FOREVER_LOCALS
local CURRENT_VERSION = GetAddOnMetadata('Bagnon_Forever', 'Version')
local NUM_EQUIPMENT_SLOTS = 19
--locals
local currentPlayer = UnitName('player') --the name of the current player that's logged on
local currentRealm = GetRealmName() --what currentRealm we're on
local playerList --a sorted list of players
--[[ Local Functions ]]--
local function ToIndex(bag, slot)
if tonumber(bag) then
return (bag < 0 and bag*100 - slot) or bag*100 + slot
end
return bag .. slot
end
local function ToBagIndex(bag)
return (tonumber(bag) and bag*100) or bag
end
--returns the full item link only for items that have enchants/suffixes, otherwise returns the item's ID
local function ToShortLink(link)
if link then
local a,b,c,d,e,f,g,h = link:match('(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+):(%-?%d+)')
--ASC sets this to unique id in the personal bank, clear it
c = 0;
if(b == '0' and b == c and c == d and d == e and e == f and f == g) then
return a
end
return format('item:%s:%s:%s:%s:%s:%s:%s:%s', a, b, c, d, e, f, g, h)
end
end
local function GetBagSize(bag)
if bag == KEYRING_CONTAINER then
return GetKeyRingSize()
end
if (bag >= ASC_PERSONAL_BANK_OFFSET) then
return 98
end
if bag == 'e' then
return NUM_EQUIPMENT_SLOTS
end
return GetContainerNumSlots(bag)
end
--[[ Addon Loading ]]--
function BagnonDB:Initialize()
self:LoadSettings()
self:SetScript('OnEvent', function(self, event, ...)
if self[event] then
self[event](self, event, ...)
end
end)
if IsLoggedIn() then
self:PLAYER_LOGIN()
else
self:RegisterEvent('PLAYER_LOGIN')
end
end
function BagnonDB:LoadSettings()
if not(BagnonForeverDB and BagnonForeverDB.version) then
BagnonForeverDB = {version = CURRENT_VERSION}
else
local cMajor, cMinor = CURRENT_VERSION:match('(%d+)%.(%d+)')
local major, minor = BagnonForeverDB.version:match('(%d+)%.(%d+)')
if major ~= cMajor then
BagnonForeverDB = {version = cVersion}
elseif minor ~= cMinor then
self:UpdateSettings()
end
if BagnonForeverDB.version ~= CURRENT_VERSION then
self:UpdateVersion()
end
end
self.db = BagnonForeverDB
if not self.db[currentRealm] then
self.db[currentRealm] = {}
end
self.rdb = self.db[currentRealm]
if not self.rdb[currentPlayer] then
self.rdb[currentPlayer] = {}
end
self.pdb = self.rdb[currentPlayer]
if not self.rdb[currentRealm] then
self.rdb[currentRealm] = {}
end
self.realmdb = self.rdb[currentRealm]
end
function BagnonDB:UpdateSettings()
end
function BagnonDB:UpdateVersion()
BagnonForeverDB.version = CURRENT_VERSION
print(format('BagnonForever: Updated to v%s', BagnonForeverDB.version))
end
--[[ Events ]]--
function BagnonDB:PLAYER_LOGIN()
self:SaveMoney()
self:UpdateBag(BACKPACK_CONTAINER)
self:UpdateBag(KEYRING_CONTAINER)
self:SaveEquipment()
self:SaveNumBankSlots()
self:RegisterEvent('GUILDBANKFRAME_OPENED')
self:RegisterEvent('GUILDBANKBAGSLOTS_CHANGED')
self:RegisterEvent('GUILDBANKFRAME_CLOSED')
self:RegisterEvent('BANKFRAME_OPENED')
self:RegisterEvent('BANKFRAME_CLOSED')
self:RegisterEvent('PLAYER_MONEY')
self:RegisterBucketEvent('BAG_UPDATE', 0.2, 'BAG_UPDATE')
self:RegisterEvent('PLAYERBANKSLOTS_CHANGED')
self:RegisterEvent('UNIT_INVENTORY_CHANGED')
self:RegisterEvent('PLAYERBANKBAGSLOTS_CHANGED')
end
function BagnonDB:PLAYER_MONEY()
self:SaveMoney()
end
function BagnonDB:BAG_UPDATE(bagIDs)
for bagID in pairs(bagIDs) do
if not(bagID == BANK_CONTAINER or bagID > NUM_BAG_SLOTS) or self.atBank then
self:OnBagUpdate(bagID)
end
end
end
function BagnonDB:PLAYERBANKSLOTS_CHANGED()
self:UpdateBag(BANK_CONTAINER)
end
function BagnonDB:PLAYERBANKBAGSLOTS_CHANGED()
self:SaveNumBankSlots()
end
function BagnonDB:BANKFRAME_OPENED()
self.atBank = true
self:UpdateBag(BANK_CONTAINER)
for i = 1, GetNumBankSlots() do
self:UpdateBag(i + 4)
end
end
function BagnonDB:BANKFRAME_CLOSED()
self.atBank = nil
end
function BagnonDB:GUILDBANKFRAME_OPENED()
-- Identify bank type from permissions payload
self.IsPersonalBank = nil
self.IsRealmBank = nil
if HasJsonCacheData and HasJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0) then
local json = GetJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0)
if json then
local jsonObject = C_Serialize:FromJSON(json)
if jsonObject then
self.IsPersonalBank = jsonObject.IsPersonalBank
self.IsRealmBank = jsonObject.IsRealmBank
end
end
end
-- Mirror onto GuildBankFrame so other modules (sortBtn, etc) can detect the bank type
if GuildBankFrame then
GuildBankFrame.IsPersonalBank = self.IsPersonalBank
GuildBankFrame.IsRealmBank = self.IsRealmBank
end
-- Notify Bagnon listeners (TitleFrame, etc.) that bank type has been detected,
-- in case GUILDBANKFRAME_OPENED already fired in another handler first.
local bagnon = LibStub and LibStub('AceAddon-3.0', true) and LibStub('AceAddon-3.0'):GetAddon('Bagnon', true)
if bagnon and bagnon.Callbacks and bagnon.Callbacks.SendMessage then
bagnon.Callbacks:SendMessage('GUILDBANK_TYPE_DETECTED')
end
self.guildBankUpdateCalls = 0
self.availableTabs = {} -- table of available tabs
-- Only pre-query tabs for personal/realm bank (these snapshot into the per-character DB).
-- Regular guild bank content belongs to the guild and is handled by Bagnon_GuildBank.
if not (self.IsPersonalBank or self.IsRealmBank) then
return
end
local currentTab = GetCurrentGuildBankTab and GetCurrentGuildBankTab() or 0
for i = 1, 6 do
local avail = GetGuildBankTabInfo(i)
if type(avail) == "string" and i ~= currentTab then
QueryGuildBankTab(i)
self.availableTabs[i] = avail
end
end
end
function BagnonDB:GUILDBANKBAGSLOTS_CHANGED()
self.guildBankUpdateCalls = self.guildBankUpdateCalls + 1
-- Regular guild bank: nothing to snapshot per-character; let Bagnon_GuildBank handle it.
if not (self.IsPersonalBank or self.IsRealmBank) then
return
end
local currentTab = GetCurrentGuildBankTab()
-- Initial pre-query window: after the first 2 calls, each queued QueryGuildBankTab
-- triggers one GUILDBANKBAGSLOTS_CHANGED that makes that tab's items available.
if ((self.guildBankUpdateCalls > GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET)
and (self.guildBankUpdateCalls <= GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET + #self.availableTabs)) then
for i, avail in pairs(self.availableTabs) do
if (i ~= currentTab and self.guildBankUpdateCalls == GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET + i) then
if self.IsPersonalBank then
self:UpdateBag(i + ASC_PERSONAL_BANK_OFFSET)
elseif self.IsRealmBank then
self:UpdateBag(i + ASC_REALM_BANK_OFFSET)
end
end
end
return
end
-- Normal operation: update current tab into per-character DB (personal) or realm DB
local avail = GetGuildBankTabInfo(currentTab)
if type(avail) ~= "string" then
return
end
if self.IsPersonalBank then
self:UpdateBag(currentTab + ASC_PERSONAL_BANK_OFFSET)
elseif self.IsRealmBank then
self:UpdateBag(currentTab + ASC_REALM_BANK_OFFSET)
end
end
function BagnonDB:GUILDBANKFRAME_CLOSED()
self.IsPersonalBank = nil
self.IsRealmBank = nil
self.guildBankUpdateCalls = 0
if GuildBankFrame then
GuildBankFrame.IsPersonalBank = nil
GuildBankFrame.IsRealmBank = nil
end
end
function BagnonDB:UNIT_INVENTORY_CHANGED(event, unit)
if unit == 'player' then
self:SaveEquipment()
end
end
--[[
Access Functions
Bagnon requires all of these functions to be present when attempting to view cached data
--]]
--[[
BagnonDB:GetPlayerList()
returns:
iterator of all players on this realm with data
usage:
for playerName, data in BagnonDB:GetPlayers()
--]]
function BagnonDB:GetPlayerList()
if(not playerList) then
playerList = {}
for player in self:GetPlayers() do
table.insert(playerList, player)
end
--sort by currentPlayer first, then alphabetically
table.sort(playerList, function(a, b)
if(a == currentPlayer) then
return true
elseif(b == currentPlayer) then
return false
end
return a < b
end)
end
return playerList
end
function BagnonDB:GetPlayers()
return pairs(self.rdb)
end
--[[
BagnonDB:GetMoney(player)
args:
player (string)
the name of the player we're looking at. This is specific to the current realm we're on
returns:
(number) How much money, in copper, the given player has
--]]
function BagnonDB:GetMoney(player)
local playerData = self.rdb[player]
if playerData then
return playerData.g or 0
end
return 0
end
--[[
BagnonDB:GetNumBankSlots(player)
args:
player (string)
the name of the player we're looking at. This is specific to the current realm we're on
returns:
(number or nil) How many bank slots the current player has purchased
--]]
function BagnonDB:GetNumBankSlots(player)
local playerData = self.rdb[player]
if playerData then
return playerData.numBankSlots
end
end
--[[
BagnonDB:GetBagData(bag, player)
args:
player (string)
the name of the player we're looking at. This is specific to the current realm we're on
bag (number)
the number of the bag we're looking at.
returns:
size (number)
How many items the bag can hold (number)
hyperlink (string)
The hyperlink of the bag
count (number)
How many items are in the bag. This is used by ammo and soul shard bags
--]]
function BagnonDB:GetBagData(bag, player)
local playerDB = self.rdb[player]
if playerDB then
local bagInfo = playerDB[ToBagIndex(bag)]
if bagInfo then
local size, link, count = strsplit(',', bagInfo)
local hyperLink = (link and select(2, GetItemInfo(link))) or nil
return tonumber(size), hyperLink, tonumber(count) or 1, GetItemIcon(link)
end
end
end
--[[
BagnonDB:GetItemData(bag, slot, player)
args:
player (string)
the name of the player we're looking at. This is specific to the current realm we're on
bag (number)
the number of the bag we're looking at.
itemSlot (number)
the specific item slot we're looking at
returns:
hyperLink (string)
The hyperLink of the item
count (number)
How many of there are of the specific item
texture (string)
The filepath of the item's texture
quality (number)
The numeric representaiton of the item's quality: from 0 (poor) to 7 (artifcat)
--]]
function BagnonDB:GetItemData(bag, slot, player)
local playerDB = self.rdb[player]
if playerDB then
local itemInfo = playerDB[ToIndex(bag, slot)]
if itemInfo then
local link, count = strsplit(',', itemInfo)
if link then
local hyperLink, quality = select(2, GetItemInfo(link))
return hyperLink, tonumber(count) or 1, GetItemIcon(link), tonumber(quality)
end
end
end
end
--[[
Returns how many of the specific item id the given player has in the given bag
--]]
function BagnonDB:GetItemCount(itemLink, bag, player)
local total = 0
local itemLink = select(2, GetItemInfo(ToShortLink(itemLink)))
local size = (self:GetBagData(bag, player)) or 0
if (bag == "e") then
size = NUM_EQUIPMENT_SLOTS
end
for slot = 1, size do
local link, count = self:GetItemData(bag, slot, player)
if link == itemLink then
total = total + (count or 1)
end
end
return total
end
--[[
Storage Functions
How we store the data (duh)
--]]
--[[ Storage Functions ]]--
function BagnonDB:SaveMoney()
self.pdb.g = GetMoney()
end
function BagnonDB:SaveNumBankSlots()
self.pdb.numBankSlots = GetNumBankSlots()
end
--saves all the player's equipment data information
function BagnonDB:SaveEquipment()
for slot = 0, NUM_EQUIPMENT_SLOTS do
local link = GetInventoryItemLink('player', slot)
local index = ToIndex('e', slot)
if link then
local link = ToShortLink(link)
local count = GetInventoryItemCount('player', slot)
count = count > 1 and count or nil
if(link and count) then
self.pdb[index] = format('%s,%d', link, count)
else
self.pdb[index] = link
end
else
self.pdb[index] = nil
end
end
end
--saves data about a specific item the current player has
function BagnonDB:SaveItem(bag, slot)
if (bag > ASC_REALM_BANK_OFFSET) then
local texture, count = GetGuildBankItemInfo(bag - ASC_REALM_BANK_OFFSET, slot)
local index = ToIndex(bag, slot)
if texture then
local link = ToShortLink(GetGuildBankItemLink(bag - ASC_REALM_BANK_OFFSET, slot))
count = count > 1 and count or nil
if(link and count) then
self.realmdb[index] = format('%s,%d', link, count)
else
self.realmdb[index] = link
end
else
self.realmdb[index] = nil
end
elseif (bag > ASC_PERSONAL_BANK_OFFSET) then
local texture, count = GetGuildBankItemInfo(bag - ASC_PERSONAL_BANK_OFFSET, slot)
local index = ToIndex(bag, slot)
if texture then
local link = ToShortLink(GetGuildBankItemLink(bag - ASC_PERSONAL_BANK_OFFSET, slot))
count = count > 1 and count or nil
if(link and count) then
self.pdb[index] = format('%s,%d', link, count)
else
self.pdb[index] = link
end
else
self.pdb[index] = nil
end
else
local texture, count = GetContainerItemInfo(bag, slot)
local index = ToIndex(bag, slot)
if texture then
local link = ToShortLink(GetContainerItemLink(bag, slot))
count = count > 1 and count or nil
if(link and count) then
self.pdb[index] = format('%s,%d', link, count)
else
self.pdb[index] = link
end
else
self.pdb[index] = nil
end
end
end
--saves all information about the given bag, EXCEPT the bag's contents
function BagnonDB:SaveBag(bag)
local data = self.pdb
if (bag >= ASC_REALM_BANK_OFFSET) then
local size = GetBagSize(bag)
local index = ToBagIndex(bag)
self.realmdb[index] = size
elseif (bag >= ASC_PERSONAL_BANK_OFFSET) then
local size = GetBagSize(bag)
local index = ToBagIndex(bag)
self.pdb[index] = size
else
local size = GetBagSize(bag)
local index = ToBagIndex(bag)
if size > 0 then
local equipSlot = bag > 0 and ContainerIDToInventoryID(bag)
local link = ToShortLink(GetInventoryItemLink('player', equipSlot))
local count = GetInventoryItemCount('player', equipSlot)
if count < 1 then
count = nil
end
if(size and link and count) then
self.pdb[index] = format('%d,%s,%d', size, link, count)
elseif(size and link) then
self.pdb[index] = format('%d,%s', size, link)
else
self.pdb[index] = size
end
else
self.pdb[index] = nil
end
end
end
--saves both relevant information about the given bag, and all information about items in the given bag
function BagnonDB:UpdateBag(bag)
self:SaveBag(bag)
for slot = 1, GetBagSize(bag) do
self:SaveItem(bag, slot)
end
end
function BagnonDB:OnBagUpdate(bag)
if self.atBank then
for i = 1, (NUM_BAG_SLOTS + GetNumBankSlots()) do
self:SaveBag(i)
end
else
for i = 1, NUM_BAG_SLOTS do
self:SaveBag(i)
end
end
for slot = 1, GetBagSize(bag) do
self:SaveItem(bag, slot)
end
end
--[[ Removal Functions ]]--
--removes all saved data about the given player
function BagnonDB:RemovePlayer(player, realm)
local realm = realm or currentRealm
local rdb = self.db[realm]
if rdb then
rdb[player] = nil
end
if realm == currentRealm and playerList then
for i,character in pairs(playerList) do
if(character == player) then
table.remove(playerList, i)
break
end
end
end
end