484 lines
16 KiB
Lua
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()
|