283 lines
8.3 KiB
Lua
283 lines
8.3 KiB
Lua
-- Guda Sort Engine
|
|
-- Adapted from Baganator sorting logic 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 (matches Baganator's logical grouping)
|
|
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)
|
|
-- In 1.12.1, GetItemInfo returns class as string, need to map to ID
|
|
-- We'll use a simple hash of the string for ordering
|
|
if not class then return 99 end
|
|
|
|
-- Map common class names to IDs (1.12.1 compatible)
|
|
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
|
|
|
|
-- Add sort keys to items
|
|
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)
|
|
|
|
-- Priority: Hearthstone = 1, everything else = 1000
|
|
item.priority = PriorityItems[itemID] and 1 or 1000
|
|
|
|
-- Get sorted class order (lower = earlier in bags)
|
|
item.sortedClass = classOrder[classID] or 99
|
|
|
|
-- Inverted quality (higher quality = earlier)
|
|
item.invertedQuality = -(item.quality or 0)
|
|
|
|
-- Item name for alphabetical sorting
|
|
item.itemName = item.name or ""
|
|
|
|
-- Inverted item ID for consistent sorting
|
|
item.invertedItemID = -(itemID)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Sort items using multi-criteria comparison
|
|
local function SortItems(items)
|
|
AddSortKeys(items)
|
|
|
|
table.sort(items, function(a, b)
|
|
-- Sort by priority first (Hearthstone)
|
|
if a.priority ~= b.priority then
|
|
return a.priority < b.priority
|
|
end
|
|
|
|
-- Then by class (Consumables, Ammo, Weapons, Armor, etc.)
|
|
if a.sortedClass ~= b.sortedClass then
|
|
return a.sortedClass < b.sortedClass
|
|
end
|
|
|
|
-- Within same class, sort by quality (Epic > Rare > Uncommon > Common > Poor)
|
|
if a.invertedQuality ~= b.invertedQuality then
|
|
return a.invertedQuality < b.invertedQuality
|
|
end
|
|
|
|
-- Then alphabetically by name
|
|
if a.itemName ~= b.itemName then
|
|
return a.itemName < b.itemName
|
|
end
|
|
|
|
-- Finally by item ID for consistency
|
|
return a.invertedItemID < b.invertedItemID
|
|
end)
|
|
|
|
return items
|
|
end
|
|
|
|
-- Collect all items from bags
|
|
function SortEngine: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
|
|
|
|
-- Build target slot positions (sequential, no gaps)
|
|
local function BuildTargetPositions(bagIDs, itemCount)
|
|
local positions = {}
|
|
local index = 1
|
|
|
|
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
|
|
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
|
|
|
|
-- Apply sorted items back to bags (two-phase move system from Baganator)
|
|
function SortEngine:ApplySort(bagIDs, method)
|
|
addon:Print("Sorting bags...")
|
|
|
|
-- Collect and sort items
|
|
local items = self:CollectItems(bagIDs)
|
|
|
|
if table.getn(items) == 0 then
|
|
addon:Print("No items to sort!")
|
|
return
|
|
end
|
|
|
|
items = SortItems(items)
|
|
|
|
-- Build target positions (sequential slots)
|
|
local targetPositions = BuildTargetPositions(bagIDs, table.getn(items))
|
|
|
|
-- Clear cursor
|
|
ClearCursor()
|
|
|
|
-- Two-phase move system (from Baganator)
|
|
-- Phase 1: Moves to empty slots
|
|
-- Phase 2: Swaps with occupied slots
|
|
|
|
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
|
|
|
|
if moveCount > 0 then
|
|
addon:Print("Sort complete! (%d items moved)", moveCount)
|
|
else
|
|
addon:Print("Items are already sorted!")
|
|
end
|
|
end
|
|
|
|
-- Sort current bags
|
|
function SortEngine:SortBags()
|
|
self:ApplySort(addon.Constants.BAGS, "type")
|
|
end
|
|
|
|
-- Sort bank
|
|
function SortEngine:SortBank()
|
|
if not addon.Modules.BankScanner:IsBankOpen() then
|
|
addon:Print("Bank must be open to sort!")
|
|
return
|
|
end
|
|
|
|
self:ApplySort(addon.Constants.BANK_BAGS, "type")
|
|
end
|