--[[-------------------------------------------------------------------- Copyright (C) 2014 Johnny C. Lam. See the file LICENSE.txt for copying permission. --]]-------------------------------------------------------------------- local OVALE, Ovale = ... local OvaleSimulationCraft = Ovale:NewModule("OvaleSimulationCraft") Ovale.OvaleSimulationCraft = OvaleSimulationCraft --<private-static-properties> local AceConfig = LibStub("AceConfig-3.0") local AceConfigDialog = LibStub("AceConfigDialog-3.0") local L = Ovale.L local OvaleOptions = Ovale.OvaleOptions local OvalePool = Ovale.OvalePool -- Forward declarations for module dependencies. local OvaleAST = nil local OvaleData = nil local OvaleHonorAmongThieves = nil local OvaleLexer = nil local OvalePower = nil local format = string.format local gmatch = string.gmatch local gsub = string.gsub local ipairs = ipairs local pairs = pairs local rawset = rawset local strfind = string.find local strlen = string.len local strlower = string.lower local strmatch = string.match local strsub = string.sub local strupper = string.upper local tconcat = table.concat local tinsert = table.insert local tonumber = tonumber local tostring = tostring local tremove = table.remove local tsort = table.sort local type = type local wipe = wipe local yield = coroutine.yield local RAID_CLASS_COLORS = RAID_CLASS_COLORS -- Keywords for SimulationCraft action lists. local KEYWORD = {} local MODIFIER_KEYWORD = { ["ammo_type"] = true, ["chain"] = true, ["choose"] = true, ["cooldown"] = true, ["cooldown_stddev"] = true, ["cycle_targets"] = true, ["damage"] = true, ["early_chain_if"] = true, ["extra_amount"] = true, ["five_stacks"] = true, ["for_next"] = true, ["if"] = true, ["interrupt"] = true, ["interrupt_if"] = true, ["lethal"] = true, ["line_cd"] = true, ["max_cycle_targets"] = true, ["max_energy"] = true, ["moving"] = true, ["name"] = true, ["sec"] = true, ["slot"] = true, ["sync"] = true, ["sync_weapons"] = true, ["target"] = true, ["travel_speed"] = true, ["type"] = true, ["wait"] = true, ["wait_on_ready"] = true, ["weapon"] = true, } local FUNCTION_KEYWORD = { ["ceil"] = true, ["floor"] = true, } local SPECIAL_ACTION = { ["apply_poison"] = true, ["auto_attack"] = true, ["call_action_list"] = true, ["cancel_buff"] = true, ["cancel_metamorphosis"] = true, ["exotic_munitions"] = true, ["flask"] = true, ["food"] = true, ["health_stone"] = true, ["pool_resource"] = true, ["potion"] = true, ["run_action_list"] = true, ["snapshot_stats"] = true, ["stance"] = true, ["start_moving"] = true, ["stealth"] = true, ["stop_moving"] = true, ["swap_action_list"] = true, ["use_item"] = true, ["wait"] = true, } local RUNE_OPERAND = { ["blood"] = "blood", ["death"] = "death", ["frost"] = "frost", ["unholy"] = "unholy", ["rune.blood"] = "blood", ["rune.death"] = "death", ["rune.frost"] = "frost", ["rune.unholy"] = "unholy", } do -- All expression keywords are keywords. for keyword, value in pairs(MODIFIER_KEYWORD) do KEYWORD[keyword] = value end -- All function keywords are keywords. for keyword, value in pairs(FUNCTION_KEYWORD) do KEYWORD[keyword] = value end -- All special actions are keywords. for keyword, value in pairs(SPECIAL_ACTION) do KEYWORD[keyword] = value end end -- Table of pattern/tokenizer pairs for SimulationCraft action lists. local MATCHES = nil -- Unary and binary operators with precedence. local UNARY_OPERATOR = { ["!"] = { "logical", 15 }, ["-"] = { "arithmetic", 50 }, } local BINARY_OPERATOR = { -- logical ["|"] = { "logical", 5, "associative" }, ["^"] = { "logical", 8, "associative" }, ["&"] = { "logical", 10, "associative" }, -- comparison ["!="] = { "compare", 20 }, ["<"] = { "compare", 20 }, ["<="] = { "compare", 20 }, ["="] = { "compare", 20 }, [">"] = { "compare", 20 }, [">="] = { "compare", 20 }, ["~"] = { "compare", 20 }, ["!~"] = { "compare", 20 }, -- addition, subtraction ["+"] = { "arithmetic", 30, "associative" }, ["-"] = { "arithmetic", 30 }, -- multiplication, division, modulus ["%"] = { "arithmetic", 40 }, ["*"] = { "arithmetic", 40, "associative" }, } -- INDENT[k] is a string of k concatenated tabs. local INDENT = {} do INDENT[0] = "" local metatable = { __index = function(tbl, key) key = tonumber(key) if key > 0 then local s = tbl[key - 1] .. "\t" rawset(tbl, key, s) return s end return INDENT[0] end, } setmetatable(INDENT, metatable) end local EMIT_DISAMBIGUATION = {} local EMIT_EXTRA_PARAMETERS = {} local OPERAND_TOKEN_PATTERN = "[^.]+" local POTION_STAT = { ["draenic_agility"] = "agility", ["draenic_armor"] = "armor", ["draenic_intellect"] = "intellect", ["draenic_strength"] = "strength", ["jade_serpent"] = "intellect", ["mogu_power"] = "strength", ["mountains"] = "armor", ["tolvir"] = "agility", ["virmens_bite"] = "agility", } local self_outputPool = OvalePool("OvaleSimulationCraft_outputPool") local self_childrenPool = OvalePool("OvaleSimulationCraft_childrenPool") local self_pool = OvalePool("OvaleSimulationCraft_pool") do self_pool.Clean = function(self, node) if node.child then self_childrenPool:Release(node.child) node.child = nil end end end -- Save the most recent profile entered into the SimulationCraft input window. local self_lastSimC = nil -- Save the most recent script translated from the profile in the SimulationCraft input window. local self_lastScript = nil do -- Add a slash command "/ovale simc" to access the GUI for this module. local actions = { simc = { name = "SimulationCraft", type = "execute", func = function() local appName = OvaleSimulationCraft:GetName() AceConfigDialog:SetDefaultSize(appName, 700, 550) AceConfigDialog:Open(appName) end, }, } -- Inject into OvaleOptions. for k, v in pairs(actions) do OvaleOptions.options.args.actions.args[k] = v end OvaleOptions:RegisterOptions(OvaleSimulationCraft) end --</private-static-properties> --<private-static-methods> -- Implementation of PHP-like print_r() taken from http://lua-users.org/wiki/TableSerialization. -- This is used to print out a table, but has been modified to print out an AST. local function print_r(node, indent, done, output) done = done or {} output = output or {} indent = indent or '' for key, value in pairs(node) do if type(value) == "table" then if done[value] then tinsert(output, indent .. "[" .. tostring(key) .. "] => (self_reference)") else -- Shortcut conditional allocation done[value] = true tinsert(output, indent .. "[" .. tostring(key) .. "] => {") print_r(value, indent .. " ", done, output) tinsert(output, indent .. "}") end else tinsert(output, indent .. "[" .. tostring(key) .. "] => " .. tostring(value)) end end return output end -- Get a new node from the pool and save it in the nodes array. local function NewNode(nodeList, hasChild) local node = self_pool:Get() if nodeList then local nodeId = #nodeList + 1 node.nodeId = nodeId nodeList[nodeId] = node end if hasChild then node.child = self_childrenPool:Get() end return node end --[[--------------------------------------------- Lexer functions (for use with OvaleLexer) --]]--------------------------------------------- local function TokenizeName(token) if KEYWORD[token] then return yield("keyword", token) else return yield("name", token) end end local function TokenizeNumber(token, options) if options and options.number then token = tonumber(token) end return yield("number", token) end local function Tokenize(token) return yield(token, token) end local function NoToken() return yield(nil) end do MATCHES = { { "^%d+%.?%d*", TokenizeNumber }, { "^[%a_][%w_]*[.:]?[%w_.]*", TokenizeName }, { "^!=", Tokenize }, { "^<=", Tokenize }, { "^>=", Tokenize }, { "^!~", Tokenize }, { "^.", Tokenize }, { "^$", NoToken }, } end local function GetTokenIterator(s) local exclude = { space = false, comments = false } return OvaleLexer.scan(s, MATCHES, exclude) end --[[------------------------ "Unparser" functions --]]------------------------ -- Return the precedence of an operator in the given node. -- Returns nil if the node is not an expression node. local function GetPrecedence(node) local precedence = node.precedence if not precedence then local operator = node.operator if operator then if node.expressionType == "unary" and UNARY_OPERATOR[operator] then precedence = UNARY_OPERATOR[operator][2] elseif node.expressionType == "binary" and BINARY_OPERATOR[operator] then precedence = BINARY_OPERATOR[operator][2] end end end return precedence end local UNPARSE_VISITOR = nil local function Unparse(node) local visitor = UNPARSE_VISITOR[node.type] if not visitor then OvaleSimulationCraft:Error("Unable to unparse node of type '%s'.", node.type) else return visitor(node) end end local function UnparseAction(node) local output = self_outputPool:Get() output[#output + 1] = node.name for modifier, expressionNode in pairs(node.child) do output[#output + 1] = modifier .. "=" .. Unparse(expressionNode) end local s = tconcat(output, ",") self_outputPool:Release(output) return s end local function UnparseActionList(node) local output = self_outputPool:Get() local listName if node.name == "_default" then listName = "action" else listName = "action." .. node.name end output[#output + 1] = "" for i, actionNode in pairs(node.child) do local operator = (i == 1) and "=" or "+=/" output[#output + 1] = listName .. operator .. Unparse(actionNode) end local s = tconcat(output, "\n") self_outputPool:Release(output) return s end local function UnparseExpression(node) local expression local precedence = GetPrecedence(node) if node.expressionType == "unary" then local rhsExpression local rhsNode = node.child[1] local rhsPrecedence = GetPrecedence(rhsNode) if rhsPrecedence and precedence >= rhsPrecedence then rhsExpression = "(" .. Unparse(rhsNode) .. ")" else rhsExpression = Unparse(rhsNode) end expression = node.operator .. rhsExpression elseif node.expressionType == "binary" then local lhsExpression, rhsExpression local lhsNode = node.child[1] local lhsPrecedence = GetPrecedence(lhsNode) if lhsPrecedence and lhsPrecedence < precedence then lhsExpression = "(" .. Unparse(lhsNode) .. ")" else lhsExpression = Unparse(lhsNode) end local rhsNode = node.child[2] local rhsPrecedence = GetPrecedence(rhsNode) if rhsPrecedence and precedence > rhsPrecedence then rhsExpression = "(" .. Unparse(rhsNode) .. ")" elseif rhsPrecedence and precedence == rhsPrecedence then if BINARY_OPERATOR[node.operator][3] == "associative" then rhsExpression = Unparse(rhsNode) else rhsExpression = "(" .. Unparse(rhsNode) .. ")" end else rhsExpression = Unparse(rhsNode) end expression = lhsExpression .. node.operator .. rhsExpression end return expression end local function UnparseFunction(node) return node.name .. "(" .. Unparse(node.child[1]) .. ")" end local function UnparseNumber(node) return tostring(node.value) end local function UnparseOperand(node) return node.name end do UNPARSE_VISITOR = { ["action"] = UnparseAction, ["action_list"] = UnparseActionList, ["arithmetic"] = UnparseExpression, ["compare"] = UnparseExpression, ["function"] = UnparseFunction, ["logical"] = UnparseExpression, ["number"] = UnparseNumber, ["operand"] = UnparseOperand, } end --[[-------------------- Parser functions --]]-------------------- -- Prints the error message and the next 20 tokens from tokenStream. local function SyntaxError(tokenStream, ...) OvaleSimulationCraft:Print(...) local context = { "Next tokens:" } for i = 1, 20 do local tokenType, token = tokenStream:Peek(i) if tokenType then context[#context + 1] = token else context[#context + 1] = "<EOS>" break end end OvaleSimulationCraft:Print(tconcat(context, " ")) end -- Left-rotate tree to preserve precedence. local function LeftRotateTree(node) local rhsNode = node.child[2] while node.type == rhsNode.type and node.operator == rhsNode.operator and BINARY_OPERATOR[node.operator][3] == "associative" and rhsNode.expressionType == "binary" do node.child[2] = rhsNode.child[1] rhsNode.child[1] = node node = rhsNode rhsNode = node.child[2] end return node end -- Forward declarations of parser functions needed to implement a recursive descent parser. local ParseAction = nil local ParseActionList = nil local ParseExpression = nil local ParseFunction = nil local ParseModifier = nil local ParseNumber = nil local ParseOperand = nil local ParseParentheses = nil local ParseSimpleExpression = nil local function TicksRemainTranslationHelper(p1, p2, p3, p4) if p4 then return p1 .. p2 .. "<" .. tostring(tonumber(p4) + 1) else return p1 .. "<" .. tostring(tonumber(p3) + 1) end end ParseAction = function(action, nodeList, annotation) local ok = true local stream = action do -- Fix "|" being silently replaced by "||" in WoW strings entered via an edit box. stream = gsub(stream, "||", "|") end do -- Fix bugs in SimulationCraft action lists. -- ",," into "," stream = gsub(stream, ",,", ",") end do -- Changes to SimulationCraft action lists for easier translation into Ovale timespan concept. -- "active_dot.dotName=0" into "!(active_dot.dotName>0)" stream = gsub(stream, "(active_dot%.[%w_]+)=0", "!(%1>0)") -- "cooldown_remains=0" into "!(cooldown_remains>0)" stream = gsub(stream, "([^_%.])(cooldown_remains)=0", "%1!(%2>0)") stream = gsub(stream, "([a-z_%.]+%.cooldown_remains)=0", "!(%1>0)") -- "remains=0" into "!(remains>0)" stream = gsub(stream, "([^_%.])(remains)=0", "%1!(%2>0)") stream = gsub(stream, "([a-z_%.]+%.remains)=0", "!(%1>0)") -- "ticks_remain=1" into "ticks_remain<2" -- "ticks_remain<=N" into "ticks_remain<N+1" stream = gsub(stream, "([^_%.])(ticks_remain)(<?=)([0-9]+)", TicksRemainTranslationHelper) stream = gsub(stream, "([a-z_%.]+%.ticks_remain)(<?=)([0-9]+)", TicksRemainTranslationHelper) end local tokenStream = OvaleLexer("SimulationCraft", GetTokenIterator(stream)) -- Consume the action. local name do local tokenType, token = tokenStream:Consume() if (tokenType == "keyword" and SPECIAL_ACTION[token]) or tokenType == "name" then name = token else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing action line; name or special action expected.", token) ok = false end end local child = self_childrenPool:Get() if ok then local tokenType, token = tokenStream:Peek() while ok and tokenType do if tokenType == "," then -- Consume the ',' token. tokenStream:Consume() local modifier, expressionNode ok, modifier, expressionNode = ParseModifier(tokenStream, nodeList, annotation) if ok then child[modifier] = expressionNode tokenType, token = tokenStream:Peek() end else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing action line; ',' expected.", token) ok = false end end end local node if ok then node = NewNode(nodeList) node.type = "action" node.action = action node.name = name node.child = child else self_childrenPool:Release(child) end return ok, node end ParseActionList = function(name, actionList, nodeList, annotation) local ok = true local child = self_childrenPool:Get() for action in gmatch(actionList, "[^/]+") do local actionNode ok, actionNode = ParseAction(action, nodeList, annotation) if ok then child[#child + 1] = actionNode else break end end local node if ok then node = NewNode(nodeList) node.type = "action_list" node.name = name node.child = child else self_childrenPool:Release(child) end return ok, node end --[[ Operator-precedence parser for logical and arithmetic expressions. Implementation taken from Wikipedia: http://en.wikipedia.org/wiki/Operator-precedence_parser --]] ParseExpression = function(tokenStream, nodeList, annotation, minPrecedence) minPrecedence = minPrecedence or 0 local ok = true local node -- Check for unary operator expressions first as they decorate the underlying expression. do local tokenType, token = tokenStream:Peek() if tokenType then local opInfo = UNARY_OPERATOR[token] if opInfo then local opType, precedence = opInfo[1], opInfo[2] local asType = (opType == "logical") and "boolean" or "value" tokenStream:Consume() local operator = token local rhsNode ok, rhsNode = ParseExpression(tokenStream, nodeList, annotation, precedence) if ok then if operator == "-" and rhsNode.type == "number" then -- Elide the unary negation operator into the number. rhsNode.value = -1 * rhsNode.value node = rhsNode else node = NewNode(nodeList, true) node.type = opType node.expressionType = "unary" node.operator = operator node.precedence = precedence node.child[1] = rhsNode rhsNode.asType = asType end end else ok, node = ParseSimpleExpression(tokenStream, nodeList, annotation) if ok and node then node.asType = "boolean" end end end end -- Peek at the next token to see if it is a binary operator. while ok do local keepScanning = false local tokenType, token = tokenStream:Peek() if tokenType then local opInfo = BINARY_OPERATOR[token] if opInfo then local opType, precedence = opInfo[1], opInfo[2] local asType = (opType == "logical") and "boolean" or "value" if precedence and precedence > minPrecedence then keepScanning = true tokenStream:Consume() local operator = token local lhsNode = node local rhsNode ok, rhsNode = ParseExpression(tokenStream, nodeList, annotation, precedence) if ok then node = NewNode(nodeList, true) node.type = opType node.expressionType = "binary" node.operator = operator node.precedence = precedence node.child[1] = lhsNode node.child[2] = rhsNode lhsNode.asType = asType rhsNode.asType = asType -- Left-rotate tree to preserve precedence. node = LeftRotateTree(node) end end end end if not keepScanning then break end end return ok, node end ParseFunction = function(tokenStream, nodeList, annotation) local ok = true local name -- Consume the name. do local tokenType, token = tokenStream:Consume() if tokenType == "keyword" and FUNCTION_KEYWORD[token] then name = token else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; name expected.", token) ok = false end end -- Consume the left parenthesis. if ok then local tokenType, token = tokenStream:Consume() if tokenType ~= "(" then SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; '(' expected.", token) ok = false end end -- Consume the function argument. local argumentNode if ok then ok, argumentNode = ParseExpression(tokenStream, nodeList, annotation) end -- Consume the right parenthesis. if ok then local tokenType, token = tokenStream:Consume() if tokenType ~= ")" then SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; ')' expected.", token) ok = false end end -- Create the AST node. local node if ok then node = NewNode(nodeList, true) node.type = "function" node.name = name node.child[1] = argumentNode end return ok, node end ParseModifier = function(tokenStream, nodeList, annotation) local ok = true local name do local tokenType, token = tokenStream:Consume() if tokenType == "keyword" and MODIFIER_KEYWORD[token] then name = token else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing action line; expression keyword expected.", token) ok = false end end if ok then -- Consume the '=' token. local tokenType, token = tokenStream:Consume() if tokenType ~= "=" then SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing action line; '=' expected.", token) ok = false end end local expressionNode if ok then ok, expressionNode = ParseExpression(tokenStream, nodeList, annotation) if ok and expressionNode and name == "sec" then expressionNode.asType = "value" end end return ok, name, expressionNode end ParseNumber = function(tokenStream, nodeList, annotation) local ok = true local value -- Consume the number. do local tokenType, token = tokenStream:Consume() if tokenType == "number" then value = tonumber(token) else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing NUMBER; number expected.", token) ok = false end end -- Create the AST node. local node if ok then node = NewNode(nodeList) node.type = "number" node.value = value end return ok, node end ParseOperand = function(tokenStream, nodeList, annotation) local ok = true local name -- Consume the operand. do local tokenType, token = tokenStream:Consume() if tokenType == "name" then name = token elseif tokenType == "keyword" and token == "target" then -- Allow a bare "target" to be used as an operand. name = token else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing OPERAND; operand expected.", token) ok = false end end -- Create the AST node. local node if ok then node = NewNode(nodeList) node.type = "operand" node.name = name node.rune = RUNE_OPERAND[name] annotation.operand = annotation.operand or {} annotation.operand[#annotation.operand + 1] = node end return ok, node end ParseParentheses = function(tokenStream, nodeList, annotation) local ok = true local leftToken, rightToken -- Consume the left parenthesis. do local tokenType, token = tokenStream:Consume() if tokenType == "(" then leftToken, rightToken = "(", ")" elseif tokenType == "{" then leftToken, rightToken = "{", "}" else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing PARENTHESES; '(' or '{' expected.", token) ok = false end end -- Consume the inner expression. local node if ok then ok, node = ParseExpression(tokenStream, nodeList, annotation) end -- Consume the right parenthesis. if ok then local tokenType, token = tokenStream:Consume() if tokenType ~= rightToken then SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing PARENTHESES; '%s' expected.", token, rightToken) ok = false end end -- Create the AST node. if ok then node.left = leftToken node.right = rightToken end return ok, node end ParseSimpleExpression = function(tokenStream, nodeList, annotation) local ok = true local node local tokenType, token = tokenStream:Peek() if tokenType == "number" then ok, node = ParseNumber(tokenStream, nodeList, annotation) elseif tokenType == "keyword" then if FUNCTION_KEYWORD[token] then ok, node = ParseFunction(tokenStream, nodeList, annotation) elseif token == "target" then ok, node = ParseOperand(tokenStream, nodeList, annotation) end elseif tokenType == "name" then ok, node = ParseOperand(tokenStream, nodeList, annotation) elseif tokenType == "(" then ok, node = ParseParentheses(tokenStream, nodeList, annotation) else SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SIMPLE EXPRESSION", token) tokenStream:Consume() ok = false end return ok, node end --[[----------------------------- Code generation functions --]]----------------------------- local CamelCase = nil do local function CamelCaseHelper(first, rest) return strupper(first) .. strlower(rest) end CamelCase = function(s) local tc = gsub(s, "(%a)(%w*)", CamelCaseHelper) return gsub(tc, "[%s_]", "") end end local function OvaleFunctionName(name, annotation) local output = self_outputPool:Get() local profileName, class, specialization = annotation.name, annotation.class, annotation.specialization if specialization then output[#output + 1] = specialization end if strmatch(profileName, "_1[hH]_") then if class == "DEATHKNIGHT" and specialization == "frost" then output[#output + 1] = "dual wield" elseif class == "WARRIOR" and specialization == "fury" then output[#output + 1] = "single minded fury" end elseif strmatch(profileName, "_2[hH]_") then if class == "DEATHKNIGHT" and specialization == "frost" then output[#output + 1] = "two hander" elseif class == "WARRIOR" and specialization == "fury" then output[#output + 1] = "titans grip" end elseif strmatch(profileName, "_[gG]ladiator_") then output[#output + 1] = "gladiator" end output[#output + 1] = name output[#output + 1] = "actions" local outputString = CamelCase(tconcat(output, " ")) self_outputPool:Release(output) return outputString end local function AddSymbol(annotation, symbol) local symbolTable = annotation.symbolTable or {} -- Add the symbol to the table if it's not already present and it's not a globally-defined spell list name. if not symbolTable[symbol] and not OvaleData.buffSpellList[symbol] then symbolTable[symbol] = true symbolTable[#symbolTable + 1] = symbol end annotation.symbolTable = symbolTable end local function AddPerClassSpecialization(tbl, name, info, class, specialization) class = class or "ALL_CLASSES" specialization = specialization or "ALL_SPECIALIZATIONS" tbl[class] = tbl[class] or {} tbl[class][specialization] = tbl[class][specialization] or {} tbl[class][specialization][name] = info end local function GetPerClassSpecialization(tbl, name, class, specialization) local info while not info do while not info do if tbl[class] and tbl[class][specialization] and tbl[class][specialization][name] then info = tbl[class][specialization][name] end if specialization ~= "ALL_SPECIALIZATIONS" then specialization = "ALL_SPECIALIZATIONS" else break end end if class ~= "ALL_CLASSES" then class = "ALL_CLASSES" else break end end return info end local function AddDisambiguation(name, info, class, specialization) AddPerClassSpecialization(EMIT_DISAMBIGUATION, name, info, class, specialization) end local function Disambiguate(name, class, specialization) return GetPerClassSpecialization(EMIT_DISAMBIGUATION, name, class, specialization) or name end local function InitializeDisambiguation() AddDisambiguation("bloodlust_buff", "burst_haste_buff") AddDisambiguation("trinket_proc_all_buff", "trinket_proc_any_buff") -- Death Knight AddDisambiguation("arcane_torrent", "arcane_torrent_runicpower", "DEATHKNIGHT") AddDisambiguation("blood_fury", "blood_fury_ap", "DEATHKNIGHT") AddDisambiguation("breath_of_sindragosa_debuff", "breath_of_sindragosa_buff", "DEATHKNIGHT") AddDisambiguation("soul_reaper", "soul_reaper_blood", "DEATHKNIGHT", "blood") AddDisambiguation("soul_reaper", "soul_reaper_frost", "DEATHKNIGHT", "frost") AddDisambiguation("soul_reaper", "soul_reaper_unholy", "DEATHKNIGHT", "unholy") -- Druid AddDisambiguation("arcane_torrent", "arcane_torrent_energy", "DRUID") AddDisambiguation("berserk", "berserk_bear", "DRUID", "guardian") AddDisambiguation("berserk", "berserk_cat", "DRUID", "feral") AddDisambiguation("blood_fury", "blood_fury_apsp", "DRUID") AddDisambiguation("dream_of_cenarius", "dream_of_cenarius_caster", "DRUID", "balance") AddDisambiguation("dream_of_cenarius", "dream_of_cenarius_melee", "DRUID", "feral") AddDisambiguation("dream_of_cenarius", "dream_of_cenarius_tank", "DRUID", "guardian") AddDisambiguation("force_of_nature", "force_of_nature_caster", "DRUID", "balance") AddDisambiguation("force_of_nature", "force_of_nature_melee", "DRUID", "feral") AddDisambiguation("force_of_nature", "force_of_nature_tank", "DRUID", "guardian") AddDisambiguation("heart_of_the_wild", "heart_of_the_wild_tank", "DRUID", "guardian") AddDisambiguation("incarnation", "incarnation_caster", "DRUID", "balance") AddDisambiguation("incarnation", "incarnation_melee", "DRUID", "feral") AddDisambiguation("incarnation", "incarnation_tank", "DRUID", "guardian") AddDisambiguation("moonfire", "moonfire_cat", "DRUID", "feral") AddDisambiguation("omen_of_clarity", "omen_of_clarity_melee", "DRUID", "feral") AddDisambiguation("rejuvenation_debuff", "rejuvenation_buff", "DRUID") -- Hunter AddDisambiguation("arcane_torrent", "arcane_torrent_focus", "HUNTER") AddDisambiguation("blood_fury", "blood_fury_ap", "HUNTER") AddDisambiguation("focusing_shot", "focusing_shot_marksmanship", "HUNTER", "marksmanship") -- Mage AddDisambiguation("arcane_torrent", "arcane_torrent_mana", "MAGE") AddDisambiguation("arcane_charge_buff", "arcane_charge_debuff", "MAGE", "arcane") AddDisambiguation("blood_fury", "blood_fury_sp", "MAGE") AddDisambiguation("water_jet", "pet_water_jet", "MAGE", "frost") -- Monk AddDisambiguation("arcane_torrent", "arcane_torrent_chi", "MONK") AddDisambiguation("blood_fury", "blood_fury_apsp", "MONK") AddDisambiguation("chi_explosion", "chi_explosion_heal", "MONK", "mistweaver") AddDisambiguation("chi_explosion", "chi_explosion_melee", "MONK", "windwalker") AddDisambiguation("chi_explosion", "chi_explosion_tank", "MONK", "brewmaster") AddDisambiguation("zen_sphere_debuff", "zen_sphere_buff", "MONK") -- Paladin AddDisambiguation("arcane_torrent", "arcane_torrent_holy", "PALADIN") AddDisambiguation("avenging_wrath", "avenging_wrath_heal", "PALADIN", "holy") AddDisambiguation("avenging_wrath", "avenging_wrath_melee", "PALADIN", "retribution") AddDisambiguation("blood_fury", "blood_fury_apsp", "PALADIN") AddDisambiguation("sacred_shield_debuff", "sacred_shield_buff", "PALADIN") -- Priest AddDisambiguation("arcane_torrent", "arcane_torrent_mana", "PRIEST") AddDisambiguation("blood_fury", "blood_fury_sp", "PRIEST") AddDisambiguation("cascade", "cascade_caster", "PRIEST", "shadow") AddDisambiguation("cascade", "cascade_heal", "PRIEST", "discipline") AddDisambiguation("cascade", "cascade_heal", "PRIEST", "holy") AddDisambiguation("devouring_plague_tick", "devouring_plague", "PRIEST") AddDisambiguation("divine_star", "divine_star_caster", "PRIEST", "shadow") AddDisambiguation("divine_star", "divine_star_heal", "PRIEST", "discipline") AddDisambiguation("divine_star", "divine_star_heal", "PRIEST", "holy") AddDisambiguation("halo", "halo_caster", "PRIEST", "shadow") AddDisambiguation("halo", "halo_heal", "PRIEST", "discipline") AddDisambiguation("halo", "halo_heal", "PRIEST", "holy") AddDisambiguation("renew_debuff", "renew_buff", "PRIEST") -- Rogue AddDisambiguation("arcane_torrent", "arcane_torrent_energy", "ROGUE") AddDisambiguation("blood_fury", "blood_fury_ap", "ROGUE") AddDisambiguation("stealth_buff", "stealthed_buff", "ROGUE") -- Shaman AddDisambiguation("arcane_torrent", "arcane_torrent_mana", "SHAMAN") AddDisambiguation("ascendance", "ascendance_caster", "SHAMAN", "elemental") AddDisambiguation("ascendance", "ascendance_heal", "SHAMAN", "restoration") AddDisambiguation("ascendance", "ascendance_melee", "SHAMAN", "enhancement") AddDisambiguation("blood_fury", "blood_fury_apsp", "SHAMAN") -- Warlock AddDisambiguation("arcane_torrent", "arcane_torrent_mana", "WARLOCK") AddDisambiguation("blood_fury", "blood_fury_sp", "WARLOCK") AddDisambiguation("dark_soul", "dark_soul_instability", "WARLOCK", "destruction") AddDisambiguation("dark_soul", "dark_soul_knowledge", "WARLOCK", "demonology") AddDisambiguation("dark_soul", "dark_soul_misery", "WARLOCK", "affliction") AddDisambiguation("glyph_of_dark_soul_instability", "glyph_of_dark_soul", "WARLOCK", "destruction") AddDisambiguation("glyph_of_dark_soul_knowledge", "glyph_of_dark_soul", "WARLOCK", "demonology") AddDisambiguation("glyph_of_dark_soul_misery", "glyph_of_dark_soul", "WARLOCK", "affliction") -- Warrior AddDisambiguation("arcane_torrent", "arcane_torrent_rage", "WARRIOR") AddDisambiguation("blood_fury", "blood_fury_ap", "WARRIOR") AddDisambiguation("execute", "execute_arms", "WARRIOR", "arms") AddDisambiguation("shield_barrier", "shield_barrier_melee", "WARRIOR", "arms") AddDisambiguation("shield_barrier", "shield_barrier_melee", "WARRIOR", "fury") AddDisambiguation("shield_barrier", "shield_barrier_tank", "WARRIOR", "protection") end local function IsTotem(name) if strsub(name, 1, 13) == "wild_mushroom" then -- Druids. return true elseif name == "prismatic_crystal" or name == "rune_of_power" then -- Mages. return true elseif strsub(name, -7, -1) == "_statue" then -- Monks. return true elseif strsub(name, -6, -1) == "_totem" then -- Shamans. return true end return false end local EMIT_VISITOR = nil -- Forward declarations of code generation functions. local Emit = nil local EmitAction = nil local EmitActionList = nil local EmitExpression = nil local EmitFunction = nil local EmitModifier = nil local EmitNumber = nil local EmitOperand = nil local EmitOperandAction = nil local EmitOperandActiveDot = nil local EmitOperandBuff = nil local EmitOperandCharacter = nil local EmitOperandCooldown = nil local EmitOperandDisease = nil local EmitOperandDot = nil local EmitOperandGlyph = nil local EmitOperandPet = nil local EmitOperandPreviousSpell = nil local EmitOperandRaidEvent = nil local EmitOperandRune = nil local EmitOperandSeal = nil local EmitOperandSetBonus = nil local EmitOperandSpecial = nil local EmitOperandTalent = nil local EmitOperandTotem = nil local EmitOperandTrinket = nil Emit = function(parseNode, nodeList, annotation, action) local visitor = EMIT_VISITOR[parseNode.type] if not visitor then OvaleSimulationCraft:Print("Unable to emit node of type '%s'.", parseNode.type) else return visitor(parseNode, nodeList, annotation, action) end end EmitAction = function(parseNode, nodeList, annotation) local node local canonicalizedName = gsub(parseNode.name, ":", "_") local class = annotation.class local specialization = annotation.specialization local action = Disambiguate(canonicalizedName, class, specialization) if action == "auto_attack" or action == "auto_shot" then -- skip elseif action == "choose_target" then -- skip elseif action == "elixir" or action == "flask" or action == "food" then -- skip elseif action == "snapshot_stats" then -- skip else local bodyNode, conditionNode local bodyCode, conditionCode local expressionType = "expression" local modifier = parseNode.child local isSpellAction = true if class == "DEATHKNIGHT" and action == "antimagic_shell" then -- Only suggest Anti-Magic Shell if there is incoming damage to absorb to generate runic power. conditionCode = "IncomingDamage(1.5) > 0" elseif class == "DEATHKNIGHT" and action == "blood_tap" then -- Blood Tap requires a minimum of five stacks of Blood Charge to be on the player. local buffName = "blood_charge_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffStacks(%s) >= 5", buffName) elseif class == "DEATHKNIGHT" and action == "dark_transformation" then -- Dark Transformation requires a five stacks of Shadow Infusion to be on the player/pet. local buffName = "shadow_infusion_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffStacks(%s) >= 5", buffName) elseif class == "DEATHKNIGHT" and action == "horn_of_winter" then -- Only cast Horn of Winter if not already raid-buffed. conditionCode = "BuffExpires(attack_power_multiplier_buff any=1)" elseif class == "DEATHKNIGHT" and action == "mind_freeze" then bodyCode = "InterruptActions()" annotation[action] = class isSpellAction = false elseif class == "DEATHKNIGHT" and action == "plague_leech" then -- Plague Leech requires diseases to exist on the target. -- Scripts should be checking that there is least one pair of fully-depleted runes, -- but they mostly don't, so add the check for all uses of Plague Leech. conditionCode = "target.DiseasesTicking() and { Rune(blood) < 1 or Rune(frost) < 1 or Rune(unholy) < 1 }" elseif class == "DRUID" and specialization == "guardian" and action == "rejuvenation" then -- Only cast Rejuvenation as a guardian druid if it is Enhanced Rejuvenation (castable in bear form). local spellName = "enhanced_rejuvenation" AddSymbol(annotation, spellName) conditionCode = format("SpellKnown(%s)", spellName) elseif class == "DRUID" and action == "prowl" then -- Don't Prowl if already stealthed. conditionCode = "BuffExpires(stealthed_buff any=1)" elseif class == "DRUID" and action == "pulverize" then -- Pulverize requires 3 stacks of Lacerate on the target. local debuffName = "lacerate_debuff" AddSymbol(annotation, debuffName) conditionCode = format("target.DebuffStacks(%s) >= 3", debuffName) elseif class == "DRUID" and action == "skull_bash" then bodyCode = "InterruptActions()" annotation[action] = class isSpellAction = false elseif class == "DRUID" and action == "wild_charge" then bodyCode = "GetInMeleeRange()" annotation[action] = class isSpellAction = false elseif class == "HUNTER" and action == "exotic_munitions" then if modifier.ammo_type then local name = Unparse(modifier.ammo_type) action = name .. "_ammo" -- Always have at least 20 minutes of an Exotic Munitions buff applied when out of combat. local buffName = "exotic_munitions_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffRemaining(%s) < 1200", buffName) else isSpellAction = false end elseif class == "HUNTER" and action == "explosive_trap" then -- Glyph of Explosive Trap removes the damage component from Explosive Trap. local glyphName = "glyph_of_explosive_trap" AddSymbol(annotation, glyphName) annotation.trap_launcher = class conditionCode = format("CheckBoxOn(opt_trap_launcher) and not Glyph(%s)", glyphName) elseif class == "HUNTER" and action == "focus_fire" then -- Focus Fire requires at least one stack of Frenzy. local buffName = "frenzy_buff" AddSymbol(annotation, buffName) if modifier.five_stacks then local value = tonumber(Unparse(modifier.five_stacks)) if value == 1 then conditionCode = format("BuffStacks(%s any=1) == 5", buffName) end end if not conditionCode then conditionCode = format("BuffPresent(%s any=1)", buffName) end elseif class == "HUNTER" and action == "kill_command" then -- Kill Command requires that a pet that can move freely. conditionCode = "pet.Present() and not pet.IsIncapacitated() and not pet.IsFeared() and not pet.IsStunned()" elseif class == "HUNTER" and action == "summon_pet" then if specialization == "beast_mastery" then bodyCode = "BeastMasterySummonPet()" else bodyCode = "SummonPet()" end annotation[action] = class isSpellAction = false elseif class == "HUNTER" and strsub(action, -5) == "_trap" then annotation.trap_launcher = class conditionCode = "CheckBoxOn(opt_trap_launcher)" elseif class == "MAGE" and action == "arcane_brilliance" then -- Only cast Arcane Brilliance if not already raid-buffed. conditionCode = "BuffExpires(critical_strike_buff any=1) or BuffExpires(spell_power_multiplier_buff any=1)" elseif class == "MAGE" and action == "arcane_missiles" then -- Arcane Missiles can only be fired if the Arcane Missiles! buff is present. local buffName = "arcane_missiles_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif class == "MAGE" and action == "counterspell" then bodyCode = "InterruptActions()" annotation[action] = class isSpellAction = false elseif class == "MAGE" and strfind(action, "pet_") then conditionCode = "pet.Present()" elseif class == "MAGE" and action == "start_pyro_chain" then bodyCode = "SetState(pyro_chain 1)" isSpellAction = false elseif class == "MAGE" and action == "stop_pyro_chain" then bodyCode = "SetState(pyro_chain 0)" isSpellAction = false elseif class == "MAGE" and action == "time_warp" then -- Only suggest Time Warp if it will have an effect. conditionCode = "CheckBoxOn(opt_time_warp) and DebuffExpires(burst_haste_debuff any=1)" annotation[action] = class elseif class == "MAGE" and action == "water_elemental" then -- Only suggest summoning the Water Elemental if the pet is not already summoned. conditionCode = "not pet.Present()" elseif class == "MONK" and action == "chi_burst" then -- Only suggest Chi Burst if it's toggled on. conditionCode = "CheckBoxOn(opt_chi_burst)" annotation[action] = class elseif class == "MONK" and action == "chi_sphere" then -- skip isSpellAction = false elseif class == "MONK" and action == "gift_of_the_ox" then -- skip isSpellAction = false elseif class == "MONK" and action == "touch_of_death" then -- Touch of Death can only be used if the Death Note buff is present on the player. local buffName = "death_note_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif class == "PALADIN" and action == "blessing_of_kings" then -- Only cast Blessing of Kings if it won't overwrite the player's own Blessing of Might. conditionCode = "BuffExpires(mastery_buff)" elseif class == "PALADIN" and action == "rebuke" then bodyCode = "InterruptActions()" annotation[action] = class isSpellAction = false elseif class == "PRIEST" and action == "insanity" then local buffName = "shadow_word_insanity_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif class == "ROGUE" and action == "apply_poison" then if modifier.lethal then local name = Unparse(modifier.lethal) action = name .. "_poison" -- Always have at least 20 minutes of a lethal poison applied when out of combat. local buffName = "lethal_poison_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffRemaining(%s) < 1200", buffName) else isSpellAction = false end elseif class == "ROGUE" and action == "blade_flurry" then annotation.blade_flurry = class conditionCode = "CheckBoxOn(opt_blade_flurry)" elseif class == "ROGUE" and action == "honor_among_thieves" then if modifier.cooldown then local cooldown = Unparse(modifier.cooldown) local buffName = action .. "_cooldown_buff" annotation[buffName] = cooldown annotation[action] = class end isSpellAction = false elseif class == "ROGUE" and action == "kick" then bodyCode = "InterruptActions()" annotation[action] = class isSpellAction = false elseif class == "ROGUE" and specialization == "combat" and action == "slice_and_dice" then -- Don't suggest Slice and Dice if a more powerful buff is already in effect. local buffName = "slice_and_dice_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffRemaining(%s) < BaseDuration(%s)", buffName, buffName) elseif class == "ROGUE" and action == "stealth" then -- Don't Stealth if already stealthed. conditionCode = "BuffExpires(stealthed_buff any=1)" elseif class == "SHAMAN" and strsub(action, 1, 11) == "ascendance_" then -- Ascendance doesn't go on cooldown until after the buff expires, so don't -- suggest Ascendance if already in Ascendance. local buffName = action .. "_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffExpires(%s)", buffName) elseif class == "SHAMAN" and action == "bloodlust" then bodyCode = "Bloodlust()" annotation[action] = class isSpellAction = false elseif class == "SHAMAN" and action == "lava_beam" then -- Lava Beam is the elemental Ascendance version of Chain Lightning. local buffName = "ascendance_caster_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif class == "SHAMAN" and action == "magma_totem" then -- Only suggest Magma Totem if within melee range of the target. local spellName = "primal_strike" AddSymbol(annotation, spellName) conditionCode = format("target.InRange(%s)", spellName) elseif class == "SHAMAN" and action == "windstrike" then -- Windstrike is the enhancement Ascendance version of Stormstrike. local buffName = "ascendance_melee_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif class == "SHAMAN" and action == "wind_shear" then bodyCode = "InterruptActions()" annotation[action] = class isSpellAction = false elseif class == "WARLOCK" and action == "cancel_metamorphosis" then local spellName = "metamorphosis" local buffName = "metamorphosis_buff" AddSymbol(annotation, spellName) AddSymbol(annotation, buffName) bodyCode = format("Spell(%s text=cancel)", spellName) conditionCode = format("BuffPresent(%s)", buffName) isSpellAction = false elseif class == "WARLOCK" and action == "felguard_felstorm" then conditionCode = "pet.Present() and pet.CreatureFamily(Felguard)" elseif class == "WARLOCK" and action == "grimoire_of_sacrifice" then -- Grimoire of Sacrifice requires a pet to already be summoned. conditionCode = "pet.Present()" elseif class == "WARLOCK" and action == "havoc" then -- Havoc requires another target. conditionCode = "Enemies() > 1" elseif class == "WARLOCK" and action == "service_pet" then if annotation.pet then local spellName = "grimoire_" .. annotation.pet AddSymbol(annotation, spellName) bodyCode = format("Spell(%s)", spellName) else bodyCode = "Texture(spell_nature_removecurse help=ServicePet)" end isSpellAction = false elseif class == "WARLOCK" and action == "summon_pet" then if annotation.pet then local spellName = "summon_" .. annotation.pet AddSymbol(annotation, spellName) bodyCode = format("Spell(%s)", spellName) else bodyCode = "Texture(spell_nature_removecurse help=L(summon_pet))" end -- Only summon a pet if one is not already summoned. conditionCode = "not pet.Present()" isSpellAction = false elseif class == "WARLOCK" and action == "wrathguard_wrathstorm" then conditionCode = "pet.Present() and pet.CreatureFamily(Wrathguard)" elseif class == "WARRIOR" and action == "charge" then conditionCode = "target.InRange(charge)" elseif class == "WARRIOR" and action == "enraged_regeneration" then -- Only suggest Enraged Regeneration at below 80% health. conditionCode = "HealthPercent() < 80" elseif class == "WARRIOR" and strsub(action, 1, 7) == "execute" then if modifier.target then local target = Unparse(modifier.target) local target = tonumber(target) if target then -- Skip "execute" actions if they are not on the main target. isSpellAction = false end end elseif class == "WARRIOR" and action == "heroic_leap" then -- Use Charge as a range-finder for Heroic Leap. local spellName = "charge" AddSymbol(annotation, spellName) conditionCode = format("target.InRange(%s)", spellName) elseif class == "WARRIOR" and action == "victory_rush" then -- Victory Rush requires the Victorious buff to be on the player. local buffName = "victorious_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif class == "WARRIOR" and action == "raging_blow" then -- Raging Blow can only be used if the Raging Blow buff is present on the player. local buffName = "raging_blow_buff" AddSymbol(annotation, buffName) conditionCode = format("BuffPresent(%s)", buffName) elseif action == "call_action_list" or action == "run_action_list" or action == "swap_action_list" then if modifier.name then local name = Unparse(modifier.name) bodyCode = OvaleFunctionName(name, annotation) .. "()" end isSpellAction = false elseif action == "cancel_buff" then if modifier.name then local spellName = Unparse(modifier.name) local buffName = spellName .. "_buff" AddSymbol(annotation, spellName) AddSymbol(annotation, buffName) bodyCode = format("Texture(%s text=cancel)", spellName) conditionCode = format("BuffPresent(%s)", buffName) isSpellAction = false end elseif action == "mana_potion" then bodyCode = "UsePotionMana()" annotation.use_potion_mana = class isSpellAction = false elseif action == "pool_resource" then -- Create a special "simc_pool_resource" AST node that will be transformed in -- a later step into something OvaleAST can understand and unparse. bodyNode = OvaleAST:NewNode(nodeList) bodyNode.type = "simc_pool_resource" bodyNode.for_next = (modifier.for_next ~= nil) if modifier.extra_amount then bodyNode.extra_amount = tonumber(Unparse(modifier.extra_amount)) end isSpellAction = false elseif action == "potion" then if modifier.name then local name = Unparse(modifier.name) local stat = POTION_STAT[name] if stat == "agility" then bodyCode = "UsePotionAgility()" annotation.use_potion_agility = class elseif stat == "armor" then bodyCode = "UsePotionArmor()" annotation.use_potion_armor = class elseif stat == "intellect" then bodyCode = "UsePotionIntellect()" annotation.use_potion_intellect = class elseif stat == "strength" then bodyCode = "UsePotionStrength()" annotation.use_potion_strength = class end isSpellAction = false end elseif action == "stance" then if modifier.choose then local name = Unparse(modifier.choose) if class == "MONK" then action = "stance_of_the_" .. name elseif class == "WARRIOR" then action = name .. "_stance" else action = name end else isSpellAction = false end elseif action == "summon_pet" then bodyCode = "SummonPet()" annotation[action] = class isSpellAction = false elseif action == "use_item" then if true then --[[ When "use_item" is encountered in an action list, it is usually meant to use all of the equipped items at the same time, so all hand tinkers and on-use trinkets. Assume a "UseItemActions()" function is available that does this. --]] bodyCode = "UseItemActions()" annotation[action] = true else if modifier.name == "name" then local name = Unparse(modifier.name) if strmatch(name, "gauntlets") or strmatch(name, "gloves") or strmatch(name, "grips") or strmatch(name, "handguards") then bodyCode = "Item(HandsSlot usable=1)" end elseif modifier.slot then local slot = Unparse(modifier.slot) if slot == "hands" then bodyCode = "Item(HandsSlot usable=1)" elseif strmatch(slot, "trinket") then bodyCode = "{ Item(Trinket0Slot usable=1) Item(Trinket1Slot usable=1) }" expressionType = "group" end end end isSpellAction = false elseif action == "wait" then --[[ Create a special "wait" AST node that will be transformed in a later step into something OvaleAST can understand and unparse. --]] bodyNode = OvaleAST:NewNode(nodeList) bodyNode.type = "simc_wait" if modifier.sec then -- "wait,sec=expr" means to halt the processing of the action list if "expr > 0". local expressionNode = Emit(modifier.sec, nodeList, annotation, action) local code = OvaleAST:Unparse(expressionNode) conditionCode = code .. " > 0" end isSpellAction = false end if isSpellAction then AddSymbol(annotation, action) if modifier.target then local actionTarget = Unparse(modifier.target) if actionTarget == "2" then actionTarget = "other" end if actionTarget ~= "1" then bodyCode = format("Spell(%s text=%s)", action, actionTarget) end end bodyCode = bodyCode or "Spell(" .. action .. ")" end annotation.astAnnotation = annotation.astAnnotation or {} if not bodyNode and bodyCode then bodyNode = OvaleAST:ParseCode(expressionType, bodyCode, nodeList, annotation.astAnnotation) end if not conditionNode and conditionCode then conditionNode = OvaleAST:ParseCode(expressionType, conditionCode, nodeList, annotation.astAnnotation) end -- Conditions from modifiers, if present. if bodyNode then -- Put the extra conditions on the right-most side. local extraConditionNode = conditionNode conditionNode = nil -- Concatenate all of the conditions from modifiers using the "and" operator. for modifier, expressionNode in pairs(parseNode.child) do local rhsNode = EmitModifier(modifier, expressionNode, nodeList, annotation, action) if rhsNode then if not conditionNode then conditionNode = rhsNode else local lhsNode = conditionNode conditionNode = OvaleAST:NewNode(nodeList, true) conditionNode.type = "logical" conditionNode.expressionType = "binary" conditionNode.operator = "and" conditionNode.child[1] = lhsNode conditionNode.child[2] = rhsNode end end end if extraConditionNode then if conditionNode then local lhsNode = conditionNode local rhsNode = extraConditionNode conditionNode = OvaleAST:NewNode(nodeList, true) conditionNode.type = "logical" conditionNode.expressionType = "binary" conditionNode.operator = "and" conditionNode.child[1] = lhsNode conditionNode.child[2] = rhsNode else conditionNode = extraConditionNode end end -- Create "if" node. if conditionNode then node = OvaleAST:NewNode(nodeList, true) node.type = "if" node.child[1] = conditionNode node.child[2] = bodyNode if bodyNode.type == "simc_pool_resource" then node.simc_pool_resource = true elseif bodyNode.type == "simc_wait" then node.simc_wait = true end else node = bodyNode end end end return node end EmitActionList = function(parseNode, nodeList, annotation) -- Function body is a group of statements. local groupNode = OvaleAST:NewNode(nodeList, true) groupNode.type = "group" local child = groupNode.child local poolResourceNode local emit = true for _, actionNode in ipairs(parseNode.child) do -- Add a comment containing the action to be translated. local commentNode = OvaleAST:NewNode(nodeList) commentNode.type = "comment" commentNode.comment = actionNode.action child[#child + 1] = commentNode if emit then -- Add the translated statement. local statementNode = EmitAction(actionNode, nodeList, annotation) if statementNode then if statementNode.type == "simc_pool_resource" then local powerType = OvalePower.POOLED_RESOURCE[annotation.class] if powerType then if statementNode.for_next then poolResourceNode = statementNode poolResourceNode.powerType = powerType else -- This is a bare "pool_resource" statement, which means pool -- continually and skip the rest of the action list. emit = false end end 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 if statementNode.child then -- This is a conditional statement, so set the body to the "then" clause. bodyNode = statementNode.child[2] else bodyNode = statementNode 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. local powerCondition if extra_amount then powerCondition = format("TimeTo%s(%d)", powerType, extra_amount) else powerCondition = format("TimeTo%sFor(%s)", powerType, name) end local code = format("SpellUsable(%s) and SpellCooldown(%s) < %s", name, name, powerCondition) local conditionNode = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) if statementNode.child then local rhsNode = conditionNode conditionNode = OvaleAST:NewNode(nodeList, true) conditionNode.type = "logical" conditionNode.expressionType = "binary" conditionNode.operator = "and" conditionNode.child[1] = statementNode.child[1] conditionNode.child[2] = rhsNode end -- Create node to hold the rest of the statements. local restNode = OvaleAST:NewNode(nodeList, true) child[#child + 1] = restNode if statementNode.type == "unless" then restNode.type = "if" else restNode.type = "unless" end restNode.child[1] = conditionNode restNode.child[2] = OvaleAST:NewNode(nodeList, true) restNode.child[2].type = "group" child = restNode.child[2].child end poolResourceNode = nil elseif statementNode.type == "simc_wait" then -- This is a bare "wait" statement, which we don't know how to process, so -- skip it. elseif statementNode.simc_wait then -- Create an "unless" node with the remaining statements as the body. local restNode = OvaleAST:NewNode(nodeList, true) child[#child + 1] = restNode restNode.type = "unless" restNode.child[1] = statementNode.child[1] restNode.child[2] = OvaleAST:NewNode(nodeList, true) restNode.child[2].type = "group" child = restNode.child[2].child else child[#child + 1] = statementNode if statementNode.simc_pool_resource then -- Flip the "if/unless" statement and change the body into a group node -- containing all of the rest of the statements. if statementNode.type == "if" then statementNode.type = "unless" elseif statementNode.type == "unless" then statementNode.type = "if" end statementNode.child[2] = OvaleAST:NewNode(nodeList, true) statementNode.child[2].type = "group" child = statementNode.child[2].child end end end end end local node = OvaleAST:NewNode(nodeList, true) node.type = "add_function" node.name = OvaleFunctionName(parseNode.name, annotation) node.child[1] = groupNode return node end EmitExpression = function(parseNode, nodeList, annotation, action) local node local msg if parseNode.expressionType == "unary" then local opInfo = UNARY_OPERATOR[parseNode.operator] if opInfo then local operator if parseNode.operator == "!" then operator = "not" elseif parseNode.operator == "-" then operator = parseNode.operator end if operator then local rhsNode = Emit(parseNode.child[1], nodeList, annotation, action) if rhsNode then if operator == "-" and rhsNode.type == "value" then rhsNode.value = -1 * rhsNode.value else node = OvaleAST:NewNode(nodeList, true) node.type = opInfo[1] node.expressionType = "unary" node.operator = operator node.precedence = opInfo[2] node.child[1] = rhsNode end end end end elseif parseNode.expressionType == "binary" then local opInfo = BINARY_OPERATOR[parseNode.operator] if opInfo then local operator if parseNode.operator == "&" then operator = "and" elseif parseNode.operator == "^" then operator = "xor" elseif parseNode.operator == "|" then operator = "or" elseif parseNode.operator == "=" then operator = "==" elseif parseNode.operator == "%" then operator = "/" elseif parseNode.type == "compare" or parseNode.type == "arithmetic" then operator = parseNode.operator end if parseNode.type == "compare" and parseNode.child[1].rune then --[[ Special handling for rune comparisons. This ONLY handles rune expressions of the form "<rune><operator><number>". These are translated to equivalent "Rune(<rune>) <operator> <number>" expressions, but with some munging of the numbers since Rune() returns a fractional number of runes. --]] local lhsNode = parseNode.child[1] local rhsNode = parseNode.child[2] local runeType = lhsNode.rune local number = (rhsNode.type == "number") and tonumber(Unparse(rhsNode)) or nil if rhsNode.type == "number" then number = tonumber(Unparse(rhsNode)) end if runeType and number then local code local op = parseNode.operator if op == ">" then code = format("Rune(%s) >= %d", runeType, number + 1) elseif op == ">=" then code = format("Rune(%s) >= %d", runeType, number) elseif op == "=" then if runeType ~= "death" and number == 2 then -- We can never have more than 2 non-death runes of the same type. code = format("Rune(%s) >= %d", runeType, number) else code = format("Rune(%s) >= %d and Rune(%s) < %d", runeType, number, runeType, number + 1) end elseif op == "<=" then code = format("Rune(%s) < %d", runeType, number + 1) elseif op == "<" then code = format("Rune(%s) < %d", runeType, number) end if not node and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end end elseif (parseNode.operator == "=" or parseNode.operator == "!=") and (parseNode.child[1].name == "target" or parseNode.child[1].name == "current_target") then --[[ Special handling for "target=X" or "current_target=X" expressions. --]] local rhsNode = parseNode.child[2] local name = rhsNode.name local code if parseNode.operator == "=" then code = format("target.Name(%s)", name) else -- if parseNode.operator == "!=" then code = format("not target.Name(%s)", name) end AddSymbol(annotation, name) annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) elseif (parseNode.operator == "=" or parseNode.operator == "!=") and parseNode.child[1].name == "last_judgment_target" then --[[ Special handling for "last_judgment_target=X" expressions. TODO: Track the target of the previous cast of a spell. --]] local code if parseNode.operator == "=" then code = "False(last_judgement_target)" else -- if parseNode.operator == "!=" then code = "True(last_judgement_target)" end annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) elseif operator then local lhsNode = Emit(parseNode.child[1], nodeList, annotation, action) local rhsNode = Emit(parseNode.child[2], nodeList, annotation, action) if lhsNode and rhsNode then node = OvaleAST:NewNode(nodeList, true) node.type = opInfo[1] node.expressionType = "binary" node.operator = operator node.precedence = opInfo[2] node.child[1] = lhsNode node.child[2] = rhsNode elseif lhsNode then msg = Ovale:MakeString("Warning: %s operator '%s' right failed.", parseNode.type, parseNode.operator) elseif rhsNode then msg = Ovale:MakeString("Warning: %s operator '%s' left failed.", parseNode.type, parseNode.operator) else msg = Ovale:MakeString("Warning: %s operator '%s' left and right failed.", parseNode.type, parseNode.operator) end end end end if node then if parseNode.left and parseNode.right then node.left = "{" node.right = "}" end else msg = msg or Ovale:MakeString("Warning: Operator '%s' is not implemented.", parseNode.operator) OvaleSimulationCraft:Print(msg) node = OvaleAST:NewNode(nodeList) node.type = "string" node.value = "FIXME_" .. parseNode.operator end return node end EmitFunction = function(parseNode, nodeList, annotation, action) local node if parseNode.name == "ceil" or parseNode.name == "floor" then -- Pretend ceil and floor have no effect. node = EmitExpression(parseNode.child[1], nodeList, annotation, action) else OvaleSimulationCraft:Print("Warning: Function '%s' is not implemented.", parseNode.name) node = OvaleAST:NewNode(nodeList) node.type = "variable" node.name = "FIXME_" .. parseNode.name end return node end EmitModifier = function(modifier, parseNode, nodeList, annotation, action) local node, code local class = annotation.class local specialization = annotation.specialization if modifier == "if" then node = Emit(parseNode, nodeList, annotation, action) elseif modifier == "line_cd" then if not SPECIAL_ACTION[action] then AddSymbol(annotation, action) local expressionCode = OvaleAST:Unparse(Emit(parseNode, nodeList, annotation, action)) code = format("TimeSincePreviousSpell(%s) > %s", action, expressionCode) end elseif modifier == "max_cycle_targets" then local debuffName = action .. "_debuff" AddSymbol(annotation, debuffName) local expressionCode = OvaleAST:Unparse(Emit(parseNode, nodeList, annotation, action)) code = format("DebuffCountOnAny(%s) <= Enemies() and DebuffCountOnAny(%s) <= %s", debuffName, debuffName, expressionCode) elseif modifier == "max_energy" then local value = tonumber(Unparse(parseNode)) if value == 1 then -- SimulationCraft's max_energy is the maximum energy cost of the action if used. code = format("Energy() >= EnergyCost(%s max=1)", action) end elseif modifier == "moving" then local value = tonumber(Unparse(parseNode)) if value == 1 then code = "Speed() > 0" end elseif modifier == "sync" then local name = Unparse(parseNode) name = Disambiguate(name, class, specialization) AddSymbol(annotation, name) code = format("not SpellCooldown(%s) > 0", name) end if not node and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end return node end EmitNumber = function(parseNode, nodeList, annotation, action) local node = OvaleAST:NewNode(nodeList) node.type = "value" node.value = parseNode.value node.origin = 0 node.rate = 0 return node end EmitOperand = function(parseNode, nodeList, annotation, action) local ok = false local node local operand = parseNode.name local token = strmatch(operand, OPERAND_TOKEN_PATTERN) -- peek local target if token == "target" then target = token operand = strsub(operand, strlen(target) + 2) -- consume token = strmatch(operand, OPERAND_TOKEN_PATTERN) -- peek end ok, node = EmitOperandRune(operand, parseNode, nodeList, annotation, action) if not ok then ok, node = EmitOperandSpecial(operand, parseNode, nodeList, annotation, action, target) end if not ok then ok, node = EmitOperandRaidEvent(operand, parseNode, nodeList, annotation, action) end if not ok then ok, node = EmitOperandAction(operand, parseNode, nodeList, annotation, action, target) end if not ok then ok, node = EmitOperandCharacter(operand, parseNode, nodeList, annotation, action, target) end if not ok then if token == "active_dot" then target = target or "target" ok, node = EmitOperandActiveDot(operand, parseNode, nodeList, annotation, action, target) elseif token == "aura" then ok, node = EmitOperandBuff(operand, parseNode, nodeList, annotation, action, target) elseif token == "buff" then ok, node = EmitOperandBuff(operand, parseNode, nodeList, annotation, action, target) elseif token == "cooldown" then ok, node = EmitOperandCooldown(operand, parseNode, nodeList, annotation, action) elseif token == "debuff" then target = target or "target" ok, node = EmitOperandBuff(operand, parseNode, nodeList, annotation, action, target) elseif token == "disease" then target = target or "target" ok, node = EmitOperandDisease(operand, parseNode, nodeList, annotation, action, target) elseif token == "dot" then target = target or "target" ok, node = EmitOperandDot(operand, parseNode, nodeList, annotation, action, target) elseif token == "glyph" then ok, node = EmitOperandGlyph(operand, parseNode, nodeList, annotation, action) elseif token == "pet" then ok, node = EmitOperandPet(operand, parseNode, nodeList, annotation, action) elseif token == "prev" or token == "prev_gcd" then ok, node = EmitOperandPreviousSpell(operand, parseNode, nodeList, annotation, action) elseif token == "seal" then ok, node = EmitOperandSeal(operand, parseNode, nodeList, annotation, action) elseif token == "set_bonus" then ok, node = EmitOperandSetBonus(operand, parseNode, nodeList, annotation, action) elseif token == "talent" then ok, node = EmitOperandTalent(operand, parseNode, nodeList, annotation, action) elseif token == "totem" then ok, node = EmitOperandTotem(operand, parseNode, nodeList, annotation, action) elseif token == "trinket" then ok, node = EmitOperandTrinket(operand, parseNode, nodeList, annotation, action) end end if not ok then node = OvaleAST:NewNode(nodeList) node.type = "variable" node.name = "FIXME_" .. parseNode.name end return node end EmitOperandAction = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local name local property if strsub(operand, 1, 7) == "action." then local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() name = tokenIterator() property = tokenIterator() else name = action property = operand end local class, specialization = annotation.class, annotation.specialization name = Disambiguate(name, class, specialization) target = target and (target .. ".") or "" local buffName = name .. "_debuff" buffName = Disambiguate(buffName, class, specialization) local prefix = strfind(buffName, "_buff$") and "Buff" or "Debuff" local buffTarget = (prefix == "Debuff") and "target." or target local talentName = name .. "_talent" talentName = Disambiguate(talentName, class, specialization) local symbol = name local code if property == "active" then if IsTotem(name) then code = format("TotemPresent(%s)", name) else code = format("%s%sPresent(%s)", target, prefix, buffName) symbol = buffName end elseif property == "cast_regen" then code = format("FocusCastingRegen(%s)", name) elseif property == "cast_time" then code = format("CastTime(%s)", name) elseif property == "charges" then code = format("Charges(%s)", name) elseif property == "charges_fractional" then code = format("Charges(%s count=0)", name) elseif property == "cooldown" then code = format("SpellCooldown(%s)", name) elseif property == "cooldown_react" then code = format("not SpellCooldown(%s) > 0", name) elseif property == "duration" then code = format("BaseDuration(%s)", buffName) symbol = buffName elseif property == "enabled" then if parseNode.asType == "boolean" then code = format("Talent(%s)", talentName) else code = format("TalentPoints(%s)", talentName) end symbol = talentName elseif property == "execute_time" then code = format("ExecuteTime(%s)", name) elseif property == "gcd" then code = "GCD()" elseif property == "in_flight" or property == "in_flight_to_target" then code = format("InFlightToTarget(%s)", name) elseif property == "miss_react" then -- "miss_react" has no meaning in Ovale. code = "True(miss_react)" elseif property == "persistent_multiplier" then code = format("PersistentMultiplier(%s)", buffName) elseif property == "recharge_time" then code = format("SpellChargeCooldown(%s)", name) elseif property == "remains" then if IsTotem(name) then code = format("TotemRemaining(%s)", name) else code = format("%s%sRemaining(%s)", buffTarget, prefix, buffName) symbol = buffName end elseif property == "shard_react" then -- XXX code = "SoulShards() >= 1" elseif property == "tick_time" then code = format("%sTickTime(%s)", buffTarget, buffName) symbol = buffName elseif property == "ticking" then code = format("%s%sPresent(%s)", buffTarget, prefix, buffName) symbol = buffName elseif property == "ticks_remain" then code = format("%sTicksRemaining(%s)", buffTarget, buffName) symbol = buffName elseif property == "travel_time" then -- Translate to the maximum travel time since we can't gauge the distance dynamically. code = format("MaxTravelTime(%s)", name) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) if symbol then AddSymbol(annotation, symbol) end end return ok, node end EmitOperandActiveDot = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "active_dot" then local name = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local dotName = name .. "_debuff" dotName = Disambiguate(dotName, annotation.class, annotation.specialization) local prefix = strfind(dotName, "_buff$") and "Buff" or "Debuff" target = target and (target .. ".") or "" local code = format("%sCountOnAny(%s)", prefix, dotName) if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, dotName) end else ok = false end return ok, node end EmitOperandBuff = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "aura" or token == "buff" or token == "debuff" then local name = tokenIterator() local property = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local buffName = (token == "debuff") and name .. "_debuff" or name .. "_buff" buffName = Disambiguate(buffName, annotation.class, annotation.specialization) local prefix = strfind(buffName, "_buff$") and "Buff" or "Debuff" local any = OvaleData.buffSpellList[buffName] and " any=1" or "" target = target and (target .. ".") or "" -- Unholy death knight's Dark Transformation applies the buff to the ghoul/pet. if buffName == "dark_transformation_buff" then if target == "" then target = "pet." end any = " any=1" end -- Assume that the "potion" action has already been seen. if buffName == "potion_buff" then if annotation.use_potion_agility then buffName = "potion_agility_buff" elseif annotation.use_potion_armor then buffName = "potion_armor_buff" elseif annotation.use_potion_intellect then buffName = "potion_intellect_buff" elseif annotation.use_potion_strength then buffName = "potion_strength_buff" end end local code if property == "cooldown_remains" then -- Assume that the spell and the buff have the same name. code = format("SpellCooldown(%s)", name) elseif property == "down" then code = format("%s%sExpires(%s%s)", target, prefix, buffName, any) elseif property == "duration" then code = format("BaseDuration(%s)", buffName) elseif property == "max_stack" then code = format("SpellData(%s max_stacks)", buffName) elseif property == "react" or property == "stack" then if parseNode.asType == "boolean" then code = format("%s%sPresent(%s%s)", target, prefix, buffName, any) else code = format("%s%sStacks(%s%s)", target, prefix, buffName, any) end elseif property == "remains" then if parseNode.asType == "boolean" then code = format("%s%sPresent(%s%s)", target, prefix, buffName, any) else code = format("%s%sRemaining(%s%s)", target, prefix, buffName, any) end elseif property == "up" then code = format("%s%sPresent(%s%s)", target, prefix, buffName, any) elseif property == "value" then code = format("%s%sAmount(%s%s)", target, prefix, buffName, any) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, buffName) end else ok = false end return ok, node end do local CHARACTER_PROPERTY = { ["active_enemies"] = "Enemies()", ["blood.frac"] = "Rune(blood)", ["chi"] = "Chi()", ["chi.max"] = "MaxChi()", ["combo_points"] = "ComboPoints()", ["demonic_fury"] = "DemonicFury()", ["eclipse_change"] = "TimeToEclipse()", -- XXX ["eclipse_energy"] = "EclipseEnergy()", -- XXX ["energy"] = "Energy()", ["energy.max"] = "MaxEnergy()", ["energy.regen"] = "EnergyRegenRate()", ["energy.time_to_max"] = "TimeToMaxEnergy()", ["focus"] = "Focus()", ["focus.deficit"] = "FocusDeficit()", ["focus.regen"] = "FocusRegenRate()", ["focus.time_to_max"] = "TimeToMaxFocus()", ["frost.frac"] = "Rune(frost)", ["health"] = "Health()", ["health.deficit"] = "HealthMissing()", ["health.max"] = "MaxHealth()", ["health.pct"] = "HealthPercent()", ["health.percent"] = "HealthPercent()", ["holy_power"] = "HolyPower()", ["level"] = "Level()", ["lunar_max"] = "TimeToEclipse(lunar)", -- XXX ["mana"] = "Mana()", ["mana.deficit"] = "ManaDeficit()", ["mana.max"] = "MaxMana()", ["mana.pct"] = "ManaPercent()", ["rage"] = "Rage()", ["rage.max"] = "MaxRage()", ["runic_power"] = "RunicPower()", ["shadow_orb"] = "ShadowOrbs()", ["soul_shard"] = "SoulShards()", ["stat.multistrike_pct"]= "MultistrikeChance()", ["time"] = "TimeInCombat()", ["time_to_die"] = "TimeToDie()", ["unholy.frac"] = "Rune(unholy)", } EmitOperandCharacter = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local class = annotation.class local specialization = annotation.specialization target = target and (target .. ".") or "" local code if CHARACTER_PROPERTY[operand] then code = target .. CHARACTER_PROPERTY[operand] elseif class == "MAGE" and operand == "incanters_flow_dir" then local name = "incanters_flow_buff" code = format("BuffDirection(%s)", name) AddSymbol(annotation, name) elseif class == "PALADIN" and operand == "time_to_hpg" then if specialization == "holy" then code = "HolyTimeToHPG()" annotation.time_to_hpg_heal = class elseif specialization == "protection" then code = "ProtectionTimeToHPG()" annotation.time_to_hpg_tank = class elseif specialization == "retribution" then code = "RetributionTimeToHPG()" annotation.time_to_hpg_melee = class end elseif class == "ROGUE" and operand == "anticipation_charges" then local name = "anticipation_buff" code = format("BuffStacks(%s)", name) AddSymbol(annotation, name) elseif class == "WARLOCK" and operand == "burning_ember" then code = format("%sBurningEmbers() / 10", target) elseif strfind(operand, "^incoming_damage_") then local seconds, measure = strmatch(operand, "^incoming_damage_([%d]+)(m?s?)$") seconds = tonumber(seconds) if measure == "ms" then seconds = seconds / 1000 end if parseNode.asType == "boolean" then code = format("IncomingDamage(%f) > 0", seconds) else code = format("IncomingDamage(%f)", seconds) end elseif operand == "mastery_value" then code = format("%sMasteryEffect() / 100", target) elseif operand == "position_front" then -- "position_front" should always be false in Ovale because we assume the -- player can get into the optimal attack position at all times. code = "False(position_front)" elseif strsub(operand, 1, 5) == "role." then local role = strmatch(operand, "^role%.([%w_]+)") if role and role == annotation.role then code = format("True(role_%s)", role) else code = format("False(role_%s)", role) end elseif operand == "spell_haste" or operand == "stat.spell_haste" then -- "spell_haste" is the player's spell factor, e.g., -- 25% haste corresponds to a "spell_haste" value of 1/(1 + 0.25) = 0.8. code = "100 / { 100 + SpellHaste() }" else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end return ok, node end end EmitOperandCooldown = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "cooldown" then local name = tokenIterator() local property = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local prefix = "Spell" -- Assume that the "potion" action has already been seen. if name == "potion" then prefix = "Item" if annotation.use_potion_agility then name = "draenic_agility_potion" elseif annotation.use_potion_armor then name = "draenic_armor_potion" elseif annotation.use_potion_intellect then name = "draenic_intellect_potion" elseif annotation.use_potion_strength then name = "draenic_strength_potion" end end local code if property == "duration" then code = format("%sCooldownDuration(%s)", prefix, name) elseif property == "remains" then if parseNode.asType == "boolean" then code = format("%sCooldown(%s) > 0", prefix, name) else code = format("%sCooldown(%s)", prefix, name) end elseif property == "up" then code = format("not %sCooldown(%s) > 0", prefix, name) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, name) end else ok = false end return ok, node end EmitOperandDisease = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "disease" then local property = tokenIterator() target = target and (target .. ".") or "" local code if property == "max_ticking" then code = target .. "DiseasesAnyTicking()" elseif property == "min_remains" then code = target .. "DiseasesRemaining()" elseif property == "min_ticking" then code = target .. "DiseasesTicking()" elseif property == "ticking" then code = target .. "DiseasesAnyTicking()" else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end else ok = false end return ok, node end EmitOperandDot = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "dot" then local name = tokenIterator() local property = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local dotName = name .. "_debuff" dotName = Disambiguate(dotName, annotation.class, annotation.specialization) local prefix = strfind(dotName, "_buff$") and "Buff" or "Debuff" target = target and (target .. ".") or "" local code if property == "duration" then code = format("%s%sDuration(%s)", target, prefix, dotName) elseif property == "pmultiplier" then code = format("%s%sPersistentMultiplier(%s)", target, prefix, dotName) elseif property == "remains" then code = format("%s%sRemaining(%s)", target, prefix, dotName) elseif property == "stack" then code = format("%s%sStacks(%s)", target, prefix, dotName) elseif property == "ticking" then code = format("%s%sPresent(%s)", target, prefix, dotName) elseif property == "ticks_remain" then code = format("%sTicksRemaining(%s)", target, dotName) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, dotName) end else ok = false end return ok, node end EmitOperandGlyph = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "glyph" then local name = tokenIterator() local property = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local glyphName = "glyph_of_" .. name glyphName = Disambiguate(glyphName, annotation.class, annotation.specialization) local code if property == "disabled" then code = format("not Glyph(%s)", glyphName) elseif property == "enabled" then code = format("Glyph(%s)", glyphName) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, glyphName) end else ok = false end return ok, node end EmitOperandPet = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "pet" then local name = tokenIterator() local property = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local isTotem = IsTotem(name) local code if isTotem and property == "active" then code = format("TotemPresent(%s)", name) elseif isTotem and property == "remains" then code = format("TotemRemaining(%s)", name) else -- Strip the "pet.<name>." from the operand and re-evaluate. local pattern = format("^pet%%.%s%%.([%%w_.]+)", name) local petOperand = strmatch(operand, pattern) local target = "pet" if petOperand then ok, node = EmitOperandSpecial(petOperand, parseNode, nodeList, annotation, action, target) if not ok then ok, node = EmitOperandAction(petOperand, parseNode, nodeList, annotation, action, target) end if not ok then ok, node = EmitOperandCharacter(petOperand, parseNode, nodeList, annotation, action, target) end if not ok then if property == "buff" then ok, node = EmitOperandBuff(petOperand, parseNode, nodeList, annotation, action, target) elseif property == "cooldown" then ok, node = EmitOperandCooldown(petOperand, parseNode, nodeList, annotation, action) elseif property == "debuff" then ok, node = EmitOperandBuff(petOperand, parseNode, nodeList, annotation, action, target) else ok = false end end else ok = false end end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, name) end else ok = false end return ok, node end EmitOperandPreviousSpell = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "prev" or token == "prev_gcd" then local name = tokenIterator() name = Disambiguate(name, annotation.class, annotation.specialization) local code if token == "prev" then code = format("PreviousSpell(%s)", name) else -- if token == "prev_gcd" then code = format("PreviousGCDSpell(%s)", name) end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, name) end else ok = false end return ok, node end EmitOperandRaidEvent = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local name local property if strsub(operand, 1, 11) == "raid_event." then local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() name = tokenIterator() property = tokenIterator() else local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) name = tokenIterator() property = tokenIterator() end local code if name == "movement" then --[[ The "movement" raid event simulates needing to move during the encounter. We always assume the fight is Patchwerk-style, meaning no movement is necessary. --]] if property == "cooldown" or property == "in" then -- Pretend the next "movement" raid event is ten minutes from now. code = "600" elseif property == "distance" then code = "0" elseif property == "exists" then code = "False(raid_event_movement_exists)" elseif property == "remains" then code = "0" else ok = false end elseif name == "adds" then --[[ The "adds" raid event simulates waves of adds on regular intervals. This is separate from the dynamic number of active enemies. We always assume that there are no add waves. --]] if property == "cooldown" then -- Pretend the next "adds" raid event is ten minutes from now. code = "600" elseif property == "count" then code = "0" elseif property == "exists" then code = "False(raid_event_adds_exists)" else ok = false end else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end return ok, node end EmitOperandRune = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local code if parseNode.rune then if parseNode.asType == "boolean" then code = format("Rune(%s) >= 1", parseNode.rune) else code = format("RuneCount(%s)", parseNode.rune) end else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end return ok, node end EmitOperandSetBonus = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local setBonus = strmatch(operand, "^set_bonus%.(.*)$") local code if setBonus then local tokenIterator = gmatch(setBonus, "[^_]+") local name = tokenIterator() local count = tokenIterator() local role = tokenIterator() if name and count then local setName, level = strmatch(name, "^(%a+)(%d*)$") if setName == "tier" then setName = "T" else setName = strupper(setName) end if level then name = setName .. tostring(level) end if role then name = name .. "_" .. role end count = strmatch(count, "(%d+)pc") if name and count then code = format("ArmorSetBonus(%s %d)", name, count) end end if not code then ok = false end else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end return ok, node end EmitOperandSeal = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "seal" then local name = tokenIterator() local code if name then code = format("Stance(paladin_seal_of_%s)", name) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end else ok = false end return ok, node end EmitOperandSpecial = function(operand, parseNode, nodeList, annotation, action, target) local ok = true local node local class = annotation.class local specialization = annotation.specialization target = target and (target .. ".") or "" local code if class == "DEATHKNIGHT" and operand == "dot.breath_of_sindragosa.ticking" then -- Breath of Sindragosa is the player buff from channeling the spell. local buffName = "breath_of_sindragosa_buff" code = format("BuffPresent(%s)", buffName) AddSymbol(annotation, buffName) elseif class == "DEATHKNIGHT" and strsub(operand, -9, -1) == ".ready_in" then local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local spellName = tokenIterator() spellName = Disambiguate(spellName, class, specialization) code = format("TimeToSpell(%s)", spellName) AddSymbol(annotation, spellName) elseif class == "DRUID" and operand == "buff.wild_charge_movement.down" then -- "wild_charge_movement" is a fake SimulationCraft buff that lasts for the -- duration of the movement during Wild Charge. code = "True(wild_charge_movement_down)" elseif class == "DRUID" and operand == "max_fb_energy" then -- SimulationCraft's max_fb_energy is the maximum cost of Ferocious Bite if used. local spellName = "ferocious_bite" code = format("EnergyCost(%s max=1)", spellName) AddSymbol(annotation, spellName) elseif class == "HUNTER" and operand == "buff.beast_cleave.down" then -- Beast Cleave is a buff on the hunter's pet. local buffName = "pet_beast_cleave_buff" code = format("pet.BuffExpires(%s any=1)", buffName) AddSymbol(annotation, buffName) elseif class == "HUNTER" and operand == "buff.careful_aim.up" then -- The "careful_aim" buff is a fake SimulationCraft buff. code = "target.HealthPercent() > 80 or BuffPresent(rapid_fire_buff)" AddSymbol(annotation, "rapid_fire_buff") elseif class == "MAGE" and (operand == "in_flight" and action == "fireball" or operand == "action.fireball.in_flight") then -- Frostfire Bolt can be substituted for Fireball when testing whether the spell is in flight. local fbName = "fireball" local ffbName = "frostfire_bolt" code = format("InFlightToTarget(%s) or InFlightToTarget(%s)", fbName, ffbName) AddSymbol(annotation, fbName) AddSymbol(annotation, ffbName) elseif class == "MAGE" and operand == "buff.rune_of_power.remains" then code = "TotemRemaining(rune_of_power)" elseif class == "MAGE" and operand == "dot.frozen_orb.ticking" then -- The Frozen Orb is ticking if fewer than 10s have elapsed since it was cast. local name = "frozen_orb" code = format("SpellCooldown(%s) > SpellCooldownDuration(%s) - 10", name, name) AddSymbol(annotation, name) elseif class == "MAGE" and operand == "pyro_chain" then if parseNode.asType == "boolean" then code = "GetState(pyro_chain) > 0" else code = "GetState(pyro_chain)" end elseif class == "MONK" and operand == "dot.zen_sphere.ticking" then -- Zen Sphere is a helpful DoT. local buffName = "zen_sphere_buff" code = format("BuffPresent(%s)", buffName) AddSymbol(annotation, buffName) elseif class == "MONK" and (operand == "stagger.heavy" or operand == "stagger.light" or operand == "stagger.moderate") then local property = strmatch(operand, "^stagger%.(%w+)") local buffName = format("%s_stagger_debuff", property) code = format("DebuffPresent(%s)", buffName) AddSymbol(annotation, buffName) elseif class == "PALADIN" and operand == "dot.sacred_shield.remains" then --[[ Sacred Shield is handled specially because SimulationCraft treats it like a damaging spell, e.g., "target.dot.sacred_shield.remains" to represent the buff on the player. --]] local buffName = "sacred_shield_buff" code = format("BuffPresent(%s)", buffName) AddSymbol(annotation, buffName) elseif class == "PRIEST" and operand == "mind_harvest" then -- TODO: "mind_harvest" on the current target is 0 if no Mind Blast has been cast on the target yet. code = "0" elseif class == "PRIEST" and operand == "primary_target" then -- TODO: "primary_target" is 1 if the current target is the "main/boss" target. code = "0" elseif class == "ROGUE" and specialization == "subtlety" and operand == "cooldown.honor_among_thieves.remains" then -- The cooldown of Honor Among Thieves is implemented as a hidden buff. code = "BuffRemaining(honor_among_thieves_cooldown_buff)" annotation.honor_among_thieves = class elseif operand == "debuff.casting.react" then code = target .. "IsInterruptible()" elseif operand == "debuff.flying.down" then code = target .. "True(debuff_flying_down)" elseif operand == "distance" then code = target .. "Distance()" elseif operand == "gcd.max" then code = "GCD()" elseif operand == "gcd.remains" then code = "GCDRemaining()" else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end return ok, node end EmitOperandTalent = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "talent" then local name = tokenIterator() local property = tokenIterator() -- Talent names need no disambiguation as they are the same across all specializations. --name = Disambiguate(name, annotation.class, annotation.specialization) local talentName = name .. "_talent" talentName = Disambiguate(talentName, annotation.class, annotation.specialization) local code if property == "disabled" then code = format("not Talent(%s)", talentName) elseif property == "enabled" then if parseNode.asType == "boolean" then code = format("Talent(%s)", talentName) else code = format("TalentPoints(%s)", talentName) end else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) AddSymbol(annotation, talentName) end else ok = false end return ok, node end EmitOperandTotem = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "totem" then local name = tokenIterator() local property = tokenIterator() local code if property == "active" then code = format("TotemPresent(%s)", name) elseif property == "remains" then code = format("TotemRemaining(%s)", name) else ok = false end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end else ok = false end return ok, node end EmitOperandTrinket = function(operand, parseNode, nodeList, annotation, action) local ok = true local node local tokenIterator = gmatch(operand, OPERAND_TOKEN_PATTERN) local token = tokenIterator() if token == "trinket" then local procType = tokenIterator() local statName = tokenIterator() local code if strsub(procType, 1, 4) == "has_" then -- Assume these conditions are always true. -- TODO: Teach OvaleEquipment to check these conditions. code = format("True(trinket_%s_%s)", procType, statName) else local property = tokenIterator() local buffName = format("trinket_%s_%s_buff", procType, statName) buffName = Disambiguate(buffName, annotation.class, annotation.specialization) if property == "cooldown_remains" then code = format("BuffCooldown(%s)", buffName) elseif property == "down" then code = format("BuffExpires(%s)", buffName) elseif property == "react" then if parseNode.asType == "boolean" then code = format("BuffPresent(%s)", buffName) else code = format("BuffStacks(%s)", buffName) end elseif property == "remains" then code = format("BuffRemaining(%s)", buffName) elseif property == "stack" then code = format("BuffStacks(%s)", buffName) elseif property == "up" then code = format("BuffPresent(%s)", buffName) else ok = false end if ok then AddSymbol(annotation, buffName) end end if ok and code then annotation.astAnnotation = annotation.astAnnotation or {} node = OvaleAST:ParseCode("expression", code, nodeList, annotation.astAnnotation) end else ok = false end return ok, node end do EMIT_VISITOR = { ["action"] = EmitAction, ["action_list"] = EmitActionList, ["arithmetic"] = EmitExpression, ["compare"] = EmitExpression, ["function"] = EmitFunction, ["logical"] = EmitExpression, ["number"] = EmitNumber, ["operand"] = EmitOperand, } end local function InsertSupportingFunctions(child, annotation) local count = 0 local nodeList = annotation.astAnnotation.nodeList if annotation.mind_freeze == "DEATHKNIGHT" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { if target.InRange(mind_freeze) Spell(mind_freeze) if not target.Classification(worldboss) { if target.InRange(asphyxiate) Spell(asphyxiate) if target.InRange(strangulate) Spell(strangulate) Spell(arcane_torrent_runicpower) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_runicpower") AddSymbol(annotation, "asphyxiate") AddSymbol(annotation, "mind_freeze") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "strangulate") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.skull_bash == "DRUID" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { if target.InRange(skull_bash) Spell(skull_bash) if not target.Classification(worldboss) { if target.InRange(mighty_bash) Spell(mighty_bash) Spell(typhoon) if target.InRange(maim) Spell(maim) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "maim") AddSymbol(annotation, "mighty_bash") AddSymbol(annotation, "skull_bash") AddSymbol(annotation, "typhoon") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.melee == "DRUID" then local code = [[ AddFunction GetInMeleeRange { if Stance(druid_bear_form) and not target.InRange(mangle) or { Stance(druid_cat_form) or Stance(druid_claws_of_shirvallah) } and not target.InRange(shred) { if target.InRange(wild_charge) Spell(wild_charge) Texture(misc_arrowlup help=L(not_in_melee_range)) } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "mangle") AddSymbol(annotation, "shred") AddSymbol(annotation, "wild_charge") AddSymbol(annotation, "wild_charge_bear") AddSymbol(annotation, "wild_charge_cat") count = count + 1 end if annotation.summon_pet == "HUNTER" then local code if annotation.specialization == "beast_mastery" then code = [[ AddFunction BeastMasterySummonPet { if not pet.Present() Texture(ability_hunter_beastcall help=L(summon_pet)) if pet.IsDead() Spell(revive_pet) } ]] else code = [[ AddFunction SummonPet { if not Talent(lone_wolf_talent) { if not pet.Present() Texture(ability_hunter_beastcall help=L(summon_pet)) if pet.IsDead() Spell(revive_pet) } } ]] AddSymbol(annotation, "lone_wolf_talent") end local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "revive_pet") count = count + 1 end if annotation.counter_shot == "HUNTER" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { Spell(counter_shot) if not target.Classification(worldboss) { Spell(arcane_torrent_focus) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_focus") AddSymbol(annotation, "counter_shot") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.counterspell == "MAGE" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { Spell(counterspell) if not target.Classification(worldboss) { Spell(arcane_torrent_mana) if target.InRange(quaking_palm) Spell(quaking_palm) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_mana") AddSymbol(annotation, "counterspell") AddSymbol(annotation, "quaking_palm") count = count + 1 end if annotation.spear_hand_strike == "MONK" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { if target.InRange(spear_hand_strike) Spell(spear_hand_strike) if not target.Classification(worldboss) { if target.InRange(paralysis) Spell(paralysis) Spell(arcane_torrent_chi) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_chi") AddSymbol(annotation, "paralysis") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "spear_hand_strike") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.time_to_hpg_melee == "PALADIN" then local code = [[ AddFunction RetributionTimeToHPG { SpellCooldown(crusader_strike exorcism exorcism_glyphed hammer_of_wrath hammer_of_wrath_empowered judgment usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "crusader_strike") AddSymbol(annotation, "exorcism") AddSymbol(annotation, "exorcism_glyphed") AddSymbol(annotation, "hammer_of_wrath") AddSymbol(annotation, "judgment") count = count + 1 end if annotation.time_to_hpg_tank == "PALADIN" then local code = [[ AddFunction ProtectionTimeToHPG { if Talent(sanctified_wrath_talent) SpellCooldown(crusader_strike holy_wrath judgment) if not Talent(sanctified_wrath_talent) SpellCooldown(crusader_strike judgment) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "crusader_strike") AddSymbol(annotation, "holy_wrath") AddSymbol(annotation, "judgment") AddSymbol(annotation, "sanctified_wrath_talent") count = count + 1 end if annotation.class == "PALADIN" then local code if annotation.specialization == "protection" then code = [[ AddFunction ProtectionRighteousFury { if CheckBoxOn(opt_righteous_fury_check) and BuffExpires(righteous_fury) Spell(righteous_fury) } ]] else code = [[ AddFunction RighteousFuryOff { if CheckBoxOn(opt_righteous_fury_check) and BuffPresent(righteous_fury) Texture(spell_holy_sealoffury text=cancel) } ]] end local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "righteous_fury") count = count + 1 end if annotation.time_to_hpg_heal == "PALADIN" then local code = [[ AddFunction HolyTimeToHPG { SpellCooldown(crusader_strike holy_shock judgment) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "crusader_strike") AddSymbol(annotation, "holy_shock") AddSymbol(annotation, "judgment") count = count + 1 end if annotation.rebuke == "PALADIN" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { if target.InRange(rebuke) Spell(rebuke) if not target.Classification(worldboss) { if target.InRange(fist_of_justice) Spell(fist_of_justice) if target.InRange(hammer_of_justice) Spell(hammer_of_justice) Spell(blinding_light) Spell(arcane_torrent_holy) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_holy") AddSymbol(annotation, "blinding_light") AddSymbol(annotation, "fist_of_justice") AddSymbol(annotation, "hammer_of_justice") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "rebuke") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.melee == "PALADIN" then local code = [[ AddFunction GetInMeleeRange { if not target.InRange(rebuke) Texture(misc_arrowlup help=L(not_in_melee_range)) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "rebuke") count = count + 1 end if annotation.silence == "PRIEST" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { Spell(silence) if not target.Classification(worldboss) { Spell(arcane_torrent_mana) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_mana") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "silence") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.kick == "ROGUE" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { if target.InRange(kick) Spell(kick) if not target.Classification(worldboss) { if target.InRange(cheap_shot) Spell(cheap_shot) if target.InRange(deadly_throw) and ComboPoints() == 5 Spell(deadly_throw) if target.InRange(kidney_shot) Spell(kidney_shot) Spell(arcane_torrent_energy) if target.InRange(quaking_palm) Spell(quaking_palm) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_energy") AddSymbol(annotation, "cheap_shot") AddSymbol(annotation, "deadly_throw") AddSymbol(annotation, "kick") AddSymbol(annotation, "kidney_shot") AddSymbol(annotation, "quaking_palm") count = count + 1 end if annotation.melee == "ROGUE" then local code = [[ AddFunction GetInMeleeRange { if not target.InRange(kick) { Spell(shadowstep) Texture(misc_arrowlup help=L(not_in_melee_range)) } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "kick") AddSymbol(annotation, "shadowstep") count = count + 1 end if annotation.wind_shear == "SHAMAN" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { Spell(wind_shear) if not target.Classification(worldboss) { Spell(arcane_torrent_mana) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_mana") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "wind_shear") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.bloodlust == "SHAMAN" then local code = [[ AddFunction Bloodlust { if CheckBoxOn(opt_bloodlust) and DebuffExpires(burst_haste_debuff any=1) { Spell(bloodlust) Spell(heroism) } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "bloodlust") AddSymbol(annotation, "heroism") count = count + 1 end if annotation.pummel == "WARRIOR" then local code = [[ AddFunction InterruptActions { if not target.IsFriend() and target.IsInterruptible() { if target.InRange(pummel) Spell(pummel) if Glyph(glyph_of_gag_order) and target.InRange(heroic_throw) Spell(heroic_throw) if not target.Classification(worldboss) { Spell(arcane_torrent_rage) if target.InRange(quaking_palm) Spell(quaking_palm) Spell(war_stomp) } } } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "arcane_torrent_rage") AddSymbol(annotation, "glyph_of_gag_order") AddSymbol(annotation, "heroic_throw") AddSymbol(annotation, "pummel") AddSymbol(annotation, "quaking_palm") AddSymbol(annotation, "war_stomp") count = count + 1 end if annotation.melee == "WARRIOR" then local code = [[ AddFunction GetInMeleeRange { if not target.InRange(pummel) Texture(misc_arrowlup help=L(not_in_melee_range)) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "pummel") count = count + 1 end if annotation.use_item then local code = [[ AddFunction UseItemActions { Item(HandSlot usable=1) Item(Trinket0Slot usable=1) Item(Trinket1Slot usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) count = count + 1 end if annotation.use_potion_strength then local code = [[ AddFunction UsePotionStrength { if CheckBoxOn(opt_potion_strength) and target.Classification(worldboss) Item(draenic_strength_potion usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_strength_potion") count = count + 1 end if annotation.use_potion_mana then local code = [[ AddFunction UsePotionMana { if CheckBoxOn(opt_potion_mana) Item(draenic_mana_potion usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_mana_potion") count = count + 1 end if annotation.use_potion_intellect then local code = [[ AddFunction UsePotionIntellect { if CheckBoxOn(opt_potion_intellect) and target.Classification(worldboss) Item(draenic_intellect_potion usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_intellect_potion") count = count + 1 end if annotation.use_potion_armor then local code = [[ AddFunction UsePotionArmor { if CheckBoxOn(opt_potion_armor) and target.Classification(worldboss) Item(draenic_armor_potion usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_armor_potion") count = count + 1 end if annotation.use_potion_agility then local code = [[ AddFunction UsePotionAgility { if CheckBoxOn(opt_potion_agility) and target.Classification(worldboss) Item(draenic_agility_potion usable=1) } ]] local node = OvaleAST:ParseCode("add_function", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_agility_potion") count = count + 1 end return count end local function InsertSupportingControls(child, annotation) local count = 0 local nodeList = annotation.astAnnotation.nodeList if annotation.trap_launcher == "HUNTER" then local code = [[ AddCheckBox(opt_trap_launcher SpellName(trap_launcher) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "trap_launcher") count = count + 1 end if annotation.time_warp == "MAGE" then local code = [[ AddCheckBox(opt_time_warp SpellName(time_warp) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "time_warp") count = count + 1 end if annotation.chi_burst == "MONK" then local code = [[ AddCheckBox(opt_chi_burst SpellName(chi_burst) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "chi_burst") count = count + 1 end if annotation.blade_flurry == "ROGUE" then local code = [[ AddCheckBox(opt_blade_flurry SpellName(blade_flurry) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "blade_flurry") count = count + 1 end if annotation.bloodlust == "SHAMAN" then local code = [[ AddCheckBox(opt_bloodlust SpellName(bloodlust) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "bloodlust") count = count + 1 end if annotation.class == "PALADIN" then local code = [[ AddCheckBox(opt_righteous_fury_check SpellName(righteous_fury) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "righteous_fury") count = count + 1 end if annotation.use_potion_strength then local code = [[ AddCheckBox(opt_potion_strength ItemName(draenic_strength_potion) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_strength_potion") count = count + 1 end if annotation.use_potion_mana then local code = [[ AddCheckBox(opt_potion_mana ItemName(draenic_mana_potion) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_mana_potion") count = count + 1 end if annotation.use_potion_intellect then local code = [[ AddCheckBox(opt_potion_intellect ItemName(draenic_intellect_potion) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_intellect_potion") count = count + 1 end if annotation.use_potion_armor then local code = [[ AddCheckBox(opt_potion_armor ItemName(draenic_armor_potion) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_armor_potion") count = count + 1 end if annotation.use_potion_agility then local code = [[ AddCheckBox(opt_potion_agility ItemName(draenic_agility_potion) default) ]] local node = OvaleAST:ParseCode("checkbox", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) AddSymbol(annotation, "draenic_agility_potion") count = count + 1 end return count end local function InsertSupportingDefines(child, annotation) local count = 0 local nodeList = annotation.astAnnotation.nodeList if annotation.honor_among_thieves == "ROGUE" then local buffName = "honor_among_thieves_cooldown_buff" do local code = format("SpellInfo(%s duration=%f)", buffName, annotation[buffName]) local node = OvaleAST:ParseCode("spell_info", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) count = count + 1 end do local code = format("Define(%s %d)", buffName, OvaleHonorAmongThieves.spellId) local node = OvaleAST:ParseCode("define", code, nodeList, annotation.astAnnotation) tinsert(child, 1, node) count = count + 1 end AddSymbol(annotation, buffName) end return count end --</private-static-methods> --<public-static-methods> function OvaleSimulationCraft:OnInitialize() -- Resolve module dependencies. OvaleAST = Ovale.OvaleAST OvaleData = Ovale.OvaleData OvaleHonorAmongThieves = Ovale.OvaleHonorAmongThieves OvaleLexer = Ovale.OvaleLexer OvalePower = Ovale.OvalePower InitializeDisambiguation() self:CreateOptions() end function OvaleSimulationCraft:DebuggingInfo() self_pool:DebuggingInfo() self_childrenPool:DebuggingInfo() self_outputPool:DebuggingInfo() end function OvaleSimulationCraft:ToString(tbl) local output = print_r(tbl) return tconcat(output, "\n") end function OvaleSimulationCraft:Release(profile) if profile.annotation then local annotation = profile.annotation if annotation.astAnnotation then OvaleAST:ReleaseAnnotation(annotation.astAnnotation) end if annotation.nodeList then for _, node in ipairs(annotation.nodeList) do self_pool:Release(node) end end for key, value in pairs(annotation) do if type(value) == "table" then wipe(value) end annotation[key] = nil end profile.annotation = nil end profile.actionList = nil end function OvaleSimulationCraft:ParseProfile(simc) local profile = {} for line in gmatch(simc, "[^\r\n]+") do -- Trim leading and trailing whitespace. line = strmatch(line, "^%s*(.-)%s*$") if not (strmatch(line, "^#.*") or strmatch(line, "^$")) then -- Line is not a comment or an empty string. local key, operator, value = strmatch(line, "([^%+=]+)(%+?=)(.*)") if operator == "=" then profile[key] = value elseif operator == "+=" then if type(profile[key]) ~= "table" then local oldValue = profile[key] profile[key] = {} tinsert(profile[key], oldValue) end tinsert(profile[key], value) end end end -- Concatenate variables defined over multiple lines using += for k, v in pairs(profile) do if type(v) == "table" then profile[k] = tconcat(v) end end -- Parse the action lists. local ok = true local annotation = {} local nodeList = {} local actionList = {} for k, v in pairs(profile) do if ok and strmatch(k, "^actions") then -- Name the default action list "_default" so it's first alphabetically. local name = strmatch(k, "^actions%.([%w_]+)") or "_default" local node ok, node = ParseActionList(name, v, nodeList, annotation) if ok then actionList[#actionList + 1] = node else break end end end -- Sort the action lists alphabetically. tsort(actionList, function(a, b) return a.name < b.name end) -- Set the name, class, specialization, and role from the profile. for class in pairs(RAID_CLASS_COLORS) do local lowerClass = strlower(class) if profile[lowerClass] then annotation.class = class annotation.name = profile[lowerClass] end end annotation.specialization = profile.spec annotation.level = profile.level ok = ok and (annotation.class and annotation.specialization and annotation.level) annotation.pet = profile.default_pet annotation.role = profile.role -- Set the attack range of the class and role. if profile.role == "tank" then annotation.melee = annotation.class elseif profile.role == "spell" then annotation.ranged = annotation.class elseif profile.role == "attack" or profile.role == "dps" then if profile.position == "ranged_back" then annotation.ranged = annotation.class else annotation.melee = annotation.class end end profile.actionList = actionList profile.annotation = annotation annotation.nodeList = nodeList if not ok then self:Release(profile) profile = nil end return profile end function OvaleSimulationCraft:Unparse(profile) local output = self_outputPool:Get() if profile.actionList then for _, node in ipairs(profile.actionList) do output[#output + 1] = Unparse(node) end end local s = tconcat(output, "\n") self_outputPool:Release(output) return s end function OvaleSimulationCraft:EmitAST(profile) local nodeList = {} local ast = OvaleAST:NewNode(nodeList, true) local child = ast.child ast.type = "script" local annotation = profile.annotation local ok = true if profile.actionList then annotation.astAnnotation = annotation.astAnnotation or {} annotation.astAnnotation.nodeList = nodeList for _, node in ipairs(profile.actionList) do local declarationNode = EmitActionList(node, nodeList, annotation) if declarationNode then child[#child + 1] = declarationNode else ok = false break end end end if ok then -- Fixups. do -- Some profiles don't include any interrupt actions. local class = annotation.class annotation.mind_freeze = class -- deathknight annotation.counter_shot = class -- hunter annotation.spear_hand_strike = class -- monk annotation.silence = class -- priest annotation.pummel = class -- warrior end annotation.supportingFunctionCount = InsertSupportingFunctions(child, annotation) annotation.supportingControlCount = InsertSupportingControls(child, annotation) annotation.supportingDefineCount = InsertSupportingDefines(child, annotation) end if not ok then OvaleAST:Release(ast) ast = nil end return ast end function OvaleSimulationCraft:Emit(profile) local nodeList = {} local ast = self:EmitAST(profile) local annotation = profile.annotation 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 if profile.talents then output[#output + 1] = "# talents=" .. profile.talents end if profile.glyphs then output[#output + 1] = "# glyphs=" .. profile.glyphs end if profile.default_pet then output[#output + 1] = "# pet=" .. profile.default_pet end end -- Includes. do output[#output + 1] = "" output[#output + 1] = "Include(ovale_common)" output[#output + 1] = format("Include(ovale_%s_spells)", strlower(annotation.class)) -- Insert an extra blank line to separate section for controls from the includes. if annotation.supportingDefineCount + annotation.supportingControlCount > 0 then output[#output + 1] = "" end end -- Output the script itself. output[#output + 1] = OvaleAST:Unparse(ast) -- Output a simplistic two-icon layout for the rotation. do -- Single-target rotation. output[#output + 1] = "" output[#output + 1] = format("AddIcon specialization=%s help=main enemies=1", annotation.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)) output[#output + 1] = "}" -- AoE rotation. output[#output + 1] = "" output[#output + 1] = format("AddIcon specialization=%s help=aoe", annotation.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)) output[#output + 1] = "}" end -- Append the required symbols for the script. if profile.annotation.symbolTable then output[#output + 1] = "" output[#output + 1] = "### Required symbols" tsort(profile.annotation.symbolTable) for _, symbol in ipairs(profile.annotation.symbolTable) do output[#output + 1] = "# " .. symbol end end local s = tconcat(output, "\n") self_outputPool:Release(output) OvaleAST:Release(ast) return s end function OvaleSimulationCraft:CreateOptions() local options = { name = OVALE .. " SimulationCraft", type = "group", args = { input = { name = L["Input"], type = "group", args = { input = { name = L["SimulationCraft Profile"], desc = L["The contents of a SimulationCraft profile (*.simc)."], type = "input", multiline = 25, width = "full", get = function(info) return self_lastSimC end, set = function(info, value) self_lastSimC = value local profile = self:ParseProfile(self_lastSimC) local code = "" if profile then code = self:Emit(profile) .. "\n" end -- Substitute spaces for tabs. self_lastScript = gsub(code, "\t", " ") end, }, }, }, output = { name = L["Output"], type = "group", args = { output = { name = L["Script"], desc = L["The script translated from the SimulationCraft profile."], type = "input", multiline = 25, width = "full", get = function() return self_lastScript end, }, }, }, }, } local appName = self:GetName() AceConfig:RegisterOptionsTable(appName, options) AceConfigDialog:AddToBlizOptions(appName, "SimulationCraft", OVALE) end --</public-static-methods>