Quantcast

Preliminary work on creating separate rotations from one SimC profile.

Johnny C. Lam [12-25-14 - 20:20]
Preliminary work on creating separate rotations from one SimC profile.

Spells are tagged as "main", "shortcd", or "cd" based on the length of
their cooldown and whether they invoke the GCD.  The tags are ordered as:
cd > shortcd > main.

The generated Ovale script now splits each action list into separate
functions corresponding to each of the tags.  Empty functions are not
generated and references to empty functions are removed from the final
script.

Enhance the generated script to use the standard four-icon layout of:

    [shortcd] [main] [aoe] [cd]

The "aoe" icon is toggled with a class- and specialization-specific
toggle.  This more closely matches the default Ovale scripts.
Filename
Data.lua
SimulationCraft.lua
diff --git a/Data.lua b/Data.lua
index 3fd3187..85feca2 100644
--- a/Data.lua
+++ b/Data.lua
@@ -475,6 +475,32 @@ function OvaleData:GetSpellInfo(spellId)
 	end
 end

+-- Returns the tag for the spell and whether the spell invokes the GCD.
+function OvaleData:GetSpellTagInfo(spellId)
+	local tag = "main"
+	local invokesGCD = true
+
+	local si = self.spellInfo[spellId]
+	if si then
+		invokesGCD = not si.gcd or si.gcd > 0
+		tag = si.tag
+		if not tag then
+			local cd = si.cd
+			if cd then
+				if cd > 90 then
+					tag = "cd"
+				elseif cd > 29 or not invokesGCD then
+					tag = "shortcd"
+				end
+			elseif not invokesGCD then
+				tag = "shortcd"
+			end
+		end
+		tag = tag or "main"
+	end
+	return tag, invokesGCD
+end
+
 -- Check "run-time" requirements specified in SpellRequire().
 -- NOTE: Mirrored in statePrototype below.
 function OvaleData:CheckRequirements(spellId, tokenIterator, target)
diff --git a/SimulationCraft.lua b/SimulationCraft.lua
index 53fe4c6..a0f075d 100644
--- a/SimulationCraft.lua
+++ b/SimulationCraft.lua
@@ -11,11 +11,13 @@ Ovale.OvaleSimulationCraft = OvaleSimulationCraft
 local AceConfig = LibStub("AceConfig-3.0")
 local AceConfigDialog = LibStub("AceConfigDialog-3.0")
 local L = Ovale.L
+local OvaleDebug = Ovale.OvaleDebug
 local OvaleOptions = Ovale.OvaleOptions
 local OvalePool = Ovale.OvalePool

 -- Forward declarations for module dependencies.
 local OvaleAST = nil
+local OvaleCompile = nil
 local OvaleData = nil
 local OvaleHonorAmongThieves = nil
 local OvaleLexer = nil
@@ -234,6 +236,15 @@ do
 	end
 	OvaleOptions:RegisterOptions(OvaleSimulationCraft)
 end
+
+-- XXX Temporary hard-coding of tags and tag priorities.
+local OVALE_TAGS = { "main", "shortcd", "cd" }
+local OVALE_TAG_PRIORITY = {}
+do
+	for i, tag in pairs(OVALE_TAGS) do
+		OVALE_TAG_PRIORITY[tag] = i * 10
+	end
+end
 --</private-static-properties>

 --<private-static-methods>
@@ -1065,6 +1076,329 @@ local function IsTotem(name)
 	return false
 end

