feat: improved sorting added analyse and prediction of maxPasses before start sorting
This commit is contained in:
+7
-5
@@ -66,9 +66,11 @@ function BagScanner:ScanSlot(bagID, slot)
|
||||
local itemLink = GetContainerItemLink(bagID, slot)
|
||||
|
||||
-- Get item info
|
||||
local name, link, itemQuality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, iconTexture
|
||||
-- Correct order: itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType,
|
||||
-- itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice
|
||||
local name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice
|
||||
if itemLink then
|
||||
name, link, itemQuality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, iconTexture = addon.Modules.Utils:GetItemInfo(itemLink)
|
||||
name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink)
|
||||
end
|
||||
|
||||
return {
|
||||
@@ -78,9 +80,9 @@ function BagScanner:ScanSlot(bagID, slot)
|
||||
quality = quality or itemQuality or 0,
|
||||
name = name,
|
||||
iLevel = iLevel,
|
||||
class = class,
|
||||
subclass = subclass,
|
||||
equipSlot = equipSlot,
|
||||
class = itemCategory, -- Category (e.g., "Consumable", "Armor")
|
||||
subclass = itemSubType, -- SubType (e.g., "Potion", "Cloth")
|
||||
equipSlot = itemEquipLoc, -- Equipment slot (e.g., "INVTYPE_HEAD") - correct now!
|
||||
locked = locked,
|
||||
}
|
||||
end
|
||||
|
||||
+230
-15
@@ -16,7 +16,7 @@ addon.Modules.SortEngine = SortEngine
|
||||
|
||||
-- Priority items that should always be sorted first
|
||||
local PRIORITY_ITEMS = {
|
||||
[6948] = true, -- Hearthstone
|
||||
[6948] = 1, -- Hearthstone (highest priority)
|
||||
}
|
||||
|
||||
-- Equipment slot ordering (for sorting equippable gear by slot)
|
||||
@@ -110,6 +110,21 @@ local function GetTexturePattern(textureName)
|
||||
return cleaned
|
||||
end
|
||||
|
||||
-- Check if an item is a mount by texture path
|
||||
local function IsMount(itemTexture)
|
||||
if not itemTexture then return false end
|
||||
|
||||
local textureLower = string.lower(itemTexture)
|
||||
|
||||
-- Check for mount patterns in texture path
|
||||
-- Mount textures typically contain "mount" or "ability_mount"
|
||||
if string.find(textureLower, "mount") then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Determine subclass order for grouping related items
|
||||
local function GetSubclassOrder(subclass, itemName)
|
||||
if not subclass then return 50 end
|
||||
@@ -362,9 +377,19 @@ local function AddSortKeys(items)
|
||||
-- Check if item is equippable (Armor or Weapon category)
|
||||
local isEquippable = itemCategory == "Armor" or itemCategory == "Weapon"
|
||||
|
||||
-- Priority
|
||||
item.priority = PRIORITY_ITEMS[itemID] and 1 or 1000
|
||||
-- Check if item is a mount (by texture path)
|
||||
local isMount = IsMount(itemTexture)
|
||||
|
||||
-- Priority (1 = hearthstone, 2 = mounts, 1000 = everything else)
|
||||
if PRIORITY_ITEMS[itemID] then
|
||||
item.priority = PRIORITY_ITEMS[itemID]
|
||||
elseif isMount then
|
||||
item.priority = 2
|
||||
else
|
||||
item.priority = 1000
|
||||
end
|
||||
item.isEquippable = isEquippable
|
||||
item.isMount = isMount
|
||||
|
||||
-- Class and slot ordering
|
||||
if isEquippable then
|
||||
@@ -454,14 +479,22 @@ local function SortItems(items)
|
||||
end
|
||||
end
|
||||
|
||||
-- 5. Final tiebreakers
|
||||
if a.invertedItemLevel ~= b.invertedItemLevel then
|
||||
return a.invertedItemLevel < b.invertedItemLevel
|
||||
end
|
||||
-- 5. Final tiebreakers (group identical items together)
|
||||
-- Sort by itemID first to ensure identical items are adjacent
|
||||
if a.invertedItemID ~= b.invertedItemID then
|
||||
return a.invertedItemID < b.invertedItemID
|
||||
end
|
||||
return a.invertedCount < b.invertedCount
|
||||
-- Then by item level
|
||||
if a.invertedItemLevel ~= b.invertedItemLevel then
|
||||
return a.invertedItemLevel < b.invertedItemLevel
|
||||
end
|
||||
-- Then by stack count (larger stacks first)
|
||||
if a.invertedCount ~= b.invertedCount then
|
||||
return a.invertedCount < b.invertedCount
|
||||
end
|
||||
-- Final stable sort: preserve original collection order for identical items
|
||||
-- This prevents unnecessary reshuffling when items are already sorted
|
||||
return a.sequence < b.sequence
|
||||
end)
|
||||
|
||||
return items
|
||||
@@ -473,22 +506,41 @@ end
|
||||
|
||||
local function CollectItems(bagIDs)
|
||||
local items = {}
|
||||
local sequence = 0 -- Add sequence number for stable sort
|
||||
|
||||
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)
|
||||
-- Scan directly from game API instead of cached data
|
||||
local texture, itemCount, locked = GetContainerItemInfo(bagID, slot)
|
||||
local itemLink = GetContainerItemLink(bagID, slot)
|
||||
|
||||
if itemData then
|
||||
if itemLink then
|
||||
-- Get fresh item info with ALL return values
|
||||
local itemID = GetItemID(itemLink)
|
||||
local name, link, quality, iLevel, category, itemType, stackCount, subType, iconTex, equipLoc, sellPrice = GetItemInfo(itemID)
|
||||
|
||||
sequence = sequence + 1
|
||||
table.insert(items, {
|
||||
bagID = bagID,
|
||||
slot = slot,
|
||||
data = itemData,
|
||||
quality = itemData.quality or 0,
|
||||
name = itemData.name or "",
|
||||
class = itemData.class or "",
|
||||
sequence = sequence, -- Preserve original order
|
||||
data = {
|
||||
link = itemLink,
|
||||
texture = texture,
|
||||
count = itemCount or 1,
|
||||
quality = quality or 0,
|
||||
name = name,
|
||||
iLevel = iLevel,
|
||||
class = category,
|
||||
subclass = subType, -- NOW INCLUDED!
|
||||
locked = locked,
|
||||
},
|
||||
quality = quality or 0,
|
||||
name = name or "",
|
||||
class = category or "",
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -608,13 +660,176 @@ local function ApplySort(bagIDs, items, targetPositions)
|
||||
end
|
||||
|
||||
--===========================================================================
|
||||
-- Main Sort Functions
|
||||
-- PHASE 6: Sort Complexity Analysis
|
||||
--===========================================================================
|
||||
|
||||
-- Analyze how many sorting passes are needed
|
||||
-- Returns: {passes, itemsOutOfPlace, totalItems, alreadySorted}
|
||||
local function AnalyzeSortComplexity(bagIDs)
|
||||
-- Collect current items
|
||||
local items = CollectItems(bagIDs)
|
||||
local totalItems = table.getn(items)
|
||||
|
||||
if totalItems == 0 then
|
||||
return {passes = 0, itemsOutOfPlace = 0, totalItems = 0, alreadySorted = true}
|
||||
end
|
||||
|
||||
-- Sort to get desired order
|
||||
local sortedItems = SortItems(items)
|
||||
|
||||
-- Build target positions
|
||||
local targetPositions = BuildTargetPositions(bagIDs, totalItems)
|
||||
|
||||
-- Create position maps for quick lookup
|
||||
local currentPositions = {} -- [bagID_slot] = itemLink
|
||||
local targetMap = {} -- [itemLink] = {targetBag, targetSlot, currentIndex}
|
||||
|
||||
for i, item in ipairs(items) do
|
||||
local key = item.bagID .. "_" .. item.slot
|
||||
currentPositions[key] = item.data.link
|
||||
end
|
||||
|
||||
for i, item in ipairs(sortedItems) do
|
||||
local target = targetPositions[i]
|
||||
if target and item.data.link then
|
||||
if not targetMap[item.data.link] then
|
||||
targetMap[item.data.link] = {}
|
||||
end
|
||||
table.insert(targetMap[item.data.link], {
|
||||
targetBag = target.bag,
|
||||
targetSlot = target.slot,
|
||||
currentIndex = i,
|
||||
sourceBag = item.bagID,
|
||||
sourceSlot = item.slot
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Count items that are already in correct position
|
||||
local itemsInPlace = 0
|
||||
local itemsOutOfPlace = 0
|
||||
local maxDisplacement = 0
|
||||
|
||||
for i, item in ipairs(sortedItems) do
|
||||
local target = targetPositions[i]
|
||||
if target then
|
||||
local sourceBag, sourceSlot = item.bagID, item.slot
|
||||
local targetBag, targetSlot = target.bag, target.slot
|
||||
|
||||
if sourceBag == targetBag and sourceSlot == targetSlot then
|
||||
itemsInPlace = itemsInPlace + 1
|
||||
else
|
||||
itemsOutOfPlace = itemsOutOfPlace + 1
|
||||
|
||||
-- Calculate displacement (how far the item needs to move)
|
||||
local displacement = math.abs(i - itemsInPlace - itemsOutOfPlace)
|
||||
if displacement > maxDisplacement then
|
||||
maxDisplacement = displacement
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If all items are in place, no sorting needed
|
||||
if itemsOutOfPlace == 0 then
|
||||
return {passes = 0, itemsOutOfPlace = 0, totalItems = totalItems, alreadySorted = true}
|
||||
end
|
||||
|
||||
-- Estimate passes needed based on displacement complexity
|
||||
-- Simple heuristic:
|
||||
-- - If displacement is low (< 20% of items), probably 1-2 passes
|
||||
-- - If displacement is medium (20-50%), probably 2-4 passes
|
||||
-- - If displacement is high (>50%), might need 3-6 passes
|
||||
local displacementRatio = itemsOutOfPlace / totalItems
|
||||
local estimatedPasses
|
||||
|
||||
if displacementRatio < 0.1 then
|
||||
estimatedPasses = 1
|
||||
elseif displacementRatio < 0.3 then
|
||||
estimatedPasses = 2
|
||||
elseif displacementRatio < 0.5 then
|
||||
estimatedPasses = 3
|
||||
elseif displacementRatio < 0.7 then
|
||||
estimatedPasses = 4
|
||||
else
|
||||
-- High complexity - analyze cycles
|
||||
-- Count how many swaps are needed vs moves to empty
|
||||
local needsSwap = 0
|
||||
for i, item in ipairs(sortedItems) do
|
||||
local target = targetPositions[i]
|
||||
if target then
|
||||
local sourceBag, sourceSlot = item.bagID, item.slot
|
||||
local targetBag, targetSlot = target.bag, target.slot
|
||||
|
||||
if sourceBag ~= targetBag or sourceSlot ~= targetSlot then
|
||||
local targetKey = targetBag .. "_" .. targetSlot
|
||||
local targetOccupied = currentPositions[targetKey]
|
||||
if targetOccupied then
|
||||
needsSwap = needsSwap + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- More swaps needed = more passes
|
||||
local swapRatio = needsSwap / itemsOutOfPlace
|
||||
if swapRatio > 0.8 then
|
||||
estimatedPasses = 6
|
||||
elseif swapRatio > 0.5 then
|
||||
estimatedPasses = 5
|
||||
else
|
||||
estimatedPasses = 4
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
passes = estimatedPasses,
|
||||
itemsOutOfPlace = itemsOutOfPlace,
|
||||
totalItems = totalItems,
|
||||
alreadySorted = false
|
||||
}
|
||||
end
|
||||
|
||||
--===========================================================================
|
||||
-- Main Sort Functions
|
||||
--===========================================================================
|
||||
|
||||
-- Analyze bags to determine how many passes are needed
|
||||
function SortEngine:AnalyzeBags()
|
||||
local bagIDs = addon.Constants.BAGS
|
||||
|
||||
-- Detect specialized bags
|
||||
local containers = DetectSpecializedBags(bagIDs)
|
||||
|
||||
-- Analyze regular bags only (specialized bags sort separately)
|
||||
local regularBagIDs = containers.regular
|
||||
if table.getn(regularBagIDs) > 0 then
|
||||
return AnalyzeSortComplexity(regularBagIDs)
|
||||
else
|
||||
return {passes = 0, itemsOutOfPlace = 0, totalItems = 0, alreadySorted = true}
|
||||
end
|
||||
end
|
||||
|
||||
-- Analyze bank to determine how many passes are needed
|
||||
function SortEngine:AnalyzeBank()
|
||||
if not addon.Modules.BankScanner:IsBankOpen() then
|
||||
return {passes = 0, itemsOutOfPlace = 0, totalItems = 0, alreadySorted = true}
|
||||
end
|
||||
|
||||
local bagIDs = addon.Constants.BANK_BAGS
|
||||
|
||||
-- Detect specialized bags
|
||||
local containers = DetectSpecializedBags(bagIDs)
|
||||
|
||||
-- Analyze regular bags only
|
||||
local regularBagIDs = containers.regular
|
||||
if table.getn(regularBagIDs) > 0 then
|
||||
return AnalyzeSortComplexity(regularBagIDs)
|
||||
else
|
||||
return {passes = 0, itemsOutOfPlace = 0, totalItems = 0, alreadySorted = true}
|
||||
end
|
||||
end
|
||||
|
||||
function SortEngine:SortBags()
|
||||
local bagIDs = addon.Constants.BAGS
|
||||
|
||||
|
||||
+22
-8
@@ -1069,17 +1069,29 @@ function Guda_BagFrame_ToggleKeyring()
|
||||
BagFrame:Update()
|
||||
end
|
||||
|
||||
-- Sort button handler with auto-repeat
|
||||
-- Sort button handler with auto-repeat and smart pass calculation
|
||||
function Guda_BagFrame_Sort()
|
||||
if currentViewChar then
|
||||
addon:Print("Cannot sort another character's bags!")
|
||||
return
|
||||
end
|
||||
|
||||
addon:Print("Sorting bags...")
|
||||
-- Analyze bags to determine how many passes are needed
|
||||
local analysis = addon.Modules.SortEngine:AnalyzeBags()
|
||||
|
||||
-- Check if already sorted
|
||||
if analysis.alreadySorted then
|
||||
addon:Print("Bags are already sorted!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Print analysis results
|
||||
addon:Print("Sorting bags... (%d/%d items need sorting, estimated %d passes)",
|
||||
analysis.itemsOutOfPlace, analysis.totalItems, analysis.passes)
|
||||
|
||||
local passCount = 0
|
||||
local maxPasses = 10 -- Safety limit
|
||||
local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1
|
||||
local safetyLimit = maxPasses + 3 -- Add 3 extra passes as safety margin
|
||||
|
||||
local function DoSortPass()
|
||||
passCount = passCount + 1
|
||||
@@ -1088,8 +1100,8 @@ function Guda_BagFrame_Sort()
|
||||
local moveCount = addon.Modules.SortEngine:SortBags()
|
||||
|
||||
-- If items were moved and we haven't hit the limit, do another pass
|
||||
if moveCount > 0 and passCount < maxPasses then
|
||||
-- Wait for items to settle, then sort again (longer delay for many items)
|
||||
if moveCount > 0 and passCount < safetyLimit then
|
||||
-- Wait for items to settle, then sort again
|
||||
local frame = CreateFrame("Frame")
|
||||
local elapsed = 0
|
||||
frame:SetScript("OnUpdate", function()
|
||||
@@ -1101,10 +1113,12 @@ function Guda_BagFrame_Sort()
|
||||
end)
|
||||
else
|
||||
-- Sorting complete
|
||||
if passCount >= maxPasses then
|
||||
addon:Print("Sort complete! (reached max passes)")
|
||||
if passCount >= safetyLimit then
|
||||
addon:Print("Sort complete! (reached safety limit after %d passes)", passCount)
|
||||
elseif passCount <= maxPasses then
|
||||
addon:Print("Sort complete! (%d passes, as predicted)", passCount)
|
||||
else
|
||||
addon:Print("Sort complete! (%d passes)", passCount)
|
||||
addon:Print("Sort complete! (%d passes, %d more than estimated)", passCount, passCount - maxPasses)
|
||||
end
|
||||
|
||||
-- Final update
|
||||
|
||||
+22
-8
@@ -494,17 +494,29 @@ function Guda_BankFrame_OnSearchChanged(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort button handler with auto-repeat
|
||||
-- Sort button handler with auto-repeat and smart pass calculation
|
||||
function Guda_BankFrame_Sort()
|
||||
if isReadOnlyMode or currentViewChar then
|
||||
addon:Print("Cannot sort in read-only mode!")
|
||||
return
|
||||
end
|
||||
|
||||
addon:Print("Sorting bank...")
|
||||
-- Analyze bank to determine how many passes are needed
|
||||
local analysis = addon.Modules.SortEngine:AnalyzeBank()
|
||||
|
||||
-- Check if already sorted
|
||||
if analysis.alreadySorted then
|
||||
addon:Print("Bank is already sorted!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Print analysis results
|
||||
addon:Print("Sorting bank... (%d/%d items need sorting, estimated %d passes)",
|
||||
analysis.itemsOutOfPlace, analysis.totalItems, analysis.passes)
|
||||
|
||||
local passCount = 0
|
||||
local maxPasses = 10 -- Safety limit
|
||||
local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1
|
||||
local safetyLimit = maxPasses + 3 -- Add 3 extra passes as safety margin
|
||||
|
||||
local function DoSortPass()
|
||||
passCount = passCount + 1
|
||||
@@ -513,8 +525,8 @@ function Guda_BankFrame_Sort()
|
||||
local moveCount = addon.Modules.SortEngine:SortBank()
|
||||
|
||||
-- If items were moved and we haven't hit the limit, do another pass
|
||||
if moveCount > 0 and passCount < maxPasses then
|
||||
-- Wait for items to settle, then sort again (longer delay for many items)
|
||||
if moveCount > 0 and passCount < safetyLimit then
|
||||
-- Wait for items to settle, then sort again
|
||||
local frame = CreateFrame("Frame")
|
||||
local elapsed = 0
|
||||
frame:SetScript("OnUpdate", function()
|
||||
@@ -526,10 +538,12 @@ function Guda_BankFrame_Sort()
|
||||
end)
|
||||
else
|
||||
-- Sorting complete
|
||||
if passCount >= maxPasses then
|
||||
addon:Print("Bank sort complete! (reached max passes)")
|
||||
if passCount >= safetyLimit then
|
||||
addon:Print("Bank sort complete! (reached safety limit after %d passes)", passCount)
|
||||
elseif passCount <= maxPasses then
|
||||
addon:Print("Bank sort complete! (%d passes, as predicted)", passCount)
|
||||
else
|
||||
addon:Print("Bank sort complete! (%d passes)", passCount)
|
||||
addon:Print("Bank sort complete! (%d passes, %d more than estimated)", passCount, passCount - maxPasses)
|
||||
end
|
||||
|
||||
-- Final update
|
||||
|
||||
Reference in New Issue
Block a user