view Modules/Mover.lua @ 225:2e4e52a589e5

Added onQueueStart and onQueueEnd events to crafting addon registering. GnomeWorks queue frame should automatically be closed before adding items to the queue and opened afterwards to speed this process up.
author Zerotorescue
date Mon, 07 Feb 2011 15:06:41 +0100
parents 5f405272cdd4
children
line wrap: on
line source
local addon = select(2, ...);
local mod = addon:NewModule("Mover", "AceEvent-3.0", "AceTimer-3.0");

local _G = _G;
local select, pairs = _G.select, _G.pairs;
local tinsert, twipe, treverse, tsort, tremove = _G.table.insert, _G.table.wipe, _G.table.reverse, _G.table.sort, _G.table.remove;

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;

local ContainerFunctions = {
	[addon.Locations.Bag] = {
		GetItemId = GetContainerItemID,
		PickupItem = SplitContainerItem,
		IsLocked = function(sourceContainer, sourceSlot)
			return select(3, GetContainerItemInfo(sourceContainer, sourceSlot));
		end,
		Event = "ITEM_LOCK_CHANGED",
	},
	[addon.Locations.Bank] = {
		GetItemId = GetContainerItemID,
		PickupItem = SplitContainerItem,
		IsLocked = function(sourceContainer, sourceSlot)
			return select(3, GetContainerItemInfo(sourceContainer, sourceSlot));
		end,
		Event = "ITEM_LOCK_CHANGED",
	},
	[addon.Locations.Guild] = {
		GetItemId = function(tabId, slotId)
			return addon:GetItemId(GetGuildBankItemLink(tabId, slotId));
		end,
		PickupItem = SplitGuildBankItem,
		IsLocked = function(sourceContainer, sourceSlot)
			return select(3, GetGuildBankItemInfo(sourceContainer, sourceSlot));
		end,
		Event = "BAG_UPDATE",
	},
	[addon.Locations.Mailbox] = {
		GetItemId = function(mailIndex, attachmentId)
			return addon:GetItemId(GetInboxItemLink(mailIndex, attachmentId));
		end,
		PickupItem = TakeInboxItem,
		IsLocked = function() return false; end,
		DoNotDrop = true, -- TakeInboxItem does not support picking up
		Synchronous = true, -- wait after every single move
		Event = "BAG_UPDATE",
	},
	[addon.Locations.Merchant] = {
		GetItemId = function(_, merchantIndex)
			return addon:GetItemId(GetMerchantItemLink(merchantIndex));
		end,
		PickupItem = function(_, merchantIndex, num)
			return BuyMerchantItem(merchantIndex, num);
		end,
		IsLocked = function() return false; end,
		DoNotDrop = true, -- BuyMerchantItem does not support picking up
		Burst = true, -- spam buy items, the source can take it
		Event = "BAG_UPDATE",
	},
};

function mod:AddMove(itemId, amount, numMissing, numAvailable, cost)
	tinsert(queuedMoves, {
		["itemId"] = itemId,
		["num"] = amount, -- can not be unlimited
		["missing"] = numMissing,
		["available"] = numAvailable,
		["cost"] = cost,
	});
end

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

function mod:GetMoves()
	return queuedMoves;
end

function mod:ResetQueue()
	twipe(queuedMoves);
end

if not treverse then
	treverse = 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

local function GetEmptySlots()
	local emptySlots = {};
	
	-- Go through all our bags, including the backpack
	for bagId = 0, NUM_BAG_SLOTS do
		-- Go through all our slots (0 = backpack)
		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
			local bagFamily = select(2, GetContainerNumFreeSlots(bagId));
			
			if not itemId then
				tinsert(emptySlots, {
					["container"] = bagId,
					["slot"] = slotId,
					["family"] = bagFamily,
				});
			end
		end
	end
	
	return emptySlots;
end

