--[[-------------------------------------------------------------------- Copyright (C) 2009, 2010, 2011, 2012 Sidoine De Wispelaere. Copyright (C) 2012, 2013, 2014 Johnny C. Lam. See the file LICENSE.txt for copying permission. --]]-------------------------------------------------------------------- local OVALE, Ovale = ... local OvaleCompile = Ovale:NewModule("OvaleCompile", "AceEvent-3.0") Ovale.OvaleCompile = OvaleCompile --<private-static-properties> local L = Ovale.L local OvaleDebug = Ovale.OvaleDebug local OvaleProfiler = Ovale.OvaleProfiler -- Forward declarations for module dependencies. local OvaleAST = nil local OvaleCondition = nil local OvaleCooldown = nil local OvaleData = nil local OvaleEquipment = nil local OvalePaperDoll = nil local OvaleScore = nil local OvaleScripts = nil local OvaleSpellBook = nil local OvaleStance = nil local ipairs = ipairs local pairs = pairs local tonumber = tonumber local tostring = tostring local type = type local strfind = string.find local strmatch = string.match local strsub = string.sub local wipe = wipe local API_GetSpellInfo = GetSpellInfo -- Register for debugging messages. OvaleDebug:RegisterDebugging(OvaleCompile) -- Register for profiling. OvaleProfiler:RegisterProfiling(OvaleCompile) -- Whether to trigger a script compilation if items or stances change. local self_compileOnItems = false local self_compileOnStances = false -- This module needs the information in other modules to be preloaded and ready for use. local self_canEvaluate = false local self_requirePreload = { "OvaleEquipment", "OvaleSpellBook", "OvaleStance" } -- Current age of the script evaluation state. -- This advances every time an event occurs that requires re-evaluating the script. local self_serial = 0 -- Number of times the script has been evaluated. local self_timesEvaluated = 0 -- Icon nodes of the current script (one node for each icon) local self_icon = {} -- Lua pattern to match a floating-point number that may start with a minus sign. local NUMBER_PATTERN = "^%-?%d+%.?%d*$" --</private-static-properties> --<public-static-properties> -- Current age of the script; this advances every time the script is evaluated. OvaleCompile.serial = nil -- AST for the current script. OvaleCompile.ast = nil --</public-static-properties> --<private-static-methods> local function HasTalent(talentId) if OvaleSpellBook:IsKnownTalent(talentId) then return OvaleSpellBook:GetTalentPoints(talentId) > 0 else OvaleCompile:Print("Warning: unknown talent ID '%s'", talentId) return false end end local function RequireValue(value) local required = (strsub(tostring(value), 1, 1) ~= "!") if not required then value = strsub(value, 2) if strmatch(value, NUMBER_PATTERN) then value = tonumber(value) end end return value, required end local function TestConditions(parameters) OvaleCompile:StartProfiling("OvaleCompile_TestConditions") local boolean = true if boolean and parameters.glyph then local glyph, required = RequireValue(parameters.glyph) local hasGlyph = OvaleSpellBook:IsActiveGlyph(glyph) boolean = (required and hasGlyph) or (not required and not hasGlyph) end if boolean and parameters.level then boolean = OvalePaperDoll.level >= parameters.level end if boolean and parameters.maxLevel then boolean = OvalePaperDoll.level <= parameters.maxLevel end if boolean and parameters.specialization then local spec, required = RequireValue(parameters.specialization) local isSpec = OvalePaperDoll:IsSpecialization(spec) boolean = (required and isSpec) or (not required and not isSpec) end if boolean and parameters.if_stance then self_compileOnStances = true local stance, required = RequireValue(parameters.if_stance) local isStance = OvaleStance:IsStance(stance) boolean = (required and isStance) or (not required and not isStance) end if boolean and parameters.if_spell then local spell, required = RequireValue(parameters.if_spell) local hasSpell = OvaleSpellBook:IsKnownSpell(spell) boolean = (required and hasSpell) or (not required and not hasSpell) end if boolean and parameters.talent then local talent, required = RequireValue(parameters.talent) local hasTalent = HasTalent(talent) boolean = (required and hasTalent) or (not required and not hasTalent) end if boolean and parameters.itemset and parameters.itemcount then local equippedCount = OvaleEquipment:GetArmorSetCount(parameters.itemset) self_compileOnItems = true boolean = (equippedCount >= parameters.itemcount) end do if boolean and parameters.checkbox then local profile = Ovale.db.profile for _, checkbox in ipairs(parameters.checkbox) do local name, required = RequireValue(checkbox) local control = Ovale.checkBox[name] or {} control.triggerEvaluation = true Ovale.checkBox[name] = control -- Check the value of the checkbox. local isChecked = profile.check[name] boolean = (required and isChecked) or (not required and not isChecked) if not boolean then break end end end if boolean and parameters.listitem then local profile = Ovale.db.profile for name, listitem in pairs(parameters.listitem) do local item, required = RequireValue(listitem) local control = Ovale.list[name] or { items = {}, default = nil } control.triggerEvaluation = true Ovale.list[name] = control -- Check the selected item in the list. local isSelected = (profile.list[name] == item) boolean = (required and isSelected) or (not required and not isSelected) if not boolean then break end end end end OvaleCompile:StopProfiling("OvaleCompile_TestConditions") return boolean end local function EvaluateAddCheckBox(node) local ok = true local name, parameters = node.name, node.params if TestConditions(parameters) then --[[ If this control was not previously existing, then age the script evaluation state so that anything that checks the value of this control are re-evaluated after the current evaluation cycle. --]] local checkBox = Ovale.checkBox[name] if not checkBox then self_serial = self_serial + 1 OvaleCompile:Debug("New checkbox '%s': advance age to %d.", name, self_serial) end checkBox = checkBox or {} checkBox.text = node.description.value for _, v in ipairs(parameters) do if v == "default" then checkBox.checked = true break end end Ovale.checkBox[name] = checkBox end return ok end local function EvaluateAddIcon(node) local ok = true if TestConditions(node.params) then self_icon[#self_icon + 1] = node end return ok end local function EvaluateAddListItem(node) local ok = true local name, item, parameters = node.name, node.item, node.params if TestConditions(parameters) then --[[ If this control was not previously existing, then age the script evaluation state so that anything that checks the value of this control are re-evaluated after the current evaluation cycle. --]] local list = Ovale.list[name] if not (list and list.items and list.items[item]) then self_serial = self_serial + 1 OvaleCompile:Debug("New list '%s': advance age to %d.", name, self_serial) end list = list or { items = {}, default = nil } list.items[item] = node.description.value for _, v in ipairs(parameters) do if v == "default" then list.default = item break end end Ovale.list[name] = list end return ok end local function EvaluateItemInfo(node) local ok = true local itemId, parameters = node.itemId, node.params if itemId and TestConditions(parameters) then for k, v in pairs(parameters) do if k == "proc" then -- Add the buff for this item proc to the spell list "item_proc_<proc>". local buff = tonumber(parameters.buff) if buff then local name = "item_proc_" .. v local list = OvaleData.buffSpellList[name] or {} list[buff] = true OvaleData.buffSpellList[name] = list else ok = false break end end end end return ok end local function EvaluateList(node) local ok = true local name, parameters = node.name, node.params local listDB if node.keyword == "ItemList" then listDB = "itemList" else -- if node.keyword == "SpellList" then listDB = "buffSpellList" end local list = OvaleData[listDB][name] or {} for i, id in ipairs(parameters) do id = tonumber(id) if id then list[id] = true else ok = false break end end OvaleData[listDB][name] = list return ok end local function EvaluateScoreSpells(node) local ok = true for _, spellId in ipairs(node.params) do spellId = tonumber(spellId) if spellId then OvaleScore:AddSpell(tonumber(spellId)) else ok = false break end end return ok end local function EvaluateSpellAuraList(node) local ok = true local spellId, parameters = node.spellId, node.params if TestConditions(parameters) then local keyword = node.keyword local si = OvaleData:SpellInfo(spellId) local auraTable if strfind(keyword, "^SpellAddTarget") then auraTable = si.aura.target elseif strfind(keyword, "^SpellDamage") then auraTable = si.aura.damage else auraTable = si.aura.player end local filter = strfind(node.keyword, "Debuff") and "HARMFUL" or "HELPFUL" local tbl = auraTable[filter] or {} local count = 0 for k, v in pairs(parameters) do if not OvaleAST.PARAMETER_KEYWORD[k] then tbl[k] = v count = count + 1 end end if count > 0 then auraTable[filter] = tbl end end return ok end local function EvaluateSpellInfo(node) local ok = true local spellId, parameters = node.spellId, node.params if spellId and TestConditions(parameters) then local si = OvaleData:SpellInfo(spellId) for k, v in pairs(parameters) do if k == "addduration" then -- Accumulate "addduration" into a single "adduration" SpellInfo property. local value = tonumber(v) if value then local addDuration = si.addduration or 0 si.addduration = addDuration + value else ok = false break end elseif k == "addcd" then -- Accumulate "addcd" into a single "addcd" SpellInfo property. local value = tonumber(v) if value then local addCd = si.addcd or 0 si.addcd = addCd + value else ok = false break end elseif k == "addlist" then -- Add this buff to the named spell list. local list = OvaleData.buffSpellList[v] or {} list[spellId] = true OvaleData.buffSpellList[v] = list elseif k == "learn" and v == 1 then -- Forcibly learn this spell. local spellName = API_GetSpellInfo(spellId) OvaleSpellBook:AddSpell(spellId, spellName) elseif k == "sharedcd" then OvaleCooldown:AddSharedCooldown(v, spellId) elseif not OvaleAST.PARAMETER_KEYWORD[k] then si[k] = v end end end return ok end local function EvaluateSpellRequire(node) local ok = true local spellId, parameters = node.spellId, node.params if TestConditions(parameters) then local property = node.property local count = 0 local si = OvaleData:SpellInfo(spellId) local tbl = si.require[property] or {} for k, v in pairs(parameters) do if not OvaleAST.PARAMETER_KEYWORD[k] then tbl[k] = v count = count + 1 end end if count > 0 then si.require[property] = tbl end end return ok end -- Scan for spell IDs used in the script that are missing from the spellbook and add them if -- they are variants of a spell with the same name as one already in the spellbook. local function AddMissingVariantSpells(annotation) if annotation.functionReference then for _, node in ipairs(annotation.functionReference) do local spellId = node.params[1] if spellId and OvaleCondition:IsSpellBookCondition(node.func) then if not OvaleSpellBook:IsKnownSpell(spellId) and not OvaleCooldown:IsSharedCooldown(spellId) then local spellName if type(spellId) == "number" then spellName = OvaleSpellBook:GetSpellName(spellId) end if spellName then local name = API_GetSpellInfo(spellName) if spellName == name then OvaleCompile:Debug("Learning spell %s with ID %d.", spellName, spellId) OvaleSpellBook:AddSpell(spellId, spellName) end else local functionCall = node.name if node.paramsAsString then functionCall = node.name .. "(" .. node.paramsAsString .. ")" end OvaleCompile:Print("Unknown spell with ID %s used in %s.", spellId, functionCall) end end end end end end --</private-static-methods> --<public-static-methods> function OvaleCompile:OnInitialize() -- Resolve module dependencies. OvaleAST = Ovale.OvaleAST OvaleCondition = Ovale.OvaleCondition OvaleCooldown = Ovale.OvaleCooldown OvaleData = Ovale.OvaleData OvaleEquipment = Ovale.OvaleEquipment OvalePaperDoll = Ovale.OvalePaperDoll OvaleScore = Ovale.OvaleScore OvaleScripts = Ovale.OvaleScripts OvaleSpellBook = Ovale.OvaleSpellBook OvaleStance = Ovale.OvaleStance end function OvaleCompile:OnEnable() self:RegisterMessage("Ovale_CheckBoxValueChanged", "ScriptControlChanged") self:RegisterMessage("Ovale_EquipmentChanged") self:RegisterMessage("Ovale_GlyphsChanged", "EventHandler") self:RegisterMessage("Ovale_ListValueChanged", "ScriptControlChanged") self:RegisterMessage("Ovale_ScriptChanged") self:RegisterMessage("Ovale_SpellsChanged", "EventHandler") self:RegisterMessage("Ovale_StanceChanged") self:RegisterMessage("Ovale_TalentsChanged", "EventHandler") self:SendMessage("Ovale_ScriptChanged") end function OvaleCompile:OnDisable() self:UnregisterMessage("Ovale_CheckBoxValueChanged") self:UnregisterMessage("Ovale_EquipmentChanged") self:UnregisterMessage("Ovale_GlyphsChanged") self:UnregisterMessage("Ovale_ListValueChanged") self:UnregisterMessage("Ovale_ScriptChanged") self:UnregisterMessage("Ovale_SpellsChanged") self:UnregisterMessage("Ovale_StanceChanged") self:UnregisterMessage("Ovale_TalentsChanged") end function OvaleCompile:Ovale_EquipmentChanged(event) if self_compileOnItems then self:EventHandler(event) end end function OvaleCompile:Ovale_ScriptChanged(event) -- Compile the script named in the current profile. self:CompileScript(Ovale.db.profile.source) -- Trigger script evaluation. self:EventHandler(event) end function OvaleCompile:Ovale_StanceChanged(event) if self_compileOnStances then self:EventHandler(event) end end function OvaleCompile:ScriptControlChanged(event, name) if not name then self:EventHandler(event) else -- Locate the correct script control definition. local control if event == "Ovale_CheckBoxValueChanged" then control = Ovale.checkBox[name] elseif event == "Ovale_ListValueChanged" then control = Ovale.list[name] end -- Only trigger script evaluation if "triggerEvaluation" was set -- for the named control. if control and control.triggerEvaluation then self:EventHandler(event) end end end function OvaleCompile:EventHandler(event) -- Advance age of the script evaluation state. self_serial = self_serial + 1 self:Debug("%s: advance age to %d.", event, self_serial) Ovale.refreshNeeded["player"] = true end function OvaleCompile:CompileScript(name) -- Reset the trace state if we compile a new script. OvaleDebug:ResetTrace() -- Generate the node tree from the named script. self:Debug("Compiling script '%s'.", name) if self.ast then OvaleAST:Release(self.ast) self.ast = nil end local ast = OvaleAST:ParseScript(name) if ast then OvaleAST:Optimize(ast) self.ast = ast end -- Reset the controls defined by the previous script. Ovale:ResetControls() end function OvaleCompile:EvaluateScript(ast, forceEvaluation) self:StartProfiling("OvaleCompile_EvaluateScript") if type(ast) ~= "table" then forceEvaluation = ast ast = self.ast end local changed = false self_canEvaluate = self_canEvaluate or Ovale:IsPreloaded(self_requirePreload) if self_canEvaluate and ast and (forceEvaluation or not self.serial or self.serial < self_serial) then self:Debug("Evaluating script.") changed = true -- Reset compilation state. local ok = true self_compileOnItems = false self_compileOnStances = false wipe(self_icon) OvaleData:ResetSpellInfo() OvaleCooldown:ResetSharedCooldowns() self_timesEvaluated = self_timesEvaluated + 1 self.serial = self_serial -- Evaluate every declaration node of the script. for _, node in ipairs(ast.child) do local nodeType = node.type if nodeType == "checkbox" then ok = EvaluateAddCheckBox(node) elseif nodeType == "icon" then ok = EvaluateAddIcon(node) elseif nodeType == "list_item" then ok = EvaluateAddListItem(node) elseif nodeType == "item_info" then ok = EvaluateItemInfo(node) elseif nodeType == "list" then ok = EvaluateList(node) elseif nodeType == "score_spells" then ok = EvaluateScoreSpells(node) elseif nodeType == "spell_aura_list" then ok = EvaluateSpellAuraList(node) elseif nodeType == "spell_info" then ok = EvaluateSpellInfo(node) elseif nodeType == "spell_require" then ok = EvaluateSpellRequire(node) else -- Any other top-level node types are no-ops when evaluating the script. end if not ok then break end end if ok then AddMissingVariantSpells(ast.annotation) end end self:StopProfiling("OvaleCompile_EvaluateScript") return changed end function OvaleCompile:GetFunctionNode(name) local node if self.ast and self.ast.annotation and self.ast.annotation.customFunction then node = self.ast.annotation.customFunction[name] end return node end function OvaleCompile:GetIconNodes() return self_icon end function OvaleCompile:DebugCompile() self:Print("Total number of times the script was evaluated: %d", self_timesEvaluated) end --</public-static-methods>