changeset 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
files Classes/ContainerItem.class.lua Core.lua Frames.lua Modules/Mover.lua Modules/Queue.lua Modules/Scanner.lua
diffstat 6 files changed, 540 insertions(+), 220 deletions(-) [+]
line wrap: on
line diff
--- a/Classes/ContainerItem.class.lua	Wed Jan 12 22:48:25 2011 +0100
+++ b/Classes/ContainerItem.class.lua	Fri Jan 14 23:25:05 2011 +0100
@@ -19,14 +19,30 @@
 	return self;
 end
 
-function addon.ContainerItem:AddLocation(container, slot, count)
+function addon.ContainerItem:AddLocation(container, slot, count, price)
 	table.insert(self.locations, {
-		container = container,
-		slot = slot,
-		count = count,
+		["container"] = container,
+		["slot"] = slot,
+		["count"] = count,
+		["price"] = price,
 	});
 	
-	self.totalCount = (self.totalCount + count);
+	-- -1 indicates unlimited supply
+	if self.totalCount ~= -1 then
+		if count == -1 then
+			self.totalCount = -1;
+		else
+			self.totalCount = (self.totalCount + count);
+		end
+	end
 	
 	return true;
 end
+
+function addon.ContainerItem:GetVendorPrice()
+	for _, loc in pairs(self.locations) do
+		if loc.price then
+			return loc.price;
+		end
+	end
+end
--- a/Core.lua	Wed Jan 12 22:48:25 2011 +0100
+++ b/Core.lua	Fri Jan 14 23:25:05 2011 +0100
@@ -494,6 +494,6 @@
 	end
 	
 	if self.debugChannel then
-		self.debugChannel:AddMessage(sformat(t, ...));
+		self.debugChannel:AddMessage("|cffffff00Inventorium|r:" .. sformat(t, ...));
 	end
 end
--- a/Frames.lua	Wed Jan 12 22:48:25 2011 +0100
+++ b/Frames.lua	Fri Jan 14 23:25:05 2011 +0100
@@ -25,7 +25,7 @@
 	GameTooltip:Hide();
 end
 
-function addon:CreateMoverFrame(onAccept, onCancel)
+function addon:CreateMoverFrame()
 	local frameWidth = 400;
 	
 	-- Main window
@@ -78,6 +78,9 @@
 			
 			PlaySound("OrcExploration");
 		end);
+		frame:SetScript("OnHide", function(this)
+			StaticPopup_CollapseTable(this);
+		end);
 	
 	-- Title (AceGUI frame-widget-title used as example)
 		local titleBackground = frame:CreateTexture(nil, "OVERLAY");
@@ -109,7 +112,8 @@
 
 		local lblTitle = frmTitle:CreateFontString(nil, "OVERLAY", "GameFontNormal");
 		lblTitle:SetPoint("TOP", titleBackground, "TOP", 0, -14);
-		lblTitle:SetText("Inventorium Bank Refill");
+		
+		frame.lblTitle = lblTitle;
 	
 	-- Resizer (vertical only)
 		local frmResizer = CreateFrame("Frame", nil, frame);
@@ -126,7 +130,6 @@
 		lblDescription:SetWidth(frameWidth - 15 - 15); -- 10 margin left & 10 margin right
 		lblDescription:SetJustifyH("LEFT");
 		lblDescription:SetJustifyV("TOP");
-		lblDescription:SetText("The items listed below can be refilled from this location, do you wish to move them to your bags?");
 		
 		frame.lblDescription = lblDescription;
 	
@@ -137,11 +140,9 @@
 			btnMove:SetWidth(125);
 			btnMove:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 15, 11);
 			btnMove:SetText("Move Items");
-			btnMove:SetScript("OnClick", onAccept);
+			btnMove:SetScript("OnClick", function(this) this.OnClick(this); end);
 			btnMove:SetScript("OnEnter", ShowTooltip);
 			btnMove:SetScript("OnLeave", HideTooltip);
-			btnMove.tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Move Items");
-			btnMove.tooltip = (not addon.db.profile.defaults.hideHelp and "Start moving these items from the bank.");
 			
 			frame.btnMove = btnMove;
 			
