diff Modules/Mover.lua @ 110:67bd5057ecb7

Implemented vendor restocking with the mover. Comitting so I can always review this working version, but I?ll be disabling all part of it as it is not going to work properly without seriously compromising the code structure. Debug messages are now appended with ?Inventorium? (my MailOpener addon was making stuff difficult). Now properly removing the refill window from the displayed static popup windows list so new popups won?t be aligned at odd locations. Changed ?CreateMoverFrame? to not contain any scenario-specific info. All settings can be set with SetFrameSettings. Items that belong to speciality bags are now put there. Other items now ignore spaciality bags. Implemented test code for mailbox refill support. It has been disabled due to some issues but may be introduced later. The guild withdrawal limit is now taken into consideration. Queue is now reset before scanning again.
author Zerotorescue
date Fri, 14 Jan 2011 23:25:05 +0100
parents 3bbad0429d87
children 41f0689dfda1
line wrap: on
line diff
--- a/Modules/Mover.lua	Wed Jan 12 22:48:25 2011 +0100
+++ b/Modules/Mover.lua	Fri Jan 14 23:25:05 2011 +0100
@@ -6,12 +6,82 @@
 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, numMissing, numAvailable)
+addon.Locations = {
+	Bag = 0,
+	Bank = 1,
+	Guild = 2,
+	Mailbox = 3,
+	Merchant = 4,
+};
+
+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 = "ITEM_LOCK_CHANGED",
+	},
+	[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)
+			-- The below behavior was changed in patch 4.0.1, it now acts as expected; quantity requested equals exact quantity bought, even with increased batchsize
+			
+			-- Some merchant items are sold in batches (e.g. of 5)
+			-- In that case BuyMerchantItem wants the num stacks, rather than the quantity to be bought
+			--local batchSize = select(4, GetMerchantItemInfo(merchantIndex));
+			
+			--local batches = math.ceil(num / batchSize);
+			
+			--BuyMerchantItem(merchantIndex, batches);
+			
+			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, price)
 	table.insert(queuedMoves, {
-		id = itemId,
-		num = amount,
-		missing = numMissing,
-		available = numAvailable,
+		["itemId"] = itemId,
+		["num"] = amount, -- can not be unlimited
+		["missing"] = numMissing,
+		["available"] = numAvailable,
+		["price"] = price,
 	});
 end
 
@@ -43,6 +113,46 @@
 	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
