view Modules/Queue.lua @ 225:2e4e52a589e5

Added onQueueStart and onQueueEnd events to crafting addon registering. GnomeWorks queue frame should automatically be closed before adding items to the queue and opened afterwards to speed this process up.
author Zerotorescue
date Mon, 07 Feb 2011 15:06:41 +0100
parents 1ed7ce9b1c5d
children
line wrap: on
line source
local addon = select(2, ...);
local mod = addon:NewModule("Queue", "AceEvent-3.0");

local _G = _G;
local tonumber, tostring, pairs, sformat, smatch, slower, floor, ceil, tinsert, twipe = _G.tonumber, _G.tostring, _G.pairs, _G.string.format, _G.string.match, _G.string.lower, _G.floor, _G.ceil, _G.table.insert, _G.table.wipe;

local queue, skipped = {}, {};

-- strings are passed by reference, so it takes no additional memory if one string was used in a thousand tables compared to any other reference type
local skipReasons = {
	["NOT_CRAFTABLE"] = {
		"|cff3d3d3dNot in profession|r", -- gray
		"This item is not part of this profession.",
		0,
	},
	["CAPPED"] = {
		"|cff66ff33Fully stocked|r", -- lime/green
		"The recorded item count is above or equal to your minimum restock target setting.",
		5,
	},
	["MIN_CRAFTING_QUEUE"] = {
		"|cffffff00Min crafting queue|r", -- yellow
		"The amount of missing items is below or equal to your \"don't queue if I only miss\"-setting.",
		10,
	},
	["LOW_VALUE"] = {
		"|cffff6633Underpriced|r", -- orange
		"The recorded auction value of this item is below your price threshold.",
		15,
	},
	["REMOVED"] = { -- because this is updated realtime, it is most useful around the top of the list
		"|cffff0000Removed|r", -- red
		"You manually removed this item from the queue.",
		45,
	},
	["FINISHED"] = { -- because this is updated realtime, it is most useful on the top of the list
		"|cff00ff00Just finished|r", -- green
		"Just finished restocking this item.",
		50,
	},
};

local function Compare(a, b, this, aRow, bRow, columnNo)
	if a == b then
		local column = this.cols[columnNo];
		if column.sortnext then
			local nextcol = this.cols[column.sortnext];
			if not(nextcol.sort) then 
				if nextcol.comparesort then 
					return nextcol.comparesort(this, aRow, bRow, column.sortnext);
				else
					return this:CompareSort(this, bRow, column.sortnext);
				end
			else
				return false;
			end
		else
			return false; 
		end 
	elseif (this.cols[columnNo].sort or this.cols[columnNo].defaultsort or "asc") == "dsc" then
		return a > b;
	else
		return a < b;
	end
end

