Make the bag sort family-aware and locale-independent

Two fixes to libbagsort's planning stage.

Family-aware placement: the sort treated every bag in the list as
interchangeable storage, so with a quiver / soul / profession bag in the
set it would try to swap a general item into a slot that can't hold it.
The client rejects that swap, but the grid was updated as if it
succeeded, desyncing the plan and corrupting the sort from there on. Now
each destination slot is tagged with its bag's family (via ClassicAPI's
GetItemFamily, which reports bag families reliably) and every item is
routed to a cell that accepts it -- matching specialty bag first,
overflowing to general -- so nothing lands where it can't go and family
items consolidate into their bags for free.

Locale-independent categories: SortCategoryPrefix compared the localized
itemType/quality strings from GetItemInfo, which silently miscategorize
everything on a non-enUS client. Switch to the numeric classID/quality
via C_Item.GetItemInfo and ClassicAPI's Enum.ItemClass / Enum.ItemQuality.
This commit is contained in:
Brues
2026-07-20 17:46:37 -05:00
parent 1c0c9dc019
commit dfa74c5756
+85 -59
View File
@@ -11,26 +11,27 @@ pfUI.api.libbagsort = libbagsort
libbagsort.itemGrid = {} libbagsort.itemGrid = {}
libbagsort.bagList = nil libbagsort.bagList = nil
local HEARTHSTONE_ITEM_ID = 6948 local ItemClass = Enum.ItemClass
local ItemQuality = Enum.ItemQuality
-- Lower prefix = sorted earlier in the bag. -- Lower prefix = sorted earlier in the bag.
local function SortCategoryPrefix(itemId, itemType, itemSubType, quality) local function SortCategoryPrefix(itemId, classID, quality)
if itemId == HEARTHSTONE_ITEM_ID then return "00" end if itemId == HEARTHSTONE_ITEM_ID then return "00" end
if quality == 0 then return "13" end -- Poor (gray) always last if quality == ItemQuality.Poor then return "13" end -- gray always last
if itemType == "Weapon" or itemType == "Armor" then if classID == ItemClass.Weapon or classID == ItemClass.Armor then
if quality and quality >= 4 then return "01" end -- Epic+ gear if quality and quality >= ItemQuality.Epic then return "01" end -- Epic+ gear
if quality == 3 then return "02" end -- Rare gear if quality == ItemQuality.Rare then return "02" end -- Rare gear
if quality == 2 then return "03" end -- Uncommon gear if quality == ItemQuality.Uncommon then return "03" end -- Uncommon gear
return "04" -- Common/poor gear return "04" -- Common/poor gear
end end
if itemType == "Consumable" then return "05" end if classID == ItemClass.Consumable then return "05" end
if itemType == "Reagent" then return "06" end if classID == ItemClass.Reagent then return "06" end
if itemType == "Trade Goods" then return "07" end if classID == ItemClass.Tradegoods then return "07" end
if itemType == "Quest" then return "08" end if classID == ItemClass.Questitem then return "08" end
-- Non-gear items without a specific type, sorted by quality -- Non-gear items without a specific type, sorted by quality
if quality and quality >= 4 then return "09" end if quality and quality >= ItemQuality.Epic then return "09" end
if quality == 3 then return "10" end if quality == ItemQuality.Rare then return "10" end
if quality == 2 then return "11" end if quality == ItemQuality.Uncommon then return "11" end
return "12" return "12"
end end
@@ -41,9 +42,12 @@ local function SortCountSuffix(count)
return string.sub(s, -6) return string.sub(s, -6)
end end
local function SortKey(itemId, name, itype, subtype, quality, count) local function SortKey(itemId, name, classID, subClassID, quality, count)
return SortCategoryPrefix(itemId, itype, subtype, quality) -- Zero-pad the class/subclass so the secondary grouping sorts numerically
.. (itype or "") .. "|" .. (subtype or "") .. "|" .. (name or "zzz") .. "|" .. SortCountSuffix(count) -- (as a string, "10" would otherwise precede "2").
return SortCategoryPrefix(itemId, classID, quality)
.. string.format("%02d|%02d|", classID or 99, subClassID or 99)
.. (name or "zzz") .. "|" .. SortCountSuffix(count)
end end
local function ClearSortData() local function ClearSortData()
@@ -112,36 +116,61 @@ local function BuildConsolidateOps(bagList)
return ops return ops
end 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 function BuildSortGrid()
local bagList = libbagsort.bagList local bagList = libbagsort.bagList
libbagsort.itemGrid = {} libbagsort.itemGrid = {}
local normalItems = {} local normalItems = {}
local poorItems = {} local poorItems = {}
local bagCount = 0
local bagSlots = {} -- 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 for _, bag in ipairs(bagList) do
bagCount = bagCount + 1 local fam = BagFamily(bag)
local numSlots = GetContainerNumSlots(bag) local numSlots = GetContainerNumSlots(bag)
bagSlots[bagCount] = numSlots
if numSlots > 0 then if numSlots > 0 then
libbagsort.itemGrid[bag] = {} libbagsort.itemGrid[bag] = {}
for slot = 1, numSlots do 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) local itemId = C_Container.GetContainerItemID(bag, slot)
if itemId then if itemId then
-- pfUI's compat layer shims GetItemInfo to the modern 10-field -- C_Item.GetItemInfo is the full 18-field tuple; classID/subClassID
-- signature (inserts nil for itemLevel between quality and -- sit at positions 12/13. We categorize on those numeric class IDs
-- minlevel) — so itype/subtype sit at positions 6/7, not 5/6. -- rather than the localized itemType/itemSubType strings. (pfUI's
local name, _, quality, _, _, itype, subtype = GetItemInfo(itemId) -- shimmed global GetItemInfo is only 10 fields and lacks them.)
local name, _, quality, _, _, _, _, _, _, _, _, classID, subClassID = C_Item.GetItemInfo(itemId)
local _, count = GetContainerItemInfo(bag, slot) local _, count = GetContainerItemInfo(bag, slot)
local item = { local item = {
key = SortKey(itemId, name, itype, subtype, quality, count), key = SortKey(itemId, name, classID, subClassID, quality, count),
-- 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, srcBag = bag,
srcSlot = slot, srcSlot = slot,
curBag = bag, curBag = bag,
curSlot = slot, curSlot = slot,
} }
if quality == 0 then if quality == ItemQuality.Poor then
tinsert(poorItems, item) tinsert(poorItems, item)
else else
tinsert(normalItems, item) tinsert(normalItems, item)
@@ -157,41 +186,38 @@ local function BuildSortGrid()
-- back-to-front (last poor item lands on the last slot). -- back-to-front (last poor item lands on the last slot).
table.sort(poorItems, function(a, b) return a.key > b.key end) table.sort(poorItems, function(a, b) return a.key > b.key end)
-- Forward pass: assign normal items from slot 1 of bag 1 onward -- Forward pass: route each normal item into the next free cell that
local bagIdx, destSlot = 1, 1 -- accepts it -- a matching specialty bag first, overflowing to general.
while bagIdx <= bagCount and bagSlots[bagIdx] == 0 do local genIdx = 1
bagIdx = bagIdx + 1 local specIdx = {} -- family -> next free index into specialtyCells[family]
end
for _, item in ipairs(normalItems) do for _, item in ipairs(normalItems) do
while bagIdx <= bagCount do local cell
if destSlot <= bagSlots[bagIdx] then break end local fam = item.family
bagIdx = bagIdx + 1 if fam ~= 0 and specialtyCells[fam] then
destSlot = 1 local i = specIdx[fam] or 1
if i <= table.getn(specialtyCells[fam]) then
cell = specialtyCells[fam][i]
specIdx[fam] = i + 1
end
end end
if bagIdx > bagCount then break end if not cell and genIdx <= table.getn(generalCells) then
local grid = libbagsort.itemGrid[item.srcBag][item.srcSlot] cell = generalCells[genIdx]
grid.destBag = bagList[bagIdx] genIdx = genIdx + 1
grid.destSlot = destSlot end
destSlot = destSlot + 1 if not cell then break end
item.destBag = cell.bag
item.destSlot = cell.slot
end end
-- Reverse pass: assign poor items from the last slot of the last bag backward -- Reverse pass: poor items are general; fill remaining general cells from
local rBagIdx = bagCount -- the back, stopping before the ones the forward pass already claimed.
local rDestSlot = 0 local genBack = table.getn(generalCells)
while rBagIdx >= 1 do
if bagSlots[rBagIdx] > 0 then rDestSlot = bagSlots[rBagIdx]; break end
rBagIdx = rBagIdx - 1
end
for _, item in ipairs(poorItems) do for _, item in ipairs(poorItems) do
while rBagIdx >= 1 and rDestSlot < 1 do if genBack < genIdx then break end
rBagIdx = rBagIdx - 1 local cell = generalCells[genBack]
rDestSlot = rBagIdx >= 1 and bagSlots[rBagIdx] or 0 genBack = genBack - 1
end item.destBag = cell.bag
if rBagIdx < 1 then break end item.destSlot = cell.slot
local grid = libbagsort.itemGrid[item.srcBag][item.srcSlot]
grid.destBag = bagList[rBagIdx]
grid.destSlot = rDestSlot
rDestSlot = rDestSlot - 1
end end
end end