@@ -150,12 +151,9 @@
 			btnCancel:SetHeight(21);
 			btnCancel:SetWidth(125);
 			btnCancel:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 11);
-			btnCancel:SetText("Cancel");
-			btnCancel:SetScript("OnClick", onCancel);
+			btnCancel:SetScript("OnClick", function(this) this.OnClick(this); end);
 			btnCancel:SetScript("OnEnter", ShowTooltip);
 			btnCancel:SetScript("OnLeave", HideTooltip);
-			btnCancel.tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Cancel");
-			btnCancel.tooltip = (not addon.db.profile.defaults.hideHelp and "Do not move anything and close the window.");
 			
 			frame.btnCancel = btnCancel;
 		
@@ -168,61 +166,11 @@
 		frame.frmMeasureDummy = frmMeasureDummy;
 		
 	-- Scrolling table with a list of items to be moved
-		local scrollTableWidth = ( frame.frmMeasureDummy:GetWidth() - 30 ); -- adjust width by the scrollbar size
-		local headers = {
-			{
-				["name"] = "Item",
-				["width"] = (scrollTableWidth * .60),
-				["defaultsort"] = "asc",
-				["comparesort"] = function(this, aRow, bRow, column)
-					local aName, _, aRarity = GetItemInfo(this:GetRow(aRow).rowData.id);
-					local bName, _, bRarity = GetItemInfo(this:GetRow(bRow).rowData.id);
-					local template = "%d%s";
-					aName = template:format((10 - (aRarity or 10)), (aName or ""):lower());
-					bName = template:format((10 - (bRarity or 10)), (bName or ""):lower());
-					
-					if this.cols[column].sort == "dsc" then
-						return aName > bName;
-					else
-						return aName < bName;
-					end
-				end,
-				["sort"] = "asc", -- when the data is set, use this column so sort the default data
-				["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"),
-				["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by item quality then item name."),
-			},
-			{
-				["name"] = "Moving",
-				["width"] = (scrollTableWidth * .15),
-				["align"] = "RIGHT",
-				["defaultsort"] = "dsc",
-				["sortnext"] = 1,
-				["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Moving"),
-				["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the amount of movable items."),
-			},
-			{
-				["name"] = "Available",
-				["width"] = (scrollTableWidth * .25),
-				["align"] = "RIGHT",
-				["defaultsort"] = "dsc",
-				["sortnext"] = 1,
-				["comparesort"] = function(this, aRow, bRow, column)
-					local aAvailablePercent = (this:GetRow(aRow).rowData.available / this:GetRow(aRow).rowData.missing);
-					local bAvailablePercent = (this:GetRow(bRow).rowData.available / this:GetRow(bRow).rowData.missing);
-					
-					if this.cols[column].sort == "dsc" then
-						return aAvailablePercent > bAvailablePercent;
-					else
-						return aAvailablePercent < bAvailablePercent;
-					end
-				end,
-				["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"),
-				["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the availibility percentage."),
-			},
-		};
-		
 		local ScrollingTable = LibStub("ScrollingTable");
-		local table = ScrollingTable:CreateST(headers, 3, 15, nil, frame);
+		local table = ScrollingTable:CreateST({}, 4, 15, nil, frame); -- inserting a dummy cols, real cols to be set in SetFrameSettings
+		table.frame:SetPoint("TOP", frame.lblDescription, "BOTTOM", 0, -18);
+		table.frame:SetPoint("LEFT", frame, "LEFT", 15, 0);
+		table.frame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 35);
 		-- When moving over a row, provide a tooltip for the item
 		table:RegisterEvents({
 			["OnEnter"] = function(rowFrame, cellFrame, data, cols, row, realrow, column, scrollingTable, ...)
@@ -256,14 +204,11 @@
 		});
 		
 		frame.scrollTable = table;
-		table.frame:SetPoint("TOP", frame.lblDescription, "BOTTOM", 0, -18);
-		table.frame:SetPoint("LEFT", frame, "LEFT", 15, 0);
-		table.frame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 35);
-		
+	
 	-- Change the amount of displayed rows based on the size of the frame
 		frame.AdjustScrollTableRows = function(this)
 			local newRows = math.floor(( this.frmMeasureDummy:GetHeight() - 5 ) / 15);