+--[[--------------------------
+	Split-by-tag functions
+--]]--------------------------
+
+local function ConcatenatedConditionNode(conditionList, nodeList, annotation)
+	local conditionNode
+	if #conditionList == 1 then
+		conditionNode = conditionList[1]
+	elseif #conditionList > 1 then
+		local lhsNode = conditionList[1]
+		local rhsNode = conditionList[2]
+		conditionNode = OvaleAST:NewNode(nodeList, true)
+		conditionNode.type = "logical"
+		conditionNode.expressionType = "binary"
+		conditionNode.operator = "or"
+		conditionNode.child[1] = lhsNode
+		conditionNode.child[2] = rhsNode
+		for k = 3, #conditionList do
+			lhsNode = conditionNode
+			rhsNode = conditionList[k]
+			conditionNode = OvaleAST:NewNode(nodeList, true)
+			conditionNode.type = "logical"
+			conditionNode.expressionType = "binary"
+			conditionNode.operator = "or"
+			conditionNode.child[1] = lhsNode
+			conditionNode.child[2] = rhsNode
+		end
+	end
+	return conditionNode
+end
+
+local function ConcatenatedBodyNode(bodyList, nodeList, annotation)
+	local bodyNode
+	if #bodyList > 0 then
+		bodyNode = OvaleAST:NewNode(nodeList, true)
+		bodyNode.type = "group"
+		for k, node in ipairs(bodyList) do
+			bodyNode.child[k] = node
+		end
+	end
+	return bodyNode
+end
+
+local function OvaleTaggedFunctionName(name, tag)
+	local prefix, suffix = strmatch(name, "([A-Za-z]+)(Actions)")
+	if prefix and suffix then
+		local camelTag
+		if tag == "shortcd" then
+			camelTag = "ShortCd"
+		else
+			camelTag = CamelCase(tag)
+		end
+		name = prefix .. camelTag .. suffix
+	end
+	return name
+end
+
+local function TagPriority(tag)
+	return OVALE_TAG_PRIORITY[tag] or 10
+end
+
+local SPLIT_BY_TAG_VISITOR = nil
+-- Forward declarations of split-by-tag functions.
+local SplitByTag = nil
+local SplitByTagAction = nil
+local SplitByTagAddFunction = nil
+local SplitByTagCustomFunction = nil
+local SplitByTagGroup = nil
+local SplitByTagIf = nil
+local SplitByTagState = nil
+
+SplitByTag = function(tag, node, nodeList, annotation)
+	local visitor = SPLIT_BY_TAG_VISITOR[node.type]
+	if not visitor then
+		OvaleSimulationCraft:Print("Unable to split-by-tag node of type '%s'.", node.type)
+	else
+		return visitor(tag, node, nodeList, annotation)
+	end
+end
+
+SplitByTagAction = function(tag, node, nodeList, annotation)
+	local bodyNode, conditionNode
+	if node.func == "spell" then
+		local spellIdNode = node.rawParams[1]
+		local spellId, spellName
+		if spellIdNode.type == "variable" then
+			spellName = spellIdNode.name
+			spellId = annotation.dictionary and annotation.dictionary[spellName]
+		elseif spellIdNode.type == "value" then
+			spellName = spellIdNode.value
+			spellId = spellName
+		end
+		if spellId then
+			local spellTag, invokesGCD = OvaleData:GetSpellTagInfo(spellId)
+			if spellTag then
+				if spellTag == tag then
+					bodyNode = node
+				elseif invokesGCD and TagPriority(spellTag) < TagPriority(tag) then
+					conditionNode = node
+				end
+			else
+				OvaleSimulationCraft:Print("Unable to determine tag for '%s'.", spellName)
+				bodyNode = node
+			end
+		else
+			OvaleSimulationCraft:Print("Unable to determine spell ID and tag for '%s'.", node.asString)
+			bodyNode = node
+		end
+	end
+	return bodyNode, conditionNode
+end
+
+SplitByTagAddFunction = function(tag, node, nodeList, annotation)
+	-- Split the function body by the tag.
+	local bodyNode = SplitByTag(tag, node.child[1], nodeList, annotation)
+	if not bodyNode or bodyNode.type ~= "group" then
+		local newGroupNode = OvaleAST:NewNode(nodeList, true)
+		newGroupNode.type = "group"
+		newGroupNode.child[1] = bodyNode
+		bodyNode = newGroupNode
+	end
+
+	local addFunctionNode = OvaleAST:NewNode(nodeList, true)
+	addFunctionNode.type = "add_function"
+	addFunctionNode.name = OvaleTaggedFunctionName(node.name, tag)
+	addFunctionNode.child[1] = bodyNode
+	return addFunctionNode
+end
+
+SplitByTagCustomFunction = function(tag, node, nodeList, annotation)
+	local bodyNode
+	local functionName = node.name
+	if annotation.taggedFunctionName[functionName] then
+		local functionNode = OvaleAST:NewNode(nodeList)
+		functionNode.name = OvaleTaggedFunctionName(functionName, tag)
+		functionNode.lowername = strlower(functionNode.name)
+		functionNode.type = "custom_function"
+		functionNode.func = functionNode.name
+		functionNode.asString = functionNode.name .. "()"
+		bodyNode = functionNode
+	else
+		local functionTag = annotation.functionTag[functionName]
+		if not functionTag then
+			if strfind(functionName, "Bloodlust") then
+				functionTag = "cd"
+			elseif strfind(functionName, "GetInMeleeRange") then
+				functionTag = "shortcd"
+			elseif strfind(functionName, "InterruptActions") then
+				functionTag = "cd"
+			elseif strfind(functionName, "RighteousFury") then
+				functionTag = "shortcd"
+			elseif strfind(functionName, "SummonPet") then
+				functionTag = "shortcd"
+			elseif strfind(functionName, "UseItemActions") then
+				functionTag = "cd"
+			elseif strfind(functionName, "UsePotion") then
+				functionTag = "cd"
+			end
+		end
+		if functionTag then
+			if functionTag == tag then
+				bodyNode = node
+			end
+		else
+			OvaleSimulationCraft:Print("Unable to determine tag for '%s()'.", node.name)
+			bodyNode = node
+		end
+	end
+	return bodyNode
+end
+
+SplitByTagGroup = function(tag, node, nodeList, annotation)
+	local index = #node.child
+	local bodyList = {}
+	local conditionList = {}
+	while index > 0 do
+		local childNode = node.child[index]
+		index = index - 1
+		if childNode.type ~= "comment" then
+			local bodyNode, conditionNode = SplitByTag(tag, childNode, nodeList, annotation)
+			if bodyNode then
+				if #conditionList == 0 then
+					tinsert(bodyList, 1, bodyNode)
+				elseif #bodyList == 0 then
+					wipe(conditionList)
+					tinsert(bodyList, 1, bodyNode)
+				else -- if #conditionList > 0 and #bodyList > 0 then
+					-- New body with pre-existing condition, so convert to an "unless" node.
+					local unlessNode = OvaleAST:NewNode(nodeList, true)
+					unlessNode.type = "unless"
+					unlessNode.child[1] = ConcatenatedConditionNode(conditionList, nodeList, annotation)
+					unlessNode.child[2] = ConcatenatedBodyNode(bodyList, nodeList, annotation)
+					wipe(bodyList)
+					wipe(conditionList)
+					tinsert(bodyList, 1, unlessNode)
+					-- Add a blank line above this "unless" node.
+					local commentNode = OvaleAST:NewNode(nodeList)
+					commentNode.type = "comment"
+					tinsert(bodyList, 1, commentNode)
+					-- Insert the new body.
+					tinsert(bodyList, 1, bodyNode)
+				end
+				-- Peek at the previous statement to check if this is part of a "pool_resource" statement pair.
+				if index > 0 then
+					childNode = node.child[index]
+					if childNode.type ~= "comment" then
+						bodyNode, conditionNode = SplitByTag(tag, childNode, nodeList, annotation)
+						if not bodyNode and index > 1 then
+							-- The previous statement is not part of this tag, so check the comments above it for "pool_resource".
+							local start = index - 1
+							for k = index - 1, 1, -1 do
+								childNode = node.child[k]
+								if childNode.type == "comment" then
+									if childNode.comment and strsub(childNode.comment, 1, 5) == "pool_" then
+										-- Found the starting "pool_resource" comment.
+										start = k
+										break
+									end
+								else
+									break
+								end
+							end
+							if start < index - 1 then
+								--[[
+									This was part of a "pool_resource" statement pair where the previous statement
+									is not part of this tag, so insert the comment block here as well for documentation,
+									and "advance" the index to skip the previous statement altogether.
+								--]]
+								for k = index - 1, start, -1 do
+									tinsert(bodyList, 1, node.child[k])
+								end
+								index = start - 1
+							end
+						end
+					end
+				end
+				-- Insert the comment block from above the new body.
+				while index > 0 do
+					childNode = node.child[index]
+					if childNode.type == "comment" then
+						tinsert(bodyList, 1, childNode)
+						index = index - 1
+					else
+						break
+					end
+				end
+			elseif conditionNode then
+				tinsert(conditionList, 1, conditionNode)
+			end
+		end
+	end
+
+	local bodyNode = ConcatenatedBodyNode(bodyList, nodeList, annotation)
+	local conditionNode = ConcatenatedConditionNode(conditionList, nodeList, annotation)
+	if bodyNode and conditionNode then
+		-- Combine conditions and body into an "unless" node.
+		local unlessNode = OvaleAST:NewNode(nodeList, true)
+		unlessNode.type = "unless"
+		unlessNode.child[1] = conditionNode
+		unlessNode.child[2] = bodyNode
+		-- Create "group" node around the "unless" node.
+		local groupNode = OvaleAST:NewNode(nodeList, true)
+		groupNode.type = "group"
+		groupNode.child[1] = unlessNode
+		-- Set return values.
+		bodyNode = groupNode
+		conditionNode = nil
+	end
+	return bodyNode, conditionNode
+end
+
+SplitByTagIf = function(tag, node, nodeList, annotation)
+	local bodyNode, conditionNode = SplitByTag(tag, node.child[2], nodeList, annotation)
+	if bodyNode then
+		-- Combine pre-existing conditions and body into an "if/unless" node.
+		local ifNode = OvaleAST:NewNode(nodeList, true)
+		ifNode.type = node.type
+		ifNode.child[1] = node.child[1]
+		ifNode.child[2] = bodyNode
+		-- Set return values.
+		bodyNode = ifNode
+		conditionNode = nil
+	elseif conditionNode then
+		-- Combine pre-existing conditions and new conditions into an "and" node.
+		local andNode = OvaleAST:NewNode(nodeList, true)
+		andNode.type = "logical"
+		andNode.expressionType = "binary"
+		andNode.operator = "and"
+		if node.type == "if" then
+			andNode.child[1] = node.child[1]
+		else -- if node.type == "unless" then
+			-- Flip the boolean condition if the original node was an "unless" node.
+			local notNode = OvaleAST:NewNode(nodeList, true)
+			notNode.type = "logical"
+			notNode.expressionType = "unary"
+			notNode.operator = "not"
+			notNode.child[1] = node.child[1]
+			andNode.child[1] = notNode
+		end
+		andNode.child[2] = conditionNode
+		-- Set return values.
+		bodyNode = nil
+		conditionNode = andNode
+	end
+	return bodyNode, conditionNode
+end
+
+SplitByTagState = function(tag, node, nodeList, annotation)
+	return node
+end
+
+do
+	SPLIT_BY_TAG_VISITOR = {
+		["action"] = SplitByTagAction,
+		["add_function"] = SplitByTagAddFunction,
+		["custom_function"] = SplitByTagCustomFunction,
+		["group"] = SplitByTagGroup,
+		["if"] = SplitByTagIf,
+		["state"] = SplitByTagState,
+		["unless"] = SplitByTagIf,
+	}
+end
+
 local EMIT_VISITOR = nil
 -- Forward declarations of code generation functions.
 local Emit = nil
