mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-27 09:56:03 +00:00
b57fc52f9a
Gear was split into four quality tiers and ordered by class, subclass and name inside each, so a helm could sit between two pairs of boots. Gear is now one category, ordered by class, then equip slot, then subclass, then best quality first, then name. The slot ranks come from a table keyed by the equipLoc token, not the token's alphabetical order (which puts chest before head) or its INVTYPE number (which parts robe from chest). The order matches ClassicAPI's C_Container.SortBags and comes from Baganator.
360 lines
13 KiB
Lua
360 lines
13 KiB
Lua
-- 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 ItemClass = Enum.ItemClass
|
|
local ItemQuality = Enum.ItemQuality
|
|
|
|
-- Lower prefix = sorted earlier in the bag.
|
|
local function SortCategoryPrefix(itemId, classID, quality)
|
|
if itemId == HEARTHSTONE_ITEM_ID then return "00" end
|
|
if quality == ItemQuality.Poor then return "13" end -- gray always last
|
|
if classID == ItemClass.Weapon or classID == ItemClass.Armor then return "01" end
|
|
if classID == ItemClass.Consumable then return "05" end
|
|
if classID == ItemClass.Reagent then return "06" end
|
|
if classID == ItemClass.Tradegoods then return "07" end
|
|
if classID == ItemClass.Questitem then return "08" end
|
|
-- Non-gear items without a specific type, sorted by quality
|
|
if quality and quality >= ItemQuality.Epic then return "09" end
|
|
if quality == ItemQuality.Rare then return "10" end
|
|
if quality == ItemQuality.Uncommon 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
|
|
|
|
-- Gear's slot order, keyed by the equipLoc token (INVTYPE_*). Not the
|
|
-- token's alphabetical order, which puts chest before head, and not its
|
|
-- INVTYPE number, which parts robe (20) from chest (5). Same order as
|
|
-- ClassicAPI's C_Container.SortBags, taken from Baganator: weapons by hand,
|
|
-- ranged and ammo, then armor with shields and off-hands first, head to
|
|
-- feet, then jewelry, shirt and tabard.
|
|
local SLOT_ORDER = {
|
|
"INVTYPE_2HWEAPON", "INVTYPE_WEAPON", "INVTYPE_WEAPONMAINHAND",
|
|
"INVTYPE_SHIELD", "INVTYPE_HOLDABLE", "INVTYPE_RANGEDRIGHT",
|
|
"INVTYPE_WEAPONOFFHAND", "INVTYPE_RANGED", "INVTYPE_THROWN",
|
|
"INVTYPE_AMMO", "INVTYPE_QUIVER", "INVTYPE_RELIC",
|
|
"INVTYPE_HEAD", "INVTYPE_SHOULDER", "INVTYPE_CLOAK", "INVTYPE_CHEST",
|
|
"INVTYPE_ROBE", "INVTYPE_WRIST", "INVTYPE_HAND", "INVTYPE_WAIST",
|
|
"INVTYPE_LEGS", "INVTYPE_FEET",
|
|
"INVTYPE_NECK", "INVTYPE_FINGER", "INVTYPE_TRINKET", "INVTYPE_BODY",
|
|
"INVTYPE_TABARD", "INVTYPE_BAG",
|
|
}
|
|
local SLOT_RANK = {}
|
|
for i, token in ipairs(SLOT_ORDER) do SLOT_RANK[token] = i end
|
|
|
|
local function SortKey(itemId, name, classID, subClassID, quality, count, equipLoc)
|
|
-- Zero-pad the numeric fields so they sort numerically as strings ("10"
|
|
-- would otherwise precede "2").
|
|
local key = SortCategoryPrefix(itemId, classID, quality)
|
|
.. string.format("%02d|", classID or 99)
|
|
if classID == ItemClass.Weapon or classID == ItemClass.Armor then
|
|
-- Gear: slot, then subtype, then best quality first, so all helms sit
|
|
-- together, then all shoulders.
|
|
key = key .. string.format("%02d|%02d|%d|",
|
|
SLOT_RANK[equipLoc] or 99, subClassID or 99, 9 - (quality or 0))
|
|
else
|
|
key = key .. string.format("%02d|", subClassID or 99)
|
|
end
|
|
return key .. (name or "zzz") .. "|" .. SortCountSuffix(count)
|
|
end
|
|
|
|
local function ClearSortData()
|
|
libbagsort.itemGrid = {}
|
|
libbagsort.bagList = nil
|
|
libbagsort.opts = nil
|
|
libbagsort:UnregisterEvent("BAG_UPDATE_DELAYED")
|
|
libbagsort:SetScript("OnEvent", nil)
|
|
end
|
|
|
|
local function ReverseArray(t)
|
|
local i, j = 1, table.getn(t)
|
|
while i < j do
|
|
t[i], t[j] = t[j], t[i]
|
|
i = i + 1
|
|
j = j - 1
|
|
end
|
|
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
|
|
|
|
-- Bag family bitmask (1 << (familyID-1)); 0 = general-purpose (holds
|
|
-- anything). Backpack (0) and bank (-1) are always general. A specialty
|
|
-- bag's family comes from the equipped bag item -- ClassicAPI derives it
|
|
-- from the container subclass when the raw field is empty (Turtle leaves
|
|
-- bags' m_bagFamily at 0), so quivers/soul/profession bags report properly.
|
|
local function BagFamily(bag)
|
|
if bag == 0 or bag == -1 then return 0 end
|
|
local id = GetInventoryItemID("player", ContainerIDToInventoryID(bag))
|
|
return id and C_Item.GetItemFamily(id) or 0
|
|
end
|
|
|
|
local function BuildSortGrid()
|
|
local bagList = libbagsort.bagList
|
|
libbagsort.itemGrid = {}
|
|
local normalItems = {}
|
|
local poorItems = {}
|
|
|
|
-- Destination cells, split by the family they can accept. A specialty
|
|
-- bag's slots only take items of its own family; general slots take
|
|
-- anything. Cells are collected in forward order (bag order, slot 1..n).
|
|
local generalCells = {} -- { {bag=,slot=}, ... }
|
|
local specialtyCells = {} -- family -> { {bag=,slot=}, ... }
|
|
|
|
for _, bag in ipairs(bagList) do
|
|
local fam = BagFamily(bag)
|
|
local numSlots = GetContainerNumSlots(bag)
|
|
if numSlots > 0 then
|
|
libbagsort.itemGrid[bag] = {}
|
|
for slot = 1, numSlots do
|
|
if fam == 0 then
|
|
tinsert(generalCells, {bag=bag, slot=slot})
|
|
else
|
|
specialtyCells[fam] = specialtyCells[fam] or {}
|
|
tinsert(specialtyCells[fam], {bag=bag, slot=slot})
|
|
end
|
|
|
|
local itemId = C_Container.GetContainerItemID(bag, slot)
|
|
if itemId then
|
|
-- C_Item.GetItemInfo is the full 18-field tuple; classID/subClassID
|
|
-- sit at positions 12/13. We categorize on those numeric class IDs
|
|
-- rather than the localized itemType/itemSubType strings. (pfUI's
|
|
-- shimmed global GetItemInfo is only 10 fields and lacks them.)
|
|
local name, _, quality, _, _, _, _, _, equipLoc, _, _, classID, subClassID = C_Item.GetItemInfo(itemId)
|
|
local _, count = GetContainerItemInfo(bag, slot)
|
|
local item = {
|
|
key = SortKey(itemId, name, classID, subClassID, quality, count, equipLoc),
|
|
-- vanilla items carry at most one family bit, so equality
|
|
-- against a bag family suffices (no bit.band needed).
|
|
family = C_Item.GetItemFamily(itemId) or 0,
|
|
srcBag = bag,
|
|
srcSlot = slot,
|
|
curBag = bag,
|
|
curSlot = slot,
|
|
}
|
|
if quality == ItemQuality.Poor then
|
|
tinsert(poorItems, item)
|
|
else
|
|
tinsert(normalItems, item)
|
|
end
|
|
libbagsort.itemGrid[bag][slot] = item
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local opts = libbagsort.opts or {}
|
|
local reverse = opts.reverse
|
|
local reversePrio = opts.reversePrio
|
|
|
|
if reverse then
|
|
ReverseArray(generalCells)
|
|
for _, cells in pairs(specialtyCells) do
|
|
ReverseArray(cells)
|
|
end
|
|
end
|
|
|
|
if reversePrio then
|
|
table.sort(normalItems, function(a, b) return a.key > b.key end)
|
|
else
|
|
table.sort(normalItems, function(a, b) return a.key < b.key end)
|
|
end
|
|
|
|
if reverse then
|
|
table.sort(poorItems, function(a, b) return a.key < b.key end)
|
|
else
|
|
table.sort(poorItems, function(a, b) return a.key > b.key end)
|
|
end
|
|
|
|
-- Forward pass: route each normal item into the next free cell that
|
|
-- accepts it -- a matching specialty bag first, overflowing to general.
|
|
local genIdx = 1
|
|
local specIdx = {} -- family -> next free index into specialtyCells[family]
|
|
for _, item in ipairs(normalItems) do
|
|
local cell
|
|
local fam = item.family
|
|
if fam ~= 0 and specialtyCells[fam] then
|
|
local i = specIdx[fam] or 1
|
|
if i <= table.getn(specialtyCells[fam]) then
|
|
cell = specialtyCells[fam][i]
|
|
specIdx[fam] = i + 1
|
|
end
|
|
end
|
|
if not cell and genIdx <= table.getn(generalCells) then
|
|
cell = generalCells[genIdx]
|
|
genIdx = genIdx + 1
|
|
end
|
|
if not cell then break end
|
|
item.destBag = cell.bag
|
|
item.destSlot = cell.slot
|
|
end
|
|
|
|
-- Reverse pass: poor items are general; fill remaining general cells from
|
|
-- the back, stopping before the ones the forward pass already claimed.
|
|
local genBack = table.getn(generalCells)
|
|
for _, item in ipairs(poorItems) do
|
|
if genBack < genIdx then break end
|
|
local cell = generalCells[genBack]
|
|
genBack = genBack - 1
|
|
item.destBag = cell.bag
|
|
item.destSlot = cell.slot
|
|
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.
|
|
--
|
|
-- opts (optional): { reverse = bool, reversePrio = bool }
|
|
-- reverse - place the first-ranked item into the last slot of the last
|
|
-- bag (junk fills from the opposite end).
|
|
-- reversePrio - flip the category ranking (e.g. hearthstone sorts last).
|
|
function libbagsort:Sort(bagList, opts)
|
|
ClearSortData()
|
|
self.bagList = bagList
|
|
self.opts = opts
|
|
|
|
-- 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
|