-			newRows = (newRows < 3 and 3) or newRows;
+			newRows = (newRows < 4 and 4) or newRows;
 			
 			this.scrollTable:SetDisplayRows(newRows, 15);
 		end;
@@ -275,3 +220,23 @@
 	
 	InventoriumItemMover:Show();
 end
+
+function addon:SetFrameSettings(title, description, proceed, cancel, headers)
+	local frame = InventoriumItemMover;
+	
+	frame.lblTitle:SetText(title);
+	
+	frame.lblDescription:SetText(description);
+	
+	frame.btnMove:SetText(proceed.text);
+	frame.btnMove.tooltipTitle = proceed.tooltipTitle;
+	frame.btnMove.tooltip = proceed.tooltip;
+	frame.btnMove.OnClick = proceed.onClick;
+	
+	frame.btnCancel:SetText(cancel.text);
+	frame.btnCancel.tooltipTitle = cancel.tooltipTitle;
+	frame.btnCancel.tooltip = cancel.tooltip;
+	frame.btnCancel.OnClick = cancel.onClick;
+	
+	frame.scrollTable:SetDisplayCols(headers);
+end
--- 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()
--- a/Modules/Queue.lua	Wed Jan 12 22:48:25 2011 +0100
+++ b/Modules/Queue.lua	Fri Jan 14 23:25:05 2011 +0100
@@ -93,7 +93,7 @@
 				-- Retrieve group settings
 				local restockTarget = addon:GetOptionByKey(groupName, "restockTarget");
 				local bonusQueue = addon:GetOptionByKey(groupName, "bonusQueue");
-				local minCraftingQueue = floor( addon:GetOptionByKey(groupName, "minCraftingQueue") * restockTarget );
+				local minCraftingQueue = floor( addon:GetOptionByKey(groupName, "minCraftingQueue") * restockTarget ); -- If the minCraftingQueue is 5% and restockTarget is 60, this will result in 3
 				
 				-- Calculate the amount to be queued
 				local amount = ( restockTarget - currentStock );
--- a/Modules/Scanner.lua	Wed Jan 12 22:48:25 2011 +0100
+++ b/Modules/Scanner.lua	Fri Jan 14 23:25:05 2011 +0100
@@ -1,30 +1,107 @@
 local addon = select(2, ...);
 local mod = addon:NewModule("Scanner", "AceEvent-3.0", "AceTimer-3.0");
 
-addon.Locations = {
-	Bag = 0,
-	Bank = 1,
-	Guild = 2,
-	Mailbox = 3,
-};
-
 local Mover, paused, currentLocation;
 local itemCache = {};
 
-local function OnMoveAccept(this)
+local function OnMoveAccept()
 	mod:Pause();
 	Mover:BeginMove(currentLocation, mod.Unpause);
 	
 	InventoriumItemMover:Hide();
 end
 
-local function OnMoveCancel(this)
+local function OnMoveCancel()
 	Mover:ResetQueue();
 	currentLocation = nil;
 	
 	InventoriumItemMover:Hide();
 end
 
