diff Modules/Mover.lua @ 84:3bec0ea44607

Cleaned the Inventorium folder; moved all classes to classes directory, modules to modules directory and support addons to plugins directory. In addition support addons are now references within XML files rather than inside the TOC. Fixed the default local item count setting, you can now exclude bag and AH data from it. Fixed some mover algorithm bugs. Mover can no longer freeze the game but instead will terminate the process after a 1000 passes. Now reversing the moves table after making it, rather than every single time it is used. Fixed guild bank support. Now displaying the amount of items moved. Scanner now scans all guild bank tabs rather than only the current. Fixed a bug with local item data not being retrieved properly. Disabled ?enterClicksFirstButton? within dialogs as this causes the dialog to consume all keypress. Events are now at the addon object rather than local.
author Zerotorescue
date Thu, 06 Jan 2011 20:05:30 +0100
parents Mover.lua@f885805da5d6
children f1c035694545
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Modules/Mover.lua	Thu Jan 06 20:05:30 2011 +0100
@@ -0,0 +1,400 @@
+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("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];
+		
+		-- sourceContainer, sourceSlot, targetContainer, targetSlot, itemId, num
+		if move and (not sourceLocationsLocked[move.sourceContainer] or not sourceLocationsLocked[move.sourceContainer][move.sourceSlot]) and 
+			(not targetLocationsLocked[move.targetContainer] or not targetLocationsLocked[move.targetContainer][move.targetSlot]) 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:BAG_UPDATE()
+	self:CancelTimer(tmrProcessNext, true); -- silent
+	tmrProcessNext = self:ScheduleTimer("ProcessMove", 1);
+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