feat: improved sorting added analyse and prediction of maxPasses before start sorting

This commit is contained in:
Salikh Gurgenidze
2025-11-20 15:01:17 +04:00
parent 6d3d570b22
commit 97e1cb502a
4 changed files with 281 additions and 36 deletions
+7 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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