From 8ddcf73da6df70b5207f07d7f526e311366ba302 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:16:48 -0500 Subject: [PATCH] extract bagsort logic --- init/libs.xml | 1 + libs/libbagsort.lua | 276 ++++++++++++++++++++++++++++++++++++++++++++ modules/bags.lua | 276 +------------------------------------------- 3 files changed, 282 insertions(+), 271 deletions(-) create mode 100644 libs/libbagsort.lua diff --git a/init/libs.xml b/init/libs.xml index 9778a9c3..47b82787 100644 --- a/init/libs.xml +++ b/init/libs.xml @@ -10,4 +10,5 @@ + diff --git a/libs/libbagsort.lua b/libs/libbagsort.lua new file mode 100644 index 00000000..c3350ebb --- /dev/null +++ b/libs/libbagsort.lua @@ -0,0 +1,276 @@ +-- load pfUI environment +setfenv(1, pfUI:GetEnvironment()) + +-- return instantly when another libbagsort is already active +if pfUI.api.libbagsort then return end + +-- Bag sorter: consolidates partial stacks then sorts by category/name. +-- Adapted from the algo in Bagnon/lib/BagSort.lua. +local libbagsort = CreateFrame("Frame", "pfLibBagSort") +pfUI.api.libbagsort = libbagsort +libbagsort.itemGrid = {} +libbagsort.bagList = nil + +local HEARTHSTONE_ITEM_ID = 6948 + +-- Lower prefix = sorted earlier in the bag. +local function SortCategoryPrefix(itemId, itemType, itemSubType, quality) + if itemId == HEARTHSTONE_ITEM_ID then return "00" end + if quality == 0 then return "13" end -- Poor (gray) always last + if itemType == "Weapon" or itemType == "Armor" then + if quality and quality >= 4 then return "01" end -- Epic+ gear + if quality == 3 then return "02" end -- Rare gear + if quality == 2 then return "03" end -- Uncommon gear + return "04" -- Common/poor gear + end + if itemType == "Consumable" then return "05" end + if itemType == "Reagent" then return "06" end + if itemType == "Trade Goods" then return "07" end + if itemType == "Quest" then return "08" end + -- Non-gear items without a specific type, sorted by quality + if quality and quality >= 4 then return "09" end + if quality == 3 then return "10" end + if quality == 2 then return "11" end + return "12" +end + +-- Larger stacks sort first among identically-named items; invert + zero-pad +-- so it sorts lexicographically. +local function SortCountSuffix(count) + local s = "000000" .. (999999 - (count or 0)) + return string.sub(s, -6) +end + +local function SortKey(itemId, name, itype, subtype, quality, count) + return SortCategoryPrefix(itemId, itype, subtype, quality) + .. (itype or "") .. "|" .. (subtype or "") .. "|" .. (name or "zzz") .. "|" .. SortCountSuffix(count) +end + +local function ClearSortData() + libbagsort.itemGrid = {} + libbagsort.bagList = nil + libbagsort:UnregisterEvent("BAG_UPDATE_DELAYED") + libbagsort:SetScript("OnEvent", nil) +end + +-- Two-pointer consolidation: sorts stacks largest-first, then merges from +-- both ends toward the middle. n is set explicitly so table.getn / table.sort +-- work correctly in Lua 5.0. +local function BuildConsolidateOps(bagList) + local groups = {} + for _, bag in ipairs(bagList) do + for slot = 1, GetContainerNumSlots(bag) do + local itemId = C_Container.GetContainerItemID(bag, slot) + if itemId then + local _, count = GetContainerItemInfo(bag, slot) + count = count or 0 + local maxStack = C_Item.GetItemMaxStackSizeByID(itemId) or 1 + if count > 0 and maxStack > 1 then + if not groups[itemId] then + groups[itemId] = {maxStack=maxStack, n=0} + end + local g = groups[itemId] + g.n = g.n + 1 + g[g.n] = {bag=bag, slot=slot, count=count} + end + end + end + end + + local ops = {} + for _, g in pairs(groups) do + local n = g.n + if n >= 2 then + local maxStack = g.maxStack + table.sort(g, function(a, b) return a.count > b.count end) + local lo, hi = 1, n + while lo < hi do + local space = maxStack - g[lo].count + if space == 0 then + lo = lo + 1 + elseif space >= g[hi].count then + tinsert(ops, { + dstBag = g[lo].bag, dstSlot = g[lo].slot, + srcBag = g[hi].bag, srcSlot = g[hi].slot, + count = g[hi].count, + }) + g[lo].count = g[lo].count + g[hi].count + hi = hi - 1 + else + tinsert(ops, { + dstBag = g[lo].bag, dstSlot = g[lo].slot, + srcBag = g[hi].bag, srcSlot = g[hi].slot, + count = space, + }) + g[hi].count = g[hi].count - space + g[lo].count = maxStack + lo = lo + 1 + end + end + end + end + return ops +end + +local function BuildSortGrid() + local bagList = libbagsort.bagList + libbagsort.itemGrid = {} + local normalItems = {} + local poorItems = {} + local bagCount = 0 + local bagSlots = {} + + for _, bag in ipairs(bagList) do + bagCount = bagCount + 1 + local numSlots = GetContainerNumSlots(bag) + bagSlots[bagCount] = numSlots + if numSlots > 0 then + libbagsort.itemGrid[bag] = {} + for slot = 1, numSlots do + local itemId = C_Container.GetContainerItemID(bag, slot) + if itemId then + -- pfUI's compat layer shims GetItemInfo to the modern 10-field + -- signature (inserts nil for itemLevel between quality and + -- minlevel) — so itype/subtype sit at positions 6/7, not 5/6. + local name, _, quality, _, _, itype, subtype = GetItemInfo(itemId) + local _, count = GetContainerItemInfo(bag, slot) + local item = { + key = SortKey(itemId, name, itype, subtype, quality, count), + srcBag = bag, + srcSlot = slot, + curBag = bag, + curSlot = slot, + } + if quality == 0 then + tinsert(poorItems, item) + else + tinsert(normalItems, item) + end + libbagsort.itemGrid[bag][slot] = item + end + end + end + end + + table.sort(normalItems, function(a, b) return a.key < b.key end) + -- Sort poor items descending so they read in ascending order when placed + -- back-to-front (last poor item lands on the last slot). + table.sort(poorItems, function(a, b) return a.key > b.key end) + + -- Forward pass: assign normal items from slot 1 of bag 1 onward + local bagIdx, destSlot = 1, 1 + while bagIdx <= bagCount and bagSlots[bagIdx] == 0 do + bagIdx = bagIdx + 1 + end + for _, item in ipairs(normalItems) do + while bagIdx <= bagCount do + if destSlot <= bagSlots[bagIdx] then break end + bagIdx = bagIdx + 1 + destSlot = 1 + end + if bagIdx > bagCount then break end + local grid = libbagsort.itemGrid[item.srcBag][item.srcSlot] + grid.destBag = bagList[bagIdx] + grid.destSlot = destSlot + destSlot = destSlot + 1 + end + + -- Reverse pass: assign poor items from the last slot of the last bag backward + local rBagIdx = bagCount + local rDestSlot = 0 + while rBagIdx >= 1 do + if bagSlots[rBagIdx] > 0 then rDestSlot = bagSlots[rBagIdx]; break end + rBagIdx = rBagIdx - 1 + end + for _, item in ipairs(poorItems) do + while rBagIdx >= 1 and rDestSlot < 1 do + rBagIdx = rBagIdx - 1 + rDestSlot = rBagIdx >= 1 and bagSlots[rBagIdx] or 0 + end + if rBagIdx < 1 then break end + local grid = libbagsort.itemGrid[item.srcBag][item.srcSlot] + grid.destBag = bagList[rBagIdx] + grid.destSlot = rDestSlot + rDestSlot = rDestSlot - 1 + end +end + +-- Build the sort grid (planned from current bag state) and fire every swap +-- needed to reach it. Items track their live position via curBag/curSlot so +-- we never read it back from a grid we're mutating. +local function RunSortPhase() + BuildSortGrid() + + -- Snapshot items that need to move before any swap runs — iterating + -- pairs() while mutating itemGrid is undefined in Lua 5.0. + local toMove = {} + for _, bagGrid in pairs(libbagsort.itemGrid) do + for _, info in pairs(bagGrid) do + if info.destBag and (info.destBag ~= info.curBag or info.destSlot ~= info.curSlot) then + tinsert(toMove, info) + end + end + end + + for _, info in ipairs(toMove) do + local curBag, curSlot = info.curBag, info.curSlot + local dBag, dSlot = info.destBag, info.destSlot + if dBag ~= curBag or dSlot ~= curSlot then + local _, _, lock1 = GetContainerItemInfo(curBag, curSlot) + local _, _, lock2 = GetContainerItemInfo(dBag, dSlot) + if not (lock1 or lock2) then + local displaced = libbagsort.itemGrid[dBag][dSlot] + C_Container.SwapItems(curBag, curSlot, dBag, dSlot) + libbagsort.itemGrid[dBag][dSlot] = info + libbagsort.itemGrid[curBag][curSlot] = displaced + info.curBag, info.curSlot = dBag, dSlot + if displaced then + displaced.curBag, displaced.curSlot = curBag, curSlot + end + end + end + end + + ClearSortData() +end + +-- Unregister immediately so the swaps we're about to fire don't re-enter +-- this handler via their own BAG_UPDATEs. +local function OnEvent() + if event == "BAG_UPDATE_DELAYED" then + libbagsort:UnregisterEvent("BAG_UPDATE_DELAYED") + libbagsort:SetScript("OnEvent", nil) + RunSortPhase() + end +end + +-- Sort the listed bag IDs in-place: consolidate partial stacks (one +-- BAG_UPDATE_DELAYED cycle), then place items by category/name/quality. +-- e.g. `libbagsort:Sort({0, 1, 2, 3, 4})` for the main bags; +-- `{-1, 5, 6, 7, 8, 9, 10}` for the bank. +function libbagsort:Sort(bagList) + ClearSortData() + self.bagList = bagList + + -- Phase 1: fire every consolidation op in a single batch. + local ops = BuildConsolidateOps(bagList) + local fired = false + for _, op in ipairs(ops) do + local _, _, lock1 = GetContainerItemInfo(op.srcBag, op.srcSlot) + local _, _, lock2 = GetContainerItemInfo(op.dstBag, op.dstSlot) + if not (lock1 or lock2) then + C_Container.MoveItem(op.srcBag, op.srcSlot, op.dstBag, op.dstSlot, op.count) + fired = true + end + end + + if fired then + -- Wait for the server to confirm the merges so the sort grid reads + -- accurate slot contents. + self:SetScript("OnEvent", OnEvent) + self:RegisterEvent("BAG_UPDATE_DELAYED") + return + end + + RunSortPhase() +end diff --git a/modules/bags.lua b/modules/bags.lua index 030b9a64..e7be3fb9 100644 --- a/modules/bags.lua +++ b/modules/bags.lua @@ -5,274 +5,8 @@ pfUI:RegisterModule("bags", function () Spell_Holy_RemoveCurse = {frame="disenchant"}, Spell_Nature_MoonKey = {frame="picklock"}, } - local scanner = libtipscan:GetScanner("openable") - -- ============================================================================ - -- Bag sorter: consolidates partial stacks then sorts by category/name. - -- Adapted from the algo in Bagnon/lib/BagSort.lua. - -- ============================================================================ - local sorter = CreateFrame("Frame") - sorter.itemGrid = {} - sorter.bagList = nil - local HEARTHSTONE_ITEM_ID = 6948 - - -- Lower prefix = sorted earlier in the bag. - local function SortCategoryPrefix(itemId, itemType, itemSubType, quality) - if itemId == HEARTHSTONE_ITEM_ID then return "00" end - if quality == 0 then return "13" end -- Poor (gray) always last - if itemType == "Weapon" or itemType == "Armor" then - if quality and quality >= 4 then return "01" end -- Epic+ gear - if quality == 3 then return "02" end -- Rare gear - if quality == 2 then return "03" end -- Uncommon gear - return "04" -- Common/poor gear - end - if itemType == "Consumable" then return "05" end - if itemType == "Reagent" then return "06" end - if itemType == "Trade Goods" then return "07" end - if itemType == "Quest" then return "08" end - -- Non-gear items without a specific type, sorted by quality - if quality and quality >= 4 then return "09" end - if quality == 3 then return "10" end - if quality == 2 then return "11" end - return "12" - end - - -- Larger stacks sort first among identically-named items; invert + zero-pad - -- so it sorts lexicographically. - local function SortCountSuffix(count) - local s = "000000" .. (999999 - (count or 0)) - return string.sub(s, -6) - end - - local function SortKey(itemId, name, itype, subtype, quality, count) - return SortCategoryPrefix(itemId, itype, subtype, quality) - .. (itype or "") .. "|" .. (subtype or "") .. "|" .. (name or "zzz") .. "|" .. SortCountSuffix(count) - end - - local function ClearSortData() - sorter.itemGrid = {} - sorter.bagList = nil - sorter:UnregisterEvent("BAG_UPDATE_DELAYED") - sorter:SetScript("OnEvent", nil) - end - - -- Two-pointer consolidation: sorts stacks largest-first, then merges from - -- both ends toward the middle. n is set explicitly so table.getn / table.sort - -- work correctly in Lua 5.0. - local function BuildConsolidateOps(bagList) - local groups = {} - for _, bag in ipairs(bagList) do - for slot = 1, GetContainerNumSlots(bag) do - local itemId = C_Container.GetContainerItemID(bag, slot) - if itemId then - local _, count = GetContainerItemInfo(bag, slot) - count = count or 0 - local maxStack = C_Item.GetItemMaxStackSizeByID(itemId) or 1 - if count > 0 and maxStack > 1 then - if not groups[itemId] then - groups[itemId] = {maxStack=maxStack, n=0} - end - local g = groups[itemId] - g.n = g.n + 1 - g[g.n] = {bag=bag, slot=slot, count=count} - end - end - end - end - - local ops = {} - for _, g in pairs(groups) do - local n = g.n - if n >= 2 then - local maxStack = g.maxStack - table.sort(g, function(a, b) return a.count > b.count end) - local lo, hi = 1, n - while lo < hi do - local space = maxStack - g[lo].count - if space == 0 then - lo = lo + 1 - elseif space >= g[hi].count then - tinsert(ops, { - dstBag = g[lo].bag, dstSlot = g[lo].slot, - srcBag = g[hi].bag, srcSlot = g[hi].slot, - count = g[hi].count, - }) - g[lo].count = g[lo].count + g[hi].count - hi = hi - 1 - else - tinsert(ops, { - dstBag = g[lo].bag, dstSlot = g[lo].slot, - srcBag = g[hi].bag, srcSlot = g[hi].slot, - count = space, - }) - g[hi].count = g[hi].count - space - g[lo].count = maxStack - lo = lo + 1 - end - end - end - end - return ops - end - - local function BuildSortGrid() - local bagList = sorter.bagList - sorter.itemGrid = {} - local normalItems = {} - local poorItems = {} - local bagCount = 0 - local bagSlots = {} - - for _, bag in ipairs(bagList) do - bagCount = bagCount + 1 - local numSlots = GetContainerNumSlots(bag) - bagSlots[bagCount] = numSlots - if numSlots > 0 then - sorter.itemGrid[bag] = {} - for slot = 1, numSlots do - local itemId = C_Container.GetContainerItemID(bag, slot) - if itemId then - -- pfUI's compat layer shims GetItemInfo to the modern 10-field - -- signature (inserts nil for itemLevel between quality and - -- minlevel) — so itype/subtype sit at positions 6/7, not 5/6. - local name, _, quality, _, _, itype, subtype = GetItemInfo(itemId) - local _, count = GetContainerItemInfo(bag, slot) - local item = { - key = SortKey(itemId, name, itype, subtype, quality, count), - srcBag = bag, - srcSlot = slot, - curBag = bag, - curSlot = slot, - } - if quality == 0 then - tinsert(poorItems, item) - else - tinsert(normalItems, item) - end - sorter.itemGrid[bag][slot] = item - end - end - end - end - - table.sort(normalItems, function(a, b) return a.key < b.key end) - -- Sort poor items descending so they read in ascending order when placed - -- back-to-front (last poor item lands on the last slot). - table.sort(poorItems, function(a, b) return a.key > b.key end) - - -- Forward pass: assign normal items from slot 1 of bag 1 onward - local bagIdx, destSlot = 1, 1 - while bagIdx <= bagCount and bagSlots[bagIdx] == 0 do - bagIdx = bagIdx + 1 - end - for _, item in ipairs(normalItems) do - while bagIdx <= bagCount do - if destSlot <= bagSlots[bagIdx] then break end - bagIdx = bagIdx + 1 - destSlot = 1 - end - if bagIdx > bagCount then break end - local grid = sorter.itemGrid[item.srcBag][item.srcSlot] - grid.destBag = bagList[bagIdx] - grid.destSlot = destSlot - destSlot = destSlot + 1 - end - - -- Reverse pass: assign poor items from the last slot of the last bag backward - local rBagIdx = bagCount - local rDestSlot = 0 - while rBagIdx >= 1 do - if bagSlots[rBagIdx] > 0 then rDestSlot = bagSlots[rBagIdx]; break end - rBagIdx = rBagIdx - 1 - end - for _, item in ipairs(poorItems) do - while rBagIdx >= 1 and rDestSlot < 1 do - rBagIdx = rBagIdx - 1 - rDestSlot = rBagIdx >= 1 and bagSlots[rBagIdx] or 0 - end - if rBagIdx < 1 then break end - local grid = sorter.itemGrid[item.srcBag][item.srcSlot] - grid.destBag = bagList[rBagIdx] - grid.destSlot = rDestSlot - rDestSlot = rDestSlot - 1 - end - end - - -- Build the sort grid (planned from current bag state) and fire every swap - -- needed to reach it. Items track their live position via curBag/curSlot so - -- we never read it back from a grid we're mutating. - local function RunSortPhase() - BuildSortGrid() - - -- Snapshot items that need to move before any swap runs — iterating - -- pairs() while mutating itemGrid is undefined in Lua 5.0. - local toMove = {} - for _, bagGrid in pairs(sorter.itemGrid) do - for _, info in pairs(bagGrid) do - if info.destBag and (info.destBag ~= info.curBag or info.destSlot ~= info.curSlot) then - tinsert(toMove, info) - end - end - end - - for _, info in ipairs(toMove) do - local curBag, curSlot = info.curBag, info.curSlot - local dBag, dSlot = info.destBag, info.destSlot - if dBag ~= curBag or dSlot ~= curSlot then - local _, _, lock1 = GetContainerItemInfo(curBag, curSlot) - local _, _, lock2 = GetContainerItemInfo(dBag, dSlot) - if not (lock1 or lock2) then - local displaced = sorter.itemGrid[dBag][dSlot] - C_Container.SwapItems(curBag, curSlot, dBag, dSlot) - sorter.itemGrid[dBag][dSlot] = info - sorter.itemGrid[curBag][curSlot] = displaced - info.curBag, info.curSlot = dBag, dSlot - if displaced then - displaced.curBag, displaced.curSlot = curBag, curSlot - end - end - end - end - - ClearSortData() - end - - -- Unregister immediately so the swaps we're about to fire don't re-enter - -- this handler via their own BAG_UPDATEs. - local function SorterOnEvent() - if event == "BAG_UPDATE_DELAYED" then - sorter:UnregisterEvent("BAG_UPDATE_DELAYED") - sorter:SetScript("OnEvent", nil) - RunSortPhase() - end - end - - local function StartSort(bagList) - ClearSortData() - sorter.bagList = bagList - - -- Phase 1: fire every consolidation op in a single batch. - local ops = BuildConsolidateOps(bagList) - local fired = false - for _, op in ipairs(ops) do - local _, _, lock1 = GetContainerItemInfo(op.srcBag, op.srcSlot) - local _, _, lock2 = GetContainerItemInfo(op.dstBag, op.dstSlot) - if not (lock1 or lock2) then - C_Container.MoveItem(op.srcBag, op.srcSlot, op.dstBag, op.dstSlot, op.count) - fired = true - end - end - - if fired then - -- Wait for the server to confirm the merges so the sort grid reads - -- accurate slot contents. - sorter:SetScript("OnEvent", SorterOnEvent) - sorter:RegisterEvent("BAG_UPDATE_DELAYED") - return - end - - RunSortPhase() - end + local scanner = libtipscan:GetScanner("input_search") -- function to detect openable items in inventory local openable = { bag = nil, slot = nil, icon = nil } @@ -608,7 +342,7 @@ pfUI:RegisterModule("bags", function () chat:Hide() end if C.appearance.bags.autoSortOnOpen == "1" then - StartSort({0, 1, 2, 3, 4}) + libbagsort:Sort({0, 1, 2, 3, 4}) end pfUI.bag:CreateBags(object) PlaySound("INTERFACESOUND_BACKPACKOPEN") @@ -1261,7 +995,7 @@ pfUI:RegisterModule("bags", function () end) frame.sort:SetScript("OnClick", function() - StartSort({0, 1, 2, 3, 4}) + libbagsort:Sort({0, 1, 2, 3, 4}) end) end @@ -1499,9 +1233,9 @@ pfUI:RegisterModule("bags", function () end) frame.sort:SetScript("OnClick", function() - StartSort({-1, 5, 6, 7, 8, 9, 10}) + libbagsort:Sort({-1, 5, 6, 7, 8, 9, 10}) end) end end end -end) \ No newline at end of file +end)