--[[--------------------------------------------------------------------
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
--