view Modules/Mover.lua @ 89:a12d22ef3f39

AceSerializer has been enabled again as it?s used when exporting/importing groups. All other unused libraries are now really removed. Adjusted debug function to format only when a debug channel is available. Fixed moving from guild banks, checking if items are locked is different then with banks. Now unregistering the item locking event so it doesn?t continue to try to move every time an item is switched after the automated cycle has finished. (geeezus, this description is a total overkill) Fixed item queueing. Queue all when there?s a group without any items inside no longer crashes. Removing cached container data after closing a container.
author Zerotorescue
date Fri, 07 Jan 2011 22:19:03 +0100
parents f1c035694545
children 31493364b163
line wrap: on
line source
local addon = select(2, ...);
local mod = addon:NewModule("Mover", "AceEvent-3.0", "AceTimer-3.0");

local Scanner;
local queuedMoves = {}; -- table storing all queued moves before BeginMove is called
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 #)
local movesSource;

function mod:AddMove(itemId, amount)
	table.insert(queuedMoves, {
		id = itemId,
		num = amount,
	});
end

function mod:HasMoves()
	return (#queuedMoves ~= 0);
end

if not table.reverse then
	table.reverse = function(orig)
		local temp = {};
		local origLength = #orig;
		for i = 1, origLength do
			temp[(origLength - i + 1)] = orig[i];
		end
		
-- 		-- Update the original table (can't do orig = temp as that would change the reference-link instead of the original table)
-- 		for i, v in pairs(temp) do
-- 			orig[i] = v;
-- 		end
		return temp; -- for speed we choose to do a return instead
	end
end

function mod:BeginMove(location, onFinish)
	addon:Debug("BeginMove");
	
	-- Find the outgoing moves
	-- 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
	
	-- Get a list of items in the source container
	local sourceContents = Scanner:CacheLocation(location, false);
	
	local outgoingMoves = {};

	addon:Debug("%d moves were queued.", #queuedMoves);
	
	for _, singleMove in pairs(queuedMoves) do
		local sourceItem = sourceContents[singleMove.id];
		if not sourceItem then
			print("Can't move " .. IdToItemLink(singleMove.id) .. ", non-existant in source");
		else
			-- 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)
			table.sort(sourceItem.locations, function(a, b)
				return a.count < b.count;
			end);
			
			for _, itemLocation in pairs(sourceItem.locations) do
				-- if this location has more items than we need, only move what we need, otherwise move everything in this stack
				local movingNum = ((itemLocation.count > singleMove.num and singleMove.num) or itemLocation.count);
				
				table.insert(outgoingMoves, {
					itemId = singleMove.id,
					num = movingNum,
					container = itemLocation.container,
					slot = itemLocation.slot,
				});
				
				singleMove.num = (singleMove.num - movingNum);
				
				if singleMove.num == 0 then
					-- If we have prepared everything we wanted, go to the next queued move
					break; -- stop the locations-loop
				end
			end
		end
	end

	addon:Debug("%d outgoing moves are possible.", #outgoingMoves);
	
	-- No longer needed
	table.wipe(queuedMoves);
	
	-- Process every single outgoing move and find fitting targets
	
	-- Get a list of items already in the target container
	local targetContents = Scanner:CacheLocation(addon.Locations.Bag, false);
	
	-- Find all empty slots
	
	local emptySlots = {};
	
	local start = 0;
	local stop = NUM_BAG_SLOTS;
	
	-- Go through all our bags, including the backpack
	for bagId = start, stop do
		-- Go through all our slots
		for slotId = 1, GetContainerNumSlots(bagId) do
			local itemId = GetContainerItemID(bagId, slotId); -- we're scanning our local bags here, so no need to get messy with guild bank support
			
			if not itemId then
				table.insert(emptySlots, {
					container = bagId,
					slot = slotId,
				});
			end
		end
	end

	addon:Debug("%d empty slots are available.", #emptySlots);
	
	-- Remember where we're moving from
	movesSource = location;
	
	local backup = 0;
	
	while #outgoingMoves ~= 0 do
		-- A not equal-comparison should be quicker than a larger/smaller than-comparison
		
		for _, outgoingMove in pairs(outgoingMoves) do
			-- itemId  will be set to nil when this outgoing move was processed - sanity check
			if outgoingMove.itemId then
				local targetItem = targetContents[outgoingMove.itemId];
				
				if not targetItem then
					-- grab an empty slot
					-- make new instance of ItemMove
					-- populate targetContents with it so future moves of this item can be put on top of it if this isn't a full stack
					
					local firstAvailableSlot = emptySlots[1];
					
					if not firstAvailableSlot then
						print("Bags are full. Skipping " .. IdToItemLink(outgoingMove.itemId) .. ".");
						
						outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
					else
						table.insert(combinedMoves, {
							itemId = outgoingMove.itemId,
							num = outgoingMove.num,
							sourceContainer = outgoingMove.container,
							sourceSlot = outgoingMove.slot,
							targetContainer = firstAvailableSlot.container,
							targetSlot = firstAvailableSlot.slot,
						});
						
						-- We filled an empty slot so the target contents now has one more item,
						-- make a new instance of the ItemMove class so any additional items with this id can be stacked on top of it
						local itemMove = addon.ContainerItem:New();
						itemMove:AddLocation(firstAvailableSlot.container, firstAvailableSlot.slot, outgoingMove.num);
						targetContents[outgoingMove.itemId] = itemMove;
						
						table.remove(emptySlots, 1); -- no longer empty
						
						outgoingMove.num = 0; -- nothing remaining - sanity check
						outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
					end
				else
					-- Find the maximum stack size for this item
					local itemStackCount = select(8, GetItemInfo(outgoingMove.itemId));
					
					-- We want to move to the largest stacks first to keep stuff pretty
					table.sort(targetItem.locations, function(a, b)
						return a.count > b.count;
					end);
					
					for _, itemLocation in pairs(targetItem.locations) do
						if itemLocation.count < itemStackCount and outgoingMove.num > 0 then
							-- Check if this stack isn't already full (and we still need to move this item)
							
							local remainingSpace = (itemStackCount - itemLocation.count);
							if remainingSpace >= outgoingMove.num then
								-- Enough room to move this entire stack
								-- Deposit this item and then forget this outgoing move as everything in it was processed
								
								table.insert(combinedMoves, {
									itemId = outgoingMove.itemId,
									num = outgoingMove.num,
									sourceContainer = outgoingMove.container,
									sourceSlot = outgoingMove.slot,
									targetContainer = itemLocation.container,
									targetSlot = itemLocation.slot,
								});
								
								itemLocation.count = (itemLocation.count + outgoingMove.num);
								outgoingMove.num = 0; -- nothing remaining
								outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
								break; -- stop the locations-loop
							else
								-- Deposit this item but don't remove the outgoing move as there are some items left to move
								
								table.insert(combinedMoves, {
									itemId = outgoingMove.itemId,
									num = outgoingMove.num,
									sourceContainer = outgoingMove.container,
									sourceSlot = outgoingMove.slot,
									targetContainer = itemLocation.container,
									targetSlot = itemLocation.slot,
								});
								
								-- The target will be full when we complete, but the source will still have remaining items left to be moved
								itemLocation.count = itemStackCount;
								outgoingMove.num = (outgoingMove.num - remainingSpace);
							end
						end
					end
					
					if outgoingMove.num > 0 then
						-- We went through all matching items and checked their stack sizes if we could move this there, no room available
						-- 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
						targetContents[outgoingMove.itemId] = nil;
					end
				end
			end
		end
		
		-- Loop through the array to find items that should be removed, start with the last element or the loop would break
		local numOutgoingMoves = #outgoingMoves; -- since LUA-tables start at an index of 1, this is actually an existing index (outgoingMoves[#outgoingMoves] would return a value)
		while numOutgoingMoves ~= 0 do
			-- A not equal-comparison should be quicker than a larger/smaller than-comparison
			
			-- Check if the item id is nil, this is set to nil when this outgoing move has been processed
			if not outgoingMoves[numOutgoingMoves].itemId or outgoingMoves[numOutgoingMoves].num == 0 then
				-- Remove this element from the array
				table.remove(outgoingMoves, numOutgoingMoves);
			end
			
			-- Proceed with the next element (or previous considering we're going from last to first)
			numOutgoingMoves = (numOutgoingMoves - 1);
		end

		addon:Debug("%d moves remaining.", #outgoingMoves);
		
		backup = (backup + 1);
		if backup > 1000 then
			dump(nil, outgoingMoves);
			table.wipe(outgoingMoves);
			self:Abort("mover crashed", "Error preparing moves, hit an endless loop");
			onFinish();
			return;
		end
	end
	
	-- 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
	combinedMoves = table.reverse(combinedMoves);

	addon:Debug("%d moves should be possible.", #combinedMoves);
	
	-- No longer needed
	table.wipe(emptySlots);
	
	self:ProcessMove();
	
	-- Even though we aren't completely done yet, allow requeueing
	onFinish();
end

function mod:ProcessMove()
	addon:Debug("ProcessMove");
	
	if #combinedMoves == 0 then
		print("Nothing to move.");
		
		self:Abort();
		
		return;
	end
	
	--self:RegisterEvent("BAG_UPDATE");
	self:RegisterEvent("ITEM_LOCK_CHANGED");
	self:RegisterEvent("UI_ERROR_MESSAGE");
	
	-- combinedMoves now has all moves in it (source -> target)
	-- go through list, move everything inside it
	-- add source and target to lists, if either is already in this list, skip the move
	-- repeat every few seconds until we're completely done
	
	local sourceLocationsLocked = {};
	local targetLocationsLocked = {};
	
	local _GetContainerItemId = GetContainerItemID;
	if movesSource == addon.Locations.Guild then
		_GetContainerItemId = function(tabId, slotId) return addon:GetItemID(GetGuildBankItemLink(tabId, slotId)); end;
	end
	local _GetContainerItemInfo = GetContainerItemInfo;
	if movesSource == addon.Locations.Guild then
		_GetContainerItemInfo = GetGuildBankItemInfo;
	end
    
	local combinedMovesOriginalLength = #combinedMoves;
	local numCurrentMove = combinedMovesOriginalLength;
	while numCurrentMove ~= 0 do
		local move = combinedMoves[numCurrentMove];
		
		local isSourceLocked = ((sourceLocationsLocked[move.sourceContainer] and sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) or select(3, _GetContainerItemInfo(move.sourceContainer, move.sourceSlot)));
		local isTargetLocked = ((targetLocationsLocked[move.targetContainer] and targetLocationsLocked[move.targetContainer][move.targetSlot]) or select(3, GetContainerItemInfo(move.targetContainer, move.targetSlot)));
		
		if move and not isSourceLocked and not isTargetLocked then
			
			print(("Moving %dx%s."):format(move.num, IdToItemLink(move.itemId)));
			
			addon:Debug("Moving %dx%s from (%d,%d) to (%d,%d)", move.num, IdToItemLink(move.itemId), move.sourceContainer, move.sourceSlot, move.targetContainer, move.targetSlot);
			
			if _GetContainerItemId(move.sourceContainer, move.sourceSlot) ~= move.itemId then
				self:Abort("source changed", "Source (" .. move.sourceContainer .. "," .. move.sourceSlot .. ") is not " .. IdToItemLink(move.itemId));
				return;
			end
			
			-- Pickup stack
			if movesSource == addon.Locations.Bank then
				SplitContainerItem(move.sourceContainer, move.sourceSlot, move.num);
			elseif movesSource == addon.Locations.Guild then
				SplitGuildBankItem(move.sourceContainer, move.sourceSlot, move.num);
			end
			
			-- Remember we picked this item up and thus it is now locked
			if not sourceLocationsLocked[move.sourceContainer] then
				sourceLocationsLocked[move.sourceContainer] = {};
			end
			sourceLocationsLocked[move.sourceContainer][move.sourceSlot] = true;
			
			if movesSource == addon.Locations.Guild or CursorHasItem() then -- CursorHasItem is always false if source is a guild tab
				-- We are moving into our local bags, so the below must check normal
				local targetItemId = GetContainerItemID(move.targetContainer, move.targetSlot);
				if targetItemId and targetItemId ~= move.itemId then
					self:Abort("target changed", "Target (" .. move.targetContainer .. "," .. move.targetSlot .. ") is not " .. IdToItemLink(move.itemId) .. " nor empty");
					return;
				end
				
				-- And drop it (this is always a local bag so no need to do any guild-checks)
				PickupContainerItem(move.targetContainer, move.targetSlot);
				
				-- Remember we dropped an item here and thus this is now locked
				if not targetLocationsLocked[move.targetContainer] then
					targetLocationsLocked[move.targetContainer] = {};
				end
				targetLocationsLocked[move.targetContainer][move.targetSlot] = true;
				
				-- This move was processed
				table.remove(combinedMoves, numCurrentMove);
			else
				self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false");
				return;
			end
		end
		
		-- Proceed with the next element (or previous considering we're going from last to first)
		numCurrentMove = (numCurrentMove - 1);
	end
	
	addon:Debug("%d moves processed. %d moves remaining.", (combinedMovesOriginalLength - #combinedMoves), #combinedMoves);
	
	if #combinedMoves == 0 then
		print("Finished.");
		
		self:Abort();
		
		return;
	end
end

local tmrProcessNext;
function mod:ITEM_LOCK_CHANGED()
	self:CancelTimer(tmrProcessNext, true); -- silent
	tmrProcessNext = self:ScheduleTimer("ProcessMove", .5);
end

function IdToItemLink(itemId)
	local itemLink = select(2, GetItemInfo(itemId));
	itemLink = itemLink or "Unknown (" .. itemId .. ")";
	return itemLink;
end

function mod:UI_ERROR_MESSAGE(e, errorMessage)
	if errorMessage == ERR_SPLIT_FAILED then
		self:Abort("splitting failed", "Splitting failed.");
	end
end

function mod:Abort(simple, debugMsg)
	if debugMsg then
		addon:Debug("Aborting:%s", debugMsg);
	end
	if simple then
		print("|cffff0000Aborting: " .. simple .. ".|r");
	end
	table.wipe(combinedMoves);
	movesSource = nil;
	
	-- Stop timer
	self:UnregisterEvent("ITEM_LOCK_CHANGED");
	self:CancelTimer(tmrProcessNext, true); -- silent
	
	self:UnregisterEvent("UI_ERROR_MESSAGE");
end

function mod:OnEnable()
	Scanner = addon:GetModule("Scanner");
end

function mod:OnDisable()
	Scanner = nil;
	
	self:Abort();
end