@@ -1614,21 +1948,25 @@ EmitActionList = function(parseNode, nodeList, annotation)
 				elseif poolResourceNode then
 					-- This is the action following "pool_resource,for_next=1".
 					child[#child + 1] = statementNode
-					local powerType = CamelCase(poolResourceNode.powerType)
-					local extra_amount = poolResourceNode.extra_amount
-					if extra_amount then
-						local commentNode = OvaleAST:NewNode(nodeList)
-						commentNode.type = "comment"
-						commentNode.comment = format("Remove any '%s() >= %d' condition from the following statement.", powerType, extra_amount)
-						child[#child + 1] = commentNode
-					end
 					local bodyNode
+					local poolingConditionNode
 					if statementNode.child then
 						-- This is a conditional statement, so set the body to the "then" clause.
+						poolingConditionNode = statementNode.child[1]
 						bodyNode = statementNode.child[2]
 					else
 						bodyNode = statementNode
 					end
+					local powerType = CamelCase(poolResourceNode.powerType)
+					local extra_amount = poolResourceNode.extra_amount
+					if extra_amount and poolingConditionNode then
+						-- Remove any 'Energy() >= N' conditions from the pooling condition.
+						local code = OvaleAST:Unparse(poolingConditionNode)
+						local extraAmountPattern = powerType .. "%(%) >= [%d.]+"
+						local replaceString = format("True(pool_%s %d)", poolResourceNode.powerType, extra_amount)
+						code = gsub(code, extraAmountPattern, replaceString)
+						poolingConditionNode = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation)
+					end
 					if bodyNode.type == "action" and bodyNode.rawParams and bodyNode.rawParams[1] then
 						local name = OvaleAST:Unparse(bodyNode.rawParams[1])
 						-- Create a condition node that includes checking that the spell is not on cooldown.
@@ -1646,7 +1984,7 @@ EmitActionList = function(parseNode, nodeList, annotation)
 							conditionNode.type = "logical"
 							conditionNode.expressionType = "binary"
 							conditionNode.operator = "and"
-							conditionNode.child[1] = statementNode.child[1]
+							conditionNode.child[1] = poolingConditionNode
 							conditionNode.child[2] = rhsNode
 						end
 						-- Create node to hold the rest of the statements.
@@ -2949,6 +3287,56 @@ do
 	}
 end