+local function UseStorageRefillST(withPrices)
+	local frame = InventoriumItemMover; -- both for speed as code-consistency
+	
+	-- Scrolling table with a list of items to be moved
+	local scrollTableWidth = ( frame.frmMeasureDummy:GetWidth() - 30 ); -- adjust width by the scrollbar size
+	local headers = {
+		{
+			["name"] = "Item",
+			["width"] = (scrollTableWidth * ((withPrices and .5) or .60)),
+			["defaultsort"] = "asc",
+			["comparesort"] = function(this, aRow, bRow, column)
+				local aName, _, aRarity = GetItemInfo(this:GetRow(aRow).rowData.itemId);
+				local bName, _, bRarity = GetItemInfo(this:GetRow(bRow).rowData.itemId);
+				local template = "%d%s";
+				aName = template:format((10 - (aRarity or 10)), (aName or ""):lower());
+				bName = template:format((10 - (bRarity or 10)), (bName or ""):lower());
+				
+				if this.cols[column].sort == "dsc" then
+					return aName > bName;
+				else
+					return aName < bName;
+				end
+			end,
+			["sort"] = "asc", -- when the data is set, use this column so sort the default data
+			["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"),
+			["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by item quality then item name."),
+		},
+		{
+			["name"] = "Moving",
+			["width"] = (scrollTableWidth * .15),
+			["align"] = "RIGHT",
+			["defaultsort"] = "dsc",
+			["sortnext"] = 1,
+			["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Moving"),
+			["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the amount of movable items."),
+		},
+		{
+			["name"] = "Available",
+			["width"] = (scrollTableWidth * ((withPrices and .2) or .25)),
+			["align"] = "RIGHT",
+			["defaultsort"] = "dsc",
+			["sortnext"] = 1,
+			["comparesort"] = function(this, aRow, bRow, column)
+				local aAvailablePercent = (this:GetRow(aRow).rowData.available / this:GetRow(aRow).rowData.missing);
+				local bAvailablePercent = (this:GetRow(bRow).rowData.available / this:GetRow(bRow).rowData.missing);
+				
+				if this.cols[column].sort == "dsc" then
+					return aAvailablePercent > bAvailablePercent;
+				else
+					return aAvailablePercent < bAvailablePercent;
+				end
+			end,
+			["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Item"),
+			["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the availibility percentage."),
+		},
+	};
+	if withPrices then
+		table.insert(headers, {
+			["name"] = "Price",
+			["width"] = (scrollTableWidth * .15),
+			["align"] = "RIGHT",
+			["defaultsort"] = "dsc",
+			["sortnext"] = 1,
+			["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Price"),
+			["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the price of this item at this vendor."),
+		});
+	end
+	
+	local proceedButton = {
+		text = "Move Items",
+		tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Move Items"),
+		tooltip = (not addon.db.profile.defaults.hideHelp and "Start moving these items from the bank."),
+		onClick = OnMoveAccept,
+	};
+	local cancelButton = {
+		text = "Cancel",
+		tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Cancel"),
+		tooltip = (not addon.db.profile.defaults.hideHelp and "Do not move anything and close the window."),
+		onClick = OnMoveCancel,
+	};
+	
+	addon:SetFrameSettings("Inventorium Bank Refill", "The items listed below can be refilled from this location, do you wish to move them to your bags?", proceedButton, cancelButton, headers);
+end
+
 function mod:ClearCache()
 	table.wipe(itemCache);
 end
@@ -76,9 +153,9 @@
 		end
 	elseif location == addon.Locations.Guild then
 		for tabId = 1, GetNumGuildBankTabs() do
-			local isViewable = select(3, GetGuildBankTabInfo(tabId));
+			local _, _, isViewable, _, _, remainingWithdrawals = GetGuildBankTabInfo(tabId);
 			
-			if isViewable == 1 then
+			if isViewable and (remainingWithdrawals > 0 or remainingWithdrawals == -1) then
 				local slotId = (MAX_GUILDBANK_SLOTS_PER_TAB or 98); -- start by scanning the last slot
 				
 				while slotId ~= 0 do
@@ -108,6 +185,54 @@
 				end
 			end
 		end
+	elseif location == addon.Locations.Mailbox then
+		for mailIndex = 1, GetInboxNumItems() do
+			-- All mail items
+			
+			for attachIndex = 1, ATTACHMENTS_MAX_RECEIVE do
+				-- All attachments
+				
+				local itemLink = GetInboxItemLink(mailIndex, attachIndex);
+				local itemId = itemLink and addon:GetItemId(itemLink);
+				local itemCount = itemLink and select(3, GetInboxItem(mailIndex, attachIndex));
+				
+				if itemLink and itemId and itemCount and itemCount > 0 then
+					local itemMove;
+					if not itemCache[itemId] then
+						-- If this is the first time we see this item, make a new object
+						itemMove = addon.ContainerItem:New();
+						itemCache[itemId] = itemMove;
+					else
+						-- If we had this item in another slot too
+						itemMove = itemCache[itemId];
+					end
+					
+					itemMove:AddLocation(mailIndex, attachIndex, itemCount);
+				end
+			end
+		end
+	elseif location == addon.Locations.Merchant then
+		for itemIndex = 1, GetMerchantNumItems() do
+			-- All merchant items
+			
+			local itemLink = GetMerchantItemLink(itemIndex);
+			local itemId = itemLink and addon:GetItemId(itemLink);
+			local _, _, vendorValue, _, numAvailable, _, extendedCost = GetMerchantItemInfo(index);
+			
+			if itemLink and itemId and numAvailable ~= 0 and not extendedCost then
+				local itemMove;
+				if not itemCache[itemId] then
+					-- If this is the first time we see this item, make a new object
+					itemMove = addon.ContainerItem:New();
+					itemCache[itemId] = itemMove;
+				else
+					-- If we had this item in another slot too
+					itemMove = itemCache[itemId];
+				end
+				
+				itemMove:AddLocation(1, itemIndex, numAvailable, vendorValue);
+			end
+		end
 	else
 		error("Invalid location provided for CacheLocation. Must be Bank or Guild.");
 	end
