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 0aac38f254
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
## SavedVariables: BagnonGlobalSettings
## SavedVariablesPerCharacter: BagnonFrameSettings
## Version: 2.13.4
## Version: 2.14.0
## OptionalDeps: Ace3, Bagnon_Armory, LibItemSearch
embeds.xml
localization.xml
+49 -51
View File
@@ -23,16 +23,14 @@ local function GetIDFromLink(link)
end
local function GetAscensionBankType()
-- The logic doesn't work because GuildBankFrame is not loaded
-- However the sort still works because it can sort using 'guild' as a fallback
-- but personal and realm branches are never reached
-- Bagnon_Forever mirrors IsPersonalBank/IsRealmBank from BANK_PERMISSIONS_PAYLOAD
-- onto GuildBankFrame on GUILDBANKFRAME_OPENED.
if GuildBankFrame and GuildBankFrame.IsPersonalBank then
return "personal"
elseif GuildBankFrame and GuildBankFrame.IsRealmBank then
return "realm"
else
return "guild"
end
return "guild"
end
local function DoGuildBankMoves()
@@ -86,55 +84,56 @@ local function DoGuildBankMoves()
isGuildBankSort = false;
end
-- Minimum time between starting consecutive container swaps. Blasting them
-- back-to-back outpaces the realm server (which rate-limits inbound packets)
-- and the predicted client state desyncs after ~10 moves. 50ms = ~20 swaps/sec
-- sustained; raise this if you see desyncs, lower it if your ping is very low.
local CONTAINER_SWAP_DELAY = 0.05
local nextSwapAt = 0
local function DoContainerMoves()
while (current ~= nil or #moves > 0) do
if current ~= nil then
-- 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
local _, id = GetCursorInfo();
if (current ~= nil and current.id == id) then
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;
-- Something is still settling (or external interference); wait a tick.
return
end
if #moves == 0 then
frame:Hide()
return
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
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
frame:Hide();
table.remove(moves, 1)
nextSwapAt = GetTime() + CONTAINER_SWAP_DELAY
end
local function DoMoves()
@@ -288,12 +287,11 @@ local function CreateGuildBankTabItems(tabID)
return items;
end
frame:SetScript("OnUpdate", function()
t = t + arg1;
if t > 0.03 then
t = 0
DoMoves();
end
-- DoContainerMoves now drains optimistically (atomic swap, no server-confirm wait),
-- so we just run on every frame; the inner loop self-throttles when the cursor
-- is unexpectedly dirty.
frame:SetScript("OnUpdate", function(self, elapsed)
DoMoves()
end)
frame:Hide();
--
+12
View File
@@ -46,6 +46,12 @@ function TitleFrame:PLAYER_UPDATE(msg, frameID, player)
end
end
function TitleFrame:GUILDBANK_TYPE_DETECTED()
if self:GetFrameID() == 'guildbank' then
self:UpdateText()
end
end
--[[ Frame Events ]]--
@@ -113,6 +119,7 @@ function TitleFrame:UpdateEvents()
if self:IsVisible() then
self:RegisterMessage('PLAYER_UPDATE')
self:RegisterMessage('GUILDBANK_TYPE_DETECTED')
end
end
@@ -149,6 +156,11 @@ function TitleFrame:GetTitleText()
end
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]]
end
@@ -18,7 +18,7 @@
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)
if not ItemSearch then return end
@@ -131,7 +131,17 @@ function ItemSearch:FindTypedSearch(itemLink, search)
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
@@ -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 ]]--
local function IsWardrobeLoaded()
+1 -1
View File
@@ -4,7 +4,7 @@
## Notes-zhTW: 儲存角色背包和銀行的物品資訊
## Notes-zhCN: 保存角色背包与银行内物品的信息
## Author: Tuller
## Version: 1.1.2
## Version: 1.1.3
## SavedVariables: BagnonForeverDB
## LoadOnDemand: 0
## RequiredDeps: Bagnon
+41 -26
View File
@@ -194,7 +194,9 @@ end
function BagnonDB:GUILDBANKFRAME_OPENED()
-- 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)
if json then
local jsonObject = C_Serialize:FromJSON(json)
@@ -205,10 +207,29 @@ function BagnonDB:GUILDBANKFRAME_OPENED()
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
-- 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
local avail = GetGuildBankTabInfo(i)
if type(avail) == "string" and i ~= currentTab then
@@ -216,61 +237,55 @@ function BagnonDB:GUILDBANKFRAME_OPENED()
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()
-- Special operation: After 2 initial calls, the QueryGuildBankTab calls above
-- trigger the GUILDBANKBAGSLOTS_CHANGED event at which the queried items are 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.
-- 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
-- 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 self.IsPersonalBank then
self:UpdateBag(i + ASC_PERSONAL_BANK_OFFSET)
elseif self.IsRealmBank then
self:UpdateBag(i + ASC_REALM_BANK_OFFSET)
else
print("[BagnonForever] Error: Unknown bank type")
end
end
end
return
end
-- Normal operation: Update current tab
-- 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
local avail = GetGuildBankTabInfo(currentTab)
if type(avail) == "string" then
self:UpdateBag(currentTab + ASC_PERSONAL_BANK_OFFSET)
end
return
end
-- Update all tabs for realm bank on any change
if self.IsRealmBank then
local avail = GetGuildBankTabInfo(currentTab)
if type(avail) == "string" then
elseif self.IsRealmBank then
self:UpdateBag(currentTab + ASC_REALM_BANK_OFFSET)
end
return
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)