+local function Prune(node, removed)
+	if node.type == "add_function" or node.type == "group" or node.type == "script" or node.type == "wait" then
+		local isChanged = false
+		local index = #node.child
+		while index > 0 do
+			local childNode = node.child[index]
+			local changed, pruned = Prune(childNode, removed)
+			if pruned then
+				tremove(node.child, index)
+				-- Remove the block of comments above this pruned line as well.
+				local k = index - 1
+				while k > 0 do
+					childNode = node.child[k]
+					if childNode.type == "comment" then
+						tremove(node.child, k)
+						k = k - 1
+					else
+						break
+					end
+				end
+				index = k + 1
+				isChanged = isChanged or changed
+			end
+			index = index - 1
+		end
+		-- Remove blank lines at the top of groups and scripts.
+		if node.type == "group" or node.type == "script" then
+			while true do
+				local childNode = node.child[1]
+				if childNode and childNode.type == "comment" and (not childNode.comment or childNode.comment == "") then
+					tremove(node.child, 1)
+					isChanged = true
+				else
+					break
+				end
+			end
+		end
+		local isPruned = #node.child == 0
+		if isPruned and node.type == "add_function" then
+			removed[node.name] = true
+		end
+		return isChanged, isPruned
+	elseif node.type == "if" or node.type == "unless" then
+		return Prune(node.child[2], removed)
+	elseif node.type == "custom_function" and removed[node.name] then
+		return true, true
+	end
+	return false, false
+end
+
 local function InsertSupportingFunctions(child, annotation)
 	local count = 0
 	local nodeList = annotation.astAnnotation.nodeList
