view Modules/Scanner.lua @ 143:8eb0f5b5a885

Fixed ?fully stocked? tooltip to say it uses the restock target setting rather than the global stock setting. Making sure the inventorium queuer frame is available before hiding it. Fixed an error with queueing items. Skip reasons are now sorted by importance (also by default). Now closing the queue window when you close your profession window.
author Zerotorescue
date Tue, 18 Jan 2011 23:48:16 +0100
parents 56f33abee1e3
children 12a8ea5af671
line wrap: on
line source
local addon = select(2, ...);
local mod = addon:NewModule("Scanner", "AceEvent-3.0", "AceTimer-3.0");

local _G = _G;
local select, pairs = _G.select, _G.pairs;
local twipe, tinsert, mceil = _G.table.wipe, _G.table.insert, _G.math.ceil;

local Mover, paused, currentLocation;
local itemCache = {};

local function OnMoveAccept()
	mod:Pause();
	Mover:BeginMove(currentLocation, mod.Unpause);
	
	if InventoriumItemMover then
		InventoriumItemMover:Hide();
	end
end

local function OnMoveCancel()
	Mover:ResetQueue();
	currentLocation = nil;
	
	if InventoriumItemMover then
		InventoriumItemMover:Hide();
	end
end

local function GetSmallCoinTextureString(coins)
	if coins >= 10000000 then
		-- When we have1000g, hide silver and copper
		coins = (mceil(coins / 10000) * 10000);
	elseif coins >= 10000 then
		-- When we have 1g, hide copper
		coins = (mceil(coins / 100) * 100);
	end
	
	return GetCoinTextureString(coins);
end

-- Refill moves window: refill form storage such as the bank, guild bank and mailbox
local function UseStorageRefillST()
	if not InventoriumItemMover then
		addon:CreateMoverFrame();
	end
	
	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 * .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 * .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 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:SetMoverFrameSettings("Inventorium Storage Refill", "The items listed below can be refilled from this location, do you wish to move them to your bags?", proceedButton, cancelButton, headers);
end

-- Merchant restock window: restock from a merchant by buying items needed
local function UseMerchantRestockST(totalCost)
	if not InventoriumItemMover then
		addon:CreateMoverFrame();
	end
	
	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 * .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"] = "Buying",
			["width"] = (scrollTableWidth * .20),
			["align"] = "RIGHT",
			["defaultsort"] = "dsc",
			["sortnext"] = 1,
			["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Buying"),
			["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the amount of purchasable items."),
		},
		{
			["name"] = "Cost",
			["width"] = (scrollTableWidth * .20),
			["align"] = "RIGHT",
			["defaultsort"] = "dsc",
			["sortnext"] = 1,
			["tooltipTitle"] = (not addon.db.profile.defaults.hideHelp and "Cost"),
			["tooltip"] = (not addon.db.profile.defaults.hideHelp and "Click to sort the list by the total cost of buying all these items."),
		},
	};
	
	local proceedButton = {
		text = "Purchase Items",
		tooltipTitle = (not addon.db.profile.defaults.hideHelp and "Purchase Items"),
		tooltip = (not addon.db.profile.defaults.hideHelp and "Start purchasing these items from this merchant."),
		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 purchase anything and close the window."),
		onClick = OnMoveCancel,
	};
	
	addon:SetMoverFrameSettings("Inventorium Merchant Restock", ("The following items can be restocked from this merchant for a total of %s. Do you wish to proceed?"):format(GetSmallCoinTextureString(totalCost)), proceedButton, cancelButton, headers);
end

function mod:ClearCache()
	twipe(itemCache);
end

