diff --git a/Data/BagScanner.lua b/Data/BagScanner.lua index b769fd1..13b308a 100644 --- a/Data/BagScanner.lua +++ b/Data/BagScanner.lua @@ -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 diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index b37e570..4742609 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -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 diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 1c21ba3..ebbd6f5 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -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 diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index 4a95e21..a1bcdfc 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -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