From 948fee6b6f247e14e8a8a7d73234e7257ed39e2a Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Wed, 19 Nov 2025 01:52:13 +0400 Subject: [PATCH] sorting fixed --- Core/Utils.lua | 146 ++++++++++++++ Sorting/SortEngine.lua | 426 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 523 insertions(+), 49 deletions(-) diff --git a/Core/Utils.lua b/Core/Utils.lua index ee7db7c..6115ba0 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -207,3 +207,149 @@ function Utils:IsAmmoQuiverBag(bagID) return false end + +-- Get specialized bag type (for sorting priority) +-- Returns: "soul", "quiver", "ammo", or nil +function Utils:GetSpecializedBagType(bagID) + -- Skip backpack, bank, and keyring + if bagID == 0 or bagID == -1 or bagID == -2 then + return nil + end + + -- Get the bag item + local invSlot = ContainerIDToInventoryID(bagID) + if not invSlot then + return nil + end + + local link = GetInventoryItemLink("player", invSlot) + if not link then + return nil + end + + -- Use tooltip scanning + local tooltip = GetScanTooltip() + tooltip:ClearLines() + tooltip:SetInventoryItem("player", invSlot) + + -- Scan tooltip lines + for i = 1, tooltip:NumLines() do + local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text then + if string.find(text, "Soul") then + return "soul" + elseif string.find(text, "Quiver") then + return "quiver" + elseif string.find(text, "Ammo Pouch") then + return "ammo" + end + end + end + end + + return nil +end + +-- Get container priority for sorting (higher = more important) +function Utils:GetContainerPriority(bagID) + local bagType = self:GetSpecializedBagType(bagID) + if bagType == "soul" then + return 40 + elseif bagType == "quiver" then + return 30 + elseif bagType == "ammo" then + return 20 + else + return 10 -- Regular bag + end +end + +-- Soul Shard item ID +local SOUL_SHARD_ID = 6265 + +-- Check if item is a Soul Shard +function Utils:IsSoulShard(itemLink) + if not itemLink then return false end + local _, _, itemID = string.find(itemLink, "item:(%d+)") + return tonumber(itemID) == SOUL_SHARD_ID +end + +-- Extract hyperlink from item link for tooltip scanning +local function ExtractHyperlink(itemLink) + if not itemLink then return nil end + local _, _, hyperlink = string.find(itemLink, "|H(.+)|h") + return hyperlink +end + +-- Check if item is Arrow or Bullet (for Quiver routing) +function Utils:IsArrowOrBullet(itemLink) + if not itemLink then return false end + + local hyperlink = ExtractHyperlink(itemLink) + if not hyperlink then return false end + + -- Use tooltip scanning + local tooltip = GetScanTooltip() + tooltip:ClearLines() + tooltip:SetHyperlink(hyperlink) + + for i = 1, tooltip:NumLines() do + local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text then + if string.find(text, "Arrow") or string.find(text, "Bullet") then + return true + end + end + end + end + + return false +end + +-- Check if item is Ammo (any type) +function Utils:IsAmmo(itemLink) + if not itemLink then return false end + + local hyperlink = ExtractHyperlink(itemLink) + if not hyperlink then return false end + + -- Use tooltip scanning + local tooltip = GetScanTooltip() + tooltip:ClearLines() + tooltip:SetHyperlink(hyperlink) + + for i = 1, tooltip:NumLines() do + local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text then + -- Check for ammo keywords + if string.find(text, "Arrow") or + string.find(text, "Bullet") or + string.find(text, "Ammo") or + string.find(text, "Projectile") then + return true + end + end + end + end + + return false +end + +-- Get preferred container type for an item +-- Returns: "soul", "quiver", "ammo", or nil +function Utils:GetItemPreferredContainer(itemLink) + if self:IsSoulShard(itemLink) then + return "soul" + elseif self:IsArrowOrBullet(itemLink) then + return "quiver" + elseif self:IsAmmo(itemLink) then + return "ammo" + end + return nil +end diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 28dabc2..ae93d63 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -1,5 +1,5 @@ -- Guda Sort Engine --- Adapted from Baganator sorting logic for WoW 1.12.1 +-- 6-Phase Advanced Sorting Algorithm for WoW 1.12.1 local addon = Guda @@ -11,7 +11,7 @@ local PriorityItems = { [6948] = true, -- Hearthstone } --- Custom ordering for item classes (matches Baganator's logical grouping) +-- Custom ordering for item classes local classOrder = { [0] = 1, -- Consumable [6] = 2, -- Projectile (ammo/arrows) @@ -38,11 +38,8 @@ end 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, @@ -61,65 +58,289 @@ local function GetItemClassID(link) return classMap[class] or 99 end --- Add sort keys to items +--=========================================================================== +-- PHASE 1: Special Container Detection +--=========================================================================== + +local function DetectSpecializedBags(bagIDs) + local containers = { + soul = {}, + quiver = {}, + ammo = {}, + regular = {} + } + + for _, bagID in ipairs(bagIDs) do + local bagType = addon.Modules.Utils:GetSpecializedBagType(bagID) + if bagType == "soul" then + table.insert(containers.soul, bagID) + elseif bagType == "quiver" then + table.insert(containers.quiver, bagID) + elseif bagType == "ammo" then + table.insert(containers.ammo, bagID) + else + table.insert(containers.regular, bagID) + end + end + + return containers +end + +--=========================================================================== +-- PHASE 2: Specialized Item Routing +--=========================================================================== + +local function RouteSpecializedItems(bagIDs, containers) + -- This phase moves specialized items to their preferred containers + -- We'll do this by marking items for routing and then executing moves + + local routingPlan = {} + + -- Scan all items + for _, bagID in ipairs(bagIDs) do + local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) + if numSlots and numSlots > 0 then + for slot = 1, numSlots do + local link = GetContainerItemLink(bagID, slot) + if link then + local preferredType = addon.Modules.Utils:GetItemPreferredContainer(link) + + -- If item has a preferred container type and isn't already in one + if preferredType then + local currentBagType = addon.Modules.Utils:GetSpecializedBagType(bagID) + + -- If not already in preferred container + if currentBagType ~= preferredType then + local targetBags = containers[preferredType] + if targetBags and table.getn(targetBags) > 0 then + -- Find first available slot in preferred containers + local foundSlot = false + for _, targetBagID in ipairs(targetBags) do + if not foundSlot then + local targetSlots = addon.Modules.Utils:GetBagSlotCount(targetBagID) + for targetSlot = 1, targetSlots do + local targetLink = GetContainerItemLink(targetBagID, targetSlot) + if not targetLink then + -- Found empty slot - plan the move + table.insert(routingPlan, { + fromBag = bagID, + fromSlot = slot, + toBag = targetBagID, + toSlot = targetSlot + }) + foundSlot = true + break + end + end + end + end + end + end + end + end + end + end + end + + -- Execute routing plan + for _, move in ipairs(routingPlan) do + PickupContainerItem(move.fromBag, move.fromSlot) + PickupContainerItem(move.toBag, move.toSlot) + ClearCursor() + end + + return table.getn(routingPlan) +end + +--=========================================================================== +-- PHASE 3: Stack Consolidation +--=========================================================================== + +local function ConsolidateStacks(bagIDs) + -- Group items by (itemID, quality, soulbound) + local itemGroups = {} + + -- Collect all items with their locations + for _, bagID in ipairs(bagIDs) do + local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) + if numSlots and numSlots > 0 then + for slot = 1, numSlots do + local link = GetContainerItemLink(bagID, slot) + if link then + local texture, count, locked = GetContainerItemInfo(bagID, slot) + local itemID = GetItemID(link) + local _, _, quality = GetItemInfo(link) + + -- Create group key + local groupKey = itemID .. "_" .. (quality or 0) + + if not itemGroups[groupKey] then + itemGroups[groupKey] = { + itemID = itemID, + link = link, + stacks = {} + } + end + + table.insert(itemGroups[groupKey].stacks, { + bagID = bagID, + slot = slot, + count = count or 1, + priority = addon.Modules.Utils:GetContainerPriority(bagID) + }) + end + end + end + end + + -- For each group, consolidate stacks + local consolidationMoves = 0 + for _, group in pairs(itemGroups) do + if table.getn(group.stacks) > 1 then + -- Get max stack size + local _, _, _, _, _, _, _, maxStack = GetItemInfo(group.link) + maxStack = maxStack or 1 + + if maxStack > 1 then + -- Sort stacks by priority DESC, then count DESC + table.sort(group.stacks, function(a, b) + if a.priority ~= b.priority then + return a.priority > b.priority + end + return a.count > b.count + end) + + -- Greedy consolidation + for i = 1, table.getn(group.stacks) do + local source = group.stacks[i] + if source.count < maxStack then + for j = i + 1, table.getn(group.stacks) do + local target = group.stacks[j] + if target.count > 0 then + local spaceAvailable = maxStack - source.count + local amountToMove = math.min(spaceAvailable, target.count) + + if amountToMove > 0 then + -- Split from target and add to source + if amountToMove < target.count then + SplitContainerItem(target.bagID, target.slot, amountToMove) + PickupContainerItem(source.bagID, source.slot) + ClearCursor() + else + -- Move entire stack + PickupContainerItem(target.bagID, target.slot) + PickupContainerItem(source.bagID, source.slot) + ClearCursor() + end + + source.count = source.count + amountToMove + target.count = target.count - amountToMove + consolidationMoves = consolidationMoves + 1 + + if source.count >= maxStack then + break + end + end + end + end + end + end + end + end + end + + return consolidationMoves +end + +--=========================================================================== +-- PHASE 4: Categorical Sorting +--=========================================================================== + 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) + local _, _, quality, iLevel = GetItemInfo(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) + -- Container priority + item.containerPriority = addon.Modules.Utils:GetContainerPriority(item.bagID) + + -- Get sorted class order item.sortedClass = classOrder[classID] or 99 -- Inverted quality (higher quality = earlier) - item.invertedQuality = -(item.quality or 0) + item.invertedQuality = -(quality or 0) + + -- Inverted item level + item.invertedItemLevel = -(iLevel or 0) -- Item name for alphabetical sorting item.itemName = item.name or "" + -- Inverted count (larger stacks first) + item.invertedCount = -(item.data.count or 1) + -- 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) + -- Priority items first (Hearthstone) if a.priority ~= b.priority then return a.priority < b.priority end - -- Then by class (Consumables, Ammo, Weapons, Armor, etc.) + -- Container type (Soul > Quiver > Ammo > Regular) + if a.containerPriority ~= b.containerPriority then + return a.containerPriority > b.containerPriority + end + + -- Item category if a.sortedClass ~= b.sortedClass then return a.sortedClass < b.sortedClass end - -- Within same class, sort by quality (Epic > Rare > Uncommon > Common > Poor) + -- Quality (Epic > Rare > Uncommon > Common > Poor) if a.invertedQuality ~= b.invertedQuality then return a.invertedQuality < b.invertedQuality end - -- Then alphabetically by name + -- Item level (descending) + if a.invertedItemLevel ~= b.invertedItemLevel then + return a.invertedItemLevel < b.invertedItemLevel + end + + -- 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 + -- Item ID (group same items together) + if a.invertedItemID ~= b.invertedItemID then + return a.invertedItemID < b.invertedItemID + end + + -- Stack count (larger stacks first within same item) + return a.invertedCount < b.invertedCount end) return items end --- Collect all items from bags -function SortEngine:CollectItems(bagIDs) +--=========================================================================== +-- PHASE 5: Empty Slot Management & Apply Sort +--=========================================================================== + +local function CollectItems(bagIDs) local items = {} for _, bagID in ipairs(bagIDs) do @@ -146,12 +367,27 @@ function SortEngine:CollectItems(bagIDs) return items end --- Build target slot positions (sequential, no gaps) local function BuildTargetPositions(bagIDs, itemCount) local positions = {} local index = 1 + -- Sort bags by priority: Soul (40) > Quiver (30) > Ammo (20) > Regular (10) + local sortedBags = {} for _, bagID in ipairs(bagIDs) do + table.insert(sortedBags, { + bagID = bagID, + priority = addon.Modules.Utils:GetContainerPriority(bagID) + }) + end + + -- Sort bags by priority descending (highest first) + table.sort(sortedBags, function(a, b) + return a.priority > b.priority + end) + + -- Build positions in priority order + for _, bagInfo in ipairs(sortedBags) do + local bagID = bagInfo.bagID local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) if addon.Modules.Utils:IsBagValid(bagID) then @@ -173,30 +409,9 @@ local function BuildTargetPositions(bagIDs, itemCount) 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 +local function ApplySort(bagIDs, items, targetPositions) 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 @@ -259,24 +474,137 @@ function SortEngine:ApplySort(bagIDs, method) end end - if moveCount > 0 then - addon:Print("Sort complete! (%d items moved)", moveCount) - else - addon:Print("Items are already sorted!") - end + return moveCount end --- Sort current bags +--=========================================================================== +-- PHASE 6: Final Validation (Informational) +--=========================================================================== + +local function ValidateSort(bagIDs) + -- This is mostly informational - in practice, the sort should be correct + -- Could add checks here if needed for debugging + return true +end + +--=========================================================================== +-- Main Sort Functions +--=========================================================================== + function SortEngine:SortBags() - self:ApplySort(addon.Constants.BAGS, "type") + addon:Print("Starting 6-phase bag sort...") + + local bagIDs = addon.Constants.BAGS + + -- Phase 1: Detect specialized bags + local containers = DetectSpecializedBags(bagIDs) + addon:Print("Phase 1: Detected %d soul, %d quiver, %d ammo, %d regular bags", + table.getn(containers.soul), table.getn(containers.quiver), + table.getn(containers.ammo), table.getn(containers.regular)) + + -- Phase 2: Route specialized items to their bags + local routeCount = RouteSpecializedItems(bagIDs, containers) + if routeCount > 0 then + addon:Print("Phase 2: Routed %d specialized items", routeCount) + end + + -- Phase 3: Consolidate stacks in ALL bags (including specialized) + local consolidateCount = ConsolidateStacks(bagIDs) + if consolidateCount > 0 then + addon:Print("Phase 3: Consolidated %d stacks", consolidateCount) + end + + -- Phase 4: Sort items WITHIN each specialized bag (soul, quiver, ammo) + local specializedMoves = 0 + for _, bagType in ipairs({"soul", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + -- Sort items within this single specialized bag + local items = CollectItems({bagID}) + if table.getn(items) > 0 then + items = SortItems(items) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local moveCount = ApplySort({bagID}, items, targetPositions) + specializedMoves = specializedMoves + moveCount + end + end + end + if specializedMoves > 0 then + addon:Print("Phase 4a: Sorted %d items within specialized bags", specializedMoves) + end + + -- Phase 5: Categorical sort regular bags + local regularBagIDs = containers.regular + if table.getn(regularBagIDs) > 0 then + local items = CollectItems(regularBagIDs) + if table.getn(items) > 0 then + items = SortItems(items) + local targetPositions = BuildTargetPositions(regularBagIDs, table.getn(items)) + local moveCount = ApplySort(regularBagIDs, items, targetPositions) + + if moveCount > 0 then + addon:Print("Phase 4b: Sorted and compressed %d items in regular bags", moveCount) + end + end + end + + -- Phase 6: Validate + ValidateSort(bagIDs) + + addon:Print("Sort complete!") 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") + addon:Print("Starting 6-phase bank sort...") + + local bagIDs = addon.Constants.BANK_BAGS + + -- Phase 1: Detect specialized bags + local containers = DetectSpecializedBags(bagIDs) + + -- Phase 2: Route specialized items + local routeCount = RouteSpecializedItems(bagIDs, containers) + if routeCount > 0 then + addon:Print("Routed %d specialized items", routeCount) + end + + -- Phase 3: Consolidate stacks + local consolidateCount = ConsolidateStacks(bagIDs) + if consolidateCount > 0 then + addon:Print("Consolidated %d stacks", consolidateCount) + end + + -- Phase 4: Sort items WITHIN each specialized bag + local specializedMoves = 0 + for _, bagType in ipairs({"soul", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + local items = CollectItems({bagID}) + if table.getn(items) > 0 then + items = SortItems(items) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local moveCount = ApplySort({bagID}, items, targetPositions) + specializedMoves = specializedMoves + moveCount + end + end + end + + -- Phase 5: Sort regular bags + local regularBagIDs = containers.regular + local regularMoves = 0 + if table.getn(regularBagIDs) > 0 then + local items = CollectItems(regularBagIDs) + if table.getn(items) > 0 then + items = SortItems(items) + local targetPositions = BuildTargetPositions(regularBagIDs, table.getn(items)) + regularMoves = ApplySort(regularBagIDs, items, targetPositions) + end + end + + addon:Print("Bank sort complete! Moved %d items", specializedMoves + regularMoves) end