local function GetFirstEmptySlot(emptySlots, prefFamily)
	while prefFamily do
		for _, slot in pairs(emptySlots) do
			if slot.family == prefFamily then
				return slot;
			end
		end
		
		-- If this was a special family, no special bag available, now check normal bags
		if prefFamily > 0 then
			prefFamily = 0;
		else
			prefFamily = nil;
		end
	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.itemId];
		if not sourceItem then
			addon:Print(("Can't move %s, this doesn't exist in the source."):format(IdToItemLink(singleMove.itemId)), addon.Colors.Red);
		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)
			tsort(sourceItem.locations, function(a, b)
				-- -1 indicates unlimited, this is always more than an actual amount
				if a.count == -1 then
					return false;
				elseif b.count == -1 then
					return true;
				end
				
				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
				-- -1 items indicates unlimited amount, in that case we must cap at missing items
				local movingNum = (((itemLocation.count == -1 or itemLocation.count > singleMove.num) and singleMove.num) or itemLocation.count);
				
				if itemLocation.count == -1 then
					-- If the source has an unlimited quantity, makes moves based on the max stacksize
					
					local stackSize = select(8, GetItemInfo(singleMove.itemId)); -- 8 = stacksize
					
					if stackSize then
						while movingNum > stackSize do
							-- Move a single stack size while the amount remaining to be moved is above the stack size num
							
							tinsert(outgoingMoves, {
								["itemId"] = singleMove.itemId,
								["num"] = stackSize,
								["container"] = itemLocation.container,
								["slot"] = itemLocation.slot,
							});
							
							movingNum = (movingNum - stackSize);
							singleMove.num = (singleMove.num - stackSize);
						end
					end
				end
				
				tinsert(outgoingMoves, {
					["itemId"] = singleMove.itemId,
					["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
	twipe(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 = GetEmptySlots();

	addon:Debug("%d empty slots are available.", #emptySlots);
	
	-- Remember where we're moving from
	movesSource = location;
	
	local backup = 0; -- the below loop should never break, but if it does for any reason, this is here to stop it from freezing the game
	
	while #outgoingMoves ~= 0 do
		-- Repeat the below loop until nothing is remaining
		
		for _, outgoingMove in pairs(outgoingMoves) do
			if outgoingMove.itemId then -- itemId  will be set to nil when this outgoing move was processed - sanity check
				local targetItem = targetContents[outgoingMove.itemId];
				
				if not targetItem or ContainerFunctions[location].DoNotDrop then
					-- There is no partial stack which can be filled or this source container doesn't allow manual allocation of items (in which case we always assume it takes a full empty slot)
					
					local family = GetItemFamily(outgoingMove.itemId);
					if family and family ~= 0 and select(9, GetItemInfo(outgoingMove.itemId)) == "INVTYPE_BAG" then
						-- Containers can only fit in general slots but GetItemFamily will return what they can contain themselves
						family = 0;
					end
					local firstAvailableSlot = GetFirstEmptySlot(emptySlots, family);
					
					if not firstAvailableSlot then
						-- No empty slot available - bags are full
						
						addon:Print(("Bags are full. Skipping %s."):format(IdToItemLink(outgoingMove.itemId)), addon.Colors.Orange);
						
						outgoingMove.itemId = nil; -- remove this record from the outgoingMoves-table
						
						-- Not a single item with this item id can be moved, since we only want the bags are full announcement once, we remove all other moves with this item id
						for _, otherMove in pairs(outgoingMoves) do
							if otherMove.itemId and otherMove.itemId == outgoingMove.itemId then
								otherMove.itemId = nil;
							end
						end
					else
						-- Consume empty slot
						
						tinsert(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;
						
						-- Find the slot used and remove it from the empty slots table
						for index, emptySlot in pairs(emptySlots) do
							if emptySlot == firstAvailableSlot then
								tremove(emptySlots, index);
								break;
							end
						end
						
						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
					tsort(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
								
								tinsert(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
								
								tinsert(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
				tremove(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
			twipe(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 = treverse(combinedMoves);

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

local tmrRetry;
function mod:ProcessMove()
	addon:Debug("ProcessMove");
	
	if #combinedMoves == 0 then
		addon:Print("Nothing to move.");
		
		self:Abort();
		
		return;
	elseif movesSource == addon.Locations.Mailbox and MailAddonBusy and MailAddonBusy ~= addon:GetName() then
		addon:Debug("Anoter addon (%s) is busy with the mailbox.", MailAddonBusy);
		
		self:CancelTimer(tmrRetry, true); -- silent
		tmrRetry = self:ScheduleTimer("ProcessMove", .5);
		
		return;
	end
	
	-- Make sure nothing is at the mouse
	ClearCursor();
	
	if movesSource == addon.Locations.Mailbox then
		MailAddonBusy = addon:GetName();
		
		-- Since mailbox indexes change as mail is emptied (emptied mail is automatically deleted, thus number 50 would become 49 when 1 disappears), we must start with the last mail first
		tsort(combinedMoves, function(a, b)
			return a.sourceContainer < b.sourceContainer;
		end);
	end
	
	self:RegisterEvent(ContainerFunctions[movesSource].Event, "SourceUpdated");
	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 hasMoved;
	
	local combinedMovesOriginalLength = #combinedMoves;
	local numCurrentMove = combinedMovesOriginalLength;
	while numCurrentMove ~= 0 do
		local move = combinedMoves[numCurrentMove];
		
		-- Only check if the source is locked when we're not bursting (some functions allow mass calling simultaneously and don't trigger item locks)
		local isSourceLocked = ((not ContainerFunctions[movesSource].Burst and sourceLocationsLocked[move.sourceContainer] and sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) or ContainerFunctions[movesSource].IsLocked(move.sourceContainer, move.sourceSlot));
		-- Target are always the local bags
		local isTargetLocked = ((targetLocationsLocked[move.targetContainer] and targetLocationsLocked[move.targetContainer][move.targetSlot]) or ContainerFunctions[addon.Locations.Bag].IsLocked(move.targetContainer, move.targetSlot));
		
		if move and not isSourceLocked and not isTargetLocked and (not ContainerFunctions[movesSource].Synchronous or not hasMoved) then
			addon: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);
			
			-- Check if the source has been changed since out scan
			if ContainerFunctions[movesSource].GetItemId(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
			ContainerFunctions[movesSource].PickupItem(move.sourceContainer, move.sourceSlot, move.num);
			
			hasMoved = true;
			
			-- 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 not ContainerFunctions[movesSource].DoNotDrop then
				-- Some sources don't actually pick items up but just move them, this makes the code below unnessary
				
				if movesSource ~= addon.Locations.Bank or CursorHasItem() then -- CursorHasItem only works when moving from the bank
					-- We are moving into our local bags, so the below must check normal bags
					
					-- Check if the target has been changed since out scan
					local targetItemId = ContainerFunctions[addon.Locations.Bag].GetItemId(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
					tremove(combinedMoves, numCurrentMove);
				else
					self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false");
					return;
				end
			else
				-- When items are deposit automatically we still need to remember when a move has been processed
				
				-- This move was processed
				tremove(combinedMoves, numCurrentMove);
			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
		addon:Print("Finished.", addon.Colors.Green);
		
		self:Abort();
		
		return;
	end
end

local tmrProcessNext;
function mod:SourceUpdated()
	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.");
	elseif errorMessage == ERR_GUILD_WITHDRAW_LIMIT then
		self:Abort("at guild withdrawal limit", "At guild withdrawal limit");
	end
end

function mod:Abort(simple, debugMsg)
	-- Announce
	if debugMsg then
		addon:Debug("Aborting:%s", debugMsg);
	end
	if simple then
		addon:Print(("Aborting: %s."):format(simple), addon.Colors.Red);
	end
	
	-- Make sure nothing is at the mouse
	ClearCursor();
	
	-- Stop timer
	self:UnregisterEvent(ContainerFunctions[movesSource].Event);
	self:CancelTimer(tmrProcessNext, true); -- silent
	
	self:UnregisterEvent("UI_ERROR_MESSAGE");
	
	-- Reset vars
	twipe(combinedMoves);
	movesSource = nil;
	if MailAddonBusy == addon:GetName() then
		MailAddonBusy = nil;
	end
end

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

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