Files
Guda-ClassicAPI/Sorting/SortEngine.lua
T
2025-11-19 02:34:46 +04:00

589 lines
21 KiB
Lua

-- Guda Sort Engine
-- 6-Phase Advanced Sorting Algorithm for WoW 1.12.1
local addon = Guda
local SortEngine = {}
addon.Modules.SortEngine = SortEngine
-- Priority items that should always be sorted first
local PriorityItems = {
[6948] = true, -- Hearthstone
}
-- Custom ordering for item classes
local classOrder = {
[0] = 1, -- Consumable
[6] = 2, -- Projectile (ammo/arrows)
[2] = 3, -- Weapon
[4] = 4, -- Armor
[11] = 5, -- Quiver
[5] = 6, -- Reagent
[7] = 7, -- Trade Goods
[9] = 8, -- Recipe
[1] = 9, -- Container
[12] = 10, -- Quest
[13] = 11, -- Key
[15] = 12, -- Miscellaneous
}
-- Extract itemID from item link
local function GetItemID(link)
if not link then return 0 end
local _, _, itemID = string.find(link, "item:(%d+)")
return tonumber(itemID) or 0
end
-- Get item class ID (numeric)
local function GetItemClassID(link)
if not link then return 99 end
local _, _, _, _, _, _, class = GetItemInfo(link)
if not class then return 99 end
local classMap = {
["Consumable"] = 0,
["Container"] = 1,
["Weapon"] = 2,
["Armor"] = 4,
["Reagent"] = 5,
["Projectile"] = 6,
["Trade Goods"] = 7,
["Recipe"] = 9,
["Quiver"] = 11,
["Quest"] = 12,
["Key"] = 13,
["Miscellaneous"] = 15,
}
return classMap[class] or 99
end
--===========================================================================
-- PHASE 1: Special Container Detection
--===========================================================================
local function DetectSpecializedBags(bagIDs)
local containers = {
soul = {},
quiver = {},
ammo = {},
regular = {}
}
for _, bagID in ipairs(bagIDs) do
local bagType = addon.Modules.Utils:GetSpecializedBagType(bagID)
if bagType == "soul" then
table.insert(containers.soul, bagID)
elseif bagType == "quiver" then
table.insert(containers.quiver, bagID)
elseif bagType == "ammo" then
table.insert(containers.ammo, bagID)
else
table.insert(containers.regular, bagID)
end
end
return containers
end
--===========================================================================
-- PHASE 2: Specialized Item Routing
--===========================================================================
local function RouteSpecializedItems(bagIDs, containers)
-- This phase moves specialized items to their preferred containers
-- We'll do this by marking items for routing and then executing moves
local routingPlan = {}
-- Scan all items
for _, bagID in ipairs(bagIDs) do
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if numSlots and numSlots > 0 then
for slot = 1, numSlots do
local link = GetContainerItemLink(bagID, slot)
if link then
local preferredType = addon.Modules.Utils:GetItemPreferredContainer(link)
-- If item has a preferred container type and isn't already in one
if preferredType then
local currentBagType = addon.Modules.Utils:GetSpecializedBagType(bagID)
-- If not already in preferred container
if currentBagType ~= preferredType then
local targetBags = containers[preferredType]
if targetBags and table.getn(targetBags) > 0 then
-- Find first available slot in preferred containers
local foundSlot = false
for _, targetBagID in ipairs(targetBags) do
if not foundSlot then
local targetSlots = addon.Modules.Utils:GetBagSlotCount(targetBagID)
for targetSlot = 1, targetSlots do
local targetLink = GetContainerItemLink(targetBagID, targetSlot)
if not targetLink then
-- Found empty slot - plan the move
table.insert(routingPlan, {
fromBag = bagID,
fromSlot = slot,
toBag = targetBagID,
toSlot = targetSlot
})
foundSlot = true
break
end
end
end
end
end
end
end
end
end
end
end
-- Execute routing plan
for _, move in ipairs(routingPlan) do
PickupContainerItem(move.fromBag, move.fromSlot)
PickupContainerItem(move.toBag, move.toSlot)
ClearCursor()
end
return table.getn(routingPlan)
end
--===========================================================================
-- PHASE 3: Stack Consolidation
--===========================================================================
local function ConsolidateStacks(bagIDs)
-- Group items by (itemID, quality, soulbound)
local itemGroups = {}
-- Collect all items with their locations
for _, bagID in ipairs(bagIDs) do
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if numSlots and numSlots > 0 then
for slot = 1, numSlots do
local link = GetContainerItemLink(bagID, slot)
if link then
local texture, count, locked = GetContainerItemInfo(bagID, slot)
local itemID = GetItemID(link)
local _, _, quality = GetItemInfo(link)
-- Create group key
local groupKey = itemID .. "_" .. (quality or 0)
if not itemGroups[groupKey] then
itemGroups[groupKey] = {
itemID = itemID,
link = link,
stacks = {}
}
end
table.insert(itemGroups[groupKey].stacks, {
bagID = bagID,
slot = slot,
count = count or 1,
priority = addon.Modules.Utils:GetContainerPriority(bagID)
})
end
end
end
end
-- For each group, consolidate stacks
local consolidationMoves = 0
for _, group in pairs(itemGroups) do
if table.getn(group.stacks) > 1 then
-- Get max stack size
local _, _, _, _, _, _, _, maxStack = GetItemInfo(group.link)
maxStack = maxStack or 1
if maxStack > 1 then
-- Sort stacks by priority DESC, then count DESC
table.sort(group.stacks, function(a, b)
if a.priority ~= b.priority then
return a.priority > b.priority
end
return a.count > b.count
end)
-- Greedy consolidation
for i = 1, table.getn(group.stacks) do
local source = group.stacks[i]
if source.count < maxStack then
for j = i + 1, table.getn(group.stacks) do
local target = group.stacks[j]
if target.count > 0 then
local spaceAvailable = maxStack - source.count
local amountToMove = math.min(spaceAvailable, target.count)
if amountToMove > 0 then
-- Split from target and add to source
if amountToMove < target.count then
SplitContainerItem(target.bagID, target.slot, amountToMove)
PickupContainerItem(source.bagID, source.slot)
ClearCursor()
else
-- Move entire stack
PickupContainerItem(target.bagID, target.slot)
PickupContainerItem(source.bagID, source.slot)
ClearCursor()
end
source.count = source.count + amountToMove
target.count = target.count - amountToMove
consolidationMoves = consolidationMoves + 1
if source.count >= maxStack then
break
end
end
end
end
end
end
end
end
end
return consolidationMoves
end
--===========================================================================
-- PHASE 4: Categorical Sorting
--===========================================================================
local function AddSortKeys(items)
for _, item in ipairs(items) do
if item.data and item.data.link then
local itemID = GetItemID(item.data.link)
local classID = GetItemClassID(item.data.link)
local _, _, quality, iLevel = GetItemInfo(item.data.link)
-- Priority: Hearthstone = 1, everything else = 1000
item.priority = PriorityItems[itemID] and 1 or 1000
-- Container priority
item.containerPriority = addon.Modules.Utils:GetContainerPriority(item.bagID)
-- Get sorted class order
item.sortedClass = classOrder[classID] or 99
-- Inverted quality (higher quality = earlier)
item.invertedQuality = -(quality or 0)
-- Inverted item level
item.invertedItemLevel = -(iLevel or 0)
-- Item name for alphabetical sorting
item.itemName = item.name or ""
-- Inverted count (larger stacks first)
item.invertedCount = -(item.data.count or 1)
-- Inverted item ID for consistent sorting
item.invertedItemID = -(itemID)
end
end
end
local function SortItems(items)
AddSortKeys(items)
table.sort(items, function(a, b)
-- Priority items first (Hearthstone)
if a.priority ~= b.priority then
return a.priority < b.priority
end
-- Container type (Soul > Quiver > Ammo > Regular)
if a.containerPriority ~= b.containerPriority then
return a.containerPriority > b.containerPriority
end
-- Item category
if a.sortedClass ~= b.sortedClass then
return a.sortedClass < b.sortedClass
end
-- Quality (Epic > Rare > Uncommon > Common > Poor)
if a.invertedQuality ~= b.invertedQuality then
return a.invertedQuality < b.invertedQuality
end
-- Item level (descending)
if a.invertedItemLevel ~= b.invertedItemLevel then
return a.invertedItemLevel < b.invertedItemLevel
end
-- Alphabetically by name
if a.itemName ~= b.itemName then
return a.itemName < b.itemName
end
-- Item ID (group same items together)
if a.invertedItemID ~= b.invertedItemID then
return a.invertedItemID < b.invertedItemID
end
-- Stack count (larger stacks first within same item)
return a.invertedCount < b.invertedCount
end)
return items
end
--===========================================================================
-- PHASE 5: Empty Slot Management & Apply Sort
--===========================================================================
local function CollectItems(bagIDs)
local items = {}
for _, bagID in ipairs(bagIDs) do
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if addon.Modules.Utils:IsBagValid(bagID) then
for slot = 1, numSlots do
local itemData = addon.Modules.BagScanner:ScanSlot(bagID, slot)
if itemData then
table.insert(items, {
bagID = bagID,
slot = slot,
data = itemData,
quality = itemData.quality or 0,
name = itemData.name or "",
class = itemData.class or "",
})
end
end
end
end
return items
end
local function BuildTargetPositions(bagIDs, itemCount)
local positions = {}
local index = 1
-- Sort bags by priority: Soul (40) > Quiver (30) > Ammo (20) > Regular (10)
local sortedBags = {}
for _, bagID in ipairs(bagIDs) do
table.insert(sortedBags, {
bagID = bagID,
priority = addon.Modules.Utils:GetContainerPriority(bagID)
})
end
-- Sort bags by priority descending (highest first), then by bag ID ascending
table.sort(sortedBags, function(a, b)
if a.priority ~= b.priority then
return a.priority > b.priority
end
-- When priorities are equal, sort by bag ID ascending (0, 1, 2, 3, 4)
return a.bagID < b.bagID
end)
-- Build positions in priority order
for _, bagInfo in ipairs(sortedBags) do
local bagID = bagInfo.bagID
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if addon.Modules.Utils:IsBagValid(bagID) then
for slot = 1, numSlots do
if index <= itemCount then
positions[index] = {bag = bagID, slot = slot}
index = index + 1
else
break
end
end
end
if index > itemCount then
break
end
end
return positions
end
local function ApplySort(bagIDs, items, targetPositions)
ClearCursor()
local moveQueue0 = {} -- Moves to empty slots
local moveQueue1 = {} -- Swaps with occupied slots
-- Build move queues
for i, item in ipairs(items) do
local target = targetPositions[i]
if target then
local sourceBag, sourceSlot = item.bagID, item.slot
local targetBag, targetSlot = target.bag, target.slot
-- Skip if already in correct position
if sourceBag ~= targetBag or sourceSlot ~= targetSlot then
-- Check if target slot is empty
local targetItem = GetContainerItemLink(targetBag, targetSlot)
if not targetItem then
-- Target is empty - Phase 1 move
table.insert(moveQueue0, {
sourceBag = sourceBag,
sourceSlot = sourceSlot,
targetBag = targetBag,
targetSlot = targetSlot,
})
else
-- Target is occupied - Phase 2 swap
table.insert(moveQueue1, {
sourceBag = sourceBag,
sourceSlot = sourceSlot,
targetBag = targetBag,
targetSlot = targetSlot,
})
end
end
end
end
-- Execute Phase 1: Move to empty slots first
local moveCount = 0
for _, move in ipairs(moveQueue0) do
local _, _, locked = GetContainerItemInfo(move.sourceBag, move.sourceSlot)
if not locked then
PickupContainerItem(move.sourceBag, move.sourceSlot)
PickupContainerItem(move.targetBag, move.targetSlot)
ClearCursor()
moveCount = moveCount + 1
end
end
-- Execute Phase 2: Swap with occupied slots
for _, move in ipairs(moveQueue1) do
local _, _, sourceLocked = GetContainerItemInfo(move.sourceBag, move.sourceSlot)
local _, _, targetLocked = GetContainerItemInfo(move.targetBag, move.targetSlot)
if not sourceLocked and not targetLocked then
PickupContainerItem(move.sourceBag, move.sourceSlot)
PickupContainerItem(move.targetBag, move.targetSlot)
ClearCursor()
moveCount = moveCount + 1
end
end
return moveCount
end
--===========================================================================
-- PHASE 6: Final Validation (Informational)
--===========================================================================
local function ValidateSort(bagIDs)
-- This is mostly informational - in practice, the sort should be correct
-- Could add checks here if needed for debugging
return true
end
--===========================================================================
-- Main Sort Functions
--===========================================================================
function SortEngine:SortBags()
local bagIDs = addon.Constants.BAGS
-- Phase 1: Detect specialized bags
local containers = DetectSpecializedBags(bagIDs)
-- Phase 2: Route specialized items to their bags
local routeCount = RouteSpecializedItems(bagIDs, containers)
-- Phase 3: Consolidate stacks in ALL bags (including specialized)
local consolidateCount = ConsolidateStacks(bagIDs)
-- Phase 4: Sort items WITHIN each specialized bag (soul, quiver, ammo)
local specializedMoves = 0
for _, bagType in ipairs({"soul", "quiver", "ammo"}) do
local specialBags = containers[bagType]
for _, bagID in ipairs(specialBags) do
-- Sort items within this single specialized bag
local items = CollectItems({bagID})
if table.getn(items) > 0 then
items = SortItems(items)
local targetPositions = BuildTargetPositions({bagID}, table.getn(items))
local moveCount = ApplySort({bagID}, items, targetPositions)
specializedMoves = specializedMoves + moveCount
end
end
end
-- Phase 5: Categorical sort regular bags
local regularMoves = 0
local regularBagIDs = containers.regular
if table.getn(regularBagIDs) > 0 then
local items = CollectItems(regularBagIDs)
if table.getn(items) > 0 then
items = SortItems(items)
local targetPositions = BuildTargetPositions(regularBagIDs, table.getn(items))
regularMoves = ApplySort(regularBagIDs, items, targetPositions)
end
end
-- Return total moves made (used to determine if another pass is needed)
return routeCount + consolidateCount + specializedMoves + regularMoves
end
function SortEngine:SortBank()
if not addon.Modules.BankScanner:IsBankOpen() then
addon:Print("Bank must be open to sort!")
return 0
end
local bagIDs = addon.Constants.BANK_BAGS
-- Phase 1: Detect specialized bags
local containers = DetectSpecializedBags(bagIDs)
-- Phase 2: Route specialized items
local routeCount = RouteSpecializedItems(bagIDs, containers)
-- Phase 3: Consolidate stacks
local consolidateCount = ConsolidateStacks(bagIDs)
-- Phase 4: Sort items WITHIN each specialized bag
local specializedMoves = 0
for _, bagType in ipairs({"soul", "quiver", "ammo"}) do
local specialBags = containers[bagType]
for _, bagID in ipairs(specialBags) do
local items = CollectItems({bagID})
if table.getn(items) > 0 then
items = SortItems(items)
local targetPositions = BuildTargetPositions({bagID}, table.getn(items))
local moveCount = ApplySort({bagID}, items, targetPositions)
specializedMoves = specializedMoves + moveCount
end
end
end
-- Phase 5: Sort regular bags
local regularBagIDs = containers.regular
local regularMoves = 0
if table.getn(regularBagIDs) > 0 then
local items = CollectItems(regularBagIDs)
if table.getn(items) > 0 then
items = SortItems(items)
local targetPositions = BuildTargetPositions(regularBagIDs, table.getn(items))
regularMoves = ApplySort(regularBagIDs, items, targetPositions)
end
end
-- Return total moves made
return routeCount + consolidateCount + specializedMoves + regularMoves
end