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
This commit is contained in:
2026-05-13 08:02:49 +02:00
parent da85c0b9fb
commit cc00318d2c
6 changed files with 182 additions and 90 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
## Notes: Single window displays for your inventory, bank, and keys ## Notes: Single window displays for your inventory, bank, and keys
## SavedVariables: BagnonGlobalSettings ## SavedVariables: BagnonGlobalSettings
## SavedVariablesPerCharacter: BagnonFrameSettings ## SavedVariablesPerCharacter: BagnonFrameSettings
## Version: 2.13.4 ## Version: 2.14.0
## OptionalDeps: Ace3, Bagnon_Armory, LibItemSearch ## OptionalDeps: Ace3, Bagnon_Armory, LibItemSearch
embeds.xml embeds.xml
localization.xml localization.xml
+56 -58
View File
@@ -23,16 +23,14 @@ local function GetIDFromLink(link)
end end
local function GetAscensionBankType() local function GetAscensionBankType()
-- The logic doesn't work because GuildBankFrame is not loaded -- Bagnon_Forever mirrors IsPersonalBank/IsRealmBank from BANK_PERMISSIONS_PAYLOAD
-- However the sort still works because it can sort using 'guild' as a fallback -- onto GuildBankFrame on GUILDBANKFRAME_OPENED.
-- but personal and realm branches are never reached
if GuildBankFrame and GuildBankFrame.IsPersonalBank then if GuildBankFrame and GuildBankFrame.IsPersonalBank then
return "personal" return "personal"
elseif GuildBankFrame and GuildBankFrame.IsRealmBank then elseif GuildBankFrame and GuildBankFrame.IsRealmBank then
return "realm" return "realm"
else
return "guild"
end end
return "guild"
end end
local function DoGuildBankMoves() local function DoGuildBankMoves()
@@ -86,55 +84,56 @@ local function DoGuildBankMoves()
isGuildBankSort = false; isGuildBankSort = false;
end end
local function DoContainerMoves() -- Minimum time between starting consecutive container swaps. Blasting them
while (current ~= nil or #moves > 0) do -- back-to-back outpaces the realm server (which rate-limits inbound packets)
if current ~= nil then -- and the predicted client state desyncs after ~10 moves. 50ms = ~20 swaps/sec
if CursorHasItem() then -- sustained; raise this if you see desyncs, lower it if your ping is very low.
local _, id = GetCursorInfo(); local CONTAINER_SWAP_DELAY = 0.05
if (current ~= nil and current.id == id) then local nextSwapAt = 0
if (current.sourcebag ~= nil) then
PickupContainerItem(current.targetbag, current.targetslot);
local link = select(7, GetContainerItemInfo(current.targetbag, current.targetslot));
if (current.id ~= GetIDFromLink(link)) then
return;
end
end
else
moves = {};
current = nil;
frame:Hide();
return;
end
else
if (current.sourcebag ~= nil) then
local link = select(7, GetContainerItemInfo(current.targetbag, current.targetslot));
if (current.id ~= GetIDFromLink(link)) then
return;
end
end
current = nil;
end
else
if (#moves > 0) then
current = table.remove(moves, 1);
if (current.sourcebag ~= nil) then
PickupContainerItem(current.sourcebag, current.sourceslot);
if CursorHasItem() == false then
return;
end
PickupContainerItem(current.targetbag, current.targetslot);
local link = select(7, GetContainerItemInfo(current.targetbag, current.targetslot));
if (current.id == GetIDFromLink(link)) then
current = nil;
else
return;
end
end
end local function DoContainerMoves()
end -- Cursor-predicted swap semantics (client-side, immediate):
-- Pickup(src) -> cursor holds src's item, src empty
-- Pickup(tgt) with item -> swap; cursor now holds tgt's old item
-- Pickup(tgt) empty target -> cursor empty, tgt has src's item
-- Pickup(src) again -> drops cursor item into the empty src slot
-- That third pickup completes a non-stackable swap so the cursor ends
-- the move empty and we're ready for the next one.
if CursorHasItem() then
-- Something is still settling (or external interference); wait a tick.
return
end end
frame:Hide(); if #moves == 0 then
frame:Hide()
return
end
if GetTime() < nextSwapAt then
return
end
local move = moves[1]
if move.sourcebag == nil then
table.remove(moves, 1)
return
end
PickupContainerItem(move.sourcebag, move.sourceslot)
if not CursorHasItem() then
-- Source slot empty/locked; drop the move and try the next.
table.remove(moves, 1)
return
end
PickupContainerItem(move.targetbag, move.targetslot)
if CursorHasItem() then
-- Cursor holds either tgt's displaced item (normal swap) or src's
-- item (tgt rejected the swap). Either way, dropping it into the
-- now-empty source slot is the right finishing move.
PickupContainerItem(move.sourcebag, move.sourceslot)
end
table.remove(moves, 1)
nextSwapAt = GetTime() + CONTAINER_SWAP_DELAY
end end
local function DoMoves() local function DoMoves()
@@ -288,12 +287,11 @@ local function CreateGuildBankTabItems(tabID)
return items; return items;
end end
frame:SetScript("OnUpdate", function() -- DoContainerMoves now drains optimistically (atomic swap, no server-confirm wait),
t = t + arg1; -- so we just run on every frame; the inner loop self-throttles when the cursor
if t > 0.03 then -- is unexpectedly dirty.
t = 0 frame:SetScript("OnUpdate", function(self, elapsed)
DoMoves(); DoMoves()
end
end) end)
frame:Hide(); frame:Hide();
-- --
+13 -1
View File
@@ -46,6 +46,12 @@ function TitleFrame:PLAYER_UPDATE(msg, frameID, player)
end end
end end
function TitleFrame:GUILDBANK_TYPE_DETECTED()
if self:GetFrameID() == 'guildbank' then
self:UpdateText()
end
end
--[[ Frame Events ]]-- --[[ Frame Events ]]--
@@ -113,6 +119,7 @@ function TitleFrame:UpdateEvents()
if self:IsVisible() then if self:IsVisible() then
self:RegisterMessage('PLAYER_UPDATE') self:RegisterMessage('PLAYER_UPDATE')
self:RegisterMessage('GUILDBANK_TYPE_DETECTED')
end end
end end
@@ -147,8 +154,13 @@ function TitleFrame:GetTitleText()
if self:GetFrameID() == 'keys' then if self:GetFrameID() == 'keys' then
return L.TitleKeys return L.TitleKeys
end end
if self:GetFrameID() == 'guildbank' then if self:GetFrameID() == 'guildbank' then
if GuildBankFrame and GuildBankFrame.IsPersonalBank then
return [[%s's Personal Bank]]
elseif GuildBankFrame and GuildBankFrame.IsRealmBank then
return [[Realm Bank]]
end
return [[%s's Guild Bank]] return [[%s's Guild Bank]]
end end
@@ -18,7 +18,7 @@
I kindof half want to make a full parser for this I kindof half want to make a full parser for this
--]] --]]
local MAJOR, MINOR = "LibItemSearch-1.0", 2 local MAJOR, MINOR = "LibItemSearch-1.0", 3
local ItemSearch = LibStub:NewLibrary(MAJOR, MINOR) local ItemSearch = LibStub:NewLibrary(MAJOR, MINOR)
if not ItemSearch then return end if not ItemSearch then return end
@@ -131,7 +131,17 @@ function ItemSearch:FindTypedSearch(itemLink, search)
end end
end end
return self:GetTypedSearch('itemTypeGeneric'):findItem(itemLink, search) or self:GetTypedSearch('itemName'):findItem(itemLink, search) if self:GetTypedSearch('itemTypeGeneric'):findItem(itemLink, search)
or self:GetTypedSearch('itemName'):findItem(itemLink, search) then
return true
end
-- Fall back to a tooltip-text scan (Agility, Strength, "Increases ...", etc).
-- Gated to 3+ chars so per-keystroke searches don't scan tooltips repeatedly.
if #search >= 3 then
return self:GetTypedSearch('tooltipText'):findItem(itemLink, search)
end
return false
end end
@@ -318,6 +328,63 @@ ItemSearch:RegisterTypedSearch{
} }
--[[ full tooltip text search: substring scan across every tooltip line ]]--
local tooltipTextCache = setmetatable({}, {__index = function(t, k) local v = {} t[k] = v return v end})
local function link_FindTextInTooltip(itemLink, search)
if not (itemLink and search) then
return false
end
local itemID = itemLink:match('item:(%d+)')
if not itemID then
return false
end
local cached = tooltipTextCache[search][itemID]
if cached ~= nil then
return cached
end
tooltipScanner:SetOwner(UIParent, 'ANCHOR_NONE')
tooltipScanner:SetHyperlink(itemLink)
local result = false
local scannerName = tooltipScanner:GetName()
for i = 2, tooltipScanner:NumLines() do
local left = _G[scannerName .. 'TextLeft' .. i]
local text = left and left:GetText()
if text and text:lower():find(search, 1, true) then
result = true
break
end
local right = _G[scannerName .. 'TextRight' .. i]
local rtext = right and right:GetText()
if rtext and rtext:lower():find(search, 1, true) then
result = true
break
end
end
tooltipScanner:Hide()
tooltipTextCache[search][itemID] = result
return result
end
ItemSearch:RegisterTypedSearch{
id = 'tooltipText',
isSearch = function(self, search)
return search and search:match('^tt:(.+)$')
end,
findItem = function(self, itemLink, search)
return link_FindTextInTooltip(itemLink, search)
end,
}
--[[ equipment set search ]]-- --[[ equipment set search ]]--
local function IsWardrobeLoaded() local function IsWardrobeLoaded()
+1 -1
View File
@@ -4,7 +4,7 @@
## Notes-zhTW: 儲存角色背包和銀行的物品資訊 ## Notes-zhTW: 儲存角色背包和銀行的物品資訊
## Notes-zhCN: 保存角色背包与银行内物品的信息 ## Notes-zhCN: 保存角色背包与银行内物品的信息
## Author: Tuller ## Author: Tuller
## Version: 1.1.2 ## Version: 1.1.3
## SavedVariables: BagnonForeverDB ## SavedVariables: BagnonForeverDB
## LoadOnDemand: 0 ## LoadOnDemand: 0
## RequiredDeps: Bagnon ## RequiredDeps: Bagnon
+42 -27
View File
@@ -194,7 +194,9 @@ end
function BagnonDB:GUILDBANKFRAME_OPENED() function BagnonDB:GUILDBANKFRAME_OPENED()
-- Identify bank type from permissions payload -- Identify bank type from permissions payload
if HasJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0) then self.IsPersonalBank = nil
self.IsRealmBank = nil
if HasJsonCacheData and HasJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0) then
local json = GetJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0) local json = GetJsonCacheData("BANK_PERMISSIONS_PAYLOAD", 0)
if json then if json then
local jsonObject = C_Serialize:FromJSON(json) local jsonObject = C_Serialize:FromJSON(json)
@@ -205,10 +207,29 @@ function BagnonDB:GUILDBANKFRAME_OPENED()
end 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.guildBankUpdateCalls = 0
self.availableTabs = {} -- table of available tabs self.availableTabs = {} -- table of available tabs
-- Query all tabs for personal and realm bank to preload data -- 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 for i = 1, 6 do
local avail = GetGuildBankTabInfo(i) local avail = GetGuildBankTabInfo(i)
if type(avail) == "string" and i ~= currentTab then if type(avail) == "string" and i ~= currentTab then
@@ -216,54 +237,44 @@ function BagnonDB:GUILDBANKFRAME_OPENED()
self.availableTabs[i] = avail self.availableTabs[i] = avail
end end
end end
end end
function BagnonDB:GUILDBANKBAGSLOTS_CHANGED() function BagnonDB:GUILDBANKBAGSLOTS_CHANGED()
self.guildBankUpdateCalls = self.guildBankUpdateCalls + 1 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() local currentTab = GetCurrentGuildBankTab()
-- Special operation: After 2 initial calls, the QueryGuildBankTab calls above -- Initial pre-query window: after the first 2 calls, each queued QueryGuildBankTab
-- trigger the GUILDBANKBAGSLOTS_CHANGED event at which the queried items are available. -- triggers one GUILDBANKBAGSLOTS_CHANGED that makes that tab's items available.
-- Only update the bank if we are within 1-6 range of the initial calls as those
-- are likely triggered by the QueryGuildBankTab calls above. Which makes items
-- in the tabs available for GetGuildBankItemInfo calls.
if ((self.guildBankUpdateCalls > GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET) if ((self.guildBankUpdateCalls > GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET)
and (self.guildBankUpdateCalls <= GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET + #self.availableTabs)) then and (self.guildBankUpdateCalls <= GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET + #self.availableTabs)) then
for i, avail in pairs(self.availableTabs) do for i, avail in pairs(self.availableTabs) do
-- Ignore current tab, and only update the tab that is next in the sequence
if (i ~= currentTab and self.guildBankUpdateCalls == GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET + i) then if (i ~= currentTab and self.guildBankUpdateCalls == GUILDBANKBAGSLOTS_CHANGED_INIT_OFFSET + i) then
if self.IsPersonalBank then if self.IsPersonalBank then
self:UpdateBag(i + ASC_PERSONAL_BANK_OFFSET) self:UpdateBag(i + ASC_PERSONAL_BANK_OFFSET)
elseif self.IsRealmBank then elseif self.IsRealmBank then
self:UpdateBag(i + ASC_REALM_BANK_OFFSET) self:UpdateBag(i + ASC_REALM_BANK_OFFSET)
else
print("[BagnonForever] Error: Unknown bank type")
end end
end end
end end
return return
end end
-- Normal operation: Update current tab -- Normal operation: update current tab into per-character DB (personal) or realm DB
if self.IsPersonalBank then local avail = GetGuildBankTabInfo(currentTab)
local avail = GetGuildBankTabInfo(currentTab) if type(avail) ~= "string" then
if type(avail) == "string" then
self:UpdateBag(currentTab + ASC_PERSONAL_BANK_OFFSET)
end
return return
end end
if self.IsPersonalBank then
-- Update all tabs for realm bank on any change self:UpdateBag(currentTab + ASC_PERSONAL_BANK_OFFSET)
if self.IsRealmBank then elseif self.IsRealmBank then
local avail = GetGuildBankTabInfo(currentTab) self:UpdateBag(currentTab + ASC_REALM_BANK_OFFSET)
if type(avail) == "string" then
self:UpdateBag(currentTab + ASC_REALM_BANK_OFFSET)
end
return
end end
end end
@@ -271,6 +282,10 @@ function BagnonDB:GUILDBANKFRAME_CLOSED()
self.IsPersonalBank = nil self.IsPersonalBank = nil
self.IsRealmBank = nil self.IsRealmBank = nil
self.guildBankUpdateCalls = 0 self.guildBankUpdateCalls = 0
if GuildBankFrame then
GuildBankFrame.IsPersonalBank = nil
GuildBankFrame.IsRealmBank = nil
end
end end
function BagnonDB:UNIT_INVENTORY_CHANGED(event, unit) function BagnonDB:UNIT_INVENTORY_CHANGED(event, unit)