@@ -2972,6 +3360,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_runicpower")
 		AddSymbol(annotation, "asphyxiate")
 		AddSymbol(annotation, "mind_freeze")
@@ -2999,6 +3388,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "maim")
 		AddSymbol(annotation, "mighty_bash")
 		AddSymbol(annotation, "skull_bash")
@@ -3019,6 +3409,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "shortcd"
 		AddSymbol(annotation, "mangle")
 		AddSymbol(annotation, "shred")
 		AddSymbol(annotation, "wild_charge")
@@ -3051,6 +3442,7 @@ local function InsertSupportingFunctions(child, annotation)
 		end
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "shortcd"
 		AddSymbol(annotation, "revive_pet")
 		count = count + 1
 	end
@@ -3072,6 +3464,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_focus")
 		AddSymbol(annotation, "counter_shot")
 		AddSymbol(annotation, "quaking_palm")
@@ -3095,6 +3488,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_mana")
 		AddSymbol(annotation, "counterspell")
 		AddSymbol(annotation, "quaking_palm")
@@ -3119,6 +3513,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_chi")
 		AddSymbol(annotation, "paralysis")
 		AddSymbol(annotation, "quaking_palm")
@@ -3177,6 +3572,7 @@ local function InsertSupportingFunctions(child, annotation)
 		end
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "shortcd"
 		AddSymbol(annotation, "righteous_fury")
 		count = count + 1
 	end