function mod:CacheLocation(location, remember)
	-- Reset cache just in case it was filled
	self:ClearCache();
	
	if location == addon.Locations.Bag or location == addon.Locations.Bank then
		local start, stop;
		if location == addon.Locations.Bag then
			start = 0;
			stop = NUM_BAG_SLOTS;
		else
			-- If we requested the bank then we don't want the bag info
			start = ( NUM_BAG_SLOTS + 1 );
			stop = ( NUM_BAG_SLOTS + NUM_BANKBAGSLOTS );
		end
		
		-- Go through all our bags, including the backpack
		for i = start, ((location == addon.Locations.Bag and stop) or (location == addon.Locations.Bank and (stop + 1))) do -- if scanning bags stop at normal bag slot, if scanning bank, stop one later to allow BANK_CONTAINER to be scanned too
			-- Scan the default 100 slots whenever we're at a non-existing index
			local bagId = (i == (stop + 1) and BANK_CONTAINER) or i;
			local slotId = GetContainerNumSlots(bagId);
			
			while slotId ~= 0 do
				-- A not equal-comparison should be quicker than a larger than-comparison
				
				local itemId = GetContainerItemID(bagId, slotId);
				local itemCount = itemId and select(2, GetContainerItemInfo(bagId, slotId));
				
				if itemId and itemCount and itemCount > 0 then
					local containerItem;
					if not itemCache[itemId] then
						-- If this is the first time we see this item, make a new object
						containerItem = addon.ContainerItem:New();
						itemCache[itemId] = containerItem;
					else
						-- If we had this item in another slot too
						containerItem = itemCache[itemId];
					end
					
					containerItem:AddLocation(bagId, slotId, itemCount);
				end
			
				-- Continue scanning a different slot
				slotId = (slotId - 1);
			end
		end
	elseif location == addon.Locations.Guild then
		for tabId = 1, GetNumGuildBankTabs() do
			local _, _, isViewable, _, _, remainingWithdrawals = GetGuildBankTabInfo(tabId);
			
			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
					-- A not equal-comparison should be quicker than a larger than-comparison
					
					local itemLink = GetGuildBankItemLink(tabId, slotId);
					local itemId = itemLink and addon:GetItemId(itemLink);
					local itemCount = itemLink and select(2, GetGuildBankItemInfo(tabId, slotId));
						
					if itemLink and itemId and itemCount and itemCount > 0 then
						-- If there is actually an item in this slot
						local containerItem;
						if not itemCache[itemId] then
							-- If this is the first time we see this item, make a new object
							containerItem = addon.ContainerItem:New();
							itemCache[itemId] = containerItem;
						else
							-- If we had this item in another slot too
							containerItem = itemCache[itemId];
						end
						
						containerItem:AddLocation(tabId, slotId, itemCount);
					end
					
					-- Continue scanning a different slot
					slotId = (slotId - 1);
				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 containerItem;
					if not itemCache[itemId] then
						-- If this is the first time we see this item, make a new object
						containerItem = addon.ContainerItem:New();
						itemCache[itemId] = containerItem;
					else
						-- If we had this item in another slot too
						containerItem = itemCache[itemId];
					end
					
					containerItem: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, quantity, numAvailable, _, extendedCost = GetMerchantItemInfo(itemIndex);
			
			if itemLink and itemId and numAvailable ~= 0 and not extendedCost then
				local containerItem;
				if not itemCache[itemId] then
					-- If this is the first time we see this item, make a new object
					containerItem = addon.ContainerItem:New();
					containerItem.price = (vendorValue / quantity); -- remember the price for this item. We assume it's the same for this entire item id
					
					itemCache[itemId] = containerItem;
				else
					-- If we had this item in another slot too
					containerItem = itemCache[itemId];
				end
				
				containerItem:AddLocation(1, itemIndex, numAvailable);
			end
		end
	else
		error("Invalid location provided for CacheLocation.");
	end
	
	if not remember then
		-- Copy the table as clearing the cache wipes it empty (and tables are passed by reference)
		local cacheCopy = CopyTable(itemCache);
		
		self:ClearCache();
		
		return cacheCopy;
	end
end

