From 0aac38f254314075957c4c205471a1c00a51107f Mon Sep 17 00:00:00 2001 From: Florian Berthold Date: Wed, 13 May 2026 08:02:49 +0200 Subject: [PATCH] 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: 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 --- Bagnon/Bagnon.toc | 2 +- Bagnon/components/sortBtn.lua | 114 +++++++++--------- Bagnon/components/titleFrame.lua | 14 ++- .../LibItemSearch-1.0/LibItemSearch-1.0.lua | 71 ++++++++++- Bagnon_Forever/Bagnon_Forever.toc | 2 +- Bagnon_Forever/db.lua | 69 ++++++----- 6 files changed, 182 insertions(+), 90 deletions(-) diff --git a/Bagnon/Bagnon.toc b/Bagnon/Bagnon.toc index bd3d9b0..b188f35 100644 --- a/Bagnon/Bagnon.toc +++ b/Bagnon/Bagnon.toc @@ -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 diff --git a/Bagnon/components/sortBtn.lua b/Bagnon/components/sortBtn.lua index 91d6eec..00d932b 100644 --- a/Bagnon/components/sortBtn.lua +++ b/Bagnon/components/sortBtn.lua @@ -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 -local function DoContainerMoves() - while (current ~= nil or #moves > 0) do - if current ~= nil then - 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; - 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 +-- 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 - end - end +local function DoContainerMoves() + -- 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 - 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 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(); -- diff --git a/Bagnon/components/titleFrame.lua b/Bagnon/components/titleFrame.lua index 83cc822..082ecb4 100644 --- a/Bagnon/components/titleFrame.lua +++ b/Bagnon/components/titleFrame.lua @@ -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 @@ -147,8 +154,13 @@ function TitleFrame:GetTitleText() if self:GetFrameID() == 'keys' then return L.TitleKeys 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 diff --git a/Bagnon/libs/LibItemSearch-1.0/LibItemSearch-1.0.lua b/Bagnon/libs/LibItemSearch-1.0/LibItemSearch-1.0.lua index e7f52a6..7a2f8a7 100644 --- a/Bagnon/libs/LibItemSearch-1.0/LibItemSearch-1.0.lua +++ b/Bagnon/libs/LibItemSearch-1.0/LibItemSearch-1.0.lua @@ -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() diff --git a/Bagnon_Forever/Bagnon_Forever.toc b/Bagnon_Forever/Bagnon_Forever.toc index def8151..8b4f152 100644 --- a/Bagnon_Forever/Bagnon_Forever.toc +++ b/Bagnon_Forever/Bagnon_Forever.toc @@ -4,7 +4,7 @@ ## Notes-zhTW: 儲存角色背包和銀行的物品資訊 ## Notes-zhCN: 保存角色背包与银行内物品的信息 ## Author: Tuller -## Version: 1.1.2 +## Version: 1.1.3 ## SavedVariables: BagnonForeverDB ## LoadOnDemand: 0 ## RequiredDeps: Bagnon diff --git a/Bagnon_Forever/db.lua b/Bagnon_Forever/db.lua index 78ec430..8a2455a 100644 --- a/Bagnon_Forever/db.lua +++ b/Bagnon_Forever/db.lua @@ -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,54 +237,44 @@ 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 - if self.IsPersonalBank then - local avail = GetGuildBankTabInfo(currentTab) - if type(avail) == "string" then - self:UpdateBag(currentTab + ASC_PERSONAL_BANK_OFFSET) - 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 - - -- Update all tabs for realm bank on any change - if self.IsRealmBank then - local avail = GetGuildBankTabInfo(currentTab) - if type(avail) == "string" then - self:UpdateBag(currentTab + ASC_REALM_BANK_OFFSET) - end - return + if self.IsPersonalBank then + self:UpdateBag(currentTab + ASC_PERSONAL_BANK_OFFSET) + elseif self.IsRealmBank then + self:UpdateBag(currentTab + ASC_REALM_BANK_OFFSET) end end @@ -271,6 +282,10 @@ 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)