local function MakeQueueWindow()
	if not InventoriumQueuer then
		addon:CreateQueueFrame();
		
		local frame = InventoriumQueuer; -- 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 * .65),
				["defaultsort"] = "asc",
				["comparesort"] = function(this, aRow, bRow, columnNo)
					local aName, _, aRarity = GetItemInfo(this:GetRow(aRow).rowData.itemId);
					local bName, _, bRarity = GetItemInfo(this:GetRow(bRow).rowData.itemId);
					aName = sformat("%d%s", (10 - (aRarity or 10)), slower(aName or ""));
					bName = sformat("%d%s", (10 - (bRarity or 10)), slower(bName or ""));
					
					return Compare(aName, bName, this, aRow, bRow, columnNo);
				end,
				["sort"] = "asc", -- when the data is set, use this column so sort the default data
				["tooltipTitle"] = "Item",
				["tooltip"] = "Click to sort the list by item quality then item name.",
			},
			{
				["name"] = "Amount",
				["width"] = (scrollTableWidth * .15),
				["align"] = "RIGHT",
				["defaultsort"] = "dsc",
				["comparesort"] = function(this, aRow, bRow, columnNo)
					local a = this:GetRow(aRow).rowData.amount;
					local b = this:GetRow(bRow).rowData.amount;
					
					return Compare(a, b, this, aRow, bRow, columnNo);
				end,
				["sortnext"] = 1,
				["tooltipTitle"] = "Amount needed",
				["tooltip"] = "Click to sort the list by the amount of items needed to reach the restock target.",
			},
			{
				["name"] = "Extra",
				["width"] = (scrollTableWidth * .15),
				["align"] = "RIGHT",
				["defaultsort"] = "dsc",
				["comparesort"] = function(this, aRow, bRow, columnNo)
					local a = this:GetRow(aRow).rowData.bonus;
					local b = this:GetRow(bRow).rowData.bonus;
					
					return Compare(a, b, this, aRow, bRow, columnNo);
				end,
				["sortnext"] = 1,
				["tooltipTitle"] = "Extra items",
				["tooltip"] = "Click to sort the list by the amount of bonus items.",
			},
			{
				["name"] = "X",
				["width"] = (scrollTableWidth * .05),
				["align"] = "CENTER",
				["sortnext"] = 1,
				["tooltipTitle"] = "Remove",
				["tooltip"] = "Click any of the fields in this column to remove this item from the queue.",
				["onClick"] = function(rowData)
					-- Remove this element from the queue
					for index, q in pairs(queue) do
						if q == rowData then
							table.remove(queue, index);
							mod:Skip(q.itemId, skipReasons.REMOVED);
							break;
						end
					end
					
					-- Rebuild our scrolltable (records were removed and added)
					mod:BuildQueue();
				end,
				["color"] = { ["r"] = 1.0, ["g"] = 0.0, ["b"] = 0.0, ["a"] = 1, },
			},
		};
		
		local scrollTableWidth = ( InventoriumQueuerUnqueueables.frmMeasureDummy:GetWidth() - 30 ); -- adjust width by the scrollbar size
		local unqueueablesHeaders = {
			{
				["name"] = "Item",
				["width"] = (scrollTableWidth * .6),
				["defaultsort"] = "asc",
				["comparesort"] = function(this, aRow, bRow, columnNo)
					local aName, _, aRarity = GetItemInfo(this:GetRow(aRow).rowData.itemId);
					local bName, _, bRarity = GetItemInfo(this:GetRow(bRow).rowData.itemId);
					aName = sformat("%d%s", (10 - (aRarity or 10)), slower(aName or ""));
					bName = sformat("%d%s", (10 - (bRarity or 10)), slower(bName or ""));
					
					return Compare(aName, bName, this, aRow, bRow, columnNo);
				end,
				["tooltipTitle"] = "Item",
				["tooltip"] = "Click to sort the list by item quality then item name.",
			},
			{
				["name"] = "Reason",
				["width"] = (scrollTableWidth * .4),
				["defaultsort"] = "dsc",
				["comparesort"] = function(this, aRow, bRow, columnNo)
					local a = this:GetRow(aRow).rowData.reason[3];
					local b = this:GetRow(bRow).rowData.reason[3];
					
					return Compare(a, b, this, aRow, bRow, columnNo);
				end,
				["sort"] = "dsc", -- when the data is set, use this column to sort the default data
				["sortnext"] = 1,
				["tooltipTitle"] = "Reason",
				["tooltip"] = "Click to sort the list by the reason the items couldn't be queued.",
			},
		};
		
		local proceedButton = {
			text = "Queue",
			tooltipTitle = "Queue",
			tooltip = "Add these items to the queue of your crafting addon.",
			onClick = function() mod:QueueProcess(); end,
		};
		local cancelButton = {
			text = "Cancel",
			tooltipTitle = "Cancel",
			tooltip = "Do not queue anything and close the window.",
			onClick = function() mod:QueueAbort(); end,
		};
		local craftButton = {
			text = "Craft",
			tooltipTitle = "Craft",
			tooltip = "Start crafting the first item.",
			onClick = function() mod:StartCrafting(); end,
		};
		
		addon:SetQueueFrameSettings("Inventorium Queue", "The following items can be added to the queue of your crafting addon. Do you wish to proceed?", proceedButton, cancelButton, craftButton, headers, unqueueablesHeaders);
	end
end

