view Modules/Scanner.lua @ 132:8460855e3d90

Rewrote queueing module to insert a GUI. Minor mover window changes.
author Zerotorescue
date Tue, 18 Jan 2011 00:30:15 +0100
parents ee4672f21586
children 2efe61ca718e
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);
	
	InventoriumItemMover:Hide();
end

local function OnMoveCancel()
	Mover:ResetQueue();
	currentLocation = nil;
	
	InventoriumItemMover:Hide();
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()
	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)
	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");
	
	InventoriumItemMover:Hide();
	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
	
	InventoriumItemMover:Hide();
	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");
	
	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");
	
	if not InventoriumItemMover then
		addon:CreateMoverFrame();
	end
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