view Modules/Mover.lua @ 88:f1c035694545

Now trying to continue moving item 0.5 seconds after the last ITEM_LOCK_CHANGED-event, rather than 1 second after BAG_UPDATE. Skipping items which are tagged locked by the client while not tagged as locked in our addon.
author Zerotorescue
date Fri, 07 Jan 2011 10:34:38 +0100
parents 3bec0ea44607
children a12d22ef3f39
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(#queuedMoves .. " moves were queued.");
	
	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(#outgoingMoves .. " outgoing moves are possible.");
	
	-- 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(#emptySlots .. " empty slots are available.");
	
	-- 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(#outgoingMoves .. " moves remaining.");
		
		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(#combinedMoves .. " moves should be possible.");
	
	-- 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 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] or targetLocationsLocked[move.targetContainer][move.targetSlot]) or select(3, GetContainerItemInfo(move.targetContainer, move.targetSlot)));
		
		-- sourceContainer, sourceSlot, targetContainer, targetSlot, itemId, num
		if move and isSourceLocked and isTargetLocked then
			
			print(("Moving %dx%s."):format(move.num, IdToItemLink(move.itemId)));
			
			addon:Debug(("Moving %dx%s from (%d,%d) to (%d,%d)"):format(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((combinedMovesOriginalLength - #combinedMoves) .. " moves processed. " .. #combinedMoves .. " moves remaining.");
	
	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", 0.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:" .. debugMsg);
	end
	if simple then
		print("|cffff0000Aborting: " .. simple .. ".|r");
	end
	table.wipe(combinedMoves);
	movesSource = nil;
	
	-- Stop timer
	self:UnregisterEvent("BAG_UPDATE");
	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