function mod:BuildQueue()
	MakeQueueWindow();
	
	-- This table is never copied, just referenced. It is the same for every row.
	local queueablesColumns = {
		{
			["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.amount;
			end,
		}, -- amount
		{
			["value"] = function(data, cols, realrow, column, table)
				return ((data[realrow].rowData.bonus == 0) and 0) or "+" .. data[realrow].rowData.bonus;
			end,
			["color"] = function(data, cols, realrow, column, table)
				return ((data[realrow].rowData.bonus == 0) and { r = 1, g = 1, b = 1, a = 0.5 }) or { r = 0, g = 1, b = 0, a = 1 };
			end,
		}, -- extra
		{
			["value"] = "X",
		},
	};
	
	-- Store the list with rows in this
	local queueables = {};
	
	for _, q in pairs(queue) do
		tinsert(queueables, {
			["rowData"] = q, -- this is not a key usually found in a row item and ignored by the library
			["cols"] = queueablesColumns,
		});
	end
	
	-- This table is never copied, just referenced. It is the same for every row.
	local unqueueablesColumns = {
		{
			["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.reason[1];
			end,
		}, -- reason
	};
	
	-- Store the list with rows in this
	local unqueueables = {};
	
	for _, s in pairs(skipped) do
		tinsert(unqueueables, {
			["rowData"] = s, -- this is not a key usually found in a row item and ignored by the library
			["cols"] = unqueueablesColumns,
		});
	end
	
	addon:SetQueueFrameData(queueables, unqueueables);
end

local function RefreshQueue()
	InventoriumQueuer.scrollTable:Refresh();
end

do -- Crafting region
	-- We are keeping these events within the module object to allow for easier testing and overriding
	-- To test: LibStub("AceAddon-3.0"):GetAddon("Inventorium"):GetModule("Queue"):FUNCTION_NAME(param1, param2, ...);
	
	-- Start crafting the selected skill (or the first in line)
	local currentQueueItem;
	function mod:StartCrafting(test)
		local frame = InventoriumQueuer; -- both for speed as code-consistency
		
		local selectedIndex = frame.scrollTable:GetSelection(); -- gets realrow index
		
		addon:Debug("%s was selected.", tostring(selectedIndex));
		
		if not selectedIndex then
			-- Select the top most element (scrolltable with index of 1 will contain a index of the related realrow of the data table)
			selectedIndex = ((frame.scrollTable.sorttable and frame.scrollTable.sorttable[1]) or 1);
			
			addon:Debug("%s should be the top record.", tostring(selectedIndex));
		end
		
		local nextQueue = frame.scrollTable.data[selectedIndex].rowData or frame.scrollTable.data[1].rowData; -- if the selected index still fails, try to get the first record
		
		if nextQueue then
			if not test then
				self:ResetTradeSkillFilters();
				
				-- Initiate spell (test will be used while debugging to fake crafts)
				DoTradeSkill(nextQueue.craft.no, ceil(nextQueue.amount / nextQueue.craft.quantity));
			end
			
			-- Remember what we're crafting (saves many loops and/or table storing)
			currentQueueItem = nextQueue;
			
			return;
		else
			addon:Print("Nothing is available in the craft queue.", addon.Colors.Red);
		end
	end
	
	function mod:SpellCastComplete(_, unit, _, _, _, spellId)
		-- Sadly the item isn't put in our inventory yet so we don't know how many were made.
		-- Because of that we assume the average amount was made, this isn't really the best solution, but it's pretty-est - for now.
		
		if unit == "player" and currentQueueItem and spellId == currentQueueItem.craft.spellId then
			-- Decrease amount remaining by one quantity
			currentQueueItem.amount = ( currentQueueItem.amount - currentQueueItem.craft.quantity );
			
			if currentQueueItem.amount < 1 then
				-- We finished crafting this item
				
				-- Remove this element from the queue
				for index, q in pairs(queue) do
					if q == currentQueueItem then
						table.remove(queue, index);
						break;
					end
				end
				
				-- Add this queue item to the "Unqueueables" frame - we finished it so it is no longer queueable and the user may become interested
				self:Skip(currentQueueItem.itemId, skipReasons.FINISHED);
				
				-- We are no longer crafting anything
				currentQueueItem = nil;
				
				-- Rebuild our scrolltable (records were removed and added)
				mod:BuildQueue();
			else
				-- Refresh the scrolltable (update item counts)
				RefreshQueue();
			end
		end
	end
	
	function mod:SpellCastStart(_, unit, _, _, _, spellId)
		if unit == "player" and currentQueueItem and spellId == currentQueueItem.craft.spellId then
			self.isProcessing = true;
		end
	end

	function mod:SpellCastStop(_, unit, _, _, _, spellId)
		if unit == "player" and currentQueueItem and spellId == currentQueueItem.craft.spellId then
			self.isProcessing = nil;
		end
	end

	function mod:SpellCastFailed(_, unit, _, _, _, spellId)
		if unit == "player" and currentQueueItem and spellId == currentQueueItem.craft.spellId then
			currentQueueItem = nil;
			self.isProcessing = nil;
		end
	end
	
	--@debug@
	function TestCraft()
		mod:StartCrafting(true);
		mod:SpellCastComplete("UNIT_SPELLCAST_SUCCEEDED", "player", "Relentless Earthsiege Diamond", nil, nil, 55400);
	end
	--@end-debug@
end

function mod:QueueProcess()
	-- Prepare a table with all possible tradeskill craftables
	local craftables = self:GetTradeskillCraftables();
	
	local craftingAddon = self:GetCraftingAddon(group);
	
	if craftingAddon.OnQueueStart then
		craftingAddon.OnQueueStart();
	end
	
	for _, q in pairs(queue) do
		if craftables[q.itemId] then
			if self:QueueWithAddon(craftables[q.itemId].no, ceil(q.amount / craftables[q.itemId].quantity), q.groupName) == -1 then
				addon:Print("Couldn't queue, no supported crafting addon found.", addon.Colors.Red);
				
				self:QueueAbort();
				return;
			else
				-- Update the crafted-item count
				for groupName, values in pairs(addon.db.profile.groups) do
					if values.items and values.items[q.itemId] then
						values.items[q.itemId] = (tonumber(values.items[q.itemId]) or 0) + 1;
						break;
					end
				end
			end
		else
			addon:Debug("Lost %s", IdToItemLink(q.itemId));
		end
	end
	
	if craftingAddon.OnQueueEnd then
		craftingAddon.OnQueueEnd();
	end
	
	self:QueueHide();
end

function mod:QueueAbort()
	self:QueueHide();
end

function mod:QueueHide()
	twipe(queue);
	twipe(skipped);
	
	if InventoriumQueuer then
		InventoriumQueuer:Hide();
	end
end

function mod:QueueAll()
	-- Prepare a table with all possible tradeskill craftables
	local craftables = self:GetTradeskillCraftables();
	
	-- Forget old queue
	twipe(queue);
	twipe(skipped);
	
	local playerName = UnitName("player");
	
	-- Go through all groups
	for groupName, values in pairs(addon.db.profile.groups) do
		local trackAt = addon:GetOptionByKey(groupName, "trackAtCharacters");
		
		if trackAt[playerName] then
			if not self:QueueGroup(groupName, craftables) then
				return;
			end
		end
	end
	
	mod:BuildQueue();
end

function mod:QueueGroup(groupName, craftables, displayQueue)
	-- Prepare a table with all possible tradeskill craftables
	if not craftables then
		craftables = self:GetTradeskillCraftables(); -- nil when no tradeskill window is open
	end
	
	if not craftables then
		addon:Print("No tradeskill window detected. Please open a profession before trying to queue.", addon.Colors.Red);
		return false; -- exit
	elseif not addon.db.profile.groups[groupName] then
		addon:Print(sformat("Tried to queue items from a group named \"%s\", but no such group exists.", groupName), addon.Colors.Red);
		return false; -- exit
	elseif not addon.db.profile.groups[groupName].items then
		addon:Debug("This group (%s) has no items.", groupName);
		return true; -- continue with next group
	end
	
	-- Retrieve group settings
	local restockTarget = addon:GetOptionByKey(groupName, "restockTarget");
	local bonusQueue = addon:GetOptionByKey(groupName, "bonusQueue");
	local minCraftingQueue = floor( addon:GetOptionByKey(groupName, "minCraftingQueue") * restockTarget ); -- If the minCraftingQueue is 5% and restockTarget is 60, this will result in 3
	local priceThreshold = addon:GetOptionByKey(groupName, "priceThreshold");
	
	for itemId, count in pairs(addon.db.profile.groups[groupName].items) do
		if craftables[itemId] then
			local currentStock = addon:GetItemCount(itemId, groupName);
			
			if currentStock >= 0 then
				-- Current stock will be -1 when no itemcount addon was found
				
				-- Calculate the amount to be queued
				local amount = ( restockTarget - currentStock );
				local bonus = 0;
				
				if currentStock == 0 and bonusQueue > 0 then
					-- If we have none left and the bonus queue is enabled, modify the amount to  be queued
					
					bonus = floor( ( amount * ( bonusQueue ) ) + .5 ); -- round
					
					-- Update amount
					amount = (amount + bonus);
				end
			
				if amount > 0 and amount > minCraftingQueue then
					-- If we are queuing at least one AND more than the minimum amount, then proceed
					
					-- Get auction value when it is relevant
					local value = (priceThreshold ~= 0 and addon:GetAuctionValue(IdToItemLink(itemId), groupName));
					
					if priceThreshold == 0 or value == -1 or value >= priceThreshold then
						-- If no price threshold is set or the auction value is equal to or larger than the price threshold, then proceed
						
						self:Queue(itemId, amount, bonus, craftables[itemId], groupName);
					else
						self:Skip(itemId, skipReasons.LOW_VALUE);
						--addon:Debug("%s is valued at %s while %s is needed", IdToItemLink(itemId), tostring(value), tostring(priceThreshold));
					end
				else
					if amount <= 0 then
						-- less than 0 = (over)capped
						self:Skip(itemId, skipReasons.CAPPED);
					else
						-- more than 0 = below min crafting queue
						self:Skip(itemId, skipReasons.MIN_CRAFTING_QUEUE);
					end
				end
			else
				-- No item count addon
				addon:Print("No usable itemcount addon found.", addon.Colors.Red);
				return false; -- exit
			end
		else
			self:Skip(itemId, skipReasons.NOT_CRAFTABLE);
		end
	end
	
	if displayQueue then
		mod:BuildQueue();
	end
	
	return true; -- continue
end

function mod:Queue(itemId, amount, bonus, craft, groupName)
	tinsert(queue, {
		["itemId"] = itemId, -- needed to display the queued item in the queue window
		["amount"] = amount, -- the amount missing
		["bonus"] = bonus, -- the amount queued by the bonus queue
		["craft"] = craft, -- (craftable) - needed to find the proper element of this parent array when crafting has finished (spellId), and to update the numCrafts (quantity)
		["groupName"] = groupName, -- related group, needed to find the selected crafting addon
	});
end

function mod:Skip(itemId, reason)
	tinsert(skipped, {
		["itemId"] = itemId,
		["reason"] = reason,
	});
end

function mod:GetCraftingAddon(group)
	local selectedExternalAddon = addon:GetOptionByKey(group, "craftingAddon");
	
	if addon.supportedAddons.crafting[selectedExternalAddon] and addon.supportedAddons.crafting[selectedExternalAddon].IsEnabled() then
		-- Try to use the default auction pricing addon
		
		return addon.supportedAddons.crafting[selectedExternalAddon], selectedExternalAddon;
	else
		-- Default not available, get the first one then
		
		for name, value in pairs(addon.supportedAddons.crafting) do
			if value.IsEnabled() then
				return value, name;
			end
		end
	end
	
	return;
end

function mod:QueueWithAddon(tradeSkillIndex, amount, group)
	-- Sanity check
	tradeSkillIndex = tonumber(tradeSkillIndex);
	amount = tonumber(amount);
	
	local craftingAddon = self:GetCraftingAddon(group);
	
	if craftingAddon then
		return craftingAddon.Queue(tradeSkillIndex, amount);
	else
		return -1;
	end
end

function mod:OnEnable()
	-- Register our own slash commands
	-- /im queue
	addon:RegisterSlash(function()
		mod:QueueAll();
	end, { "q", "que", "queue" }, "|Hfunction:InventoriumCommandHandler:queue|h|cff00fff7/im queue|r|h (or /im q) - Queue all items found in the currently opened profession that are within the groups tracked at this current character.");
	
	self:RegisterMessage("IM_QUEUE_ALL");
	self:RegisterMessage("IM_QUEUE_GROUP");
	
	-- When closing the tradeskill window also hide the queue screen.
	-- We scan the recipes right before queueing not when the tradeskill is opened because we really don't need it at any other time.
	self:RegisterEvent("TRADE_SKILL_CLOSE", "QueueAbort");
	
	-- Crafting events
	self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED", "SpellCastComplete");
	
	-- Button en-/disabling
	self:RegisterEvent("UNIT_SPELLCAST_START", "SpellCastStart");
	self:RegisterEvent("UNIT_SPELLCAST_STOP", "SpellCastStop");
	
	self:RegisterEvent("UNIT_SPELLCAST_FAILED", "SpellCastFailed");
	self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED", "SpellCastFailed");
end

do -- Addon messages (Ace3) region
	function mod:IM_QUEUE_ALL()
		self:QueueAll();
	end

	function mod:IM_QUEUE_GROUP(event, groupName)
		self:QueueGroup(groupName, nil, true);
	end
end

do -- Trade skill recipes region
	-- Reset all filters so no crafts are hidden
	function mod:ResetTradeSkillFilters()
		SetTradeSkillSubClassFilter(0, 1, 1);
		SetTradeSkillItemNameFilter("");
		SetTradeSkillItemLevelFilter(0, 0);
		TradeSkillOnlyShowSkillUps(false);
		TradeSkillOnlyShowMakeable(false);
		
		-- Expand all categories so no crafts are hidden
		for i = GetNumTradeSkills(), 1, -1 do
			local _, skillType, _, isExpanded = GetTradeSkillInfo(i);

			if skillType == "header" and not isExpanded then
				ExpandTradeSkillSubClass(i);
			end
		end
	end

	-- Get all craftable items into a table. Each record contains "no", "spellId" and "quantity". The last is the average amount made per craft.
	function mod:GetTradeskillCraftables()
		local craftables = {};
		
		if GetTradeSkillLine() ~= "UNKNOWN" then
			self:ResetTradeSkillFilters();
			
			-- Cache all craftable items
			for i = 1, GetNumTradeSkills() do
				local itemLink = GetTradeSkillItemLink(i);
				
				if itemLink then
					local itemId = addon:GetItemId(itemLink);
					if not itemId then
						-- If this isn't an item, it can only be an enchant instead
						itemId = tonumber(smatch(itemLink, "|Henchant:([-0-9]+)|h"));
						
						if itemId and addon.scrollIds[itemId] then
							-- Only if this scroll id actually exists
							itemId = addon.scrollIds[itemId]; -- change enchantIds into scrollIds
						end
					end
					
					-- Remember the average amount of items created per craft (doesn't need to be a round number, since we multiply this by the amount of items to be queued we're better off rounding at that time)
					local minMade, maxMade = GetTradeSkillNumMade(i);
					local average = ((minMade == maxMade) and minMade) or ((minMade + maxMade) / 2);
					
					local recipeLink = GetTradeSkillRecipeLink(i);
					local spellId = tonumber(smatch(recipeLink, "|Henchant:([-0-9]+)|h"));
					
					craftables[itemId] = {
						["no"] = i, -- needed to start crafting at the end of the entire cycle
						["spellId"] = spellId, -- needed to detect creation of this item was finished
						["quantity"] = average, -- needed to calculate the amount of crafts
					};
				end
			end
		else
			return;
		end
		
		return craftables;
	end
end