From b0c35d83afefc1c10cf3c85c78d2e82e095e0910 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sun, 4 Jan 2026 04:02:15 +0400 Subject: [PATCH] feat: Restack --- UI/BagFrame.lua | 184 ++++++++++++++++++++++++++++++++++++++++++++++- UI/BagFrame.xml | 7 +- UI/BankFrame.lua | 174 +++++++++++++++++++++++++++++++++++++++++++- UI/BankFrame.xml | 7 +- 4 files changed, 366 insertions(+), 6 deletions(-) diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 7832b74..99cb8e0 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -342,10 +342,19 @@ function BagFrame:Update() if viewType == "category" then self:DisplayItemsByCategory(bagData, isOtherChar, charName) - if getglobal("Guda_BagFrame_SortButton") then getglobal("Guda_BagFrame_SortButton"):Hide() end + -- Show sort button with merge icon/tooltip for category view + local sortBtn = getglobal("Guda_BagFrame_SortButton") + if sortBtn then + sortBtn:Show() + sortBtn.isCategoryView = true + end else self:DisplayItems(bagData, isOtherChar, charName) - if getglobal("Guda_BagFrame_SortButton") then getglobal("Guda_BagFrame_SortButton"):Show() end + local sortBtn = getglobal("Guda_BagFrame_SortButton") + if sortBtn then + sortBtn:Show() + sortBtn.isCategoryView = false + end end -- Update money @@ -1304,6 +1313,13 @@ function Guda_BagFrame_Sort() return end + -- Check if we're in category view - only merge stacks + local sortBtn = getglobal("Guda_BagFrame_SortButton") + if sortBtn and sortBtn.isCategoryView then + Guda_BagFrame_MergeStacks() + return + end + local success, message = addon.Modules.SortEngine:ExecuteSort( function() return addon.Modules.SortEngine:SortBagsPass() end, function() return addon.Modules.SortEngine:AnalyzeBags() end, @@ -1316,6 +1332,170 @@ function Guda_BagFrame_Sort() end end +-- Merge stacks only (for category view) - queue-based approach like BagShui +function Guda_BagFrame_MergeStacks() + if currentViewChar then + addon:Print("Cannot merge stacks for another character!") + return + end + + local bagIDs = addon.Constants.BAGS + local moveQueue = {} + + -- Collect all partial stacks grouped by item + local partialStacks = {} + 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 = GetContainerItemInfo(bagID, slot) + local _, _, itemID = string.find(link, "item:(%d+)") + itemID = tonumber(itemID) + + if itemID then + local _, _, _, _, _, _, itemStackCount = GetItemInfo(itemID) + local maxStack = tonumber(itemStackCount) or 1 + + -- Only track items that can stack and aren't full + if maxStack > 1 and count < maxStack then + local groupKey = tostring(itemID) + if not partialStacks[groupKey] then + partialStacks[groupKey] = { + maxStack = maxStack, + stacks = {} + } + end + table.insert(partialStacks[groupKey].stacks, { + bagID = bagID, + slot = slot, + count = count + }) + end + end + end + end + end + end + + -- Build move queue for each item group + for _, group in pairs(partialStacks) do + if table.getn(group.stacks) > 1 then + -- Sort stacks: larger stacks first (targets), smaller stacks last (sources) + table.sort(group.stacks, function(a, b) + return a.count > b.count + end) + + local sourceLoopStart = table.getn(group.stacks) + + -- Process targets from start, sources from end + for targetIdx = 1, table.getn(group.stacks) - 1 do + local target = group.stacks[targetIdx] + + if target.count < group.maxStack and target.count > 0 then + for sourceIdx = sourceLoopStart, targetIdx + 1, -1 do + local source = group.stacks[sourceIdx] + + if source.count > 0 and target.count < group.maxStack then + -- Queue this move + table.insert(moveQueue, { + source = source, + target = target, + maxStack = group.maxStack + }) + + -- Calculate changes (for queue planning) + local oldTargetCount = target.count + target.count = math.min(target.count + source.count, group.maxStack) + source.count = source.count - (target.count - oldTargetCount) + + -- Move source pointer if depleted + if source.count == 0 then + sourceLoopStart = sourceLoopStart - 1 + end + + -- Stop if target is full + if target.count >= group.maxStack then + break + end + end + end + end + end + end + end + + if table.getn(moveQueue) == 0 then + addon:Print("No stacks to merge") + return + end + + -- Process queue with delays + local queueIndex = 1 + local retryCount = 0 + local totalMoves = table.getn(moveQueue) + + local function ProcessNextMove() + if queueIndex > table.getn(moveQueue) then + addon:Print("Merged " .. totalMoves .. " stack(s)") + BagFrame:Update() + return + end + + local move = moveQueue[queueIndex] + local source = move.source + local target = move.target + + -- Check if items are locked + local _, _, sourceLocked = GetContainerItemInfo(source.bagID, source.slot) + local _, _, targetLocked = GetContainerItemInfo(target.bagID, target.slot) + + if sourceLocked or targetLocked then + retryCount = retryCount + 1 + if retryCount < 10 then + -- Retry after delay + Guda_ScheduleTimer(0.3, ProcessNextMove) + return + else + -- Give up on this move + retryCount = 0 + queueIndex = queueIndex + 1 + Guda_ScheduleTimer(0.1, ProcessNextMove) + return + end + end + + -- Perform the move + ClearCursor() + PickupContainerItem(source.bagID, source.slot) + PickupContainerItem(target.bagID, target.slot) + ClearCursor() + + -- Move to next + retryCount = 0 + queueIndex = queueIndex + 1 + Guda_ScheduleTimer(0.15, ProcessNextMove) + end + + ProcessNextMove() +end + +-- Simple timer helper +function Guda_ScheduleTimer(delay, callback) + local frame = CreateFrame("Frame") + frame.elapsed = 0 + frame.delay = delay + frame.callback = callback + frame:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + arg1 + if this.elapsed >= this.delay then + this:SetScript("OnUpdate", nil) + this.callback() + end + end) +end + -- Hook bag container buttons to open Guda Bag View local function HookBagContainers() -- Hook the main bag container buttons (bags 1-4) diff --git a/UI/BagFrame.xml b/UI/BagFrame.xml index e13f8cf..ec71d30 100644 --- a/UI/BagFrame.xml +++ b/UI/BagFrame.xml @@ -145,7 +145,12 @@ GameTooltip:SetOwner(this, "ANCHOR_TOP") - GameTooltip:SetText("Sort Bags") + if this.isCategoryView then + GameTooltip:SetText("Merge Stacks") + GameTooltip:AddLine("Combines split stacks of the same item", 0.7, 0.7, 0.7) + else + GameTooltip:SetText("Sort Bags") + end GameTooltip:Show() diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index 18c9b63..b876140 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -192,10 +192,19 @@ function BankFrame:Update() if viewType == "category" then self:DisplayItemsByCategory(bankData, isOtherChar, charName) - if getglobal("Guda_BankFrame_SortButton") then getglobal("Guda_BankFrame_SortButton"):Hide() end + -- Show sort button with merge icon/tooltip for category view + local sortBtn = getglobal("Guda_BankFrame_SortButton") + if sortBtn then + sortBtn:Show() + sortBtn.isCategoryView = true + end else self:DisplayItems(bankData, isOtherChar, charName) - if getglobal("Guda_BankFrame_SortButton") then getglobal("Guda_BankFrame_SortButton"):Show() end + local sortBtn = getglobal("Guda_BankFrame_SortButton") + if sortBtn then + sortBtn:Show() + sortBtn.isCategoryView = false + end end -- Update money @@ -939,6 +948,13 @@ function Guda_BankFrame_Sort() return end + -- Check if we're in category view - only merge stacks + local sortBtn = getglobal("Guda_BankFrame_SortButton") + if sortBtn and sortBtn.isCategoryView then + Guda_BankFrame_MergeStacks() + return + end + local success, message = addon.Modules.SortEngine:ExecuteSort( function() return addon.Modules.SortEngine:SortBankPass() end, function() return addon.Modules.SortEngine:AnalyzeBank() end, @@ -951,6 +967,160 @@ function Guda_BankFrame_Sort() end end +-- Merge stacks only (for category view) - queue-based approach like BagShui +function Guda_BankFrame_MergeStacks() + if isReadOnlyMode or currentViewChar then + addon:Print("Cannot merge stacks in read-only mode!") + return + end + + if not addon.Modules.BankScanner:IsBankOpen() then + addon:Print("Bank must be open to merge stacks!") + return + end + + local bagIDs = addon.Constants.BANK_BAGS + local moveQueue = {} + + -- Collect all partial stacks grouped by item + local partialStacks = {} + 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 = GetContainerItemInfo(bagID, slot) + local _, _, itemID = string.find(link, "item:(%d+)") + itemID = tonumber(itemID) + + if itemID then + local _, _, _, _, _, _, itemStackCount = GetItemInfo(itemID) + local maxStack = tonumber(itemStackCount) or 1 + + -- Only track items that can stack and aren't full + if maxStack > 1 and count < maxStack then + local groupKey = tostring(itemID) + if not partialStacks[groupKey] then + partialStacks[groupKey] = { + maxStack = maxStack, + stacks = {} + } + end + table.insert(partialStacks[groupKey].stacks, { + bagID = bagID, + slot = slot, + count = count + }) + end + end + end + end + end + end + + -- Build move queue for each item group + for _, group in pairs(partialStacks) do + if table.getn(group.stacks) > 1 then + -- Sort stacks: larger stacks first (targets), smaller stacks last (sources) + table.sort(group.stacks, function(a, b) + return a.count > b.count + end) + + local sourceLoopStart = table.getn(group.stacks) + + -- Process targets from start, sources from end + for targetIdx = 1, table.getn(group.stacks) - 1 do + local target = group.stacks[targetIdx] + + if target.count < group.maxStack and target.count > 0 then + for sourceIdx = sourceLoopStart, targetIdx + 1, -1 do + local source = group.stacks[sourceIdx] + + if source.count > 0 and target.count < group.maxStack then + -- Queue this move + table.insert(moveQueue, { + source = source, + target = target, + maxStack = group.maxStack + }) + + -- Calculate changes (for queue planning) + local oldTargetCount = target.count + target.count = math.min(target.count + source.count, group.maxStack) + source.count = source.count - (target.count - oldTargetCount) + + -- Move source pointer if depleted + if source.count == 0 then + sourceLoopStart = sourceLoopStart - 1 + end + + -- Stop if target is full + if target.count >= group.maxStack then + break + end + end + end + end + end + end + end + + if table.getn(moveQueue) == 0 then + addon:Print("No stacks to merge") + return + end + + -- Process queue with delays + local queueIndex = 1 + local retryCount = 0 + local totalMoves = table.getn(moveQueue) + + local function ProcessNextMove() + if queueIndex > table.getn(moveQueue) then + addon:Print("Merged " .. totalMoves .. " stack(s)") + BankFrame:Update() + return + end + + local move = moveQueue[queueIndex] + local source = move.source + local target = move.target + + -- Check if items are locked + local _, _, sourceLocked = GetContainerItemInfo(source.bagID, source.slot) + local _, _, targetLocked = GetContainerItemInfo(target.bagID, target.slot) + + if sourceLocked or targetLocked then + retryCount = retryCount + 1 + if retryCount < 10 then + -- Retry after delay + Guda_ScheduleTimer(0.3, ProcessNextMove) + return + else + -- Give up on this move + retryCount = 0 + queueIndex = queueIndex + 1 + Guda_ScheduleTimer(0.1, ProcessNextMove) + return + end + end + + -- Perform the move + ClearCursor() + PickupContainerItem(source.bagID, source.slot) + PickupContainerItem(target.bagID, target.slot) + ClearCursor() + + -- Move to next + retryCount = 0 + queueIndex = queueIndex + 1 + Guda_ScheduleTimer(0.15, ProcessNextMove) + end + + ProcessNextMove() +end + -- Switch to Blizzard bank UI function Guda_BankFrame_SwitchToBlizzardUI() -- Hide custom bank frame diff --git a/UI/BankFrame.xml b/UI/BankFrame.xml index 225f974..0784841 100644 --- a/UI/BankFrame.xml +++ b/UI/BankFrame.xml @@ -98,7 +98,12 @@ GameTooltip:SetOwner(this, "ANCHOR_TOP") - GameTooltip:SetText("Sort Bank") + if this.isCategoryView then + GameTooltip:SetText("Merge Stacks") + GameTooltip:AddLine("Combines split stacks of the same item", 0.7, 0.7, 0.7) + else + GameTooltip:SetText("Sort Bank") + end GameTooltip:Show()