+				table.insert(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");
 	
@@ -57,24 +167,52 @@
 	addon:Debug("%d moves were queued.", #queuedMoves);
 	
 	for _, singleMove in pairs(queuedMoves) do
-		local sourceItem = sourceContents[singleMove.id];
+		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.id)), addon.Colors.Red);
+			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)
 			table.sort(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
-				local movingNum = ((itemLocation.count > singleMove.num and singleMove.num) or itemLocation.count);
+				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
+							
+							table.insert(outgoingMoves, {
+								["itemId"] = singleMove.itemId,
+								["num"] = stackSize,
+								["container"] = itemLocation.container,
+								["slot"] = itemLocation.slot,
+							});
+							
+							movingNum = (movingNum - stackSize);
+						end
+					end
+				end
 				
 				table.insert(outgoingMoves, {
-					itemId = singleMove.id,
-					num = movingNum,
-					container = itemLocation.container,
-					slot = itemLocation.slot,
+					["itemId"] = singleMove.itemId,
+					["num"] = movingNum,
+					["container"] = itemLocation.container,
+					["slot"] = itemLocation.slot,
 				});
 				
 				singleMove.num = (singleMove.num - movingNum);
@@ -92,56 +230,43 @@
 	-- 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
+	local emptySlots = GetEmptySlots();
 
 	addon:Debug("%d empty slots are available.", #emptySlots);
 	
 	-- Remember where we're moving from
 	movesSource = location;
 	
-	local backup = 0;
+	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
-		-- A not equal-comparison should be quicker than a larger/smaller than-comparison
+		-- Repeat the below loop until nothing is remaining
 		
 		for _, outgoingMove in pairs(outgoingMoves) do
-			-- itemId  will be set to nil when this outgoing move was processed - sanity check
-			if outgoingMove.itemId then
+			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 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
+				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 firstAvailableSlot = emptySlots[1];
+					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
@@ -153,13 +278,15 @@
 							end
 						end
 					else
+						-- Consume empty slot
+						
 						table.insert(combinedMoves, {
-							itemId = outgoingMove.itemId,
-							num = outgoingMove.num,
-							sourceContainer = outgoingMove.container,
-							sourceSlot = outgoingMove.slot,
-							targetContainer = firstAvailableSlot.container,
-							targetSlot = firstAvailableSlot.slot,
+							["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,
@@ -192,12 +319,12 @@
 								-- 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,
+									["itemId"] = outgoingMove.itemId,
+									["num"] = outgoingMove.num,
+									["sourceContainer"] = outgoingMove.container,
+									["sourceSlot"] = outgoingMove.slot,
+									["targetContainer"] = itemLocation.container,
+									["targetSlot"] = itemLocation.slot,
 								});
 								
 								itemLocation.count = (itemLocation.count + outgoingMove.num);
@@ -208,12 +335,12 @@
 								-- 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,
+									["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
@@ -251,7 +378,6 @@
 		
 		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();
@@ -273,41 +399,7 @@
 	onFinish();
 end
 
-local ContainerFunctions = {
-	[addon.Locations.Bag] = {
-		GetItemId = GetContainerItemID,
-		PickupItem = SplitContainerItem,
-		IsLocked = function(sourceContainer, sourceSlot)
-			return select(3, GetContainerItemInfo(sourceContainer, sourceSlot);
-		end,
-	},
-	[addon.Locations.Bank] = {
-		GetItemId = GetContainerItemID,
-		PickupItem = SplitContainerItem,
-		IsLocked = function(sourceContainer, sourceSlot)
-			return select(3, GetContainerItemInfo(sourceContainer, sourceSlot);
-		end,
-	},
-	[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,
-	},
-	--[[ Even though support is possible, it will require a little more work than just this and there are currently higher priorities
-	[addon.Locations.Mailbox] = {
-		GetItemId = function(mailIndex, attachmentId)
-			return addon:GetItemId(GetInboxItemLink(mailIndex, attachmentId));
-		end,
-		PickupItem = TakeInboxItem,
-		IsLocked = function() return false; end,
-		DoNotDrop = true,
-	},]]
-};
-
+local tmrRetry;
 function mod:ProcessMove()
 	addon:Debug("ProcessMove");
 	
@@ -317,12 +409,23 @@
 		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();
 	
-	self:RegisterEvent("ITEM_LOCK_CHANGED");
+	if movesSource == addon.Locations.Mailbox then
+		MailAddonBusy = addon:GetName();
+	end
+	
+	self:RegisterEvent(ContainerFunctions[movesSource].Event, "SourceUpdated");
 	self:RegisterEvent("UI_ERROR_MESSAGE");
 	
 	-- combinedMoves now has all moves in it (source -> target)
@@ -333,20 +436,24 @@
 	local sourceLocationsLocked = {};
 	local targetLocationsLocked = {};
 	
+	local hasMoved;
+	
 	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 ContainerFunctions[movesSource].IsLocked(move.sourceContainer, move.sourceSlot));
+		-- 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 then
+		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;
@@ -355,34 +462,47 @@
 			-- 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 movesSource ~= addon.Locations.Bank or CursorHasItem() then -- CursorHasItem only works when moving outside of the bank
-				-- We are moving into our local bags, so the below must check normal
-				local targetItemId = ContainerFunctions[movesSource].get(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");
+			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
+					table.remove(combinedMoves, numCurrentMove);
+				else
+					self:Abort("item disappeared from mouse", "Couldn't move " .. IdToItemLink(move.itemId) .. ", CursorHasItem() is false");
 					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;
+			else
+				-- When items are deposit automatically we still need to remember when a move has been processed
 				
 				-- 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
 		
@@ -402,7 +522,7 @@
 end
 
 local tmrProcessNext;
-function mod:ITEM_LOCK_CHANGED()
+function mod:SourceUpdated()
 	self:CancelTimer(tmrProcessNext, true); -- silent
 	tmrProcessNext = self:ScheduleTimer("ProcessMove", .5);
 end
@@ -416,27 +536,35 @@
 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
-	table.wipe(combinedMoves);
-	movesSource = nil;
 	
 	-- Make sure nothing is at the mouse
 	ClearCursor();
 	
 	-- Stop timer
-	self:UnregisterEvent("ITEM_LOCK_CHANGED");
+	self:UnregisterEvent(ContainerFunctions[movesSource].Event);
 	self:CancelTimer(tmrProcessNext, true); -- silent
 	
 	self:UnregisterEvent("UI_ERROR_MESSAGE");
+	
+	-- Reset vars
+	table.wipe(combinedMoves);
+	movesSource = nil;
+	if MailAddonBusy == addon:GetName() then
+		MailAddonBusy = nil;
+	end
 end
 
 function mod:OnEnable()