Files
Nanami-UI/Bags/Sort.lua
2026-03-16 13:48:46 +08:00

484 lines
16 KiB
Lua

--------------------------------------------------------------------------------
-- S-Frames: Bag Sorting Logic (Bags/Sort.lua)
-- Manual sorting algorithm compatible with Vanilla/Turtle WoW
--------------------------------------------------------------------------------
SFrames.Bags.Sort = SFrames.Bags.Sort or {}
local isSorting = false
local sortQueue = {}
local sortTimer = CreateFrame("Frame")
local sortDelay = 0.05 -- Increased speed from 0.15 to accelerate sorting.
local timeSinceLast = 0
local activeCompleteMessage = nil
local activeCompleteUpdate = nil
local activeBagOrder = nil
local activePhase = nil
local TEXT_LOCKED = "\233\131\168\229\136\134\231\137\169\229\147\129\229\183\178\233\148\129\229\174\154\239\188\140\232\175\183\231\168\141\229\144\142\233\135\141\232\175\149\227\128\130"
local TEXT_OFFLINE_BAGS = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\232\131\140\229\140\133\227\128\130"
local TEXT_BAG_DONE = "\232\131\140\229\140\133\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local TEXT_OFFLINE_BANK = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\233\147\182\232\161\140\227\128\130"
local TEXT_BANK_DONE = "\233\147\182\232\161\140\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local itemMaxStackCache = {}
local function GetItemSortValue(link)
if not link then return 0, "" end
local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
if not itemString then itemString = link end
local itemName, _, itemRarity, _, itemType = GetItemInfo(itemString)
if not itemName then return 0, "" end
local score = 0
local _, _, itemID = string.find(link, "item:(%d+)")
if itemID == "6948" or string.find(itemName, "Hearthstone") then
score = 1000000
else
score = score + (itemRarity or 0) * 100000
if itemType == "Weapon" then
score = score + 50000
elseif itemType == "Armor" then
score = score + 40000
elseif itemType == "Consumable" then
score = score + 30000
elseif itemType == "Trade Goods" then
score = score + 20000
elseif itemType == "Recipe" then
score = score + 10000
end
end
return score, itemName
end
local function GetItemIdentity(link)
if not link then return nil end
local _, _, itemString = string.find(link, "|H([^|]+)|h")
if not itemString then itemString = link end
return itemString
end
local function GetItemMaxStack(link)
local itemKey = GetItemIdentity(link)
if not itemKey then return 1 end
if itemMaxStackCache[itemKey] then
return itemMaxStackCache[itemKey]
end
local maxStack = 1
local res = {GetItemInfo(itemKey)}
-- In standard vanilla, maxStack is parameter 7. In some modified schema (TBC) it is parameter 8.
local v7 = tonumber(res[7])
local v8 = tonumber(res[8])
if v7 and v7 > 1 then
maxStack = v7
elseif v8 and v8 > 1 then
maxStack = v8
end
itemMaxStackCache[itemKey] = maxStack
return maxStack
end
local function GetSpecialType(link, isBag)
if not link then return "Normal" end
local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
if not itemString then itemString = link end
local itemName, _, _, _, itemType, itemSubType = GetItemInfo(itemString)
if not itemType and not itemName then return "Normal" end
local _, _, itemID = string.find(itemString, "item:(%d+)")
if isBag then
if itemType == "Quiver" or itemType == "箭袋" then return "Ammo" end
if itemSubType == "Quiver" or itemSubType == "Ammo Pouch" or itemSubType == "箭袋" or itemSubType == "子弹袋" then return "Ammo" end
if itemSubType == "Soul Bag" or itemSubType == "灵魂袋" then return "Soul" end
if itemSubType == "Enchanting Bag" or itemSubType == "附魔材料袋" then return "Enchanting" end
if itemSubType == "Herb Bag" or itemSubType == "草药袋" then return "Herb" end
else
if itemType == "Projectile" or itemType == "弹药" then return "Ammo" end
if itemSubType == "Arrow" or itemSubType == "Bullet" or itemSubType == "" or itemSubType == "子弹" then return "Ammo" end
if itemID == "6265" or itemName == "Soul Shard" or itemName == "灵魂碎片" then return "Soul" end
end
return "Normal"
end
local function SortItemsByRule(items)
table.sort(items, function(a, b)
if a.score ~= b.score then
return a.score > b.score
elseif a.name ~= b.name then
return a.name < b.name
else
return a.count > b.count
end
end)
end
local function ResetSortState()
isSorting = false
sortQueue = {}
activeCompleteMessage = nil
activeCompleteUpdate = nil
activeBagOrder = nil
activePhase = nil
timeSinceLast = 0
sortTimer:Hide()
end
local function FinishSort()
local msg = activeCompleteMessage
local updater = activeCompleteUpdate
ResetSortState()
if msg then SFrames:Print(msg) end
if updater then updater() end
end
function SFrames.Bags.Sort:ExecuteSimpleSort(items, bagOrder)
sortQueue = {}
local slotPool = { Normal = {}, Ammo = {}, Soul = {}, Enchanting = {}, Herb = {} }
local allocatedStream = {}
for _, bag in ipairs(bagOrder) do
local btype = "Normal"
if bag ~= 0 and bag ~= -1 then
local invID
if bag >= 1 and bag <= 4 then
invID = ContainerIDToInventoryID(bag)
elseif bag >= 5 and bag <= 11 then
invID = bag + 34
end
if invID then
local ok, link = pcall(GetInventoryItemLink, "player", invID)
if ok and link then btype = GetSpecialType(link, true) end
end
end
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local s = { bag = bag, slot = slot, type = btype }
table.insert(allocatedStream, s)
if slotPool[btype] then
table.insert(slotPool[btype], s)
else
table.insert(slotPool.Normal, s)
end
end
end
for _, item in ipairs(items) do
item.specialType = GetSpecialType(item.link, false)
end
-- Now assign target slot to each item
for _, item in ipairs(items) do
local targetSlot = nil
if item.specialType ~= "Normal" and slotPool[item.specialType] and table.getn(slotPool[item.specialType]) > 0 then
targetSlot = table.remove(slotPool[item.specialType], 1)
elseif table.getn(slotPool.Normal) > 0 then
targetSlot = table.remove(slotPool.Normal, 1)
end
if targetSlot then
targetSlot.idealItem = item
end
end
-- Build virtual moves to align physical items to targetSlots
for _, target in ipairs(allocatedStream) do
if target.idealItem then
local idealItem = target.idealItem
if idealItem.bag ~= target.bag or idealItem.slot ~= target.slot then
table.insert(sortQueue, {
fromBag = idealItem.bag,
fromSlot = idealItem.slot,
toBag = target.bag,
toSlot = target.slot,
retries = 0
})
-- Reflect the virtual swap in our model so later moves stay correct.
for j = 1, table.getn(items) do
if items[j].bag == target.bag and items[j].slot == target.slot then
items[j].bag = idealItem.bag
items[j].slot = idealItem.slot
break
end
end
idealItem.bag = target.bag
idealItem.slot = target.slot
end
end
end
end
function SFrames.Bags.Sort:ScanItems(bagOrder, ignoreLocked)
local items = {}
for _, bag in ipairs(bagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local link = GetContainerItemLink(bag, slot)
local _, itemCount, locked = GetContainerItemInfo(bag, slot)
if locked and not ignoreLocked then
return nil, TEXT_LOCKED
end
if link then
local score, name = GetItemSortValue(link)
table.insert(items, {
bag = bag,
slot = slot,
link = link,
score = score,
name = name,
count = itemCount or 1,
stackKey = GetItemIdentity(link),
maxStack = GetItemMaxStack(link)
})
end
end
end
return items, nil
end
function SFrames.Bags.Sort:BuildStackMergeMoves(items)
local grouped = {}
local moves = {}
for _, item in ipairs(items) do
if item and item.stackKey and item.maxStack and item.maxStack > 1 and (item.count or 0) > 0 then
if not grouped[item.stackKey] then
grouped[item.stackKey] = {
maxStack = item.maxStack,
slots = {}
}
end
if item.maxStack > grouped[item.stackKey].maxStack then
grouped[item.stackKey].maxStack = item.maxStack
end
table.insert(grouped[item.stackKey].slots, {
bag = item.bag,
slot = item.slot,
count = item.count
})
end
end
for _, group in pairs(grouped) do
local slots = group.slots
if table.getn(slots) > 1 then
table.sort(slots, function(a, b)
if a.count ~= b.count then
return a.count > b.count
elseif a.bag ~= b.bag then
return a.bag < b.bag
else
return a.slot < b.slot
end
end)
local left = 1
local right = table.getn(slots)
while left < right do
local target = slots[left]
local source = slots[right]
if target.count >= group.maxStack then
left = left + 1
elseif source.count <= 0 then
right = right - 1
else
local transfer = math.min(group.maxStack - target.count, source.count)
if transfer > 0 then
table.insert(moves, {
fromBag = source.bag,
fromSlot = source.slot,
toBag = target.bag,
toSlot = target.slot,
transferCount = transfer,
isStackMove = true
})
target.count = target.count + transfer
source.count = source.count - transfer
end
if source.count <= 0 then
right = right - 1
end
if target.count >= group.maxStack then
left = left + 1
end
end
end
end
end
return moves
end
function SFrames.Bags.Sort:StartPlacementPhase(ignoreLocked)
if not activeBagOrder then
FinishSort()
return
end
local items, err = self:ScanItems(activeBagOrder, ignoreLocked)
if not items then
ResetSortState()
if err then SFrames:Print(err) end
return
end
SortItemsByRule(items)
self:ExecuteSimpleSort(items, activeBagOrder)
if table.getn(sortQueue) == 0 then
FinishSort()
return
end
activePhase = "sort"
sortTimer:Show()
end
function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdate)
if isSorting then return end
isSorting = true
sortQueue = {}
timeSinceLast = 0
activeCompleteMessage = completeMessage
activeCompleteUpdate = completeUpdate
activeBagOrder = bagOrder
activePhase = nil
local items, err = self:ScanItems(bagOrder, false)
if not items then
ResetSortState()
if err then SFrames:Print(err) end
return
end
local stackMoves = self:BuildStackMergeMoves(items)
if table.getn(stackMoves) > 0 then
sortQueue = stackMoves
activePhase = "stack"
sortTimer:Show()
return
end
self:StartPlacementPhase(false)
end
function SFrames.Bags.Sort:Start()
if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then
SFrames:Print(TEXT_OFFLINE_BAGS)
return
end
self:StartForBags(
{0, 1, 2, 3, 4},
TEXT_BAG_DONE,
function()
if SFrames.Bags.Container then
SFrames.Bags.Container:UpdateLayout()
end
end
)
end
function SFrames.Bags.Sort:StartBank()
if SFrames.Bags.Bank and SFrames.Bags.Bank.isOffline then
SFrames:Print(TEXT_OFFLINE_BANK)
return
end
self:StartForBags(
{-1, 5, 6, 7, 8, 9, 10, 11},
TEXT_BANK_DONE,
function()
if SFrames.Bags.Bank then
SFrames.Bags.Bank:UpdateLayout()
end
end
)
end
sortTimer:SetScript("OnUpdate", function()
if not isSorting then
this:Hide()
return
end
timeSinceLast = timeSinceLast + arg1
if timeSinceLast > sortDelay then
timeSinceLast = 0
if table.getn(sortQueue) > 0 then
local move = sortQueue[1]
local _, _, lockedFrom = GetContainerItemInfo(move.fromBag, move.fromSlot)
local _, _, lockedTo = GetContainerItemInfo(move.toBag, move.toSlot)
if lockedFrom or lockedTo or CursorHasItem() then
move.retries = (move.retries or 0) + 1
if move.retries > 40 then
table.remove(sortQueue, 1)
if CursorHasItem() then
PickupContainerItem(move.fromBag, move.fromSlot)
end
end
return
end
table.remove(sortQueue, 1)
if move.isStackMove and move.transferCount then
SplitContainerItem(move.fromBag, move.fromSlot, move.transferCount)
PickupContainerItem(move.toBag, move.toSlot)
else
PickupContainerItem(move.fromBag, move.fromSlot)
PickupContainerItem(move.toBag, move.toSlot)
if CursorHasItem() then
PickupContainerItem(move.fromBag, move.fromSlot)
if CursorHasItem() then
PickupContainerItem(move.toBag, move.toSlot)
end
end
end
else
if activePhase == "stack" then
if activeBagOrder then
for _, bag in ipairs(activeBagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local _, _, locked = GetContainerItemInfo(bag, slot)
if locked then
return
end
end
end
end
-- After consolidation, rescan inventory and run normal sorting.
SFrames.Bags.Sort:StartPlacementPhase(true)
else
if CursorHasItem() then return end
FinishSort()
end
end
end
end)
sortTimer:Hide()