view Modules/Queue.lua @ 209:1ed7ce9b1c5d

Cleaned up Queue; errors should now stop a queue all loop rather than occur at every single group.
author Zerotorescue
date Sat, 05 Feb 2011 20:09:03 +0100
parents 5d6b3d116b80
children 2e4e52a589e5
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();
	
	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
	
	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:QueueWithAddon(tradeSkillIndex, amount, group)
	-- Sanity check
	tradeSkillIndex = tonumber(tradeSkillIndex);
	amount = tonumber(amount);
	
	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].Queue(tradeSkillIndex, amount);
	else
		-- Default not available, get the first one then
		
		for name, value in pairs(addon.supportedAddons.crafting) do
			if value.IsEnabled() then
				return value.Queue(tradeSkillIndex, amount);
			end
		end
	end
	
	return -1;
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