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) local itemLink = GetContainerItemLink(bagID, slot)
-- Get item info -- 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 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 end
return { return {
@@ -78,9 +80,9 @@ function BagScanner:ScanSlot(bagID, slot)
quality = quality or itemQuality or 0, quality = quality or itemQuality or 0,
name = name, name = name,
iLevel = iLevel, iLevel = iLevel,
class = class, class = itemCategory, -- Category (e.g., "Consumable", "Armor")
subclass = subclass, subclass = itemSubType, -- SubType (e.g., "Potion", "Cloth")
equipSlot = equipSlot, equipSlot = itemEquipLoc, -- Equipment slot (e.g., "INVTYPE_HEAD") - correct now!
locked = locked, locked = locked,
} }
end end
+230 -15
View File
@@ -16,7 +16,7 @@ addon.Modules.SortEngine = SortEngine
-- Priority items that should always be sorted first -- Priority items that should always be sorted first
local PRIORITY_ITEMS = { local PRIORITY_ITEMS = {
[6948] = true, -- Hearthstone [6948] = 1, -- Hearthstone (highest priority)
} }
-- Equipment slot ordering (for sorting equippable gear by slot) -- Equipment slot ordering (for sorting equippable gear by slot)
@@ -110,6 +110,21 @@ local function GetTexturePattern(textureName)
return cleaned return cleaned
end 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 -- Determine subclass order for grouping related items
local function GetSubclassOrder(subclass, itemName) local function GetSubclassOrder(subclass, itemName)
if not subclass then return 50 end if not subclass then return 50 end
@@ -362,9 +377,19 @@ local function AddSortKeys(items)
-- Check if item is equippable (Armor or Weapon category) -- Check if item is equippable (Armor or Weapon category)
local isEquippable = itemCategory == "Armor" or itemCategory == "Weapon" local isEquippable = itemCategory == "Armor" or itemCategory == "Weapon"
-- Priority -- Check if item is a mount (by texture path)
item.priority = PRIORITY_ITEMS[itemID] and 1 or 1000 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.isEquippable = isEquippable
item.isMount = isMount
-- Class and slot ordering -- Class and slot ordering
if isEquippable then if isEquippable then
@@ -454,14 +479,22 @@ local function SortItems(items)
end end
end end
-- 5. Final tiebreakers -- 5. Final tiebreakers (group identical items together)
if a.invertedItemLevel ~= b.invertedItemLevel then -- Sort by itemID first to ensure identical items are adjacent
return a.invertedItemLevel < b.invertedItemLevel
end
if a.invertedItemID ~= b.invertedItemID then if a.invertedItemID ~= b.invertedItemID then
return a.invertedItemID < b.invertedItemID return a.invertedItemID < b.invertedItemID
end 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) end)
return items return items
@@ -473,22 +506,41 @@ end
local function CollectItems(bagIDs) local function CollectItems(bagIDs)
local items = {} local items = {}
local sequence = 0 -- Add sequence number for stable sort
for _, bagID in ipairs(bagIDs) do for _, bagID in ipairs(bagIDs) do
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if addon.Modules.Utils:IsBagValid(bagID) then if addon.Modules.Utils:IsBagValid(bagID) then
for slot = 1, numSlots do 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, { table.insert(items, {
bagID = bagID, bagID = bagID,
slot = slot, slot = slot,
data = itemData, sequence = sequence, -- Preserve original order
quality = itemData.quality or 0, data = {
name = itemData.name or "", link = itemLink,
class = itemData.class or "", 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
end end
@@ -608,13 +660,176 @@ local function ApplySort(bagIDs, items, targetPositions)
end 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 -- 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() function SortEngine:SortBags()
local bagIDs = addon.Constants.BAGS local bagIDs = addon.Constants.BAGS
+22 -8
View File
@@ -1069,17 +1069,29 @@ function Guda_BagFrame_ToggleKeyring()
BagFrame:Update() BagFrame:Update()
end end
-- Sort button handler with auto-repeat -- Sort button handler with auto-repeat and smart pass calculation
function Guda_BagFrame_Sort() function Guda_BagFrame_Sort()
if currentViewChar then if currentViewChar then
addon:Print("Cannot sort another character's bags!") addon:Print("Cannot sort another character's bags!")
return return
end 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 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() local function DoSortPass()
passCount = passCount + 1 passCount = passCount + 1
@@ -1088,8 +1100,8 @@ function Guda_BagFrame_Sort()
local moveCount = addon.Modules.SortEngine:SortBags() local moveCount = addon.Modules.SortEngine:SortBags()
-- If items were moved and we haven't hit the limit, do another pass -- If items were moved and we haven't hit the limit, do another pass
if moveCount > 0 and passCount < maxPasses then if moveCount > 0 and passCount < safetyLimit then
-- Wait for items to settle, then sort again (longer delay for many items) -- Wait for items to settle, then sort again
local frame = CreateFrame("Frame") local frame = CreateFrame("Frame")
local elapsed = 0 local elapsed = 0
frame:SetScript("OnUpdate", function() frame:SetScript("OnUpdate", function()
@@ -1101,10 +1113,12 @@ function Guda_BagFrame_Sort()
end) end)
else else
-- Sorting complete -- Sorting complete
if passCount >= maxPasses then if passCount >= safetyLimit then
addon:Print("Sort complete! (reached max passes)") 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 else
addon:Print("Sort complete! (%d passes)", passCount) addon:Print("Sort complete! (%d passes, %d more than estimated)", passCount, passCount - maxPasses)
end end
-- Final update -- Final update
+22 -8
View File
@@ -494,17 +494,29 @@ function Guda_BankFrame_OnSearchChanged(self)
end end
end end
-- Sort button handler with auto-repeat -- Sort button handler with auto-repeat and smart pass calculation
function Guda_BankFrame_Sort() function Guda_BankFrame_Sort()
if isReadOnlyMode or currentViewChar then if isReadOnlyMode or currentViewChar then
addon:Print("Cannot sort in read-only mode!") addon:Print("Cannot sort in read-only mode!")
return return
end 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 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() local function DoSortPass()
passCount = passCount + 1 passCount = passCount + 1
@@ -513,8 +525,8 @@ function Guda_BankFrame_Sort()
local moveCount = addon.Modules.SortEngine:SortBank() local moveCount = addon.Modules.SortEngine:SortBank()
-- If items were moved and we haven't hit the limit, do another pass -- If items were moved and we haven't hit the limit, do another pass
if moveCount > 0 and passCount < maxPasses then if moveCount > 0 and passCount < safetyLimit then
-- Wait for items to settle, then sort again (longer delay for many items) -- Wait for items to settle, then sort again
local frame = CreateFrame("Frame") local frame = CreateFrame("Frame")
local elapsed = 0 local elapsed = 0
frame:SetScript("OnUpdate", function() frame:SetScript("OnUpdate", function()
@@ -526,10 +538,12 @@ function Guda_BankFrame_Sort()
end) end)
else else
-- Sorting complete -- Sorting complete
if passCount >= maxPasses then if passCount >= safetyLimit then
addon:Print("Bank sort complete! (reached max passes)") 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 else
addon:Print("Bank sort complete! (%d passes)", passCount) addon:Print("Bank sort complete! (%d passes, %d more than estimated)", passCount, passCount - maxPasses)
end end
-- Final update -- Final update