@@ -3215,6 +3611,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_holy")
 		AddSymbol(annotation, "blinding_light")
 		AddSymbol(annotation, "fist_of_justice")
@@ -3233,6 +3630,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "shortcd"
 		AddSymbol(annotation, "rebuke")
 		count = count + 1
 	end
@@ -3254,6 +3652,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_mana")
 		AddSymbol(annotation, "quaking_palm")
 		AddSymbol(annotation, "silence")
@@ -3280,6 +3679,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_energy")
 		AddSymbol(annotation, "cheap_shot")
 		AddSymbol(annotation, "deadly_throw")
@@ -3301,6 +3701,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "shortcd"
 		AddSymbol(annotation, "kick")
 		AddSymbol(annotation, "shadowstep")
 		count = count + 1
@@ -3323,6 +3724,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_mana")
 		AddSymbol(annotation, "quaking_palm")
 		AddSymbol(annotation, "wind_shear")
@@ -3342,6 +3744,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "bloodlust")
 		AddSymbol(annotation, "heroism")
 		count = count + 1
@@ -3365,6 +3768,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "arcane_torrent_rage")
 		AddSymbol(annotation, "glyph_of_gag_order")
 		AddSymbol(annotation, "heroic_throw")
@@ -3382,6 +3786,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "shortcd"
 		AddSymbol(annotation, "pummel")
 		count = count + 1
 	end
@@ -3396,6 +3801,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		count = count + 1
 	end
 	if annotation.use_potion_strength then
@@ -3407,6 +3813,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "draenic_strength_potion")
 		count = count + 1
 	end
@@ -3419,6 +3826,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "draenic_mana_potion")
 		count = count + 1
 	end
@@ -3431,6 +3839,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "draenic_intellect_potion")
 		count = count + 1
 	end
@@ -3443,6 +3852,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "draenic_armor_potion")
 		count = count + 1
 	end
@@ -3455,6 +3865,7 @@ local function InsertSupportingFunctions(child, annotation)
 		]]
 		local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation)
 		tinsert(child, 1, node)
+		annotation.functionTag[node.name] = "cd"
 		AddSymbol(annotation, "draenic_agility_potion")
 		count = count + 1
 	end
@@ -3587,12 +3998,32 @@ local function InsertSupportingDefines(child, annotation)
 	end
 	return count
 end
+
+local function GenerateIconBody(output, tag, profile)
+	local annotation = profile.annotation
+	local precombatName = OvaleFunctionName("precombat", annotation)
+	local defaultName = OvaleFunctionName("_default", annotation)
+	local removed = annotation.removedFunction
+
+	local functionName
+	if profile["actions.precombat"] then
+		functionName = OvaleTaggedFunctionName(precombatName, tag)
+		if not removed[functionName] then
+			tinsert(output, format("	if not InCombat() %s()", functionName))
+		end
+	end
+	functionName = OvaleTaggedFunctionName(defaultName, tag)
+	if not removed[functionName] then
+		tinsert(output, format("	%s()", functionName))
+	end
+end
 --</private-static-methods>

 --<public-static-methods>
 function OvaleSimulationCraft:OnInitialize()
 	-- Resolve module dependencies.
 	OvaleAST = Ovale.OvaleAST
+	OvaleCompile = Ovale.OvaleCompile
 	OvaleData = Ovale.OvaleData
 	OvaleHonorAmongThieves = Ovale.OvaleHonorAmongThieves
 	OvaleLexer = Ovale.OvaleLexer
@@ -3708,6 +4139,19 @@ function OvaleSimulationCraft:ParseProfile(simc)
 		end
 	end

