From 1140e0b9d744adb0ceed676cac19a7cc1de32b24 Mon Sep 17 00:00:00 2001 From: "Johnny C. Lam" Date: Sun, 13 Jul 2014 11:31:31 +0000 Subject: [PATCH] Update Ovale to use abstract syntax trees. OvaleCompile has been restructured to only evaluate the declarations in the script and populate any databases used by other modules. It makes a distinction between compiling the script (using OvaleAST) and evaluating the script. Compiling the script only needs to happen when the script code changes. All other events only require re-evaluating the script. OvaleBestAction has been restructured to more readily cache computed results of nodes during each update cycle. Combined with the common function elimination optimization in OvaleAST, this allows for all function calls to be computed only once per AddIcon group. Other miscellaenous changes to modules were made to update to correct method names and to remove unused code. git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1543 d5049fe3-3747-40f7-a4b5-f36d6801af5f --- OvaleBestAction.lua | 1418 +++++++++++++++++++----------------- OvaleCompile.lua | 1177 ++++++++---------------------- OvaleFrame.lua | 36 +- OvaleOptions.lua | 12 - conditions/Damage.lua | 2 +- conditions/LastEstimatedDamage.lua | 2 +- 6 files changed, 1057 insertions(+), 1590 deletions(-) diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua index 3fcfa05..29d1824 100644 --- a/OvaleBestAction.lua +++ b/OvaleBestAction.lua @@ -9,7 +9,7 @@ --]]-------------------------------------------------------------------- local _, Ovale = ... -local OvaleBestAction = Ovale:NewModule("OvaleBestAction") +local OvaleBestAction = Ovale:NewModule("OvaleBestAction", "AceEvent-3.0") Ovale.OvaleBestAction = OvaleBestAction -- @@ -17,7 +17,9 @@ local OvalePool = Ovale.OvalePool local OvaleTimeSpan = Ovale.OvaleTimeSpan -- Forward declarations for module dependencies. +local OvaleAST = nil local OvaleActionBar = nil +local OvaleCompile = nil local OvaleCondition = nil local OvaleCooldown = nil local OvaleData = nil @@ -103,423 +105,718 @@ end local OVALE_DEFAULT_PRIORITY = 3 +-- Table of node types to visitor methods. +local COMPUTE_VISITOR = { + ["action"] = "ComputeAction", + ["arithmetic"] = "ComputeArithmetic", + ["compare"] = "ComputeCompare", + ["custom_function"] = "ComputeCustomFunction", + ["function"] = "ComputeFunction", + ["group"] = "ComputeGroup", + ["if"] = "ComputeIf", + ["logical"] = "ComputeLogical", + ["lua"] = "ComputeLua", + ["unless"] = "ComputeIf", + ["value"] = "ComputeValue", + ["wait"] = "ComputeWait", +} + -- Age of the current computation. local self_serial = 0 + -- Pool of time-span tables. -local self_pool = OvalePool("OvaleBestAction_pool") +local self_timeSpanPool = OvalePool("OvaleBestAction_timeSpanPool") +-- timeSpan[node] = computed time span for that node. +local self_timeSpan = {} + +-- Pool of value nodes for results. +local self_valuePool = OvalePool("OvaleBestAction_valuePool") +-- value[node] = result node of that node. +local self_value = {} -- -- -local function PutValue(element, value, origin, rate) - if not element.result then - element.result = { type = "value" } +local function SetValue(node, value, origin, rate) + -- Re-use existing result. + local result = self_value[node] + if not result then + result = self_valuePool:Get() + self_value[node] = result end - local result = element.result - result.value = value - result.origin = origin - result.rate = rate + -- Overwrite any pre-existing values. + result.type = "value" + result.value = value or 0 + result.origin = origin or 0 + result.rate = rate or 0 return result end -local function ComputeAction(element, state) - profiler.Start("OvaleBestAction_ComputeAction") - local self = OvaleBestAction - local action = element.params[1] +local function GetTimeSpan(node) + local timeSpan = self_timeSpan[node] + if timeSpan then + timeSpan:Reset() + else + timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) + self_timeSpan[node] = timeSpan + end + return timeSpan +end + +local function GetActionItemInfo(element, state, target) + profiler.Start("OvaleBestAction_GetActionItemInfo") + local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, - actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId = self:GetActionInfo(element, state) - local timeSpan = element.timeSpan - timeSpan:Reset() + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId - if not actionTexture then - Ovale:Logf("Action %s not found", action) - profiler.Stop("OvaleBestAction_ComputeAction") - return timeSpan - elseif not (actionEnable and actionEnable > 0) then - Ovale:Logf("Action %s not enabled", action) - profiler.Stop("OvaleBestAction_ComputeAction") - return timeSpan - elseif element.params.usable == 1 and not actionUsable then - Ovale:Logf("Action %s not usable", action) - profiler.Stop("OvaleBestAction_ComputeAction") - return timeSpan + local itemId = element.params[1] + if type(itemId) ~= "number" then + itemId = OvaleEquipement:GetEquippedItem(itemId) end + if not itemId then + Ovale:Logf("Unknown item '%s'.", element.params[1]) + else + Ovale:Logf("Item ID '%s'", itemId) + local action = OvaleActionBar:GetForItem(itemId) + local spellName = API_GetItemSpell(itemId) - -- Set the cast time of the action. - if actionType == "spell" then - local spellId = actionId - local si = spellId and OvaleData.spellInfo[spellId] - if si and si.casttime then - element.castTime = si.casttime - else - local _, _, _, _, _, _, castTime = API_GetSpellInfo(spellId) - if castTime then - element.castTime = castTime / 1000 - else - element.castTime = nil - end + -- Use texture specified in the action if given. + if element.params.texture then + actionTexture = "Interface\\Icons\\" .. element.params.texture end - else - element.castTime = 0 + actionTexture = actionTexture or API_GetItemIcon(itemId) + actionInRange = API_IsItemInRange(itemId, target) + actionCooldownStart, actionCooldownDuration, actionEnable = API_GetItemCooldown(itemId) + actionUsable = spellName and API_IsUsableItem(itemId) + if action then + actionShortcut = OvaleActionBar:GetBinding(action) + actionIsCurrent = API_IsCurrentAction(action) + end + actionType = "item" + actionId = itemId end - -- If the action is not on cooldown, then treat it like it's immediately ready. - local start - if actionCooldownDuration and actionCooldownStart and actionCooldownStart > 0 then - start = actionCooldownDuration + actionCooldownStart + profiler.Stop("OvaleBestAction_GetActionItemInfo") + return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target +end + +local function GetActionMacroInfo(element, state, target) + profiler.Start("OvaleBestAction_GetActionMacroInfo") + + local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId + + local macro = element.params[1] + local action = OvaleActionBar:GetForMacro(macro) + if not action then + Ovale:Logf("Unknown macro '%s'.", macro) else - start = state.currentTime + -- Use texture specified in the action if given. + if element.params.texture then + actionTexture = "Interface\\Icons\\" .. element.params.texture + end + actionTexture = actionTexture or API_GetActionTexture(action) + actionInRange = API_IsActionInRange(action, target) + actionCooldownStart, actionCooldownDuration, actionEnable = API_GetActionCooldown(action) + actionUsable = API_IsUsableAction(action) + actionShortcut = OvaleActionBar:GetBinding(action) + actionIsCurrent = API_IsCurrentAction(action) + actionType = "macro" + actionId = macro end - Ovale:Logf("start=%f nextCast=%s [%d]", start, state.nextCast, element.nodeId) + profiler.Stop("OvaleBestAction_GetActionMacroInfo") + return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target +end - -- If the action is available before the end of the current spellcast, then wait until we can first cast the action. - if start < state.nextCast then - -- Default to starting at next available cast time. - local newStart = state.nextCast - -- If we are currently channeling a spellcast, then see if it is interruptible. - -- If we are allowed to interrupt it, then start after the next tick of the channel. - if state.isChanneling then - local spellId = state.currentSpellId - local si = spellId and OvaleData.spellInfo[spellId] - if si then - -- "channel=N" means that the channel has N total ticks and can be interrupted. - local channel = si.channel or si.canStopChannelling - if channel then - local hasteMultiplier = 1 - if si.haste == "spell" then - hasteMultiplier = state:GetSpellHasteMultiplier() - elseif si.haste == "melee" then - hasteMultiplier = state:GetMeleeHasteMultiplier() - end - local numTicks = floor(channel * hasteMultiplier + 0.5) - local tick = (state.nextCast - state.startCast) / numTicks - local tickTime = state.startCast - for i = 1, numTicks do - tickTime = tickTime + tick - if start <= tickTime then +local function GetActionSpellInfo(element, state, target) + profiler.Start("OvaleBestAction_GetActionSpellInfo") + + local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId + + local spellId = element.params[1] + local action = OvaleActionBar:GetForSpell(spellId) + if not OvaleSpellBook:IsKnownSpell(spellId) and not action then + Ovale:Logf("Unknown spell ID '%s'.", spellId) + else + -- Use texture specified in the action if given. + if element.params.texture then + actionTexture = "Interface\\Icons\\" .. element.params.texture + end + actionTexture = actionTexture or API_GetSpellTexture(spellId) + actionInRange = API_IsSpellInRange(OvaleSpellBook:GetSpellName(spellId), target) + actionCooldownStart, actionCooldownDuration, actionEnable = state:GetSpellCooldown(spellId) + actionUsable = OvaleSpellBook:IsUsableSpell(spellId) + if action then + actionShortcut = OvaleActionBar:GetBinding(action) + actionIsCurrent = API_IsCurrentAction(action) + end + actionType = "spell" + actionId = spellId + + local si = OvaleData.spellInfo[spellId] + if si then + -- Verify that the spell may be cast given restrictions specified in SpellInfo(). + local meetsRequirements = true + if si.stance and not OvaleStance:IsStance(si.stance) then + Ovale:Logf("Spell ID '%s' requires the player to be in stance '%s'", spellId, si.stance) + meetsRequirements = false + elseif si.combo then + -- Spell requires combo points. + local cost = state:ComboPointCost(spellId) + if state.combo < cost then + Ovale:Logf("Spell ID '%s' requires at least %d combo points.", spellId, cost) + meetsRequirements = false + end + else + for powerType in pairs(OvalePower.SECONDARY_POWER) do + if si[powerType] then + -- Spell requires "secondary" resources, e.g., chi, focus, rage, etc., + local cost = state:PowerCost(spellId, powerType) + if state[powerType] < cost then + Ovale:Logf("Spell ID '%s' requires at least %d %s.", spellId, cost, powerType) + meetsRequirements = false break end end - newStart = tickTime - Ovale:Logf("%s start=%f, numTicks=%d, tick=%f, tickTime=%f", spellId, newStart, numTicks, tick, tickTime) end end + + -- Fix spell cooldown information using primary resource requirements specified in SpellInfo(). + if actionCooldownStart and actionCooldownDuration then + -- Get the maximum time before all "primary" resources are ready. + local atTime = state.currentTime + for powerType in pairs(OvalePower.PRIMARY_POWER) do + if si[powerType] then + local t = state.currentTime + state:TimeToPower(spellId, powerType) + if atTime < t then + atTime = t + end + end + end + if actionCooldownStart > 0 then + if atTime > actionCooldownStart + actionCooldownDuration then + Ovale:Logf("Delaying spell ID '%s' for primary resource.", spellId) + actionCooldownDuration = atTime - actionCooldownStart + end + else + actionCooldownStart = state.currentTime + actionCooldownDuration = atTime - actionCooldownStart + end + + if si.blood or si.frost or si.unholy or si.death then + -- Spell requires runes. + local needRunes = true + -- "buff_runes_none" is the spell ID of the buff that makes casting the spell cost no runes. + local buffNoRunes = si.buff_runes_none + if buffNoRunes then + local aura = state:GetAura("player", buffNoRunes) + if state:IsActiveAura(aura) then + needRunes = false + end + end + if needRunes then + local ending = state.currentTime + state:GetRunesCooldown(si.blood, si.unholy, si.frost, si.death) + if ending > actionCooldownStart + actionCooldownDuration then + actionCooldownDuration = ending - actionCooldownStart + end + end + end + end + + -- Use texture specified in the SpellInfo() if given. + if si.texture then + actionTexture = "Interface\\Icons\\" .. si.texture + end + + if not meetsRequirements then + -- Assign all return values to nil. + actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target = nil + end end - start = newStart end - Ovale:Logf("Action %s can start at %f", action, start) - timeSpan[1], timeSpan[2] = start, math.huge - --[[ - Allow for the return value of an to be "typecast" to a constant value by specifying - asValue=1 as a parameter. + profiler.Stop("OvaleBestAction_GetActionSpellInfo") + return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target +end - Return 1 if the action is off of cooldown, or 0 if it is on cooldown. - --]] - local value - if element.params.asValue and element.params.asValue == 1 then - local atTime = state.currentTime - if HasTime(timeSpan, atTime) then - value = 1 +local function GetActionTextureInfo(element, state, target) + profiler.Start("OvaleBestAction_GetActionTextureInfo") + + local texture = element.params[1] + local actionTexture = "Interface\\Icons\\" .. texture + local actionInRange = nil + local actionCooldownStart = 0 + local actionCooldownDuration = 0 + local actionEnable = 1 + local actionUsable = true + local actionShortcut = nil + local actionIsCurrent = nil + local actionType = "texture" + local actionId = texture + + profiler.Stop("OvaleBestAction_GetActionTextureInfo") + return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target +end +-- + +-- +function OvaleBestAction:OnInitialize() + -- Resolve module dependencies. + OvaleAST = Ovale.OvaleAST + OvaleActionBar = Ovale.OvaleActionBar + OvaleCompile = Ovale.OvaleCompile + OvaleCondition = Ovale.OvaleCondition + OvaleCooldown = Ovale.OvaleCooldown + OvaleData = Ovale.OvaleData + OvaleEquipement = Ovale.OvaleEquipement + OvaleFuture = Ovale.OvaleFuture + OvalePower = Ovale.OvalePower + OvaleSpellBook = Ovale.OvaleSpellBook + OvaleStance = Ovale.OvaleStance +end + +function OvaleBestAction:OnEnable() + self:RegisterMessage("Ovale_ScriptChanged") +end + +function OvaleBestAction:OnDisable() + self:UnregisterMessage("Ovale_ScriptChanged") +end + +function OvaleBestAction:Ovale_ScriptChanged() + -- Clean-up tables that are referenced using obsolete nodes as keys. + for node, timeSpan in pairs(self_timeSpan) do + self_timeSpanPool:Release(timeSpan) + self_timeSpan[node] = nil + end + for node, value in pairs(self_value) do + self_valuePool:Release(value) + self_value[node] = nil + end +end + +function OvaleBestAction:StartNewAction(state) + state:Reset() + OvaleFuture:ApplyInFlightSpells(state) + self_serial = self_serial + 1 +end + +function OvaleBestAction:GetActionInfo(element, state) + if element and element.type == "action" then + local target = element.params.target or OvaleCondition.defaultTarget + if element.lowername == "item" then + return GetActionItemInfo(element, state, target) + elseif element.lowername == "macro" then + return GetActionMacroInfo(element, state, target) + elseif element.lowername == "spell" then + return GetActionSpellInfo(element, state, target) + elseif element.lowername == "texture" then + return GetActionTextureInfo(element, state, target) + end + end + return nil +end + +function OvaleBestAction:Compute(element, state) + local timeSpan, priority, result + if element then + if element.asString then + Ovale:Logf("[%d] >>> Computing '%s': %s", element.nodeId, element.type, element.asString) else - value = 0 + Ovale:Logf("[%d] >>> Computing '%s'", element.nodeId, element.type) + end + -- Check for recently cached computation results. + if element.serial and element.serial >= self_serial then + timeSpan = element.timeSpan + priority = element.priority + result = element.result + Ovale:Logf("[%d] using cached result (age = %d)", element.nodeId, element.serial) + else + local visitor = COMPUTE_VISITOR[element.type] + if visitor and self[visitor] then + timeSpan, priority, result = self[visitor](self, element, state) + element.serial = self_serial + element.timeSpan = timeSpan + element.priority = priority + element.result = result + else + Ovale:Logf("[%d] Runtime error: unable to compute node of type '%s'.", element.nodeId, element.type) + end + end + if result and result.type == "value" then + local value, origin, rate = result.value, result.origin, result.rate + Ovale:Logf("[%d] <<< '%s' returns %s with value = %f, %f, %f", element.nodeId, element.type, tostring(timeSpan), value, origin, rate) + elseif result and result.nodeId then + Ovale:Logf("[%d] <<< '%s' returns [%d] %s", element.nodeId, element.type, result.nodeId, tostring(timeSpan)) + else + Ovale:Logf("[%d] <<< '%s' returns %s", element.nodeId, element.type, tostring(timeSpan)) end - timeSpan[1], timeSpan[2] = 0, math.huge end + return timeSpan, priority, result +end - local priority = element.params.priority or OVALE_DEFAULT_PRIORITY - if value then - local result = PutValue(element, value, 0, 0) - profiler.Stop("OvaleBestAction_ComputeAction") - return timeSpan, priority, result +function OvaleBestAction:ComputeBool(element, state) + local timeSpan, _, newElement = self:Compute(element, state) + -- Match SimulationCraft: 0 is false, non-zero is true. + -- (https://code.google.com/p/simulationcraft/wiki/ActionLists#Logical_operators) + if newElement and newElement.type == "value" and newElement.value == 0 and newElement.rate == 0 then + return nil else - profiler.Stop("OvaleBestAction_ComputeAction") - return timeSpan, priority, element + return timeSpan end end -local function ComputeAnd(element, state) - profiler.Start("OvaleBestAction_Compute") - Ovale:Logf("%s [%d]", element.type, element.nodeId) - local self = OvaleBestAction - local timeSpanA = self:ComputeBool(element.a, state) - local timeSpan = element.timeSpan - - -- Short-circuit evaluation of left argument to AND. - if Measure(timeSpanA) == 0 then - timeSpan:Reset(timeSpanA) +function OvaleBestAction:ComputeAction(element, state) + profiler.Start("OvaleBestAction_ComputeAction") + local nodeId = element.nodeId + local timeSpan = GetTimeSpan(element) + local priority, result + + Ovale:Logf("[%d] evaluating action: %s(%s)", element.nodeId, element.name, element.paramsAsString) + local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, + actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId = self:GetActionInfo(element, state) + + local action = element.params[1] + if not actionTexture then + Ovale:Logf("[%s] Action %s not found.", nodeId, action) + elseif not (actionEnable and actionEnable > 0) then + Ovale:Logf("[%s] Action %s not enabled.", nodeId, action) + elseif element.params.usable == 1 and not actionUsable then + Ovale:Logf("[%s] Action %s not usable.", nodeId, action) else - local timeSpanB = self:ComputeBool(element.b, state) - -- Take intersection of A and B. - timeSpan:Reset() - Intersect(timeSpanA, timeSpanB, timeSpan) + -- Set the cast time of the action. + if actionType == "spell" then + local spellId = actionId + local si = spellId and OvaleData.spellInfo[spellId] + if si and si.casttime then + element.castTime = si.casttime + else + local _, _, _, _, _, _, castTime = API_GetSpellInfo(spellId) + if castTime then + element.castTime = castTime / 1000 + else + element.castTime = nil + end + end + else + element.castTime = 0 + end + + -- If the action is not on cooldown, then treat it like it's immediately ready. + local start + if actionCooldownDuration and actionCooldownStart and actionCooldownStart > 0 then + start = actionCooldownDuration + actionCooldownStart + else + start = state.currentTime + end + + Ovale:Logf("[%d] start=%f nextCast=%s", nodeId, start, state.nextCast) + + -- If the action is available before the end of the current spellcast, then wait until we can first cast the action. + if start < state.nextCast then + -- Default to starting at next available cast time. + local newStart = state.nextCast + -- If we are currently channeling a spellcast, then see if it is interruptible. + -- If we are allowed to interrupt it, then start after the next tick of the channel. + if state.isChanneling then + local spellId = state.currentSpellId + local si = spellId and OvaleData.spellInfo[spellId] + if si then + -- "channel=N" means that the channel has N total ticks and can be interrupted. + local channel = si.channel or si.canStopChannelling + if channel then + local hasteMultiplier = 1 + if si.haste == "spell" then + hasteMultiplier = state:GetSpellHasteMultiplier() + elseif si.haste == "melee" then + hasteMultiplier = state:GetMeleeHasteMultiplier() + end + local numTicks = floor(channel * hasteMultiplier + 0.5) + local tick = (state.nextCast - state.startCast) / numTicks + local tickTime = state.startCast + for i = 1, numTicks do + tickTime = tickTime + tick + if start <= tickTime then + break + end + end + newStart = tickTime + Ovale:Logf("[%d] %s start=%f, numTicks=%d, tick=%f, tickTime=%f", nodeId, spellId, newStart, numTicks, tick, tickTime) + end + end + end + start = newStart + end + Ovale:Logf("[%d] Action %s can start at %f.", nodeId, action, start) + timeSpan[1], timeSpan[2] = start, math.huge + + --[[ + Allow for the return value of an to be "typecast" to a constant value by specifying + asValue=1 as a parameter. + + Return 1 if the action is off of cooldown, or 0 if it is on cooldown. + --]] + local value + if element.params.asValue == 1 then + local atTime = state.currentTime + local value = HasTime(timeSpan, atTime) and 1 or 0 + result = SetValue(element, value) + timeSpan[1], timeSpan[2] = 0, math.huge + Ovale:Logf("[%d] Action %s typecast to value %f.", nodeId, action, value) + else + result = element + end + priority = element.params.priority or OVALE_DEFAULT_PRIORITY end - Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan + + profiler.Stop("OvaleBestAction_ComputeAction") + return timeSpan, priority, result end -local function ComputeArithmetic(element, state) +function OvaleBestAction:ComputeArithmetic(element, state) profiler.Start("OvaleBestAction_Compute") - local self = OvaleBestAction - local timeSpanA, _, elementA = self:Compute(element.a, state) - local timeSpanB, _, elementB = self:Compute(element.b, state) - local timeSpan = element.timeSpan - timeSpan:Reset() + local timeSpanA, _, elementA = self:Compute(element.child[1], state) + local timeSpanB, _, elementB = self:Compute(element.child[2], state) + local timeSpan = GetTimeSpan(element) + local result -- Take intersection of A and B. Intersect(timeSpanA, timeSpanB, timeSpan) if Measure(timeSpan) == 0 then - Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId) - local result = PutValue(element, 0, 0, 0) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan, OVALE_DEFAULT_PRIORITY, result - end - - --[[ - A(t) = a + (t - b)*c - B(t) = x + (t - y)*z - - Silently "typecast" non-values to a constant value of 0. - --]] - local a = elementA and elementA.value or 0 - local b = elementA and elementA.origin or 0 - local c = elementA and elementA.rate or 0 - local x = elementB and elementB.value or 0 - local y = elementB and elementB.origin or 0 - local z = elementB and elementB.rate or 0 - local atTime = state.currentTime + Ovale:Logf("[%d] arithmetic '%s' returns %s with zero measure", element.nodeId, element.operator, tostring(timeSpan)) + result = SetValue(element, 0) + else + --[[ + A(t) = a + (t - b)*c + B(t) = x + (t - y)*z - Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, element.operator, x, y, z, element.nodeId) + Silently "typecast" non-values to a constant value of 0. + --]] + local a = elementA and elementA.value or 0 + local b = elementA and elementA.origin or 0 + local c = elementA and elementA.rate or 0 + local x = elementB and elementB.value or 0 + local y = elementB and elementB.origin or 0 + local z = elementB and elementB.rate or 0 + local operator = element.operator + local atTime = state.currentTime - -- result(t) = l + (t - m) * n - local l, m, n + Ovale:Logf("[%d] %f+(t-%f)*%f %s %f+(t-%f)*%f", element.nodeId, a, b, c, operator, x, y, z) - --[[ - A(t) = a + (t - b)*c = a + (t - t0 + t0 - b)*c = [a + (t0 - b)*c] + (t - t0)*c = A(t0) + (t - t0)*c - B(t) = x + (t - y)*z = x + (t - t0 + t0 - y)*z = [x + (t0 - y)*z] + (t - t0)*z = B(t0) + (t - t0)*z - --]] - local A = a + (atTime - b)*c - local B = x + (atTime - y)*z + -- result(t) = l + (t - m)*n + local l, m, n - if element.operator == "+" then --[[ - A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c - B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z - - A(t) + B(t) = (A + B) + (t - t0)*(c + z) + A(t) = a + (t - b)*c = a + (t - t0 + t0 - b)*c = [a + (t0 - b)*c] + (t - t0)*c = A(t0) + (t - t0)*c + B(t) = x + (t - y)*z = x + (t - t0 + t0 - y)*z = [x + (t0 - y)*z] + (t - t0)*z = B(t0) + (t - t0)*z --]] - l = A + B - m = atTime - n = c + z - elseif element.operator == "-" then - --[[ - A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c - B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z + local A = a + (atTime - b)*c + local B = x + (atTime - y)*z - A(t) - B(t) = (A - B) + (t - t0)*(c - z) - --]] - l = A - B - m = atTime - n = c - z - elseif element.operator == "*" then - --[[ - A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c - B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z - A(t)*B(t) = A*B + (t - t0)*[A*z + B*c] + [(t - t0)^2]*(c*z) - = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere. - --]] - l = A*B + if operator == "+" then + --[[ + A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c + B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z + + A(t) + B(t) = (A + B) + (t - t0)*(c + z) + --]] + l = A + B m = atTime - n = A*z + B*c - elseif element.operator == "/" then - --[[ - A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c - B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z - A(t)/B(t) = A/B + (t - t0)*[(B*c - A*z)/B^2] + O(t^2) converges when |t - t0| < |B/z|. - --]] - l = A/B - m = atTime - n = (B*c - A*z)/(B^2) - local bound - if z == 0 then - bound = math.huge - else - bound = abs(B/z) - end - local scratch = OvaleTimeSpan(self_pool:Get()) - scratch:Reset(timeSpan) - timeSpan:Reset() - IntersectInterval(scratch, atTime - bound, atTime + bound, timeSpan) - self_pool:Release(scratch) - elseif element.operator == "%" then - if c == 0 and z == 0 then - l = A % B + n = c + z + elseif operator == "-" then + --[[ + A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c + B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z + + A(t) - B(t) = (A - B) + (t - t0)*(c - z) + --]] + l = A - B m = atTime - n = 0 - else - Ovale:Error("Parameters of % must be constants") - l = 0 - m = 0 - n = 0 + n = c - z + elseif operator == "*" then + --[[ + A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c + B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z + A(t)*B(t) = A*B + (t - t0)*[A*z + B*c] + [(t - t0)^2]*(c*z) + = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere. + --]] + l = A*B + m = atTime + n = A*z + B*c + elseif operator == "/" then + --[[ + A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c + B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z + A(t)/B(t) = A/B + (t - t0)*[(B*c - A*z)/B^2] + O(t^2) converges when |t - t0| < |B/z|. + --]] + l = A/B + m = atTime + n = (B*c - A*z)/(B^2) + local bound + if z == 0 then + bound = math.huge + else + bound = abs(B/z) + end + local scratch = OvaleTimeSpan(self_timeSpanPool:Get()) + scratch:Reset(timeSpan) timeSpan:Reset() + IntersectInterval(scratch, atTime - bound, atTime + bound, timeSpan) + self_timeSpanPool:Release(scratch) + elseif operator == "%" then + if c == 0 and z == 0 then + l = A % B + m = atTime + n = 0 + else + Ovale:Errorf("[%d] Parameters of modulus operator '%' must be constants.", element.nodeId) + l = 0 + m = 0 + n = 0 + end end + Ovale:Logf("[%d] arithmetic '%s' returns %f+(t-%f)*%f", element.nodeId, operator, l, m, n) + result = SetValue(element, l, m, n) end - Ovale:Logf("result = %f+(t-%f)*%f [%d]", l, m, n, element.nodeId) - local result = PutValue(element, l, m, n) profiler.Stop("OvaleBestAction_Compute") return timeSpan, OVALE_DEFAULT_PRIORITY, result end -local function ComputeCompare(element, state) +function OvaleBestAction:ComputeCompare(element, state) profiler.Start("OvaleBestAction_Compute") - local self = OvaleBestAction - local timeSpanA, _, elementA = self:Compute(element.a, state) - local timeSpanB, _, elementB = self:Compute(element.b, state) - local timeSpan = element.timeSpan - timeSpan:Reset() + local timeSpanA, _, elementA = self:Compute(element.child[1], state) + local timeSpanB, _, elementB = self:Compute(element.child[2], state) + local timeSpan = GetTimeSpan(element) -- Take intersection of A and B. Intersect(timeSpanA, timeSpanB, timeSpan) if Measure(timeSpan) == 0 then - profiler.Stop("OvaleBestAction_Compute") - return timeSpan - end - - --[[ - A(t) = a + (t - b)*c - B(t) = x + (t - y)*z + Ovale:Logf("[%d] compare '%s' returns %s with zero measure", element.nodeId, element.operator, tostring(timeSpan)) + else + --[[ + A(t) = a + (t - b)*c + B(t) = x + (t - y)*z - Silently "typecast" non-values to a constant value of 0. - --]] - local a = elementA and elementA.value or 0 - local b = elementA and elementA.origin or 0 - local c = elementA and elementA.rate or 0 - local x = elementB and elementB.value or 0 - local y = elementB and elementB.origin or 0 - local z = elementB and elementB.rate or 0 - local operator = element.operator + Silently "typecast" non-values to a constant value of 0. + --]] + local a = elementA and elementA.value or 0 + local b = elementA and elementA.origin or 0 + local c = elementA and elementA.rate or 0 + local x = elementB and elementB.value or 0 + local y = elementB and elementB.origin or 0 + local z = elementB and elementB.rate or 0 + local operator = element.operator - Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, operator, x, y, z, element.nodeId) + Ovale:Logf("[%d] %f+(t-%f)*%f %s %f+(t-%f)*%f", element.nodeId, a, b, c, operator, x, y, z) - --[[ - A(t) = B(t) - a + (t - b)*c = x + (t - y)*z - a + t*c - b*c = x + t*z - y*z - t*c - t*z = (x - y*z) - (a - b*c) - t*(c - z) = B(0) - A(0) - --]] - local A = a - b*c - local B = x - y*z - if c == z then - if not ((operator == "==" and A == B) - or (operator == "<" and A < B) - or (operator == "<=" and A <= B) - or (operator == ">" and A > B) - or (operator == ">=" and A >= B)) then + --[[ + A(t) = B(t) + a + (t - b)*c = x + (t - y)*z + a + t*c - b*c = x + t*z - y*z + t*c - t*z = (x - y*z) - (a - b*c) + t*(c - z) = B(0) - A(0) + --]] + local A = a - b*c + local B = x - y*z + if c == z then + if not ((operator == "==" and A == B) + or (operator == "!=" and A ~= B) + or (operator == "<" and A < B) + or (operator == "<=" and A <= B) + or (operator == ">" and A > B) + or (operator == ">=" and A >= B)) then + timeSpan:Reset() + end + else + local scratch = OvaleTimeSpan(self_timeSpanPool:Get()) + scratch:Reset(timeSpan) timeSpan:Reset() + local t = (B - A)/(c - z) + t = (t > 0) and t or 0 + Ovale:Logf("[%d] intersection at t = %f", element.nodeId, t) + if (c > z and operator == "<") + or (c > z and operator == "<=") + or (c < z and operator == ">") + or (c < z and operator == ">=") then + IntersectInterval(scratch, 0, t, timeSpan) + elseif (c < z and operator == "<") + or (c < z and operator == "<=") + or (c > z and operator == ">") + or (c > z and operator == ">=") then + IntersectInterval(scratch, t, math.huge, timeSpan) + end + self_timeSpanPool:Release(scratch) end - else - local scratch = OvaleTimeSpan(self_pool:Get()) - scratch:Reset(timeSpan) - timeSpan:Reset() - local t = (B - A)/(c - z) - t = (t > 0) and t or 0 - Ovale:Logf("t = %f", t) - if (c > z and operator == "<") - or (c > z and operator == "<=") - or (c < z and operator == ">") - or (c < z and operator == ">=") then - IntersectInterval(scratch, 0, t, timeSpan) - elseif (c < z and operator == "<") - or (c < z and operator == "<=") - or (c > z and operator == ">") - or (c > z and operator == ">=") then - IntersectInterval(scratch, t, math.huge, timeSpan) - end - self_pool:Release(scratch) + Ovale:Logf("[%d] compare '%s' returns %s", element.nodeId, operator, tostring(timeSpan)) end - Ovale:Logf("compare %s returns %s [%d]", operator, tostring(timeSpan), element.nodeId) profiler.Stop("OvaleBestAction_Compute") return timeSpan end -local function ComputeCustomFunction(element, state) +function OvaleBestAction:ComputeCustomFunction(element, state) profiler.Start("OvaleBestAction_Compute") - Ovale:Logf("custom function %s", element.name) - local self = OvaleBestAction - if not element.serial or element.serial < self_serial then - -- Cache new values in element. - element.timeSpanA, element.priorityA, element.elementA = self:Compute(element.a, state) - element.serial = self_serial - else - Ovale:Logf("Using cached values for %s", element.name) - end - - local timeSpanA, priorityA, elementA = element.timeSpanA, element.priorityA, element.elementA - local timeSpan = element.timeSpan - timeSpan:Reset() - - if element.params.asValue and element.params.asValue == 1 then - --[[ - Allow for the return value of a custom function to be "typecast" to a constant value. - - If the return value is a time span (a "boolean" value), then if the current time of - the simulation is within the time span, then return 1, or 0 otherwise. - - If the return value is a linear function, then if the current time of the simulation - is within the function's domain, then the function is simply evaluated at the current - time, or 0 otherwise. - - If the return value is an action, then return 1 if the action is off of cooldown, or - 0 if it is on cooldown. - --]] - local atTime = state.currentTime - local value = 0 - if HasTime(timeSpanA, atTime) then - if not elementA then -- boolean - value = 1 - elseif elementA.type == "value" then - value = elementA.value + (atTime - elementA.origin) * elementA.rate - elseif elementA.type == "action" then - value = 1 + local timeSpan = GetTimeSpan(element) + local priority, result + + local node = OvaleCompile:GetFunctionNode(element.name) + if node then + Ovale:Logf("[%d] evaluating function: %s(%s)", element.nodeId, node.name, node.paramsAsString) + local timeSpanA, priorityA, elementA = self:Compute(node.child[1], state) + if element.params.asValue == 1 or node.params.asValue == 1 then + --[[ + Allow for the return value of a custom function to be "typecast" to a constant value. + + If the return value is a time span (a "boolean" value), then if the current time of + the simulation is within the time span, then return 1, or 0 otherwise. + + If the return value is a linear function, then if the current time of the simulation + is within the function's domain, then the function is simply evaluated at the current + time, or 0 otherwise. + + If the return value is an action, then return 1 if the action is off of cooldown, or + 0 if it is on cooldown. + --]] + local atTime = state.currentTime + local value = 0 + if HasTime(timeSpanA, atTime) then + if not elementA then -- boolean + value = 1 + elseif elementA.type == "value" then + value = elementA.value + (atTime - elementA.origin) * elementA.rate + elseif elementA.type == "action" then + value = 1 + end end + Ovale:Logf("[%d] function '%s' typecast to value %f", element.nodeId, element.name, value) + timeSpan[1], timeSpan[2] = 0, math.huge + result = SetValue(element, value) + else + CopyTimeSpan(timeSpanA, timeSpan) + result = elementA end - timeSpan[1], timeSpan[2] = 0, math.huge - local result = PutValue(element, value, 0, 0) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan, priorityA, result - else - CopyTimeSpan(timeSpanA, timeSpan) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan, priorityA, elementA end + + profiler.Stop("OvaleBestAction_Compute") + return timeSpan, priority, result end -local function ComputeFunction(element, state) +function OvaleBestAction:ComputeFunction(element, state) profiler.Start("OvaleBestAction_ComputeFunction") - local timeSpan = element.timeSpan - timeSpan:Reset() - - if not OvaleCondition:IsCondition(element.func) then - Ovale:Errorf("Condition %s not found", element.func) - profiler.Stop("OvaleBestAction_ComputeFunction") - return timeSpan - end + local timeSpan = GetTimeSpan(element) + local priority, result + Ovale:Logf("[%d] evaluating condition: %s(%s)", element.nodeId, element.name, element.paramsAsString) local start, ending, value, origin, rate = OvaleCondition:EvaluateCondition(element.func, element.params) if start and ending then timeSpan[1], timeSpan[2] = start, ending end - - if Ovale.trace then - local conditionCall = element.func .. "(" - for k, v in pairs(element.params) do - conditionCall = conditionCall .. k .. "=" .. v .. "," - end - conditionCall = conditionCall .. ")" - Ovale:FormatPrint("Condition %s returned %s, %s, %s, %s, %s", conditionCall, start, ending, value, origin, rate) - end + Ovale:Logf("[%d] condition '%s' returns %s, %s, %s, %s, %s", element.nodeId, element.name, start, ending, value, origin, rate) --[[ Allow for the return value of a script condition to be "typecast" to a constant value @@ -532,7 +829,7 @@ local function ComputeFunction(element, state) is within the function's domain, then the function is simply evaluated at the current time, or 0 otherwise. --]] - if element.params.asValue and element.params.asValue == 1 then + if element.params.asValue == 1 then local atTime = state.currentTime if HasTime(timeSpan, atTime) then if value then @@ -543,44 +840,33 @@ local function ComputeFunction(element, state) else value = 0 end - origin, rate = 0, 0 + result = SetValue(element, value) timeSpan[1], timeSpan[2] = 0, math.huge + priority = OVALE_DEFAULT_PRIORITY + Ovale:Logf("[%d] condition '%s' typecast to value %f", element.nodeId, element.name, value) + elseif value then + result = SetValue(element, value, origin, rate) end - if value then - local result = PutValue(element, value, origin, rate) - profiler.Stop("OvaleBestAction_ComputeFunction") - return timeSpan, OVALE_DEFAULT_PRIORITY, result - else - profiler.Stop("OvaleBestAction_ComputeFunction") - return timeSpan - end + profiler.Stop("OvaleBestAction_ComputeFunction") + return timeSpan, priority, result end -local function ComputeGroup(element, state) +function OvaleBestAction:ComputeGroup(element, state) profiler.Start("OvaleBestAction_Compute") - local self = OvaleBestAction local bestTimeSpan, bestPriority, bestElement, bestCastTime - local timeSpan = element.timeSpan - timeSpan:Reset() + local timeSpan = GetTimeSpan(element) - Ovale:Logf("%s [%d]", element.type, element.nodeId) - - if #element.nodes == 1 then - profiler.Stop("OvaleBestAction_Compute") - return self:Compute(element.nodes[1], state) - end + local best = OvaleTimeSpan(self_timeSpanPool:Get()) + local current = OvaleTimeSpan(self_timeSpanPool:Get()) - local best = OvaleTimeSpan(self_pool:Get()) - local current = OvaleTimeSpan(self_pool:Get()) - - for k, v in ipairs(element.nodes) do - local currentTimeSpan, currentPriority, currentElement = self:Compute(v, state) + for _, node in ipairs(element.child) do + local currentTimeSpan, currentPriority, currentElement = self:Compute(node, state) -- We only care about actions that are available at time t > state.currentTime. current:Reset() IntersectInterval(currentTimeSpan, state.currentTime, math.huge, current) if Measure(current) > 0 then - Ovale:Logf(" group checking %s [%d]", tostring(current), element.nodeId) + Ovale:Logf("[%d] group checking %s", element.nodeId, tostring(current)) local currentCastTime if currentElement then currentCastTime = currentElement.castTime @@ -590,24 +876,24 @@ local function ComputeGroup(element, state) currentCastTime = gcd end - local replace = false + local currentIsBetter = false if Measure(best) == 0 then - Ovale:Logf(" group first best %s [%d]", tostring(current), element.nodeId) - replace = true + Ovale:Logf("[%d] group first best is %s", element.nodeId, tostring(current)) + currentIsBetter = true elseif not currentPriority or not bestPriority or currentPriority == bestPriority then -- If the spells have the same priority, then pick the one with an earlier cast time. local threshold = (bestElement and bestElement.params) and bestElement.params.wait or 0 if best[1] - current[1] > threshold then - Ovale:Logf(" group new best %s [%d]", tostring(current), element.nodeId) - replace = true + Ovale:Logf("[%d] group new best is %s", element.nodeId, tostring(current)) + currentIsBetter = true end elseif currentPriority > bestPriority then -- If the current spell has a higher priority than the best one found, then choose the -- higher priority spell if its cast is pushed back too far by the lower priority one. local threshold = (currentElement and currentElement.params) and currentElement.params.wait or (bestCastTime * 0.75) if current[1] - best[1] < threshold then - Ovale:Logf(" group new best (lower prio) %s [%d]", tostring(current), element.nodeId) - replace = true + Ovale:Logf("[%d] group new best (lower prio) is %s", element.nodeId, tostring(current)) + currentIsBetter = true end elseif currentPriority < bestPriority then -- If the current spell has a lower priority than the best one found, then choose the @@ -615,11 +901,11 @@ local function ComputeGroup(element, state) -- one by too much. local threshold = (bestElement and bestElement.params) and bestElement.params.wait or (currentCastTime * 0.75) if best[1] - current[1] > threshold then - Ovale:Logf(" group new best (higher prio) %s [%d]", tostring(current), element.nodeId) - replace = true + Ovale:Logf("[%d] group new best (higher prio) is %s", element.nodeId, tostring(current)) + currentIsBetter = true end end - if replace then + if currentIsBetter then best:Reset(current) bestTimeSpan = currentTimeSpan bestPriority = currentPriority @@ -631,39 +917,30 @@ local function ComputeGroup(element, state) end end - self_pool:Release(best) - self_pool:Release(current) + self_timeSpanPool:Release(best) + self_timeSpanPool:Release(current) - if not bestTimeSpan then - Ovale:Logf("group return %s [%d]", tostring(timeSpan), element.nodeId) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan - else - CopyTimeSpan(bestTimeSpan, timeSpan) - if bestElement then - local id = bestElement.value - if bestElement.params then - id = bestElement.params[1] - end - Ovale:Logf("group best action %s remains %s [%d]", id, tostring(timeSpan), element.nodeId) - else - Ovale:Logf("group no best action returns %s [%d]", tostring(timeSpan), element.nodeId) + CopyTimeSpan(bestTimeSpan, timeSpan) + if bestElement then + local id = bestElement.value + if bestElement.params then + id = bestElement.params[1] end - profiler.Stop("OvaleBestAction_Compute") - return timeSpan, bestPriority, bestElement + Ovale:Logf("[%d] group best action %s remains %s", element.nodeId, id, tostring(timeSpan)) + else + Ovale:Logf("[%d] group no best action returns %s", element.nodeId, tostring(timeSpan)) end + + profiler.Stop("OvaleBestAction_Compute") + return timeSpan, bestPriority, bestElement end -local function ComputeIf(element, state) +function OvaleBestAction:ComputeIf(element, state) profiler.Start("OvaleBestAction_Compute") - Ovale:Logf("%s [%d]", element.type, element.nodeId) - local self = OvaleBestAction - - local timeSpanA = self:ComputeBool(element.a, state) - local timeSpan = element.timeSpan - timeSpan:Reset() - - local conditionTimeSpan = OvaleTimeSpan(self_pool:Get()) + local timeSpanA = self:ComputeBool(element.child[1], state) + local timeSpan = GetTimeSpan(element) + local priority, result + local conditionTimeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) if element.type == "if" then conditionTimeSpan:Reset(timeSpanA) elseif element.type == "unless" then @@ -673,323 +950,102 @@ local function ComputeIf(element, state) -- Short-circuit evaluation of left argument to IF. if Measure(conditionTimeSpan) == 0 then timeSpan:Reset(conditionTimeSpan) - self_pool:Release(conditionTimeSpan) - Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId) - local result = PutValue(element, 0, 0, 0) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan, OVALE_DEFAULT_PRIORITY, result - end - - local timeSpanB, priorityB, elementB = self:Compute(element.b, state) - -- If the "then" clause is a "wait" node, then only wait if the conditions are true. - if elementB and elementB.wait and not HasTime(conditionTimeSpan, state.currentTime) then - elementB.wait = nil + Ovale:Logf("[%d] '%s' returns %s with zero measure", element.nodeId, element.type, tostring(timeSpan)) + priority = OVALE_DEFAULT_PRIORITY + result = SetValue(element, 0) + else + local timeSpanB, priorityB, elementB = self:Compute(element.child[2], state) + -- If the "then" clause is a "wait" node, then only wait if the conditions are true. + if elementB and elementB.wait and not HasTime(conditionTimeSpan, state.currentTime) then + elementB.wait = nil + end + -- Take intersection of the condition and B. + Intersect(conditionTimeSpan, timeSpanB, timeSpan) + Ovale:Logf("[%d] '%s' returns %s", element.nodeId, element.type, tostring(timeSpan)) + priority = priorityB + result = elementB end - -- Take intersection of the condition and B. - Intersect(conditionTimeSpan, timeSpanB, timeSpan) - self_pool:Release(conditionTimeSpan) + self_timeSpanPool:Release(conditionTimeSpan) - Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId) profiler.Stop("OvaleBestAction_Compute") - return timeSpan, priorityB, elementB + return timeSpan, priority, result end -local function ComputeLua(element, state) - profiler.Start("OvaleBestAction_ComputeLua") - local ret = loadstring(element.lua)() - Ovale:Logf("lua %s [%d]", ret, element.nodeId) - - local timeSpan = element.timeSpan - timeSpan:Reset() - - timeSpan[1], timeSpan[2] = 0, math.huge - local result = PutValue(element, ret, 0, 0) - profiler.Stop("OvaleBestAction_ComputeLua") - return timeSpan, OVALE_DEFAULT_PRIORITY, result -end - -local function ComputeNot(element, state) +function OvaleBestAction:ComputeLogical(element, state) profiler.Start("OvaleBestAction_Compute") - Ovale:Logf("%s [%d]", element.type, element.nodeId) - local self = OvaleBestAction - local timeSpanA = self:ComputeBool(element.a, state) - local timeSpan = element.timeSpan - timeSpan:Reset() - - Complement(timeSpanA, timeSpan) - Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId) + local timeSpanA = self:ComputeBool(element.child[1], state) + local timeSpan = GetTimeSpan(element) + + if element.operator == "and" then + -- Short-circuit evaluation of left argument to AND. + if Measure(timeSpanA) == 0 then + timeSpan:Reset(timeSpanA) + Ovale:Logf("[%d] logical '%s' short-circuits with zero measure left argument", element.nodeId, element.operator, tostring(timeSpan)) + else + local timeSpanB = self:ComputeBool(element.child[2], state) + -- Take intersection of A and B. + Intersect(timeSpanA, timeSpanB, timeSpan) + end + elseif element.operator == "not" then + Complement(timeSpanA, timeSpan) + elseif element.operator == "or" then + -- Short-circuit evaluation of left argument to OR. + if timeSpanA[1] == 0 and timeSpanA[1] == math.huge then + timeSpan:Reset(timeSpanA) + Ovale:Logf("[%d] logical '%s' short-circuits with universe as left argument", element.nodeId, element.operator, tostring(timeSpan)) + else + local timeSpanB = self:ComputeBool(element.child[2], state) + -- Take union of A and B. + Union(timeSpanA, timeSpanB, timeSpan) + end + end + + Ovale:Logf("[%d] logical '%s' returns %s", element.nodeId, element.operator, tostring(timeSpan)) profiler.Stop("OvaleBestAction_Compute") return timeSpan end -local function ComputeOr(element, state) - profiler.Start("OvaleBestAction_Compute") - Ovale:Logf("%s [%d]", element.type, element.nodeId) - local self = OvaleBestAction - local timeSpanA = self:ComputeBool(element.a, state) - local timeSpanB = self:ComputeBool(element.b, state) - local timeSpan = element.timeSpan - timeSpan:Reset() - - -- Take union of A and B. - Union(timeSpanA, timeSpanB, timeSpan) - Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId) - profiler.Stop("OvaleBestAction_Compute") - return timeSpan +function OvaleBestAction:ComputeLua(element, state) + profiler.Start("OvaleBestAction_ComputeLua") + local value = loadstring(element.lua)() + Ovale:Logf("[%d] lua returns %s", element.nodeId, value) + + local timeSpan = GetTimeSpan(element) + local priority, result + if value then + timeSpan[1], timeSpan[2] = 0, math.huge + result = SetValue(element, value) + priority = OVALE_DEFAULT_PRIORITY + end + profiler.Stop("OvaleBestAction_ComputeLua") + return timeSpan, priority, result end -local function ComputeValue(element, state) +function OvaleBestAction:ComputeValue(element, state) profiler.Start("OvaleBestAction_ComputeValue") - Ovale:Logf("value %s", element.value) - local timeSpan = element.timeSpan - timeSpan:Reset() - + Ovale:Logf("[%d] value is %s", element.nodeId, element.value) + local timeSpan = GetTimeSpan(element) timeSpan[1], timeSpan[2] = 0, math.huge profiler.Stop("OvaleBestAction_ComputeValue") return timeSpan, OVALE_DEFAULT_PRIORITY, element end -local function ComputeWait(element, state) +function OvaleBestAction:ComputeWait(element, state) profiler.Start("OvaleBestAction_Compute") - Ovale:Logf("%s [%d]", element.type, element.nodeId) - local self = OvaleBestAction - local timeSpanA, priorityA, elementA = self:Compute(element.a, state) - local timeSpan = element.timeSpan - timeSpan:Reset() + local timeSpanA, priorityA, elementA = self:Compute(element.child[1], state) + local timeSpan = GetTimeSpan(element) if elementA then elementA.wait = true CopyTimeSpan(timeSpanA, timeSpan) - Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId) + Ovale:Logf("[%d] '%s' returns %s", element.nodeId, element.type, tostring(timeSpan)) end profiler.Stop("OvaleBestAction_Compute") return timeSpan, priorityA, elementA end --- - --- -local OVALE_COMPUTE_VISITOR = -{ - ["action"] = ComputeAction, - ["and"] = ComputeAnd, - ["arithmetic"] = ComputeArithmetic, - ["compare"] = ComputeCompare, - ["customfunction"] = ComputeCustomFunction, - ["function"] = ComputeFunction, - ["group"] = ComputeGroup, - ["if"] = ComputeIf, - ["lua"] = ComputeLua, - ["not"] = ComputeNot, - ["or"] = ComputeOr, - ["unless"] = ComputeIf, - ["value"] = ComputeValue, - ["wait"] = ComputeWait, -} --- - --- -function OvaleBestAction:OnInitialize() - -- Resolve module dependencies. - OvaleActionBar = Ovale.OvaleActionBar - OvaleCooldown = Ovale.OvaleCooldown - OvaleCondition = Ovale.OvaleCondition - OvaleData = Ovale.OvaleData - OvaleEquipement = Ovale.OvaleEquipement - OvaleFuture = Ovale.OvaleFuture - OvalePower = Ovale.OvalePower - OvaleSpellBook = Ovale.OvaleSpellBook - OvaleStance = Ovale.OvaleStance -end - -function OvaleBestAction:StartNewAction(state) - state:Reset() - OvaleFuture:ApplyInFlightSpells(state) - self_serial = self_serial + 1 -end - -function OvaleBestAction:GetActionInfo(element, state) - if not element then - return nil - end - - profiler.Start("OvaleBestAction_GetActionInfo") - local target = element.params.target or OvaleCondition.defaultTarget - local action - local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, - actionUsable, actionShortcut, actionIsCurrent, actionEnable, - actionType, actionId - - if element.func == "spell" then - local spellId = element.params[1] - action = OvaleActionBar:GetForSpell(spellId) - if not OvaleSpellBook:IsKnownSpell(spellId) and not action then - Ovale:Logf("Spell %s not learnt", spellId) - profiler.Stop("OvaleBestAction_GetActionInfo") - return nil - end - - actionTexture = actionTexture or API_GetSpellTexture(spellId) - actionInRange = API_IsSpellInRange(OvaleSpellBook:GetSpellName(spellId), target) - actionCooldownStart, actionCooldownDuration, actionEnable = state:GetSpellCooldown(spellId) - actionType = "spell" - actionId = spellId - -- Verify that the spell may be cast given restrictions specified in SpellInfo(). - local si = OvaleData.spellInfo[spellId] - if si then - if si.stance and not OvaleStance:IsStance(si.stance) then - -- Spell requires a stance that player is not in. - profiler.Stop("OvaleBestAction_GetActionInfo") - return nil - end - if si.combo then - -- Spell requires combo points. - local cost = state:ComboPointCost(spellId) - if state.combo < cost then - profiler.Stop("OvaleBestAction_GetActionInfo") - return nil - end - end - for powerType in pairs(OvalePower.SECONDARY_POWER) do - if si[powerType] then - -- Spell requires "secondary" resources, e.g., chi, focus, rage, etc., - local cost = state:PowerCost(spellId, powerType) - if state[powerType] < cost then - profiler.Stop("OvaleBestAction_GetActionInfo") - return nil - end - end - end - - if actionCooldownStart and actionCooldownDuration then - -- Get the maximum time before all "primary" resources are ready. - local atTime = state.currentTime - for powerType in pairs(OvalePower.PRIMARY_POWER) do - if si[powerType] then - local t = state.currentTime + state:TimeToPower(spellId, powerType) - if atTime < t then - atTime = t - end - end - end - if actionCooldownStart > 0 then - if atTime > actionCooldownStart + actionCooldownDuration then - Ovale:Logf("Delaying spell %s for primary resource.", spellId) - actionCooldownDuration = atTime - actionCooldownStart - end - else - actionCooldownStart = state.currentTime - actionCooldownDuration = atTime - actionCooldownStart - end - - if si.blood or si.frost or si.unholy or si.death then - -- Spell requires runes. - local needRunes = true - -- "buff_runes_none" is the spell ID of the buff that makes casting the spell cost no runes. - local buffNoRunes = si.buff_runes_none - if buffNoRunes then - local aura = state:GetAura("player", buffNoRunes) - if state:IsActiveAura(aura) then - needRunes = false - end - end - if needRunes then - local ending = state.currentTime + state:GetRunesCooldown(si.blood, si.unholy, si.frost, si.death) - if ending > actionCooldownStart + actionCooldownDuration then - actionCooldownDuration = ending - actionCooldownStart - end - end - end - end - -- Use a custom texture if given. - if si.texture then - actionTexture = "Interface\\Icons\\" .. si.texture - end - end - - actionUsable = OvaleSpellBook:IsUsableSpell(spellId) - - elseif element.func == "macro" then - local macro = element.params[1] - action = OvaleActionBar:GetForMacro(macro) - if not action then - Ovale:Logf("Unknown macro %s", macro) - profiler.Stop("OvaleBestAction_GetActionInfo") - return nil - end - actionTexture = API_GetActionTexture(action) - actionInRange = API_IsActionInRange(action, target) - actionCooldownStart, actionCooldownDuration, actionEnable = API_GetActionCooldown(action) - actionUsable = API_IsUsableAction(action) - actionType = "macro" - actionId = macro - - elseif element.func == "item" then - local itemId = element.params[1] - if itemId and type(itemId) ~= "number" then - itemId = OvaleEquipement:GetEquippedItem(itemId) - end - if not itemId then - Ovale:Logf("Unknown item %s", element.params[1]) - profiler.Stop("OvaleBestAction_GetActionInfo") - return nil - end - Ovale:Logf("Item %s", itemId) - action = OvaleActionBar:GetForItem(itemId) - - actionTexture = API_GetItemIcon(itemId) - actionInRange = API_IsItemInRange(itemId, target) - actionCooldownStart, actionCooldownDuration, actionEnable = API_GetItemCooldown(itemId) - - local spellName = API_GetItemSpell(itemId) - actionUsable = spellName and API_IsUsableItem(action) - actionType = "item" - actionId = itemId - - elseif element.func == "texture" then - local texture = element.params[1] - actionTexture = "Interface\\Icons\\" .. texture - actionInRange = nil - actionCooldownStart = API_GetTime() - actionCooldownDuration = 0 - actionEnable = 1 - actionUsable = true - actionType = "texture" - actionId = texture - end - - if action then - actionShortcut = OvaleActionBar:GetBinding(action) - actionIsCurrent = API_IsCurrentAction(action) - end - - profiler.Stop("OvaleBestAction_GetActionInfo") - return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, - actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target, element.params.nored -end - -function OvaleBestAction:Compute(element, state) - if not element or (Ovale.bug and not Ovale.trace) then - return nil - end - - local visitor = OVALE_COMPUTE_VISITOR[element.type] - if visitor then - return visitor(element, state) - end - - Ovale:Logf("unknown element %s, return nil", element.type) - return nil -end - -function OvaleBestAction:ComputeBool(element, state) - local timeSpan, _, newElement = self:Compute(element, state) - -- Match SimC: 0 is false, non-zero is true. - -- (https://code.google.com/p/simulationcraft/wiki/ActionLists#Logical_operators) - if newElement and newElement.type == "value" and newElement.value == 0 and newElement.rate == 0 then - return nil - else - return timeSpan - end +function OvaleBestAction:Debug() + self_timeSpanPool:Debug() + self_valuePool:Debug() end -- diff --git a/OvaleCompile.lua b/OvaleCompile.lua index 4456bd4..f5efc2b 100644 --- a/OvaleCompile.lua +++ b/OvaleCompile.lua @@ -13,11 +13,8 @@ local OvaleCompile = Ovale:NewModule("OvaleCompile", "AceEvent-3.0") Ovale.OvaleCompile = OvaleCompile -- -local L = Ovale.L -local OvalePool = Ovale.OvalePool -local OvaleTimeSpan = Ovale.OvaleTimeSpan - -- Forward declarations for module dependencies. +local OvaleAST = nil local OvaleCondition = nil local OvaleCooldown = nil local OvaleData = nil @@ -32,913 +29,345 @@ local OvaleStance = nil local ipairs = ipairs local pairs = pairs local tonumber = tonumber -local strgmatch = string.gmatch -local strgsub = string.gsub -local strlen = string.len -local strlower = string.lower +local strfind = string.find local strmatch = string.match local strsub = string.sub -local tinsert = table.insert local wipe = table.wipe -local API_GetItemInfo = GetItemInfo -local API_GetSpellInfo = GetSpellInfo -- Profiling set-up. local Profiler = Ovale.Profiler local profiler = nil do local group = OvaleCompile:GetName() - - local function EnableProfiling() - API_GetItemInfo = Profiler:Wrap(group, "OvaleCompile_API_GetItemInfo", GetItemInfo) - API_GetSpellInfo = Profiler:Wrap(group, "OvaleCompile_API_GetSpellInfo", GetSpellInfo) - end - - local function DisableProfiling() - API_GetItemInfo = GetItemInfo - API_GetSpellInfo = GetSpellInfo - end - - Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling) + Profiler:RegisterProfilingGroup(group) profiler = Profiler:GetProfilingGroup(group) end -local self_node = {} -local self_pool = OvalePool("OvaleCompile_pool") -local self_timeSpanPool = OvalePool("OvaleCompile_timeSpanPool") -local self_defines = {} -local self_sharedCooldownNames = {} -local self_customFunctions = {} -local self_missingSpellList = {} --- table of functions called within the script: self_functionCalls[functionName] = node -local self_functionCalls = {} - -- 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_canCompile = false +local self_canEvaluate = false local self_requirePreload = { "OvaleEquipement", "OvaleSpellBook", "OvaleStance" } --- Current age of compilation state. +-- 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 compiled. -local self_compileCount = 0 --- Master nodes of the current script (one node for each icon) -local self_masterNodes = {} +-- 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 key=value pair, returning key and value. -local KEY_VALUE_PATTERN = "([%w_]+)=(!?[-%w\\_%.]+)" -- 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" - --- Parameters used as conditionals in script declarations. -local OVALE_PARAMETER = { - checkboxoff = true, - checkboxon = true, - glyph = true, - if_spell = true, - if_stance = true, - item = true, - itemcount = true, - itemset = true, - list = true, - mastery = true, - talent = true, -} - --- Known script functions other than conditions. -local OVALE_FUNCTIONS = { - item = true, - macro = true, - spell = true, - texture = true, -} -- -- --- Current age of the current compiled script. +-- Current age of the script; this advances every time the script is evaluated. OvaleCompile.serial = nil -OvaleCompile.customFunctionNode = {} +-- AST for the current script. +OvaleCompile.ast = nil -- -- -local function AddNode(node) - tinsert(self_node, node) - node.nodeId = #self_node - return "node" .. #self_node -end - --- Parse params string into key=value pairs and positional arguments stored in paramList table. -local function ParseParameters(params, paramList) - profiler.Start("OvaleCompile_ParseParameters") - paramList = paramList or {} - if params then - -- Handle key=value pairs. - for key, value in strgmatch(params, KEY_VALUE_PATTERN) do - if strmatch(key, NUMBER_PATTERN) then - key = tonumber(key) - end - if strmatch(value, NUMBER_PATTERN) then - value = tonumber(value) - end - paramList[key] = value - end - -- Strip out all key=value pairs and handle positional arguments. - params = strgsub(params, KEY_VALUE_PATTERN, "") - local k = 1 - for word in strgmatch(params, "[-%w_\\%.]+") do - if strmatch(word, NUMBER_PATTERN) then - word = tonumber(word) - end - paramList[k] = word - k = k + 1 - end - end - profiler.Stop("OvaleCompile_ParseParameters") - return paramList -end - local function HasTalent(talentId) if OvaleSpellBook:IsKnownTalent(talentId) then return OvaleSpellBook:GetTalentPoints(talentId) > 0 else - Ovale:FormatPrint("Unknown talent %s", talentId) + Ovale:FormatPrint("Warning: unknown talent ID '%s'", talentId) return false end end local function RequireValue(value) - local requireValue = (strsub(value, 1, 1) ~= "!") - if not requireValue then + 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, requireValue + return value, required end -local function TestConditions(paramList) +local function TestConditions(parameters) profiler.Start("OvaleCompile_TestConditions") local boolean = true - if boolean and paramList.glyph then - local glyph, requireGlyph = RequireValue(paramList.glyph) + if boolean and parameters.glyph then + local glyph, required = RequireValue(parameters.glyph) local hasGlyph = OvaleSpellBook:IsActiveGlyph(glyph) - if (requireGlyph and not hasGlyph) or (not requireGlyph and hasGlyph) then - boolean = false - end + boolean = (required and hasGlyph) or (not required and not hasGlyph) end - if boolean and paramList.mastery then - local spec, requireSpec = RequireValue(paramList.mastery) + if boolean and parameters.mastery then + local spec, required = RequireValue(parameters.mastery) local isSpec = OvalePaperDoll:IsSpecialization(spec) - if (requireSpec and not isSpec) or (not requireSpec and isSpec) then - boolean = false - end + boolean = (required and isSpec) or (not required and not isSpec) end - if boolean and paramList.if_stance then + if boolean and parameters.if_stance then self_compileOnStances = true - local stance, requireStance = RequireValue(paramList.if_stance) + local stance, required = RequireValue(parameters.if_stance) local isStance = OvaleStance:IsStance(stance) - if (requireStance and not isStance) or (not requireStance and isStance) then - boolean = false - end + boolean = (required and isStance) or (not required and not isStance) end - if boolean and paramList.if_spell then - local spell, requireSpell = RequireValue(paramList.if_spell) + if boolean and parameters.if_spell then + local spell, required = RequireValue(parameters.if_spell) local hasSpell = OvaleSpellBook:IsKnownSpell(spell) - if (requireSpell and not hasSpell) or (not requireSpell and hasSpell) then - boolean = false - end + boolean = (required and hasSpell) or (not required and not hasSpell) end - if boolean and paramList.talent then - local talent, requireTalent = RequireValue(paramList.talent) + if boolean and parameters.talent then + local talent, required = RequireValue(parameters.talent) local hasTalent = HasTalent(talent) - if (requireTalent and not hasTalent) or (not requireTalent and hasTalent) then - boolean = false - end + boolean = (required and hasTalent) or (not required and not hasTalent) end - if boolean and paramList.checkboxon then - local cb = paramList.checkboxon - if not Ovale.casesACocher[cb] then - Ovale.casesACocher[cb] = {} - end - Ovale.casesACocher[cb].compile = true - if not OvaleOptions:GetProfile().check[cb] then - boolean = false - end - end - if boolean and paramList.checkboxoff then - local cb = paramList.checkboxoff - if not Ovale.casesACocher[cb] then - Ovale.casesACocher[cb] = {} - end - Ovale.casesACocher[cb].compile = true - if OvaleOptions:GetProfile().check[cb] then - boolean = false - end - end - if boolean and paramList.list and paramList.item then - local list = paramList.list - local key = paramList.item - if not Ovale.listes[list] then - Ovale.listes[list] = { items = {}} - end - Ovale.listes[list].compile = true - if OvaleOptions:GetProfile().list[list] ~= key then - boolean = false - end - end - if boolean and paramList.itemset and paramList.itemcount then - local equippedCount = OvaleEquipement:GetArmorSetCount(paramList.itemset) + if boolean and parameters.itemset and parameters.itemcount then + local equippedCount = OvaleEquipement:GetArmorSetCount(parameters.itemset) self_compileOnItems = true - if equippedCount < paramList.itemcount then - boolean = false + boolean = (equippedCount >= parameters.itemcount) + end + do + local profile + if boolean and parameters.checkbox then + local name, required = RequireValue(parameters.checkbox) + local checkBox = Ovale.casesACocher[name] or {} + checkBox.compile = true + Ovale.casesACocher[name] = checkBox + -- Check the value of the checkbox. + profile = profile or OvaleOptions:GetProfile() + local isChecked = (profile.check[name] ~= nil) + boolean = (required and isChecked) or (not required and not isChecked) + end + -- XXX Deprecated: checkboxon + if boolean and parameters.checkboxon then + -- Flag this checkbox as triggering a script evaluation. + local name = parameters.checkboxon + local checkBox = Ovale.casesACocher[name] or {} + checkBox.compile = true + Ovale.casesACocher[name] = checkBox + -- Check the value of the checkbox. + profile = profile or OvaleOptions:GetProfile() + boolean = (profile.check[name] ~= nil) + end + -- XXX Deprecated: checkboxoff + if boolean and parameters.checkboxoff then + -- Flag this checkbox as triggering a script evaluation. + local name = parameters.checkboxon + local checkBox = Ovale.casesACocher[name] or {} + checkBox.compile = true + Ovale.casesACocher[name] = checkBox + -- Check the value of the checkbox. + profile = profile or OvaleOptions:GetProfile() + boolean = (profile.check[name] == nil) + end + if boolean and parameters.list and parameters.item then + -- Flag this list as triggering a script evaluation. + local name = parameters.list + local item, required = RequireValue(parameters.item) + local list = Ovale.listes[name] or { items = {}, default = nil } + list.compile = true + Ovale.listes[name] = list + -- 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) end end profiler.Stop("OvaleCompile_TestConditions") return boolean end -local function ParseNumber(dummy, value) - local node = self_pool:Get() - node.type = "value" - node.value = tonumber(value) - node.origin = 0 - node.rate = 0 - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return dummy..AddNode(node) -end - -local function ParseFunction(prefix, func, params) - local paramList = ParseParameters(params) - if func ~= "" then - paramList.target = prefix - else - func = prefix - end - - if not paramList.target then - if strsub(func, 1, 6) == "Target" then - paramList.target = "target" - func = strsub(func, 7) - end - end - - if self_customFunctions[func] then - self_functionCalls[func] = self_customFunctions[func] - return self_customFunctions[func] - end - - func = strlower(func) - - -- "debuff" and "buff" conditions implicitly set their aura filter. - if not paramList.filter then - if strsub(func, 1, 6) == "debuff" then - paramList.filter = "debuff" - elseif strsub(func, 1, 4) == "buff" then - paramList.filter = "buff" - elseif strsub(func, 1, 11) == "otherdebuff" then - paramList.filter = "debuff" - elseif strsub(func, 1, 9) == "otherbuff" then - paramList.filter = "buff" - end - end - - local node = self_pool:Get() - if func == "spell" or func == "macro" or func == "item" or func == "texture" then - node.type = "action" - else - node.type = "function" - end - node.func = func - node.params = paramList - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - local nodeName = AddNode(node) - self_functionCalls[func] = node - - local mine = true - if paramList.any then - mine = false - end - - local spellId = paramList[1] - if spellId then - -- For the conditions that refer to player's spells, check if the spell ID - -- is a variant of a spell with the same name as one already in the - -- spellbook. If it is, then add that variant spell ID to our spellList. - if OvaleCondition:IsSpellbookCondition(func) then - if not OvaleSpellBook:IsKnownSpell(spellId) and not self_missingSpellList[spellId] and not self_sharedCooldownNames[spellId] then - local spellName - if type(spellId) == "number" then - spellName = API_GetSpellInfo(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) - self_missingSpellList[spellId] = spellName - end - else - Ovale:DebugPrintf(OVALE_UNKNOWN_SPELL_DEBUG, "Unknown spell with ID %s", spellId) - end - end - end - end - - return nodeName -end - ---[[ - Parse the various Spell*{Buff,Debuff}() declarations. - Check for test conditions to see whether this declaration is active. - Filter out then test conditions and copy the rest of the key=value pairs - into the aura table. ---]] -local function ParseSpellAuraList(auraTable, filter, paramList) - if TestConditions(paramList) then - paramList[1] = nil - if not auraTable[filter] then - for k, v in pairs(paramList) do - if OVALE_PARAMETER[k] then - paramList[k] = nil - end - end - auraTable[filter] = paramList - else - local tbl = auraTable[filter] - for k, v in pairs(paramList) do - if not OVALE_PARAMETER[k] then - tbl[k] = v - 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. + --]] + if not Ovale.casesACocher[name] then + self_serial = self_serial + 1 + end + local checkBox = Ovale.casesACocher[name] or {} + checkBox.text = node.description.value + for _, v in ipairs(parameters) do + if v == "default" then + checkBox.checked = true + break end end + Ovale.casesACocher[name] = checkBox end - return "" -end - -local function ParseSpellAddBuff(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - local si = OvaleData:SpellInfo(spellId) - return ParseSpellAuraList(si.aura.player, "HELPFUL", paramList) -end - -local function ParseSpellAddDebuff(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - local si = OvaleData:SpellInfo(spellId) - return ParseSpellAuraList(si.aura.player, "HARMFUL", paramList) -end - -local function ParseSpellAddTargetBuff(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - local si = OvaleData:SpellInfo(spellId) - return ParseSpellAuraList(si.aura.target, "HELPFUL", paramList) + return ok end -local function ParseSpellAddTargetDebuff(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - local si = OvaleData:SpellInfo(spellId) - return ParseSpellAuraList(si.aura.target, "HARMFUL", paramList) -end - -local function ParseSpellDamageBuff(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - local si = OvaleData:SpellInfo(spellId) - return ParseSpellAuraList(si.aura.damage, "HELPFUL", paramList) -end - -local function ParseSpellDamageDebuff(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - local si = OvaleData:SpellInfo(spellId) - return ParseSpellAuraList(si.aura.damage, "HARMFUL", paramList) -end - -local function ParseSpellInfo(params) - local paramList = ParseParameters(params) - local spellId = paramList[1] - if spellId and TestConditions(paramList) then - local si = OvaleData:SpellInfo(spellId) - for k,v in pairs(paramList) do - if k == "addduration" then - si.duration = si.duration + v - elseif k == "addcd" then - si.cd = si.cd + v - elseif k == "addlist" then - -- Add this buff to the named spell list. - if not OvaleData.buffSpellList[v] then - OvaleData.buffSpellList[v] = {} - end - OvaleData.buffSpellList[v][spellId] = true - elseif k == "sharedcd" then - OvaleCooldown:AddSharedCooldown(v, spellId) - self_sharedCooldownNames[v] = true - else - si[k] = v - end - end +local function EvaluateAddIcon(node) + local ok = true + if TestConditions(node.params) then + self_icon[#self_icon + 1] = node end - return "" + return ok end -local function ParseScoreSpells(params) - for v in strgmatch(params, "(%d+)") do - local spellId = tonumber(v) - if spellId then - OvaleScore:AddSpell(spellId) - else - Ovale:FormatPrint("ScoreSpell with unknown spell %s", v) +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. + --]] + if not (Ovale.listes[name] and Ovale.listes[name][item]) then + self_serial = self_serial + 1 end - end -end - -local function ParseSpellList(name, params) - OvaleData.buffSpellList[name] = {} - for v in strgmatch(params, "(%d+)") do - v = tonumber(v) - if v then - OvaleData.buffSpellList[name][v] = true + local list = Ovale.listes[name] 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.listes[name] = list end + return ok end -local function ParseItemInfo(params) - local paramList = ParseParameters(params) - local itemId = paramList[1] - if itemId and TestConditions(paramList) then - for k, v in pairs(paramList) do +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(paramList.buff) + local buff = tonumber(parameters.buff) if buff then - local listName = "item_proc_" .. v - if not OvaleData.buffSpellList[listName] then - OvaleData.buffSpellList[listName] = {} - end - OvaleData.buffSpellList[listName][buff] = true + 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 "" -end - -local function ParseItemList(name, params) - OvaleData.itemList[name] = {} - local i = 1 - for v in strgmatch(params, "(%d+)") do - OvaleData.itemList[name][i] = tonumber(v) - i = i + 1 - end -end - -local function ParseIf(a, b) - local node = self_pool:Get() - node.type = "if" - node.a = self_node[tonumber(a)] - node.b = self_node[tonumber(b)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseUnless(a, b) - local node = self_pool:Get() - node.type = "unless" - node.a = self_node[tonumber(a)] - node.b = self_node[tonumber(b)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseWait(a) - local node = self_pool:Get() - node.type = "wait" - node.a = self_node[tonumber(a)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseAnd(a,b) - local node = self_pool:Get() - node.type = "and" - node.a = self_node[tonumber(a)] - node.b = self_node[tonumber(b)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseNot(a) - local node = self_pool:Get() - node.type = "not" - node.a = self_node[tonumber(a)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseOr(a,b) - local node = self_pool:Get() - node.type = "or" - node.a = self_node[tonumber(a)] - node.b = self_node[tonumber(b)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local ParseOp -do - local operator = { - ["+"] = "arithmetic", - ["-"] = "arithmetic", - ["*"] = "arithmetic", - ["/"] = "arithmetic", - ["%"] = "arithmetic", - ["<"] = "compare", - ["<="] = "compare", - ["=="] = "compare", - [">="] = "compare", - [">"] = "compare", - } - - function ParseOp(a, op, b) - local node = self_pool:Get() - node.type = operator[op] - node.operator = op - node.a = self_node[tonumber(a)] - node.b = self_node[tonumber(b)] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) - end -end - -local function ParseGroup(text) - local nodes = {} - - for w in strgmatch(text, "node(%d+)") do - tinsert(nodes, self_node[tonumber(w)]) - end - - text = strgsub(text, "node%d+", "") - - if (strmatch(text,"[^ ]")) then - Ovale:FormatPrint("syntax error: %s", text) - return nil - end - - local node = self_pool:Get() - node.type = "group" - node.nodes = nodes - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseAddListItem(list, item, text, params) - local paramList = ParseParameters(params) - if not TestConditions(paramList) then - return "" - end - if (not Ovale.listes[list]) then - Ovale.listes[list] = {items={},default=nil} - end - Ovale.listes[list].items[item] = text - if paramList[1] and paramList[1] == "default" then - Ovale.listes[list].default=item - end - return "" -end - -local function ParseAddCheckBox(item, text, params) - local paramList = ParseParameters(params) - if not TestConditions(paramList) then - return "" - end - if not Ovale.casesACocher[item] then - Ovale.casesACocher[item] = {} - end - Ovale.casesACocher[item].text = text - if paramList[1] and paramList[1]=="default" then - Ovale.casesACocher[item].checked = true - end - return "" + return ok end -local function ParseDefine(key, value) - self_defines[key] = value - return "" -end - -local function ReplaceDefine(key) - return self_defines[key] -end - -local function ParseLua(text) - local node = self_pool:Get() - node.type = "lua" - node.lua = strsub(text, 2, strlen(text)-1) - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) -end - -local function ParseInclude(name) - local code - local script = OvaleScripts.script[name] - if script then - code = script.code - end - if not code then - Ovale:FormatPrint("Cannot Include(...): script named \"%s\" not found", name) - end - return code or "" -end - -local function ParseCommands(text) - local original = text - text = strgsub(text,"(%b[])", ParseLua) - - while true do - local was = text - text = strgsub(text, "(%w+)%.?(%w*)%s*%((.-)%)", ParseFunction) - text = strgsub(text, "([^%w])(%d+%.?%d*)", ParseNumber) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then - break - end - end - - while true do - local was = text - while true do - local was = text - text = strgsub(text, "node(%d+)%s*([%*%/%%])%s*node(%d+)", ParseOp) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then - break - end - end - while true do - local was = text - text = strgsub(text, "node(%d+)%s*([%+%-])%s*node(%d+)", ParseOp) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then - break - end - end - if was == text then - break - 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 - - while true do - local was = text - text = strgsub(text, "node(%d+)%s*([%>%<]=?)%s*node(%d+)", ParseOp) - text = strgsub(text, "node(%d+)%s*(==)%s*node(%d+)", ParseOp) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then - break - end - end - - while true do - local was = text - while true do - local was = text - text = strgsub(text, "not%s+node(%d+)", ParseNot) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then - break - end - end - while true do - local was = text - text = strgsub(text, "node(%d+)%s+and%s+node(%d+)", ParseAnd) - text = strgsub(text, "node(%d+)%s+or%s+node(%d+)", ParseOr) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then - break - end - end - if was == text then + 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 - while true do - local was = text - text = strgsub(text, "if%s+node(%d+)%s+node(%d+)",ParseIf) - text = strgsub(text, "unless%s+node(%d+)%s+node(%d+)",ParseUnless) - text = strgsub(text, "wait%s+node(%d+)",ParseWait) - text = strgsub(text, "{([node%d ]*)}", ParseGroup) - if was == text then +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 - - local nodeId - if text then - nodeId = tonumber(strmatch(text, "node(%d+)")) - end - if nodeId then - -- If there is anything other than spaces, this is a syntax error. - text = strgsub(text, "node%d+", "", 1) - if strmatch(text,"[^ ]") then - Ovale:FormatPrint("Group: %s", original) - Ovale:FormatPrint("syntax error: %s", text) - nodeId = nil - end - else - Ovale:Print("no master node") - end - return nodeId + return ok end -local function ParseAddFunction(name, params, text) - local paramList = ParseParameters(params) - if TestConditions(paramList) then - local nodeId = ParseCommands(text) - if nodeId then - local node = self_pool:Get() - node.type = "customfunction" - node.name = name - node.params = paramList - node.a = self_node[nodeId] - node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get()) - return AddNode(node) +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 - end -end - -local function ParseAddIcon(params, text, secure) - local paramList = ParseParameters(params) - if TestConditions(paramList) then - local masterNodeId = ParseCommands(text) - if masterNodeId then - local masterNode = self_node[masterNodeId] - masterNode.params = paramList - masterNode.secure = secure - return masterNode + if count > 0 then + auraTable[filter] = tbl end end + return ok end -local function ParseItemName(text) - local itemId = tonumber(text) - if itemId then - local item = API_GetItemInfo(itemId) or "Item " .. itemId - return '"' .. item .. '"' - else - Ovale:FormatPrint("ItemName of %s unknown\n", text) - return nil - end -end - -local function ParseSpellName(text) - local spellId = tonumber(text) - local spell = OvaleSpellBook:GetSpellName(spellId) - if spell then - return '"' .. spell .. '"' - else - Ovale:FormatPrint("SpellName of %s unknown", text) - return nil - end -end - -local function ParseL(text) - return '"'..L[text]..'"' -end - --- On compile les AddCheckBox et AddListItem -local function CompileInputs(text) - Ovale.casesACocher = {} - Ovale.listes = {} - - text = strgsub(text, "AddListItem%s*%(%s*([%w_]+)%s+([%w_]+)%s+\"(.-)\"%s*(.-)%s*%)", ParseAddListItem) - text = strgsub(text, "AddCheckBox%s*%(%s*([%w_]+)%s+\"(.-)\"%s*(.-)%s*%)", ParseAddCheckBox) - return text -end - --- Compile non-function and non-icon declarations. -local function CompileDeclarations(text) - -- Define(CONSTANTE valeur) - text = strgsub(text, "Define%s*%(%s*([%w_]+)%s+([%w_.=]+)%s*%)", ParseDefine) - -- On remplace les constantes par leur valeur - text = strgsub(text, "([%w_]+)", ReplaceDefine) - - -- Fonctions - text = strgsub(text, "ItemName%s*%(%s*(%w+)%s*%)", ParseItemName) - text = strgsub(text, "SpellName%s*%(%s*(%w+)%s*%)", ParseSpellName) - text = strgsub(text, "L%s*%(%s*(%w+)%s*%)", ParseL) - - -- Options diverses - OvaleData:ResetSpellInfo() - text = strgsub(text, "SpellAddBuff%s*%((.-)%)", ParseSpellAddBuff) - text = strgsub(text, "SpellAddDebuff%s*%((.-)%)", ParseSpellAddDebuff) - text = strgsub(text, "SpellAddTargetBuff%s*%((.-)%)", ParseSpellAddTargetBuff) - text = strgsub(text, "SpellAddTargetDebuff%s*%((.-)%)", ParseSpellAddTargetDebuff) - text = strgsub(text, "SpellDamageBuff%s*%((.-)%)", ParseSpellDamageBuff) - text = strgsub(text, "SpellDamageDebuff%s*%((.-)%)", ParseSpellDamageDebuff) - text = strgsub(text, "SpellInfo%s*%((.-)%)", ParseSpellInfo) - text = strgsub(text, "ItemInfo%s*%((.-)%)", ParseItemInfo) - text = strgsub(text, "ScoreSpells%s*%((.-)%)", ParseScoreSpells) - text = strgsub(text, "SpellList%s*%(%s*([%w_]+)%s*(.-)%)", ParseSpellList) - text = strgsub(text, "ItemList%s*%(%s*([%w_]+)%s*(.-)%)", ParseItemList) - - -- On vire les espaces en trop - text = strgsub(text, "\n", " ") - text = strgsub(text, "%s+", " ") - - return text -end - -local function CompileScript(text) - profiler.Start("OvaleCompile_CompileScript") - local self = OvaleCompile - self_compileOnItems = false - self_compileOnStances = false - Ovale.bug = false - - wipe(self_defines) - wipe(self_sharedCooldownNames) - wipe(self_customFunctions) - wipe(self_missingSpellList) - wipe(self_functionCalls) - wipe(self.customFunctionNode) - OvaleCooldown:ResetSharedCooldowns() - - -- Return all existing nodes to the node pool. - for i, node in pairs(self_node) do - self_node[i] = nil - self_timeSpanPool:Release(node.timeSpan) - self_pool:Release(node) - end - wipe(self_node) - - -- Loop and strip out comments and replace Include() directives until there - -- are no more inclusions to make. - while true do - local was = text - text = strgsub(text, "#.-\n","") - text = strgsub(text, "#.*$","") - text = strgsub(text, "Include%s*%(%s*([%w_]+)%s*%)", ParseInclude) - if was == text then - break - end - end - - text = CompileDeclarations(text) - text = CompileInputs(text) - - for name, p, t in strgmatch(text, "AddFunction%s+(%w+)%s*(.-)%s*(%b{})") do - local node = ParseAddFunction(name, p, t) - if node then - self_customFunctions[name] = node - local nodeId = strmatch(node, "node(%d+)") - self.customFunctionNode[name] = self_node[tonumber(nodeId)] - end - end - - -- On compile les AddIcon - wipe(self_masterNodes) - - for p,t in strgmatch(text, "AddActionIcon%s*(.-)%s*(%b{})") do - local node = ParseAddIcon(p,t,true) - if node then - tinsert(self_masterNodes, node) - end - end - - for p,t in strgmatch(text, "AddIcon%s*(.-)%s*(%b{})") do - local node = ParseAddIcon(p,t) - if node then - tinsert(self_masterNodes, node) - end - end - - -- Verify that all the functions called within the script are defined. - for p, v in pairs(self_functionCalls) do - if not (OVALE_FUNCTIONS[p] or self_customFunctions[p] or OvaleCondition:IsCondition(p)) then - Ovale:Errorf("Unknown function call: %s (node%s)", p, v.nodeId) +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 - - -- Add any missing spells found while compiling the script into the spellbook. - for k, v in pairs(self_missingSpellList) do - OvaleSpellBook:AddSpell(k, v) - end - profiler.Stop("OvaleCompile_CompileScript") + return ok end -- -- function OvaleCompile:OnInitialize() -- Resolve module dependencies. + OvaleAST = Ovale.OvaleAST OvaleCondition = Ovale.OvaleCondition OvaleCooldown = Ovale.OvaleCooldown OvaleData = Ovale.OvaleData @@ -952,15 +381,15 @@ function OvaleCompile:OnInitialize() end function OvaleCompile:OnEnable() - self:RegisterMessage("PLAYER_REGEN_ENABLED") self:RegisterMessage("Ovale_CheckBoxValueChanged", "EventHandler") self:RegisterMessage("Ovale_EquipmentChanged") self:RegisterMessage("Ovale_GlyphsChanged", "EventHandler") self:RegisterMessage("Ovale_ListValueChanged", "EventHandler") - self:RegisterMessage("Ovale_ScriptChanged", "EventHandler") + 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() @@ -972,11 +401,6 @@ function OvaleCompile:OnDisable() self:UnregisterMessage("Ovale_SpellsChanged") self:UnregisterMessage("Ovale_StanceChanged") self:UnregisterMessage("Ovale_TalentsChanged") - self_pool:Drain() -end - -function OvaleCompile:PLAYER_REGEN_ENABLED(event) - self_pool:Drain() end function OvaleCompile:Ovale_EquipmentChanged(event) @@ -992,93 +416,92 @@ function OvaleCompile:Ovale_StanceChanged(event) end function OvaleCompile:EventHandler(event) - Ovale:DebugPrint(OVALE_COMPILE_DEBUG, event) - -- Advance age of current compilation state. + -- 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:Compile() - self_canCompile = self_canCompile or Ovale:IsPreloaded(self_requirePreload) - if self_canCompile then - local profile = OvaleOptions:GetProfile() - local source = profile.source - local code - if source and OvaleScripts.script[source] then - code = OvaleScripts.script[source].code - else - code = "" +function OvaleCompile:CompileScript(event) + 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 + self:EventHandler(event) +end + +function OvaleCompile:EvaluateScript() + profiler.Start("OvaleCompile_EvaluateScript") + self_canEvaluate = self_canEvaluate or Ovale:IsPreloaded(self_requirePreload) + if self_canEvaluate and self.ast 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 + + -- 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 + Ovale:UpdateFrame() end - CompileScript(code) - self_compileCount = self_compileCount + 1 - Ovale:UpdateFrame() end + profiler.Stop("OvaleCompile_EvaluateScript") end function OvaleCompile:GetFunctionNode(name) - return self.customFunctionNode[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:GetMasterNodes() - -- Compile the script if it is outdated. +function OvaleCompile:GetIconNodes() + -- Evaluate the script if it is outdated. if not self.serial or self.serial < self_serial then self.serial = self_serial - self:Compile() + self:EvaluateScript() end - return self_masterNodes + return self_icon end -function OvaleCompile:Debug(iconNumber) - iconNumber = iconNumber or 1 - self_pool:Debug() - local masterNodes = self:GetMasterNodes() - Ovale:Print(self:DebugNode(masterNodes[iconNumber])) - Ovale:FormatPrint("Total number of script compilations: %d", self_compileCount) -end - -function OvaleCompile:DebugNode(node) - local text - if (not node) then - return "#nil" - end - if (node.type == "group") then - text = "{" - for k,n in ipairs(node.nodes) do - text = text .. self:DebugNode(n) .. " " - end - text = text .. "}\n" - elseif (node.type == "action" or node.type == "function") then - text = node.func.."(" - for k,p in pairs(node.params) do - text = text .. k.."=" .. p .. " " - end - text = text .. ")" - elseif (node.type == "customfunction") then - text = self:DebugNode(node.a) - elseif (node.type == "if") then - text = "if "..self:DebugNode(node.a).." "..self:DebugNode(node.b) - elseif (node.type == "unless") then - text = "unless "..self:DebugNode(node.a).." "..self:DebugNode(node.b) - elseif (node.type == "wait") then - text = "wait "..self:DebugNode(node.a) - elseif (node.type == "and") then - text = self:DebugNode(node.a).." and "..self:DebugNode(node.b) - elseif (node.type == "or") then - text = self:DebugNode(node.a).." or "..self:DebugNode(node.b) - elseif (node.type == "not") then - text = "not "..self:DebugNode(node.a) - elseif node.type == "compare" then - text = self:DebugNode(node.a)..node.operator..self:DebugNode(node.b) - elseif node.type == "arithmetic" then - text = self:DebugNode(node.a)..node.operator..self:DebugNode(node.b) - elseif node.type == "lua" then - text = "["..node.lua.."]" - elseif node.type == "value" then - text = node.value - else - text = "#unknown node type "..node.type.."#" - end - - return text +function OvaleCompile:Debug() + Ovale:FormatPrint("Total number of times the script was evaluated: %d", self_timesEvaluated) end -- diff --git a/OvaleFrame.lua b/OvaleFrame.lua index b5cebd0..ec62251 100644 --- a/OvaleFrame.lua +++ b/OvaleFrame.lua @@ -171,26 +171,25 @@ do local refresh = forceRefresh or next(Ovale.refreshNeeded) if not refresh then return end - local masterNodes = OvaleCompile:GetMasterNodes() - if not masterNodes then return end + local iconNodes = OvaleCompile:GetIconNodes() + if not iconNodes then return end self.lastUpdate = now local state = OvaleState.state state:Initialize() - for k,node in pairs(masterNodes) do - local target + for k, node in ipairs(iconNodes) do + -- Set the true target of "target" references in the icon's body. if node.params and node.params.target then - target = node.params.target + OvaleCondition.defaultTarget = node.params.target else - target = "target" + OvaleCondition.defaultTarget = "target" end - OvaleCondition.defaultTarget = target if refresh then - Ovale:Logf("****Master Node %d", k) + Ovale:Logf("+++ Icon %d", k) OvaleBestAction:StartNewAction(state) - local timeSpan, _, element = OvaleBestAction:Compute(node, state) + local timeSpan, _, element = OvaleBestAction:Compute(node.child[1], state) local start = NextTime(timeSpan, state.currentTime) if start then Ovale:Logf("Compute start = %f", start) @@ -213,8 +212,11 @@ do else local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration, actionUsable, actionShortcut, actionIsCurrent, actionEnable, - actionType, actionId, actionTarget, noRed = OvaleBestAction:GetActionInfo(element, state) - if noRed then + actionType, actionId, actionTarget = OvaleBestAction:GetActionInfo(element, state) + + -- Use the start time of the best action instead of the intersection of its start time + -- with any conditions used to determine the best action. + if element and element.params and element.params.nored == 1 then start = actionCooldownStart + actionCooldownDuration if start < state.currentTime then start = state.currentTime @@ -238,9 +240,7 @@ do action.spellId = nil end if start and start <= now and actionUsable then - if not action.waitStart then - action.waitStart = now - end + action.waitStart = action.waitStart or now else action.waitStart = nil end @@ -269,7 +269,7 @@ do spellTarget = OvaleCondition.defaultTarget end state:ApplySpell(spellId, OvaleGUID:GetGUID(spellTarget)) - timeSpan, _, element = OvaleBestAction:Compute(node, state) + timeSpan, _, element = OvaleBestAction:Compute(node.child[1], state) start = NextTime(timeSpan, state.currentTime) icons[2]:Update(element, start, OvaleBestAction:GetActionInfo(element, state)) else @@ -313,14 +313,14 @@ do local maxWidth = 0 local top = 0 - local masterNodes = OvaleCompile:GetMasterNodes() - if not masterNodes then return end + local iconNodes = OvaleCompile:GetIconNodes() + if not iconNodes then return end local BARRE = 8 local margin = profile.apparence.margin - for k,node in pairs(masterNodes) do + for k, node in ipairs(iconNodes) do if not self.actions[k] then self.actions[k] = {icons={}, secureIcons={}} end diff --git a/OvaleOptions.lua b/OvaleOptions.lua index 8da2c4d..742d031 100644 --- a/OvaleOptions.lua +++ b/OvaleOptions.lua @@ -544,12 +544,6 @@ local self_options = desc = L["Debug GUID"], type = "toggle", }, - missing_spells = - { - name = "Missing spells", - desc = L["Debug missing spells"], - type = "toggle", - }, paper_doll = { name = "Paper doll updates", @@ -562,12 +556,6 @@ local self_options = desc = L["Debug stat snapshots"], type = "toggle", }, - unknown_spells = - { - name = "Unknown spells", - desc = L["Debug unknown spells"], - type = "toggle", - }, }, get = function(info) return OvaleOptions.db.profile.debug[info[#info]] end, set = function(info, value) OvaleOptions.db.profile.debug[info[#info]] = value end, diff --git a/conditions/Damage.lua b/conditions/Damage.lua index 42e5242..890eea3 100644 --- a/conditions/Damage.lua +++ b/conditions/Damage.lua @@ -27,7 +27,7 @@ do local name = si[paramName] local node = OvaleCompile:GetFunctionNode(name) if node then - local timeSpan, priority, element = OvaleBestAction:Compute(node, state) + local timeSpan, priority, element = OvaleBestAction:Compute(node.child[1], state) if element and element.type == "value" then local value = element.value + (state.currentTime - element.origin) * element.rate return value diff --git a/conditions/LastEstimatedDamage.lua b/conditions/LastEstimatedDamage.lua index 0c7d623..fe9d95f 100644 --- a/conditions/LastEstimatedDamage.lua +++ b/conditions/LastEstimatedDamage.lua @@ -29,7 +29,7 @@ do local name = si[paramName] local node = OvaleCompile:GetFunctionNode(name) if node then - local timeSpan, priority, element = OvaleBestAction:Compute(node, state) + local timeSpan, priority, element = OvaleBestAction:Compute(node.child[1], state) if element and element.type == "value" then local value = element.value + (state.currentTime - element.origin) * element.rate return value -- 1.7.9.5