Zerotorescue@80: local addon = select(2, ...); Zerotorescue@80: local mod = addon:NewModule("Mover", "AceEvent-3.0", "AceTimer-3.0"); Zerotorescue@80: Zerotorescue@80: local Scanner; Zerotorescue@80: local queuedMoves = {}; -- table storing all queued moves before BeginMove is called Zerotorescue@80: local combinedMoves = {}; -- table storing all combined moves (with source and target) that is to be processed by the actual mover in the order of the index (1 to #) Zerotorescue@81: local movesSource; Zerotorescue@80: Zerotorescue@101: function mod:AddMove(itemId, amount, numMissing, numAvailable) Zerotorescue@80: table.insert(queuedMoves, { Zerotorescue@80: id = itemId, Zerotorescue@80: num = amount, Zerotorescue@101: missing = numMissing, Zerotorescue@101: available = numAvailable, Zerotorescue@80: }); Zerotorescue@80: end Zerotorescue@80: Zerotorescue@81: function mod:HasMoves() Zerotorescue@81: return (#queuedMoves ~= 0); Zerotorescue@81: end Zerotorescue@81: Zerotorescue@101: function mod:GetMoves() Zerotorescue@101: return queuedMoves; Zerotorescue@101: end Zerotorescue@101: Zerotorescue@101: function mod:ResetQueue() Zerotorescue@101: table.wipe(queuedMoves); Zerotorescue@101: end Zerotorescue@101: Zerotorescue@84: if not table.reverse then Zerotorescue@84: table.reverse = function(orig) Zerotorescue@84: local temp = {}; Zerotorescue@84: local origLength = #orig; Zerotorescue@84: for i = 1, origLength do Zerotorescue@84: temp[(origLength - i + 1)] = orig[i]; Zerotorescue@84: end Zerotorescue@84: Zerotorescue@84: -- -- Update the original table (can't do orig = temp as that would change the reference-link instead of the original table) Zerotorescue@84: -- for i, v in pairs(temp) do Zerotorescue@84: -- orig[i] = v; Zerotorescue@84: -- end Zerotorescue@84: return temp; -- for speed we choose to do a return instead Zerotorescue@84: end Zerotorescue@84: end Zerotorescue@84: Zerotorescue@80: function mod:BeginMove(location, onFinish) Zerotorescue@81: addon:Debug("BeginMove"); Zerotorescue@80: Zerotorescue@80: -- Find the outgoing moves Zerotorescue@80: -- We need the source container and slot, find all the requires sources and put them in a list which we go through later to find matching targets Zerotorescue@80: Zerotorescue@80: -- Get a list of items in the source container Zerotorescue@80: local sourceContents = Scanner:CacheLocation(location, false); Zerotorescue@80: Zerotorescue@80: local outgoingMoves = {}; Zerotorescue@80: Zerotorescue@89: addon:Debug("%d moves were queued.", #queuedMoves); Zerotorescue@82: Zerotorescue@81: for _, singleMove in pairs(queuedMoves) do Zerotorescue@80: local sourceItem = sourceContents[singleMove.id]; Zerotorescue@80: if not sourceItem then Zerotorescue@101: addon:Print(("Can't move %s, this doesn't exist in the source."):format(IdToItemLink(singleMove.id)), addon.Colors.Red); Zerotorescue@80: else Zerotorescue@82: -- We want to move the smallest stacks first to keep stuff pretty (and minimize space usage, splitting a stack takes 2 slots, moving something only 1) Zerotorescue@80: table.sort(sourceItem.locations, function(a, b) Zerotorescue@81: return a.count < b.count; Zerotorescue@80: end); Zerotorescue@80: Zerotorescue@81: for _, itemLocation in pairs(sourceItem.locations) do Zerotorescue@80: -- if this location has more items than we need, only move what we need, otherwise move everything in this stack Zerotorescue@80: local movingNum = ((itemLocation.count > singleMove.num and singleMove.num) or itemLocation.count); Zerotorescue@80: Zerotorescue@80: table.insert(outgoingMoves, { Zerotorescue@80: itemId = singleMove.id, Zerotorescue@82: num = movingNum, Zerotorescue@80: container = itemLocation.container, Zerotorescue@80: slot = itemLocation.slot, Zerotorescue@80: }); Zerotorescue@80: Zerotorescue@80: singleMove.num = (singleMove.num - movingNum); Zerotorescue@80: Zerotorescue@80: if singleMove.num == 0 then Zerotorescue@80: -- If we have prepared everything we wanted, go to the next queued move Zerotorescue@81: break; -- stop the locations-loop Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@82: Zerotorescue@89: addon:Debug("%d outgoing moves are possible.", #outgoingMoves); Zerotorescue@80: Zerotorescue@80: -- No longer needed Zerotorescue@80: table.wipe(queuedMoves); Zerotorescue@80: Zerotorescue@80: -- Process every single outgoing move and find fitting targets Zerotorescue@80: Zerotorescue@80: -- Get a list of items already in the target container Zerotorescue@80: local targetContents = Scanner:CacheLocation(addon.Locations.Bag, false); Zerotorescue@80: Zerotorescue@80: -- Find all empty slots Zerotorescue@80: Zerotorescue@80: local emptySlots = {}; Zerotorescue@80: Zerotorescue@80: local start = 0; Zerotorescue@80: local stop = NUM_BAG_SLOTS; Zerotorescue@80: Zerotorescue@80: -- Go through all our bags, including the backpack Zerotorescue@80: for bagId = start, stop do Zerotorescue@80: -- Go through all our slots Zerotorescue@80: for slotId = 1, GetContainerNumSlots(bagId) do Zerotorescue@82: local itemId = GetContainerItemID(bagId, slotId); -- we're scanning our local bags here, so no need to get messy with guild bank support Zerotorescue@80: Zerotorescue@80: if not itemId then Zerotorescue@80: table.insert(emptySlots, { Zerotorescue@81: container = bagId, Zerotorescue@81: slot = slotId, Zerotorescue@80: }); Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@82: Zerotorescue@89: addon:Debug("%d empty slots are available.", #emptySlots); Zerotorescue@80: Zerotorescue@81: -- Remember where we're moving from Zerotorescue@81: movesSource = location; Zerotorescue@81: Zerotorescue@84: local backup = 0; Zerotorescue@84: Zerotorescue@80: while #outgoingMoves ~= 0 do Zerotorescue@80: -- A not equal-comparison should be quicker than a larger/smaller than-comparison Zerotorescue@80: Zerotorescue@81: for _, outgoingMove in pairs(outgoingMoves) do Zerotorescue@80: -- itemId will be set to nil when this outgoing move was processed - sanity check Zerotorescue@80: if outgoingMove.itemId then Zerotorescue@80: local targetItem = targetContents[outgoingMove.itemId]; Zerotorescue@80: Zerotorescue@80: if not targetItem then Zerotorescue@80: -- grab an empty slot Zerotorescue@80: -- make new instance of ItemMove Zerotorescue@80: -- populate targetContents with it so future moves of this item can be put on top of it if this isn't a full stack Zerotorescue@80: Zerotorescue@80: local firstAvailableSlot = emptySlots[1]; Zerotorescue@80: Zerotorescue@80: if not firstAvailableSlot then Zerotorescue@98: addon:Print(("Bags are full. Skipping %s."):format(IdToItemLink(outgoingMove.itemId)), addon.Colors.Orange); Zerotorescue@80: Zerotorescue@82: outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table Zerotorescue@80: else Zerotorescue@80: table.insert(combinedMoves, { Zerotorescue@82: itemId = outgoingMove.itemId, Zerotorescue@82: num = outgoingMove.num, Zerotorescue@80: sourceContainer = outgoingMove.container, Zerotorescue@80: sourceSlot = outgoingMove.slot, Zerotorescue@80: targetContainer = firstAvailableSlot.container, Zerotorescue@80: targetSlot = firstAvailableSlot.slot, Zerotorescue@80: }); Zerotorescue@80: Zerotorescue@80: -- We filled an empty slot so the target contents now has one more item, Zerotorescue@80: -- make a new instance of the ItemMove class so any additional items with this id can be stacked on top of it Zerotorescue@81: local itemMove = addon.ContainerItem:New(); Zerotorescue@82: itemMove:AddLocation(firstAvailableSlot.container, firstAvailableSlot.slot, outgoingMove.num); Zerotorescue@80: targetContents[outgoingMove.itemId] = itemMove; Zerotorescue@80: Zerotorescue@81: table.remove(emptySlots, 1); -- no longer empty Zerotorescue@80: Zerotorescue@82: outgoingMove.num = 0; -- nothing remaining - sanity check Zerotorescue@80: outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table Zerotorescue@80: end Zerotorescue@80: else Zerotorescue@80: -- Find the maximum stack size for this item Zerotorescue@80: local itemStackCount = select(8, GetItemInfo(outgoingMove.itemId)); Zerotorescue@80: Zerotorescue@80: -- We want to move to the largest stacks first to keep stuff pretty Zerotorescue@80: table.sort(targetItem.locations, function(a, b) Zerotorescue@81: return a.count > b.count; Zerotorescue@80: end); Zerotorescue@80: Zerotorescue@81: for _, itemLocation in pairs(targetItem.locations) do Zerotorescue@82: if itemLocation.count < itemStackCount and outgoingMove.num > 0 then Zerotorescue@80: -- Check if this stack isn't already full (and we still need to move this item) Zerotorescue@80: Zerotorescue@80: local remainingSpace = (itemStackCount - itemLocation.count); Zerotorescue@84: if remainingSpace >= outgoingMove.num then Zerotorescue@80: -- Enough room to move this entire stack Zerotorescue@80: -- Deposit this item and then forget this outgoing move as everything in it was processed Zerotorescue@80: Zerotorescue@80: table.insert(combinedMoves, { Zerotorescue@82: itemId = outgoingMove.itemId, Zerotorescue@82: num = outgoingMove.num, Zerotorescue@80: sourceContainer = outgoingMove.container, Zerotorescue@80: sourceSlot = outgoingMove.slot, Zerotorescue@80: targetContainer = itemLocation.container, Zerotorescue@80: targetSlot = itemLocation.slot, Zerotorescue@80: }); Zerotorescue@80: Zerotorescue@82: itemLocation.count = (itemLocation.count + outgoingMove.num); Zerotorescue@84: outgoingMove.num = 0; -- nothing remaining Zerotorescue@80: outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table Zerotorescue@80: break; -- stop the locations-loop Zerotorescue@80: else Zerotorescue@80: -- Deposit this item but don't remove the outgoing move as there are some items left to move Zerotorescue@80: Zerotorescue@80: table.insert(combinedMoves, { Zerotorescue@82: itemId = outgoingMove.itemId, Zerotorescue@82: num = outgoingMove.num, Zerotorescue@80: sourceContainer = outgoingMove.container, Zerotorescue@80: sourceSlot = outgoingMove.slot, Zerotorescue@80: targetContainer = itemLocation.container, Zerotorescue@80: targetSlot = itemLocation.slot, Zerotorescue@80: }); Zerotorescue@80: Zerotorescue@80: -- The target will be full when we complete, but the source will still have remaining items left to be moved Zerotorescue@80: itemLocation.count = itemStackCount; Zerotorescue@82: outgoingMove.num = (outgoingMove.num - remainingSpace); Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: Zerotorescue@82: if outgoingMove.num > 0 then Zerotorescue@80: -- We went through all matching items and checked their stack sizes if we could move this there, no room available Zerotorescue@80: -- So forget about the target item (even though it may just have full locations, these are useless anyway) and the next loop move it onto an empty slot Zerotorescue@84: targetContents[outgoingMove.itemId] = nil; Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: Zerotorescue@80: -- Loop through the array to find items that should be removed, start with the last element or the loop would break Zerotorescue@80: local numOutgoingMoves = #outgoingMoves; -- since LUA-tables start at an index of 1, this is actually an existing index (outgoingMoves[#outgoingMoves] would return a value) Zerotorescue@80: while numOutgoingMoves ~= 0 do Zerotorescue@80: -- A not equal-comparison should be quicker than a larger/smaller than-comparison Zerotorescue@80: Zerotorescue@80: -- Check if the item id is nil, this is set to nil when this outgoing move has been processed Zerotorescue@84: if not outgoingMoves[numOutgoingMoves].itemId or outgoingMoves[numOutgoingMoves].num == 0 then Zerotorescue@80: -- Remove this element from the array Zerotorescue@80: table.remove(outgoingMoves, numOutgoingMoves); Zerotorescue@80: end Zerotorescue@80: Zerotorescue@80: -- Proceed with the next element (or previous considering we're going from last to first) Zerotorescue@80: numOutgoingMoves = (numOutgoingMoves - 1); Zerotorescue@80: end Zerotorescue@84: Zerotorescue@89: addon:Debug("%d moves remaining.", #outgoingMoves); Zerotorescue@84: Zerotorescue@84: backup = (backup + 1); Zerotorescue@84: if backup > 1000 then Zerotorescue@84: dump(nil, outgoingMoves); Zerotorescue@84: table.wipe(outgoingMoves); Zerotorescue@84: self:Abort("mover crashed", "Error preparing moves, hit an endless loop"); Zerotorescue@84: onFinish(); Zerotorescue@84: return; Zerotorescue@84: end Zerotorescue@80: end Zerotorescue@84: Zerotorescue@84: -- Reverse table, we need to go through it from last to first because we'll be removing elements, but we don't want the actions to be executed in a different order Zerotorescue@84: combinedMoves = table.reverse(combinedMoves); Zerotorescue@82: Zerotorescue@89: addon:Debug("%d moves should be possible.", #combinedMoves); Zerotorescue@80: Zerotorescue@80: -- No longer needed Zerotorescue@80: table.wipe(emptySlots); Zerotorescue@80: Zerotorescue@81: self:ProcessMove(); Zerotorescue@80: Zerotorescue@82: -- Even though we aren't completely done yet, allow requeueing Zerotorescue@81: onFinish(); Zerotorescue@80: end Zerotorescue@80: Zerotorescue@81: function mod:ProcessMove() Zerotorescue@81: addon:Debug("ProcessMove"); Zerotorescue@81: Zerotorescue@81: if #combinedMoves == 0 then Zerotorescue@98: addon:Print("Nothing to move."); Zerotorescue@81: Zerotorescue@81: self:Abort(); Zerotorescue@81: Zerotorescue@81: return; Zerotorescue@81: end Zerotorescue@81: Zerotorescue@98: -- Make sure nothing is at the mouse Zerotorescue@98: ClearCursor(); Zerotorescue@98: Zerotorescue@88: self:RegisterEvent("ITEM_LOCK_CHANGED"); Zerotorescue@81: self:RegisterEvent("UI_ERROR_MESSAGE"); Zerotorescue@81: Zerotorescue@80: -- combinedMoves now has all moves in it (source -> target) Zerotorescue@80: -- go through list, move everything inside it Zerotorescue@81: -- add source and target to lists, if either is already in this list, skip the move Zerotorescue@81: -- repeat every few seconds until we're completely done Zerotorescue@80: Zerotorescue@80: local sourceLocationsLocked = {}; Zerotorescue@80: local targetLocationsLocked = {}; Zerotorescue@80: Zerotorescue@84: local _GetContainerItemId = GetContainerItemID; Zerotorescue@82: if movesSource == addon.Locations.Guild then Zerotorescue@95: _GetContainerItemId = function(tabId, slotId) return addon:GetItemId(GetGuildBankItemLink(tabId, slotId)); end; Zerotorescue@82: end Zerotorescue@89: local _GetContainerItemInfo = GetContainerItemInfo; Zerotorescue@89: if movesSource == addon.Locations.Guild then Zerotorescue@89: _GetContainerItemInfo = GetGuildBankItemInfo; Zerotorescue@89: end Zerotorescue@82: Zerotorescue@82: local combinedMovesOriginalLength = #combinedMoves; Zerotorescue@82: local numCurrentMove = combinedMovesOriginalLength; Zerotorescue@80: while numCurrentMove ~= 0 do Zerotorescue@80: local move = combinedMoves[numCurrentMove]; Zerotorescue@80: Zerotorescue@89: local isSourceLocked = ((sourceLocationsLocked[move.sourceContainer] and sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) or select(3, _GetContainerItemInfo(move.sourceContainer, move.sourceSlot))); Zerotorescue@89: local isTargetLocked = ((targetLocationsLocked[move.targetContainer] and targetLocationsLocked[move.targetContainer][move.targetSlot]) or select(3, GetContainerItemInfo(move.targetContainer, move.targetSlot))); Zerotorescue@88: Zerotorescue@89: if move and not isSourceLocked and not isTargetLocked then Zerotorescue@80: Zerotorescue@98: addon:Print(("Moving %dx%s."):format(move.num, IdToItemLink(move.itemId))); Zerotorescue@80: Zerotorescue@89: addon:Debug("Moving %dx%s from (%d,%d) to (%d,%d)", move.num, IdToItemLink(move.itemId), move.sourceContainer, move.sourceSlot, move.targetContainer, move.targetSlot); Zerotorescue@81: Zerotorescue@84: if _GetContainerItemId(move.sourceContainer, move.sourceSlot) ~= move.itemId then Zerotorescue@81: self:Abort("source changed", "Source (" .. move.sourceContainer .. "," .. move.sourceSlot .. ") is not " .. IdToItemLink(move.itemId)); Zerotorescue@81: return; Zerotorescue@81: end Zerotorescue@81: Zerotorescue@80: -- Pickup stack Zerotorescue@81: if movesSource == addon.Locations.Bank then Zerotorescue@81: SplitContainerItem(move.sourceContainer, move.sourceSlot, move.num); Zerotorescue@81: elseif movesSource == addon.Locations.Guild then Zerotorescue@81: SplitGuildBankItem(move.sourceContainer, move.sourceSlot, move.num); Zerotorescue@81: end Zerotorescue@80: Zerotorescue@80: -- Remember we picked this item up and thus it is now locked Zerotorescue@80: if not sourceLocationsLocked[move.sourceContainer] then Zerotorescue@80: sourceLocationsLocked[move.sourceContainer] = {}; Zerotorescue@80: end Zerotorescue@80: sourceLocationsLocked[move.sourceContainer][move.sourceSlot] = true; Zerotorescue@80: Zerotorescue@81: if movesSource == addon.Locations.Guild or CursorHasItem() then -- CursorHasItem is always false if source is a guild tab Zerotorescue@84: -- We are moving into our local bags, so the below must check normal Zerotorescue@84: local targetItemId = GetContainerItemID(move.targetContainer, move.targetSlot); Zerotorescue@84: if targetItemId and targetItemId ~= move.itemId then Zerotorescue@81: self:Abort("target changed", "Target (" .. move.targetContainer .. "," .. move.targetSlot .. ") is not " .. IdToItemLink(move.itemId) .. " nor empty"); Zerotorescue@81: return; Zerotorescue@81: end Zerotorescue@81: Zerotorescue@82: -- And drop it (this is always a local bag so no need to do any guild-checks) Zerotorescue@80: PickupContainerItem(move.targetContainer, move.targetSlot); Zerotorescue@80: Zerotorescue@80: -- Remember we dropped an item here and thus this is now locked Zerotorescue@81: if not targetLocationsLocked[move.targetContainer] then Zerotorescue@81: targetLocationsLocked[move.targetContainer] = {}; Zerotorescue@80: end Zerotorescue@81: targetLocationsLocked[move.targetContainer][move.targetSlot] = true; Zerotorescue@80: Zerotorescue@80: -- This move was processed Zerotorescue@80: table.remove(combinedMoves, numCurrentMove); Zerotorescue@81: else Zerotorescue@81: self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false"); Zerotorescue@81: return; Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: Zerotorescue@80: -- Proceed with the next element (or previous considering we're going from last to first) Zerotorescue@80: numCurrentMove = (numCurrentMove - 1); Zerotorescue@80: end Zerotorescue@81: Zerotorescue@89: addon:Debug("%d moves processed. %d moves remaining.", (combinedMovesOriginalLength - #combinedMoves), #combinedMoves); Zerotorescue@82: Zerotorescue@81: if #combinedMoves == 0 then Zerotorescue@98: addon:Print("Finished.", addon.Colors.Green); Zerotorescue@81: Zerotorescue@81: self:Abort(); Zerotorescue@81: Zerotorescue@81: return; Zerotorescue@81: end Zerotorescue@80: end Zerotorescue@80: Zerotorescue@80: local tmrProcessNext; Zerotorescue@88: function mod:ITEM_LOCK_CHANGED() Zerotorescue@81: self:CancelTimer(tmrProcessNext, true); -- silent Zerotorescue@89: tmrProcessNext = self:ScheduleTimer("ProcessMove", .5); Zerotorescue@80: end Zerotorescue@80: Zerotorescue@81: function IdToItemLink(itemId) Zerotorescue@81: local itemLink = select(2, GetItemInfo(itemId)); Zerotorescue@81: itemLink = itemLink or "Unknown (" .. itemId .. ")"; Zerotorescue@81: return itemLink; Zerotorescue@81: end Zerotorescue@81: Zerotorescue@81: function mod:UI_ERROR_MESSAGE(e, errorMessage) Zerotorescue@81: if errorMessage == ERR_SPLIT_FAILED then Zerotorescue@81: self:Abort("splitting failed", "Splitting failed."); Zerotorescue@80: end Zerotorescue@80: end Zerotorescue@80: Zerotorescue@81: function mod:Abort(simple, debugMsg) Zerotorescue@81: if debugMsg then Zerotorescue@89: addon:Debug("Aborting:%s", debugMsg); Zerotorescue@81: end Zerotorescue@81: if simple then Zerotorescue@98: addon:Print(("Aborting: %s."):format(simple), addon.Colors.Red); Zerotorescue@81: end Zerotorescue@81: table.wipe(combinedMoves); Zerotorescue@81: movesSource = nil; Zerotorescue@80: Zerotorescue@98: -- Make sure nothing is at the mouse Zerotorescue@98: ClearCursor(); Zerotorescue@98: Zerotorescue@81: -- Stop timer Zerotorescue@89: self:UnregisterEvent("ITEM_LOCK_CHANGED"); Zerotorescue@81: self:CancelTimer(tmrProcessNext, true); -- silent Zerotorescue@80: Zerotorescue@81: self:UnregisterEvent("UI_ERROR_MESSAGE"); Zerotorescue@80: end Zerotorescue@80: Zerotorescue@80: function mod:OnEnable() Zerotorescue@80: Scanner = addon:GetModule("Scanner"); Zerotorescue@80: end Zerotorescue@80: Zerotorescue@80: function mod:OnDisable() Zerotorescue@80: Scanner = nil; Zerotorescue@81: Zerotorescue@81: self:Abort(); Zerotorescue@80: end