+	-- Create table of tagged Ovale function names.
+	local taggedFunctionName = {}
+	for _, node in ipairs(actionList) do
+		local fname = OvaleFunctionName(node.name, annotation)
+		taggedFunctionName[fname] = true
+		for _, tag in pairs(OVALE_TAGS) do
+			local taggedName = OvaleTaggedFunctionName(fname, tag)
+			taggedFunctionName[taggedName] = true
+		end
+	end
+	annotation.taggedFunctionName = taggedFunctionName
+	annotation.functionTag = {}
+
 	profile.actionList = actionList
 	profile.annotation = annotation
 	annotation.nodeList = nodeList
@@ -3742,15 +4186,59 @@ function OvaleSimulationCraft:EmitAST(profile)
 	if profile.actionList then
 		annotation.astAnnotation = annotation.astAnnotation or {}
 		annotation.astAnnotation.nodeList = nodeList
+		-- Load the dictionary of defined symbols from evaluating the script headers.
+		local dictionaryAST
+		do
+			OvaleDebug:ResetTrace()
+			local dictionaryAnnotation = { nodeList = {} }
+			local dictionaryCode = "Include(ovale_common) Include(ovale_" .. strlower(annotation.class) .. "_spells)"
+			dictionaryAST = OvaleAST:ParseCode("script", dictionaryCode, dictionaryAnnotation.nodeList, dictionaryAnnotation)
+			if dictionaryAST then
+				dictionaryAST.annotation = dictionaryAnnotation
+				annotation.dictionary = dictionaryAnnotation.definition
+				OvaleAST:PropagateConstants(dictionaryAST)
+				OvaleAST:PropagateStrings(dictionaryAST)
+				OvaleAST:FlattenParameters(dictionaryAST)
+				Ovale:ResetControls()
+				OvaleCompile:EvaluateScript(dictionaryAST, true)
+			end
+		end
+		-- Generate the AST "add_function" nodes from the action lists.
+		local removedFunction = {}
 		for _, node in ipairs(profile.actionList) do
