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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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();
|
||||
--
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
## Notes-zhTW: 儲存角色背包和銀行的物品資訊
|
||||
## Notes-zhCN: 保存角色背包与银行内物品的信息
|
||||
## Author: Tuller
|
||||
## Version: 1.1.2
|
||||
## Version: 1.1.3
|
||||
## SavedVariables: BagnonForeverDB
|
||||
## LoadOnDemand: 0
|
||||
## RequiredDeps: Bagnon
|
||||
|
||||
+42
-27
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user