--[[-------------------------------------------------------------------- Ovale Spell Priority Copyright (C) 2009, 2010, 2011, 2012 Sidoine Copyright (C) 2012, 2013, 2014 Johnny C. Lam This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License in the LICENSE file accompanying this program. --]]-------------------------------------------------------------------- local _, Ovale = ... local OvaleCompile = Ovale:NewModule("OvaleCompile", "AceEvent-3.0") Ovale.OvaleCompile = OvaleCompile -- -- Forward declarations for module dependencies. local OvaleAST = nil local OvaleCondition = nil local OvaleCooldown = nil local OvaleData = nil local OvaleEquipement = nil local OvaleOptions = 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 strfind = string.find local strmatch = string.match local strsub = string.sub local wipe = table.wipe local API_GetSpellInfo = GetSpellInfo -- Profiling set-up. local Profiler = Ovale.Profiler local profiler = nil do local group = OvaleCompile:GetName() Profiler:RegisterProfilingGroup(group) profiler = Profiler:GetProfilingGroup(group) end -- 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 = { "OvaleEquipement", "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*$" local OVALE_COMPILE_DEBUG = "compile" local OVALE_MISSING_SPELL_DEBUG = "missing_spells" local OVALE_UNKNOWN_SPELL_DEBUG = "unknown_spells" -- -- -- Current age of the script; this advances every time the script is evaluated. OvaleCompile.serial = nil -- AST for the current script. OvaleCompile.ast = nil -- -- local function HasTalent(talentId) if OvaleSpellBook:IsKnownTalent(talentId) then return OvaleSpellBook:GetTalentPoints(talentId) > 0 else Ovale:FormatPrint("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) profiler.Start("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.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 = OvaleEquipement:GetArmorSetCount(parameters.itemset) self_compileOnItems = true boolean = (equippedCount >= parameters.itemcount) end do local profile if boolean and parameters.checkbox then 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. profile = profile or OvaleOptions:GetProfile() 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 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. profile = profile or OvaleOptions:GetProfile() 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 profiler.Stop("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 Ovale:DebugPrintf(OVALE_COMPILE_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 Ovale:DebugPrintf(OVALE_COMPILE_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_". 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 local value = tonumber(v) if value then si.duration = si.duration + value else ok = false break end elseif k == "addcd" then local value = tonumber(v) if value then si.cd = si.cd + 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 == "sharedcd" then OvaleCooldown:AddSharedCooldown(v, spellId) elseif not OvaleAST.PARAMETER_KEYWORD[k] then si[k] = v end 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 if spellName == API_GetSpellInfo(spellName) then Ovale:DebugPrintf(OVALE_MISSING_SPELL_DEBUG, "Learning spell %s with ID %d.", spellName, spellId) OvaleSpellBook:AddSpell(spellId, spellName) end else Ovale:DebugPrintf(OVALE_UNKNOWN_SPELL_DEBUG, "Unknown spell with ID %d.", spellId) end end end end end end -- -- function OvaleCompile:OnInitialize() -- Resolve module dependencies. OvaleAST = Ovale.OvaleAST OvaleCondition = Ovale.OvaleCondition OvaleCooldown = Ovale.OvaleCooldown OvaleData = Ovale.OvaleData OvaleEquipement = Ovale.OvaleEquipement OvaleOptions = Ovale.OvaleOptions 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", "CompileScript") 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_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 Ovale:DebugPrintf(OVALE_COMPILE_DEBUG, "%s: advance age to %d.", event, self_serial) Ovale.refreshNeeded["player"] = true end function OvaleCompile:CompileScript(event) -- Reset the trace state if we compile a new script. Ovale:ResetTrace() -- Compile the selected script from the profile. local profile = OvaleOptions:GetProfile() local source = profile.source Ovale:DebugPrintf(OVALE_COMPILE_DEBUG, "Compiling script '%s'.", source) if self.ast then OvaleAST:Release(self.ast) self.ast = nil end local ast = OvaleAST:ParseScript(source) if ast then OvaleAST:Optimize(ast) self.ast = ast end -- Reset the controls defined by the previous script. Ovale:ResetControls() -- Trigger script evaluation. self:EventHandler(event) end function OvaleCompile:EvaluateScript(forceEvaluation) profiler.Start("OvaleCompile_EvaluateScript") self_canEvaluate = self_canEvaluate or Ovale:IsPreloaded(self_requirePreload) if self_canEvaluate and self.ast and (forceEvaluation or not self.serial or self.serial < self_serial) then Ovale:DebugPrint(OVALE_COMPILE_DEBUG, "Evaluating script.") -- Reset compilation state. local ok = true self_compileOnItems = false self_compileOnStances = false wipe(self_icon) OvaleCooldown:ResetSharedCooldowns() self_timesEvaluated = self_timesEvaluated + 1 self.serial = self_serial -- Evaluate every declaration node of the script. for _, node in ipairs(self.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) 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(self.ast.annotation) Ovale:UpdateFrame() end end profiler.Stop("OvaleCompile_EvaluateScript") 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:Debug() Ovale:FormatPrint("Total number of times the script was evaluated: %d", self_timesEvaluated) end --