function mod:Scan(location)
	-- We might pause the scanning when we invoke moves ourself
	if paused then
		addon:Debug("Not scanning; paused...");
		return;
	end
	
	local playerName = UnitName("player");
	
	currentLocation = location;
	self:CacheLocation(location, true);
	
	-- Ensure previous queue isn't remaining
	Mover:ResetQueue();
	
	-- Restock = obtaining more, refill = merely moving local. IsRestock = are we buying/making more?
	local isRestock = (location == addon.Locations.Merchant);
	
	-- Go through all groups
	for groupName, values in pairs(addon.db.profile.groups) do
		-- Settings
		local trackAt = addon:GetOptionByKey(groupName, "trackAtCharacters");
		local localItemData = addon:GetOptionByKey(groupName, "localItemData");
		local requiredItems, bonusQueue, minCraftingQueue, isRefillEnabled;
		if isRestock then
			requiredItems = addon:GetOptionByKey(groupName, "restockTarget");
			bonusQueue = addon:GetOptionByKey(groupName, "bonusQueue");
			minCraftingQueue = floor( addon:GetOptionByKey(groupName, "minCraftingQueue") * requiredItems ); -- If the minCraftingQueue is 5% and restockTarget is 60, this will result in 3
		else
			isRefillEnabled = addon:GetOptionByKey(groupName, "autoRefill");
			requiredItems = addon:GetOptionByKey(groupName, "minLocalStock");
		end
		
		local isTracked = (trackAt and trackAt[playerName]); -- Is this character interested in this data?
		local isConsideredLocal = (localItemData and localItemData[location]); -- if this location was checked as local storage, don't refill from it
		
		if values.items and isTracked and (isRestock or isRefillEnabled) and not isConsideredLocal then
			addon:Debug("Scanning |cff00ff00%s|r", groupName);
			
			for itemId, _ in pairs(values.items) do
				-- Find this item in the source
				local containerItem = itemCache[itemId];
				
				if containerItem then
					-- Only do all the CPU intensive checks if this item is available
					
					-- When restocking use the global item count, when refilling use the local
					local currentItemCount = ((isRestock and addon:GetItemCount(itemId, groupName)) or addon:GetLocalItemCount(itemId, groupName));
					
					-- Check if we have enough items local (but only do so if this location also has enough available)
					local missingItems = (requiredItems - currentItemCount);
					
					if isRestock and currentItemCount == 0 and bonusQueue and bonusQueue > 0 then
						-- If we have none left and the bonus queue is enabled, modify the amount to  be queued
						
						missingItems = floor( ( missingItems * ( bonusQueue + 1 ) ) + .5 ); -- round
					end
					
					if missingItems > 0 and (not isRestock or missingItems >= minCraftingQueue) then
						-- Check how many are available
						local availableItems = ((containerItem.totalCount) or 0);
						-- Calculate how many we'll be moving (less missing than available? use missing, otherwise use available)
						-- -1 available items indicates unlimited amount, in that case we must cap at missing items
						local moving = (((availableItems == -1 or missingItems <= availableItems) and missingItems) or availableItems);
						
						if availableItems == -1 or availableItems > 0 then
							addon:Debug("Insufficient %s but this location has %d (moving %d)", IdToItemLink(itemId), availableItems, moving);
							
							Mover:AddMove(itemId, moving, missingItems, availableItems, containerItem.price);
						end
					end
				end
			end
		end
	end
	
	self:ClearCache();
	
	if Mover:HasMoves() then
		if addon.db.profile.defaults.autoRefillSkipConfirm then
			OnMoveAccept();
		else
			local moves = Mover:GetMoves();
			
			-- This table is never copied, just referenced. It is the same for every row.
			local columns;
			
			if isRestock then
				local totalCost = 0;
				for _, move in pairs(moves) do
					totalCost = (totalCost + (move.cost * move.num));
				end
				UseMerchantRestockST(totalCost);
				
				columns = {
					{
						["value"] = function(data, cols, realrow, column, table)
							return IdToItemLink(data[realrow].rowData.itemId);
						end,
					}, -- item
					{
						["value"] = function(data, cols, realrow, column, table)
							return data[realrow].rowData.num;
						end,
					}, -- buying
					{
						["value"] = function(data, cols, realrow, column, table)
							return GetSmallCoinTextureString((data[realrow].rowData.cost * data[realrow].rowData.num));
						end,
					}, -- cost
				};
			else
				UseStorageRefillST();
				
				columns = {
					{
						["value"] = function(data, cols, realrow, column, table)
							return IdToItemLink(data[realrow].rowData.itemId);
						end,
					}, -- item
					{
						["value"] = function(data, cols, realrow, column, table)
							return data[realrow].rowData.num;
						end,
					}, -- moving
					{
						["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)
							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
				};
			end
			
			-- Store the list with rows in this
			local data = {};
			
			for i, move in pairs(moves) do
				tinsert(data, {
					["rowData"] = move, -- this is not a key usually found in a row item and ignored by the library
					["cols"] = columns,
				});
			end
			
			addon:SetMoverFrameData(data);
		end
	end
end



-- Events

-- Player bank

function mod:BANKFRAME_OPENED()
	addon:Debug("Scanner:BANKFRAME_OPENED");
	
	mod:RegisterEvent("BANKFRAME_CLOSED");
	
	-- Scan once when the bank is opened, but no need to scan after
	mod:Scan(addon.Locations.Bank);
end

function mod:BANKFRAME_CLOSED()
	addon:Debug("Scanner:BANKFRAME_CLOSED");
	
	self:ClearCache();
	
	mod:UnregisterEvent("BANKFRAME_CLOSED");
	
	if InventoriumItemMover then
		InventoriumItemMover:Hide();
	end
	Mover:ResetQueue();
end

-- Guild bank

local tmrScanGuild, scanned;
function mod:GUILDBANKBAGSLOTS_CHANGED()
	-- This event is spammed the first time the guild bank is opened
	if not scanned then
		self:CancelTimer(tmrScanGuild, true); -- silent
		tmrScanGuild = self:ScheduleTimer("DoScanGuild", 1);
	end
end

function mod:DoScanGuild()
	if not scanned then
		addon:Debug("Scanner:DoScanGuild");
		
		scanned = true;
		
		self:Scan(addon.Locations.Guild);
	end
end

function mod:GUILDBANKFRAME_CLOSED()
	addon:Debug("Scanner:GUILDBANKFRAME_CLOSED");
	
	scanned = nil;
	self:ClearCache();
	
	self:UnregisterEvent("GUILDBANKFRAME_CLOSED");
	self:UnregisterEvent("GUILDBANKBAGSLOTS_CHANGED");
	
	self:CancelTimer(tmrScanGuild, true); -- silent
	
	if InventoriumItemMover then
		InventoriumItemMover:Hide();
	end
	Mover:ResetQueue();
end

function mod:GUILDBANKFRAME_OPENED()
	addon:Debug("Scanner:GUILDBANKFRAME_OPENED");
	
	scanned = nil;
	
	-- Get the contents for every tab into our cache
	for tabId = 1, GetNumGuildBankTabs() do
		local _, _, isViewable, _, _, remainingWithdrawals = GetGuildBankTabInfo(tabId);
		
		if isViewable and (remainingWithdrawals > 0 or remainingWithdrawals == -1) then
			QueryGuildBankTab(tabId);
		end
	end
	
	self:RegisterEvent("GUILDBANKFRAME_CLOSED");
	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");
	
	if InventoriumItemMover then
		InventoriumItemMover:Hide();
	end
	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");
	
	if InventoriumItemMover then
		InventoriumItemMover:Hide();
	end
	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");
end

function mod:OnDisable()
	Mover = nil;
	currentLocation = nil;
	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");
	
	-- Merchant
	self:MERCHANT_CLOSED();
	self:UnregisterEvent("MERCHANT_SHOW");
end

function mod:Pause()
	paused = true;
end

function mod:Unpause()
	paused = nil;
end