@@ -125,6 +250,7 @@
 function mod:Scan(location)
 	-- We might pause the scanning when we invoke moves ourself
 	if paused then
+		addon:Debug("Not scanning; paused...");
 		return;
 	end
 	
@@ -133,6 +259,9 @@
 	currentLocation = location;
 	self:CacheLocation(location, true);
 	
+	-- Ensure previous queue isn't remaining
+	Mover:ResetQueue();
+	
 	-- Go through all groups
 	for groupName, values in pairs(addon.db.profile.groups) do
 		local trackAt = addon:GetOptionByKey(groupName, "trackAtCharacters");
@@ -153,14 +282,12 @@
 					-- Check how many are available
 					local availableItems = ((itemCache[itemId] and itemCache[itemId].totalCount) or 0);
 					-- Calculate how many we'll be moving (less missing than available? use missing, otherwise use available)
-					local moving = (((missingItems <= availableItems) and missingItems) or availableItems);
+					local moving = (((availableItems == -1 or missingItems <= availableItems) and missingItems) or availableItems);
 					
-					if availableItems > 0 then
-						--addon:Print("Insufficient " .. IdToItemLink(itemId) .. " but this location has " .. availableItems .. " (moving " .. moving .. ")");
+					if availableItems ~= 0 then
+						addon:Debug("Insufficient %s but this location has %s (moving %d)", IdToItemLink(itemId), ((availableItems == -1 and "unlimited") or availableItems), moving);
 						
-						Mover:AddMove(itemId, moving, missingItems, availableItems);
-					else
-						--addon:Print("Insufficient " .. IdToItemLink(itemId));
+						Mover:AddMove(itemId, moving, missingItems, availableItems, itemCache[itemId]:GetVendorPrice());
 					end
 				end
 			end
@@ -171,29 +298,38 @@
 	
 	if Mover:HasMoves() then
 		if addon.db.profile.defaults.autoRefillSkipConfirm then
-			OnMoveAccept(true);
+			OnMoveAccept();
 		else
+			UseStorageRefillST((location == addon.Locations.Merchant));
+			
 			-- This table is never copied, just referenced. It is the same for every row.
 			local columns = {
 				{
-					value = function(data, cols, realrow, column, table)
-						return IdToItemLink(data[realrow].rowData.id);
+					["value"] = function(data, cols, realrow, column, table)
+						return IdToItemLink(data[realrow].rowData.itemId);
 					end,
 				}, -- item
 				{
-					value = function(data, cols, realrow, column, table)
+					["value"] = function(data, cols, realrow, column, table)
 						return data[realrow].rowData.num;
 					end,
 				}, -- moving
 				{
-					value = function(data, cols, realrow, column, table)
+					["value"] = function(data, cols, realrow, column, table)
 						return addon:DisplayItemCount(data[realrow].rowData.available, data[realrow].rowData.missing); -- available / missing
 					end,
-					color = function(data, cols, realrow, column, table)
+					["color"] = function(data, cols, realrow, column, table)
 						return ((data[realrow].rowData.available < data[realrow].rowData.missing) and { r = 1, g = 0, b = 0, a = 1 }) or { r = 1, g = 1, b = 1, a = 1 };
 					end,
 				}, -- missing / available
 			};
