-------------------------------------------------------------------------------- -- 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()