--[[ sortBtn.lua imagine a button that sorts your inventory in Bagnon, crazy am I right?!1 --]] local Bagnon = LibStub('AceAddon-3.0'):GetAddon('Bagnon') local L = LibStub('AceLocale-3.0'):GetLocale('Bagnon') local SortBtn = Bagnon.Classy:New('Button') Bagnon.SortBtn = SortBtn local SIZE = 20 local NORMAL_TEXTURE_SIZE = 64 * (SIZE / 36) -- Bag Sorter code from Sushi Regular local moves = {}; local frame = CreateFrame("Frame"); local current = nil; local isGuildBankSort = false; local function GetIDFromLink(link) return link and tonumber(string.match(link, "item:(%d+)")); end local function GetAscensionBankType() -- 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" end return "guild" end local function DoGuildBankMoves() 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.sourcetab ~= nil) then PickupGuildBankItem(current.targettab, current.targetslot); local link = GetGuildBankItemLink(current.targettab, current.targetslot); if (current.id ~= GetIDFromLink(link)) then return; end end else moves = {}; current = nil; frame:Hide(); return; end else if (current.sourcetab ~= nil) then local link = GetGuildBankItemLink(current.targettab, 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.sourcetab ~= nil) then PickupGuildBankItem(current.sourcetab, current.sourceslot); if CursorHasItem() == false then return; end PickupGuildBankItem(current.targettab, current.targetslot); local link = GetGuildBankItemLink(current.targettab, current.targetslot); if (current.id == GetIDFromLink(link)) then current = nil; else return; end end end end end frame:Hide(); 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. 150ms = ~6 swaps/sec -- sustained; raise if you see desyncs, lower if your ping is very low. local CONTAINER_SWAP_DELAY = 0.15 -- A pickup that doesn't land on the cursor is almost always a lag race where -- the previous swap's state hasn't propagated yet. Retry instead of dropping -- the move (which used to leave items un-sorted on laggy servers). local MAX_PICKUP_RETRIES = 8 local nextSwapAt = 0 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 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 -- Pickup didn't take. On a laggy server this usually means the -- previous swap's client state hasn't caught up; retry a few times -- before giving up so we don't silently drop the move. move.retries = (move.retries or 0) + 1 if move.retries < MAX_PICKUP_RETRIES then nextSwapAt = GetTime() + CONTAINER_SWAP_DELAY return end table.remove(moves, 1) return end move.retries = nil 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() if isGuildBankSort then DoGuildBankMoves() else DoContainerMoves() end end local function CompareItems(lItem, rItem) if (rItem.id == nil) then return true; elseif (lItem.id == nil) then return false; elseif (lItem.class ~= rItem.class) then if lItem.class == "Weapon" or rItem.class == "Weapon" then return lItem.class == "Weapon" end return (lItem.class < rItem.class); elseif (lItem.subclass ~= rItem.subclass) then return (lItem.subclass < rItem.subclass); elseif (lItem.quality ~= rItem.quality) then return (lItem.quality > rItem.quality); elseif (lItem.name ~= rItem.name) then return (lItem.name < rItem.name); elseif ((lItem.count) ~= (rItem.count)) then return ((lItem.count) >= (rItem.count)); else return true; end end local function BeginSort() current = nil; moves = {}; ClearCursor(); end local function SortGuildBankTab(tabItems) for i = 1, #tabItems, 1 do local lowest = i; for j = #tabItems, i + 1, -1 do if (CompareItems(tabItems[lowest], tabItems[j]) == false) then lowest = j; end end if (i ~= lowest) then local move = {}; move.id = tabItems[lowest].id; move.name = tabItems[lowest].name; move.sourcetab = tabItems[lowest].tab; move.sourceslot = tabItems[lowest].slot; move.targettab = tabItems[i].tab; move.targetslot = tabItems[i].slot; table.insert(moves, move); local tmp = tabItems[i]; tabItems[i] = tabItems[lowest]; tabItems[lowest] = tmp; tmp = tabItems[i].slot; tabItems[i].slot = tabItems[lowest].slot; tabItems[lowest].slot = tmp; tmp = tabItems[i].tab; tabItems[i].tab = tabItems[lowest].tab; tabItems[lowest].tab = tmp; end end end local function SortBag(bag) for i = 1, #bag, 1 do local lowest = i; for j = #bag, i + 1, -1 do if (CompareItems(bag[lowest], bag[j]) == false) then lowest = j; end end if (i ~= lowest) then -- store move local move = {}; move.id = bag[lowest].id; move.name = bag[lowest].name; move.sourcebag = bag[lowest].bag; move.sourcetab = bag[lowest].tab; move.sourceslot = bag[lowest].slot; move.targetbag = bag[i].bag; move.targettab = bag[i].tab; move.targetslot = bag[i].slot; table.insert(moves, move); -- swap items local tmp = bag[i]; bag[i] = bag[lowest]; bag[lowest] = tmp; -- swap slots tmp = bag[i].slot; bag[i].slot = bag[lowest].slot; bag[lowest].slot = tmp; tmp = bag[i].bag; bag[i].bag = bag[lowest].bag; bag[lowest].bag = tmp; tmp = bag[i].tab; bag[i].tab = bag[lowest].tab; bag[lowest].tab = tmp; end end end -- Returns true iff every slot in `bagID` whose GetContainerItemLink is nil -- is also reported empty by GetContainerNumFreeSlots. When the server is -- laggy GetContainerItemInfo/Link can transiently return nil for slots that -- actually contain items; sorting on that snapshot generates moves that -- treat occupied slots as empty destinations, leaving holes in the result. local function IsBagReadConsistent(bagID) local slots = GetContainerNumSlots(bagID) or 0 local reportedFree = select(1, GetContainerNumFreeSlots(bagID)) or 0 local actualEmpty = 0 for s = 1, slots do if not GetContainerItemLink(bagID, s) then actualEmpty = actualEmpty + 1 end end return actualEmpty == reportedFree, actualEmpty, reportedFree end local function CreateBagFromID(bagID) local items = GetContainerNumSlots(bagID); local bag = {}; for i = 1, items, 1 do local item = {}; local _, count, _, _, _, _, link = GetContainerItemInfo(bagID, i); item.bag = bagID; item.slot = i; item.name = ""; item.id = GetIDFromLink(link); if (item.id ~= nil) then item.count = count; item.name, _, item.quality, _, _, item.class, item.subclass, _, item.type, _, item.price = GetItemInfo(item.id); end table.insert(bag, item); end return bag; end local function CreateGuildBankTabItems(tabID) local items = {}; local numSlots = 98; for i = 1, numSlots, 1 do local item = {}; local texture, count, locked = GetGuildBankItemInfo(tabID, i); local link = GetGuildBankItemLink(tabID, i); item.tab = tabID; item.slot = i; item.name = ""; item.id = GetIDFromLink(link); if (item.id ~= nil) then item.count = count or 1; item.name, _, item.quality, _, _, item.class, item.subclass, _, item.type, _, item.price = GetItemInfo(item.id); end table.insert(items, item); end return items; 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(); -- --[[ Constructor ]] -- function SortBtn:New(frameID, parent) local b = self:Bind(CreateFrame('Button', nil, parent)) b:SetWidth(SIZE) b:SetHeight(SIZE) b:RegisterForClicks('anyUp') local nt = b:CreateTexture() nt:SetTexture([[Interface\Buttons\UI-Quickslot2]]) nt:SetWidth(NORMAL_TEXTURE_SIZE) nt:SetHeight(NORMAL_TEXTURE_SIZE) nt:SetPoint('CENTER', 0, -1) b:SetNormalTexture(nt) local pt = b:CreateTexture() pt:SetTexture([[Interface\Buttons\UI-Quickslot-Depress]]) pt:SetAllPoints(b) b:SetPushedTexture(pt) local ht = b:CreateTexture() ht:SetTexture([[Interface\Buttons\ButtonHilight-Square]]) ht:SetAllPoints(b) b:SetHighlightTexture(ht) local icon = b:CreateTexture() icon:SetAllPoints(b) icon:SetTexture([[Interface\Icons\ability_racial_bagoftricks]]) b:SetScript('OnClick', b.OnClick) b:SetScript('OnEnter', b.OnEnter) b:SetScript('OnLeave', b.OnLeave) b:SetFrameID(frameID) return b end --[[ Frame Events ]] -- function SortBtn:OnClick() local bags = {}; if self.frameID == "inventory" then -- Refuse to sort if any bag's read is inconsistent with the server's -- reported free-slot count — sorting on that snapshot is what causes -- the post-sort holes on a laggy server. for i = 0, NUM_BAG_FRAMES, 1 do local ok, empty, reported = IsBagReadConsistent(i) if not ok then DEFAULT_CHAT_FRAME:AddMessage(format( "|cFFFFAA00Bagnon: bag %d not fully loaded (read %d empty, server says %d) — try sort again in a moment.|r", i, empty, reported)) return end end isGuildBankSort = false; for i = 0, NUM_BAG_FRAMES, 1 do local bag = CreateBagFromID(i); local type = select(2, GetContainerNumFreeSlots(i)); if type == nil then type = "ALL" else type = tostring(type); end if bags[type] == nil then bags[type] = bag; else for j = 1, #bag, 1 do table.insert(bags[type], bag[j]); end end end elseif self.frameID == "bank" then isGuildBankSort = false; local i = -1 local bag = CreateBagFromID(i); local type = select(2, GetContainerNumFreeSlots(i)); if type == nil then type = "ALL" else type = tostring(type); end if bags[type] == nil then bags[type] = bag; else for j = 1, #bag, 1 do table.insert(bags[type], bag[j]); end end for i = NUM_BAG_FRAMES+1, NUM_BAG_FRAMES + NUM_BANKBAGSLOTS, 1 do local bag = CreateBagFromID(i); local type = select(2, GetContainerNumFreeSlots(i)); if type == nil then type = "ALL" else type = tostring(type); end if bags[type] == nil then bags[type] = bag; else for j = 1, #bag, 1 do table.insert(bags[type], bag[j]); end end end elseif self.frameID == "guildbank" then isGuildBankSort = true; local currentTab = GetCurrentGuildBankTab and GetCurrentGuildBankTab() or 0 if currentTab and currentTab > 0 then local bankType = GetAscensionBankType() local canSort = false if bankType == "personal" or bankType == "realm" then canSort = true else local _, _, canView, canDeposit, _, remainingWithdrawals = GetGuildBankTabInfo(currentTab) if canDeposit and (remainingWithdrawals == -1 or remainingWithdrawals > 0) then canSort = true end end if canSort then local tabItems = CreateGuildBankTabItems(currentTab) bags["GUILDBANK"] = tabItems else return end else return end end local bagCount = 0 for k, v in pairs(bags) do if v ~= nil then bagCount = bagCount + 1 end end if bagCount == 0 then return end BeginSort(); for k, v in pairs(bags) do if v ~= nil then if isGuildBankSort then SortGuildBankTab(v); else SortBag(v); end end end frame:Show(); end function SortBtn:OnEnter() if self:GetRight() > (GetScreenWidth() / 2) then GameTooltip:SetOwner(self, 'ANCHOR_LEFT') else GameTooltip:SetOwner(self, 'ANCHOR_RIGHT') end self:UpdateTooltip() end function SortBtn:OnLeave() if GameTooltip:IsOwned(self) then GameTooltip:Hide() end end --[[ Update Methods ]] -- function SortBtn:UpdateTooltip() if GameTooltip:IsOwned(self) then GameTooltip:SetText(L.TipShowSortBtn) end end --[[ Properties ]] -- function SortBtn:SetFrameID(frameID) if self:GetFrameID() ~= frameID then self.frameID = frameID end end function SortBtn:GetFrameID() return self.frameID end