+			if location == addon.Locations.Merchant then
+				table.insert(columns, {
+					["value"] = function(data, cols, realrow, column, table)
+						return GetCoinTextureString(data[realrow].rowData.price * data[realrow].rowData.num);
+					end,
+				});
+			end
 			
 			-- Store the list with rows in this
 			local data = {};
@@ -279,8 +415,9 @@
 	
 	-- Get the contents for every tab into our cache
 	for tabId = 1, GetNumGuildBankTabs() do
-		local isViewable = select(3, GetGuildBankTabInfo(tabId));
-		if isViewable == 1 then		
+		local _, _, isViewable, _, _, remainingWithdrawals = GetGuildBankTabInfo(tabId);
+		
+		if isViewable and (remainingWithdrawals > 0 or remainingWithdrawals == -1) then
 			QueryGuildBankTab(tabId);
 		end
 	end
@@ -289,10 +426,79 @@
 	self:RegisterEvent("GUILDBANKBAGSLOTS_CHANGED");
 end
 
+function mod:MERCHANT_SHOW()
+	addon:Debug("Scanner:MERCHANT_SHOW");
+	
+	self:RegisterEvent("MERCHANT_CLOSED");
+	
+	self:Scan(addon.Locations.Merchant);
+end
+
+function mod:MERCHANT_CLOSED()
+	addon:Debug("Scanner:MERCHANT_CLOSED");
+	
+	self:ClearCache();
+	
+	self:UnregisterEvent("MERCHANT_CLOSED");
+	
+	InventoriumItemMover:Hide();
+	Mover:ResetQueue();
+end
+
+--local previousMailCount;
+--function mod:MAIL_SHOW()
+--	addon:Debug("Scanner:MAIL_SHOW");
+--	
+--	self:RegisterEvent("MAIL_INBOX_UPDATE");
+--	self:RegisterEvent("MAIL_CLOSED");
+--	
+--	scanned = nil;
+--	previousMailCount = nil;
+--	
+--	self:Scan(addon.Locations.Mailbox);
+--end
+
+--function mod:MAIL_INBOX_UPDATE()
+--	if not scanned then
+--		addon:Debug("Scanner:MAIL_INBOX_UPDATE");
+--		
+--		local current, total = GetInboxNumItems();
+--		
+--		if not previousMailCount or current > previousMailCount then
+--			-- New mail received
+--			
+--			scanned = true;
+--			
+--			self:Scan(addon.Locations.Mailbox);
+--		end
+--		
+--		-- Also remember the new mailcount when losing items, otherwise deleting item 50 and getting to 50 again wouldn't trigger a re-scan
+--		previousMailCount = current;
+--	else
+--		addon:Debug("Scanner:MAIL_INBOX_UPDATE skipped, already scanned");
+--	end
+--end
+
+--function mod:MAIL_CLOSED()
+--	addon:Debug("Scanner:MAIL_CLOSED");
+--	
+--	previousMailCount = nil;
+--	scanned = nil;
+--	self:ClearCache();
+--	
+--	self:UnregisterEvent("MAIL_INBOX_UPDATE");
+--	self:UnregisterEvent("MAIL_CLOSED");
+--	
+--	InventoriumItemMover:Hide();
+--	Mover:ResetQueue();
+--end
+
 function mod:OnEnable()
 	-- Scan once when the bankframe is opened
 	self:RegisterEvent("BANKFRAME_OPENED");
 	self:RegisterEvent("GUILDBANKFRAME_OPENED");
+--	self:RegisterEvent("MAIL_SHOW");
+	self:RegisterEvent("MERCHANT_SHOW");
 	
 	Mover = addon:GetModule("Mover");
 	
@@ -307,11 +513,16 @@
 	paused = nil;
 	
 	-- Bank
+	self:BANKFRAME_CLOSED();
 	self:UnregisterEvent("BANKFRAME_OPENED");
 	
 	-- Guild
 	self:GUILDBANKFRAME_CLOSED();
 	self:UnregisterEvent("GUILDBANKFRAME_OPENED");
+	
+--	-- Mailbox
+--	self:MAIL_CLOSED();
+--	self:UnregisterEvent("MAIL_SHOW");
 end
 
 function mod:Pause()