-			local declarationNode = EmitActionList(node, nodeList, annotation)
-			if declarationNode then
-				child[#child + 1] = declarationNode
+			local addFunctionNode = EmitActionList(node, nodeList, annotation)
+			if addFunctionNode then
+				local actionListName = gsub(node.name, "^_+", "")
+				local commentNode = OvaleAST:NewNode(nodeList)
+				commentNode.type = "comment"
+				commentNode.comment = "## actions." .. actionListName
+				child[#child + 1] = commentNode
+				-- Split this action list function by each tag.
+				for _, tag in pairs(OVALE_TAGS) do
+					local bodyNode = SplitByTag(tag, addFunctionNode, nodeList, annotation)
+					local groupNode = bodyNode.child[1]
+					if #groupNode.child > 0 then
+						child[#child + 1] = bodyNode
+					else
+						removedFunction[bodyNode.name] = true
+					end
+				end
 			else
 				ok = false
 				break
 			end
 		end
+		-- Walk the AST and remove empty functions.
+		local changed = Prune(ast, removedFunction)
+		while changed do
+			changed = Prune(ast, removedFunction)
+		end
+		annotation.removedFunction = removedFunction
+		-- Clean up the dictionary that was only needed to generate the AST.
+		if dictionaryAST then
+			OvaleAST:Release(dictionaryAST)
+			annotation.dictionary = nil
+		end
 	end
 	if ok then
 		-- Fixups.
@@ -3778,13 +4266,16 @@ function OvaleSimulationCraft:Emit(profile)
 	local nodeList = {}
 	local ast = self:EmitAST(profile)
 	local annotation = profile.annotation
+	local class = annotation.class
+	local lowerclass = strlower(class)
+	local specialization = annotation.specialization

 	local output = self_outputPool:Get()
 	-- Prepend a comment block header for the script.
 	do
 		output[#output + 1] = "# Based on SimulationCraft profile " .. annotation.name .. "."
-		output[#output + 1] = "#	class=" .. strlower(annotation.class)
-		output[#output + 1] = "#	spec=" .. annotation.specialization
+		output[#output + 1] = "#	class=" .. lowerclass
+		output[#output + 1] = "#	spec=" .. specialization
 		if profile.talents then
 			output[#output + 1] = "#	talents=" .. profile.talents
 		end
@@ -3799,7 +4290,7 @@ function OvaleSimulationCraft:Emit(profile)
 	do
 		output[#output + 1] = ""
 		output[#output + 1] = "Include(ovale_common)"
-		output[#output + 1] = format("Include(ovale_%s_spells)", strlower(annotation.class))
+		output[#output + 1] = format("Include(ovale_%s_spells)", lowerclass)
 		-- Insert an extra blank line to separate section for controls from the includes.
 		if annotation.supportingDefineCount + annotation.supportingControlCount > 0 then
 			output[#output + 1] = ""
@@ -3807,25 +4298,46 @@ function OvaleSimulationCraft:Emit(profile)
 	end
 	-- Output the script itself.
 	output[#output + 1] = OvaleAST:Unparse(ast)
-	-- Output a simplistic two-icon layout for the rotation.
+	-- Output a standard four-icon layout for the rotation: [shortcd] [main] [aoe] [cd]
 	do
+		local aoeToggle = "opt_" .. lowerclass .. "_" .. specialization .. "_aoe"
+		output[#output + 1] = ""
+		output[#output + 1] = "### " .. CamelCase(specialization) .. " icons."
+		output[#output + 1] = format("AddCheckBox(%s L(AOE) specialization=%s default)", aoeToggle, specialization)
+
+		-- Short CD rotation.
+		output[#output + 1] = ""
+		output[#output + 1] = format("AddIcon specialization=%s help=shortcd enemies=1 checkbox=!%s", specialization, aoeToggle)
+		output[#output + 1] = "{"
+		GenerateIconBody(output, "shortcd", profile)
+		output[#output + 1] = "}"
+		output[#output + 1] = ""
+		output[#output + 1] = format("AddIcon specialization=%s help=shortcd checkbox=%s", specialization, aoeToggle)
+		output[#output + 1] = "{"
+		GenerateIconBody(output, "shortcd", profile)
+		output[#output + 1] = "}"
 		-- Single-target rotation.
 		output[#output + 1] = ""
-		output[#output + 1] = format("AddIcon specialization=%s help=main enemies=1", annotation.specialization)
+		output[#output + 1] = format("AddIcon specialization=%s help=main enemies=1", specialization)
 		output[#output + 1] = "{"
-		if profile["actions.precombat"] then
-			output[#output + 1] = format("	if not InCombat() %s()", OvaleFunctionName("precombat", annotation))
-		end
-		output[#output + 1] = format("	%s()", OvaleFunctionName("_default", annotation))
+		GenerateIconBody(output, "main", profile)
 		output[#output + 1] = "}"
 		-- AoE rotation.
 		output[#output + 1] = ""
-		output[#output + 1] = format("AddIcon specialization=%s help=aoe", annotation.specialization)
+		output[#output + 1] = format("AddIcon specialization=%s help=aoe checkbox=%s", specialization, aoeToggle)
 		output[#output + 1] = "{"
-		if profile["actions.precombat"] then
-			output[#output + 1] = format("	if not InCombat() %s()", OvaleFunctionName("precombat", annotation))
-		end
-		output[#output + 1] = format("	%s()", OvaleFunctionName("_default", annotation))
+		GenerateIconBody(output, "main", profile)
+		output[#output + 1] = "}"
+		-- CD rotation.
+		output[#output + 1] = ""
+		output[#output + 1] = format("AddIcon specialization=%s help=cd enemies=1 checkbox=!%s", specialization, aoeToggle)
+		output[#output + 1] = "{"
+		GenerateIconBody(output, "cd", profile)
+		output[#output + 1] = "}"
+		output[#output + 1] = ""
+		output[#output + 1] = format("AddIcon specialization=%s help=cd checkbox=%s", specialization, aoeToggle)
+		output[#output + 1] = "{"
+		GenerateIconBody(output, "cd", profile)
 		output[#output + 1] = "}"
 	end
 	-- Append the required symbols for the script.