You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2018 lines
70 KiB

-- Scripts.lua
-- December 2014
local addon, ns = ...
local Hekili = _G[ addon ]
local class = Hekili.Class
local scripts = Hekili.Scripts
local state = Hekili.State
local GetResourceInfo, GetResourceID = ns.GetResourceInfo, ns.GetResourceID
local SpaceOut = ns.SpaceOut
local ceil = math.ceil
local orderedPairs = ns.orderedPairs
local roundUp = ns.roundUp
local safeMax = ns.safeMax
local format, trim = string.format, string.trim
local twipe, insert = table.wipe, table.insert
-- Forgive the name, but this should properly replace ! characters with not, accounting for appropriate bracketing.
-- Why so complex? Because "! 0 > 1" converted to Lua is "not 0 > 1" which evaluates to "false > 1" -- not the goal.
-- This should convert:
-- example 1: ! 0 > 1
-- not ( 0 > 1 )
--
-- example 2: ! ( 0 > 1 & ! ( false | true ) )
-- not ( 0 > 1 & not ( false | true ) )
--
-- example 3: ! cooldown.x.remains > 1 * ( gcd * ( 8 % 3 ) )
-- not ( cooldown.x.remains > 1 * ( gcd * ( 8 % 3 ) ) )
--
-- Hopefully.
local exprBreak = {
["&"] = true,
["|"] = true,
}
local function forgetMeNots( str )
-- First, handle already bracketed "!(X)" -> "not (X)".
local found = 1
while found > 0 do
str, found = str:gsub( "%s*!%s*(%b())%s*", " not %1 " )
end
-- The remaining conditions are not bracketed, but may include brackets.
-- Such as !5>2+(1*3).
-- So we'll start from the !, then go through the string until it's time to stop.
local i = 1
local substring
while( str:find("!") ) do
local start = str:find("!")
--while str:sub( start, start ):match("%s") do
-- start = start + 1
-- end
local parens = 0
local finish = -1
for j = start, str:len() do
local char = str:sub( j, j )
if char == "(" then
parens = parens + 1
elseif char == ")" then
if parens > 0 then parens = parens - 1
else finish = j - 1; break end
elseif parens == 0 then
-- We are not within a bracketed part of the string. We can end here.
if exprBreak[ char ] then
finish = j - 1
break
end
end
end
if finish == -1 then finish = str:len() end
substring = str:sub( start + 1, finish )
substring = substring:trim()
str = format( "%s not ( %s ) %s", str:sub( 1, start - 1 ) or "", substring, str:sub( finish + 1, str:len() ) or "" )
i = i + 1
if i >= 100 then Hekili:Debug( "Was unable to convert '!' to 'not' in string [%s].", str ); break end
end
str = str:gsub( "%s%s", " " )
return str
end
local function atToAbs( str )
-- First, handle already bracketed "!(X)" -> "not (X)".
local found = 1
while found > 0 do
str, found = str:gsub( "%s*@%s*(%b())%s*", " abs(safenum%1) " )
end
-- The remaining conditions are not bracketed, but may include brackets.
-- Such as !5>2+(1*3).
-- So we'll start from the !, then go through the string until it's time to stop.
local i = 1
local substring
while( str:find("@") ) do
local start = str:find("@")
--while str:sub( start, start ):match("%s") do
-- start = start + 1
-- end
local parens = 0
local finish = -1
for j = start, str:len() do
local char = str:sub( j, j )
if char == "(" then
parens = parens + 1
elseif char == ")" then
if parens > 0 then parens = parens - 1
else finish = j - 1; break end
elseif parens == 0 then
-- We are not within a bracketed part of the string. We can end here.
if exprBreak[ char ] then
finish = j - 1
break
end
end
end
if finish == -1 then finish = str:len() end
substring = str:sub( start + 1, finish )
substring = substring:trim()
str = format( "%s abs( %s ) %s", str:sub( 1, start - 1 ) or "", substring, str:sub( finish + 1, str:len() ) or "" )
i = i + 1
if i >= 100 then Hekili:Error( "Was unable to convert '@' to 'abs' in string [%s].", str ); break end
end
str = str:gsub( "%s%s", " " )
return str
end
local mathBreak = {
["<"] = true,
[">"] = true,
["="] = true,
["&"] = true,
["|"] = true,
[","] = true,
}
local function HandleDeprecatedOperators( str, opStr, prefix )
--str = str:gsub("%s", "")
for left, op, right in str:gmatch("(.+)(" .. opStr .. ")(.+)") do
local leftLen, rightLen = left:len(), right:len()
local val1, val2, len1, len2, b1, b2
if left:sub(-1) == ")" then
val1 = left:match("(%b())$")
len1 = val1:len()
val1 = val1:sub( 2, -2 )
else
-- We need to traverse the left side, backwards.
local parens = 0
local eos = -1
for i = 1, leftLen do
local char = left:sub(-i, -i)
if char == ")" then
-- Grab the full bracketed pair and move on.
i = i + left:sub( 1, 1 + leftLen - i ):match( "(%b())$" ):len()
elseif mathBreak[ char ] or char == "(" then
eos = i - 1
break
end
end
if eos == -1 then
val1 = left
len1 = leftLen
else
val1 = left:sub( 1 + leftLen - eos, leftLen):trim()
len1 = eos
end
end
val1 = val1:trim()
if right:sub(1, 1) == "(" then
val2 = right:match("^(%b())")
len2 = val2:len()
val2 = val2:sub( 2, -2 )
else
local parens = 0
local eos = -1
for i = 1, right:len() do
local char = right:sub(i, i)
if char == "(" then
i = i + right:sub( i ):match("^(%b())" ):len()
elseif mathBreak[char] or char == ")" then
eos = i - 1
break
end
end
if eos == -1 then
val2 = right
len2 = rightLen
else
val2 = right:sub(1, eos)
len2 = eos
end
end
val2 = val2:trim()
str = left:sub( 1, leftLen - len1 ) .. " " .. prefix .. "(safenum(" .. val1 .. "),safenum(" .. val2 .. ")) " .. right:sub( 1 + len2 )
end
if str:find(opStr) then return scripts.HandleDeprecatedOperators( str, opStr, prefix ) end
return str
end
scripts.HandleDeprecatedOperators = HandleDeprecatedOperators
local invalid = "([^a-zA-Z0-9_.[])"
local function extendExpression( str, expr, suffix )
if str:find( expr ) then
str = str:gsub( "^" .. expr .. invalid, expr .. "." .. suffix .. "%1" )
str = str:gsub( invalid .. expr .. "$", "%1" .. expr .. "." .. suffix )
str = str:gsub( "^" .. expr .. "$", expr .. "." .. suffix )
str = str:gsub( invalid .. expr .. invalid, "%1" .. expr .. "." .. suffix .. "%2" )
end
return str
end
local function SimcWithResources( str )
for k in pairs( GetResourceInfo() ) do
if str:find( k ) then
str = extendExpression( str, k, "current" )
end
end
if str:find( "health" ) then
str = extendExpression( str, "health", "current" )
end
if str:find( "rune" ) then
str = extendExpression( str, "rune", "current" )
end
if str:find( "spell_targets" ) then
str = extendExpression( str, "spell_targets", "any" )
end
if str:find( "gcd" ) then
str = extendExpression( str, "gcd", "execute" )
end
return str
end
local function space_killer(s)
return s:gsub("%s", "")
end
local function HandleLanguageIncompatibilities( str )
-- Address equipped.number => equipped[number]
str = str:gsub("%.(%d+)%.", "[%1].")
str = str:gsub("equipped%.(%d+)", "equipped[%1]")
str = str:gsub("main_hand%.(%d[a-z0-9_]+)", "main_hand['%1']")
str = str:gsub("off_hand%.(%d[a-z0-9_]+)", "off_hand['%1']")
str = str:gsub("lowest_vuln_within%.(%d+)", "lowest_vuln_within[%1]")
str = str:gsub("%.in([^a-zA-Z0-9_])", "['in']%1" )
str = str:gsub("%.in$", "['in']" )
str = str:gsub("imps_spawned_during%.([^!=<>&|]+)", "imps_spawned_during['%1'] ")
str = str:gsub("time_to_imps%.(%b()).remains", "time_to_imps[%1].remains")
str = str:gsub("time_to_imps%.(%d+).remains", "time_to_imps[%1].remains")
-- str = str:gsub("incanters_flow_time_to%.(%d+)[.any]?", "incanters_flow_time_to[%1]")
str = str:gsub("prev%.(%d+)", "prev[%1]")
str = str:gsub("prev_gcd%.(%d+)", "prev_gcd[%1]")
str = str:gsub("prev_off_gcd%.(%d+)", "prev_off_gcd[%1]")
str = str:gsub("time_to_sht%.(%d+)", "time_to_sht[%1]")
str = str:gsub("time_to_sht_plus%.(%d+)", "time_to_sht_plus[%1]")
-- str = str:gsub("([a-z0-9_]+)%.(%d+)", "%1[%2]")
return str
end
-- Convert SimC syntax to Lua conditionals.
local function SimToLua( str, modifier )
-- If no conditions were provided, function should return true.
if not str or type( str ) == "number" then return str end
local orig = str
str = str:trim()
if str == "" then return orig end
-- Strip comments.
str = str:gsub("^%-%-.-\n", "")
-- Replace '!' with ' not '.
str = str:gsub( "!=", "~=" )
if str:find("!") then str = forgetMeNots( str ) end
if str:find("@") then str = atToAbs( str ) end
-- Replace '^' (simc XOR) with '~=' (functionally identical for booleans).
if str:find("%^") then str = str:gsub("%^", "~=") end
-- Replace '>?' and '<?' with max/min.
if str:find("<%?") then str = HandleDeprecatedOperators( str, "<%?", "max" ) end
if str:find(">%?") then str = HandleDeprecatedOperators( str, ">%?", "min" ) end
str = SimcWithResources( str )
-- Replace '%' for division with actual division operator '/'.
-- str = str:gsub("([^%%])[ ]+%%[ ]+([^%%])", "%1/%2")
-- Replace '%%' for modulus with '%'.
-- str = str:gsub( "%%%%", "%%" )
-- Replace '&' with ' and '.
str = str:gsub("&", " and ")
-- Replace '|' with ' or '.
str = str:gsub("||", " or "):gsub("|", " or ")
if not modifier then
-- Replace assignment '=' with comparison '=='
str = str:gsub("([^=])=([^=])", "%1==%2" )
-- Fix any conditional '==' that got impacted by previous.
str = str:gsub("==+", "==")
str = str:gsub(">=+", ">=")
str = str:gsub("<=+", "<=")
str = str:gsub("!=+", "~=")
str = str:gsub("~=+", "~=")
end
-- Condense whitespace.
str = str:gsub("%s%s", " ")
-- Condense parenthetical spaces.
str = str:gsub("[(][%s+]", "("):gsub("[%s+][)]", ")")
-- Condense bracketed expressions.
str = str:gsub("%b[]", space_killer)
return HandleLanguageIncompatibilities( str )
end
scripts.SimToLua = SimToLua
do
-- Okay, this is the auto-recheck parser.
-- Part I: Split into parts.
-- ex. combo_points<5&energy>=action.rake.cost&dot.rake.pmultiplier<2.1&buff.tigers_fury.up&(buff.bloodtalons.up|!talent.bloodtalons.enabled)&(!talent.incarnation.enabled|cooldown.incarnation.remains>18)&!buff.incarnation.up
-- ...and break it into each compartmentalized expression:
-- combo_points<5, energy>=action.rake.cost, dot.rake_multiplier<2.1.
local boundaries = {
["&"] = true,
["|"] = true
}
function scripts:SplitExpr( str )
local output = {}
while( str:len() > 0 ) do
local finish = str:len()
local parens = 0
for i = 1, str:len() do
local char = str:sub( i, i )
if char == "(" then parens = parens + 1
elseif char == ")" then
if parens > 0 then parens = parens - 1 end
elseif boundaries[ char ] and parens == 0 then
finish = i - 1
break
end
end
local expr = str:sub( 1, finish )
local meat = expr:match( "(%b())" )
if meat then
meat = meat:sub( 2, -2 )
if meat:find( "[|&]" ) then
local subExpr = scripts:SplitExpr( meat )
for _, v in ipairs( subExpr ) do
table.insert( output, v )
end
else
table.insert( output, expr )
end
else
table.insert( output, expr )
end
str = str:sub( finish + 2, str:len() )
end
return output
end
local timely = {
{ "^(d?e?buff%.[a-z0-9_]+)%.down$" , "%1.remains" },
{ "^(dot%.[a-z0-9_]+)%.down$" , "%1.remains" },
{ "^!(d?e?buff%.[a-z0-9_]+)%.up$" , "%1.remains" },
{ "^!(dot%.[a-z0-9_]+)%.up$" , "%1.remains" },
{ "^!(d?e?buff%.[a-z0-9_]+)%.react$" , "%1.remains" },
{ "^!(dot%.[a-z0-9_]+)%.react$" , "%1.remains" },
{ "^!(d?e?buff%.[a-z0-9_]+)%.ticking$" , "%1.remains" },
{ "^!(dot%.[a-z0-9_]+)%.ticking$" , "%1.remains" },
{ "^!?(d?e?buff%.[a-z0-9_]+)%.remains$", "%1.remains" },
{ "^!ticking" , "remains" },
{ "^!?remains$" , "remains" },
{ "^!up$" , "remains" },
{ "^down$" , "remains" },
{ "^refreshable$" , "time_to_refresh" },
{ "^time>=?(.-)$" , "0.01+%1-time" },
{ "^gcd.remains$" , "gcd.remains" },
{ "^gcd.remains<?=(.+)$" , "gcd.remains-%1" },
{ "^swing.([a-z_]+).remains$", "swing.%1.remains" },
{ "^(.-)%.deficit<=?(.-)$" , "0.01+%1.timeTo(%1.max-(%2))" },
{ "^(.-)%.deficit>=?(.-)$" , "0.01+%1.timeTo(%1.max-(%2))" },
{ "^cooldown%.([a-z0-9_]+)%.ready$" , "cooldown.%1.remains" },
{ "^cooldown%.([a-z0-9_]+)%.up$" , "cooldown.%1.remains" },
{ "^!?cooldown%.([a-z0-9_]+)%.remains$" , "cooldown.%1.remains" },
{ "^charges_fractional=(.-)$" , "(%1-charges_fractional)*recharge" },
{ "^charges_fractional>=?(.-)$" , "0.01+(%1-charges_fractional)*recharge" },
{ "^charges=(.-)$" , "(%1-charges_fractional)*recharge" },
{ "^charges>=?(.-)$" , "0.01+(%1-charges_fractional)*recharge" },
{ "^(cooldown%.[a-z0-9_]+)%.charges_fractional[>=]+(.-)$", "(%2-%1.charges_fractional)*%1.recharge" },
{ "^(cooldown%.[a-z0-9_]+)%.charges>=?(.-)$" , "(1+%2-%1.charges_fractional)*recharge" },
{ "^(action%.[a-z0-9_]+)%.charges_fractional[>=]+(.-)$" , "(%2-%1.charges_fractional)*%1.recharge" },
{ "^(action%.[a-z0-9_]+)%.charges>=?(.-)$" , "(1+%2-%1.charges_fractional)*%1.recharge" },
{ "^full_recharge_time[<>]=?(.-)$" , "0.01+full_recharge_time-%1" },
{ "^!(action%.[a-z0-9]+)%.executing$" , "%1.execute_remains" },
{ "^!(action%.[a-z0-9]+)%.channeling$" , "%1.channel_remains" },
{ "^(.-time_to_die)<=?(.-)$" , "%1-%2" },
{ "^(.-)%.time_to_(.-)<=?(.-)$", "%1.time_to_%2-%3" },
{ "^debuff%.festering_wound%.stack[>=]=?(.-)$" , "time_to_wounds(%1)" },
{ "^dot%.festering_wound%.stack[>=]=?(.-)$" , "time_to_wounds(%1)" },
{ "^rune<=?(.-)$" , "rune.timeTo(%1)" },
{ "^rune>=?(.-)$" , "rune.timeTo(1+%1)" },
{ "^rune.current<=?(.-)$" , "rune.timeTo(%1)" },
{ "^rune.current>=?(.-)$" , "rune.timeTo(1+%1)" },
{ "^master_assassin_remains[<=]+(.-)$" , "0.01+master_assassin_remains-(%1)" },
{ "^exsanguinated$" , "remains" }, -- Assassination
{ "^!?(debuff%.[a-z0-9_]+)%.exsanguinated$" , "%1.remains" }, -- Assassination
{ "^!?(dot%.[a-z0-9_]+)%.exsanguinated$" , "%1.remains" }, -- Assassination
{ "^ss_buffed$" , "remains" }, -- Assassination
{ "^!?(debuff%.[a-z0-9_]+)%.ss_buffed$" , "%1.remains" }, -- Assassination
{ "^!?(dot%.[a-z0-9_]+)%.ss_buffed$" , "%1.remains" }, -- Assassination
{ "^dot%.([a-z0-9_]+).haste_pct_next_tick$" , "0.01+query_time+(dot.%1.last_tick+dot.%1.tick_time)-query_time" }, -- Assassination
{ "^!?stealthed%.(.-)$" , "stealthed.%1_remains" },
{ "^!?time_to_hpg$" , "time_to_hpg" }, -- Retribution Paladin
{ "^!?time_to_hpg[<=]=?(.-)$" , "time_to_hpg-%1" }, -- Retribution Paladin
{ "^!?consecration.up" , "consecration.remains" }, -- Prot Paladin
{ "^!?contagion<=?(.-)" , "contagion-%1" }, -- Affliction Warlock
{ "^time_to_imps%.(.+)$" , "time_to_imps[%1]" }, -- Demo Warlock
{ "^active_bt_triggers$" , "time_to_bt_triggers(0)" }, -- Feral Druid w/ Bloodtalons.
{ "^active_bt_triggers<?=0$" , "time_to_bt_triggers(0)" }, -- Feral Druid w/ Bloodtalons.
{ "^active_bt_triggers<(%d+)$" , "time_to_bt_triggers(%1-1)" }, -- Feral Druid w/ Bloodtalons.
{ "^!?action%.([a-z0-9_]+)%.in_flight$" , "action.%1.in_flight_remains" }, -- Fire Mage, but others too, potentially.
{ "^!?action%.([a-z0-9_]+)%.in_flight_remains<=?(.-)$", "action.%1.in_flight_remains-%2" }, -- Fire Mage, but others too, potentially.
{ "^!?fiery_brand_dot_primary_remains$", "fiery_brand_dot_primary_remains" }, -- Vengeance
{ "^!?fiery_brand_dot_primary_ticking$", "fiery_brand_dot_primary_remains" }, -- Vengeance
{ "^!?variable%.([a-z0-9_]+)$", "safenum(variable.%1)" },
{ "^!?variable%.([a-z0-9_]+)<=?(.-)$", "0.01+%2-safenum(variable.%1)" },
{ "^raid_events%.([a-z0-9_]+)%.remains$", "raid_events.%1.remains" },
{ "^raid_events%.([a-z0-9_]+)%.remains$<=?(.-)$", "raid_events.%1.remains-%2" },
{ "^!?raid_events%.([a-z0-9_]+)%.up$", "raid_events.%1.up" },
{ "^!?(pet%.[a-z0-9_]+)%.up$", "%1.remains" },
{ "^!?(pet%.[a-z0-9_]+)%.active$", "%1.remains" },
{ "^(action%.[a-z0-9_]+)%.ready$", "%1.ready_time" }
}
-- Things that tick down.
local decreases = {
["remains$"] = true,
["ticks_remain$"] = true,
["execute_remains$"] = true,
["channel_remains$"] = true,
["^time_to_hpg$"] = true,
["time_to_max$"] = true,
["remains_expected$"] = true,
["expiration_delay_remains$"] = true,
-- ["time_to_%d+$"] = true,
-- ["deficit$"] = true,
}
-- Things that tick up.
local increases = {
["^time$"] = true,
-- ["charges"] = true,
-- ["charges_fractional"] = true,
}
local removals = {
["%.current"] = ""
}
local lessOrEqual = {
-- ["<"] = true,
["<="] = true,
["="] = true,
["=="] = true,
}
local moreOrEqual = {
-- [">"] = true,
[">="] = true,
["="] = true,
["=="] = true,
}
-- Given an expression, can we assess whether it is time-based and progressing in a meaningful way?
-- 1. Cooldowns
local function ConvertTimeComparison( expr, verbose )
while expr:match( "^%b()$" ) do
expr = expr:sub( 2, -2 )
end
local orig = expr
for k, v in pairs( removals ) do
expr = expr:gsub( k, v )
end
local lhs, comp, rhs = expr:match( "^(.-)([<>=~?]+)(.-)$" )
if comp and comp:match( "?" ) then
comp = nil
end
if lhs and comp and rhs then
-- We are looking at a mathematic comparison.
for key in pairs( decreases ) do
if lhs:match( key ) then
if comp == "<" then
return true, "(" .. lhs .. " + 0.01) - (" .. rhs .. ")"
elseif lessOrEqual[ comp ] then
return true, lhs .. " - " .. rhs
end
end
end
for key in pairs( increases ) do
if lhs:match( key ) then
if comp == ">" then
return true, "(" .. rhs .. " + 0.01) - (" .. lhs .. ")"
elseif moreOrEqual[ comp ] then
return true, rhs .. " - " .. lhs
end
end
end
-- resources also tick up (usually, anyway)
for key in pairs( GetResourceInfo() ) do
if lhs == key then
if comp == ">" then
return true, "0.01 + " .. lhs .. ".timeTo( " .. rhs .. " )"
elseif moreOrEqual[ comp ] then
return true, lhs .. ".timeTo( " .. rhs .. " )"
end
end
if rhs == key then
if comp == "<" then
return true, "0.01 + " .. rhs .. ".timeTo( " .. lhs .. " )"
elseif lessOrEqual[ comp ] then
return true, rhs .. ".timeTo( " .. lhs .. " )"
end
end
end
if lhs == "rune" then
if comp == ">" then
return true, "0.01 + rune.timeTo( " .. rhs .. " )"
elseif moreOrEqual[ comp ] then
return true, "rune.timeTo( " .. rhs .. " )"
end
end
if rhs == "rune" then
if comp == "<" then
return true, "0.01 + rune.timeTo( " .. lhs .. " )"
elseif lessOrEqual[ comp ] then
return true, "rune.timeTo( " .. lhs .. " )"
end
end
--[[ if comp:match( "<=?" ) then
return true, lhs .. " - " .. rhs .. " + 0.01"
end
if comp:match( ">=?" ) then
return true, rhs .. " - " .. lhs .. " + 0.01"
end ]]
end
-- If we didn't convert a resource.current to resource.timeTo then let's revert our string.
expr = orig
for i, swap in ipairs( timely ) do
if expr:match( swap[1] ) then
return true, expr:gsub( swap[1], swap[2]), swap[1]
end
end
return false, nil
end
scripts.CTC = ConvertTimeComparison
function scripts:RecheckExpr( expr )
return ConvertTimeComparison( expr )
end
function scripts:BuildRecheck( conditions )
if type( conditions ) ~= "string" then return end
local recheck
conditions = conditions:gsub( " +", "" )
conditions = self:EmulateSyntax( conditions, true )
local exprs = self:SplitExpr( conditions )
if #exprs > 0 then
for i, expr in ipairs( exprs ) do
local converted, calc, why = ConvertTimeComparison( expr )
if converted then
calc = SimToLua( calc )
calc = self:EmulateSyntax( calc, true )
recheck = ( recheck and ( recheck .. ", " ) or "" ) .. calc
end
end
end
return recheck
end
local ops = {
["+"] = true,
["-"] = true,
["*"] = true,
["/"] = true,
["%"] = true,
["|"] = true,
["&"] = true,
["<"] = true,
[">"] = true,
["?"] = true,
["="] = true,
["~"] = true,
["!"] = true,
["!="] = true,
["~="] = true,
["@"] = true
}
local math_ops = {
["+"] = true,
["-"] = true,
["*"] = true,
["/"] = true,
["%"] = true,
["<"] = true,
[">"] = true,
-- ["="] = true,
-- ["!="] = true,
-- ["~="] = true,
["<="] = true,
[">="] = true,
[">?"] = true,
["<?"] = true,
}
local equality = {
["="] = true,
["!="] = true,
["~="] = true,
}
local comp_ops = {
["<"] = true,
[">"] = true,
["?"] = true,
}
local bool_ops = {
["|"] = true,
["&"] = true,
["!"] = true,
}
local funcs = {
["floor"] = true,
["ceil"] = true
}
-- This is hideous.
local esDepth = 0
local esString
function scripts:EmulateSyntax( p, numeric )
if not p or type( p ) ~= "string" then return p end
if esDepth == 0 then
esString = p
end
esDepth = esDepth + 1
local results = {}
local i, maxlen = 1, p:len()
local depth = 0
local bracketed = p:match("(%b())" ) == p
if bracketed then
p = p:sub( 2, p:len() - 1 )
end
local ands = p:find( " and " )
local ors = p:find( " or " )
local nots = p:find( " not " )
local abss = p:find( " abs " )
if ands then p = p:gsub( " and ", "&" ) end
if ors then p = p:gsub( " or ", "|" ) end
if nots then p = p:gsub( " not ", "!" ) end
if abss then p = p:gsub( " abs ", "@" ) end
p = p:gsub( "([!%|&%-%+%*=%%/<>%?%~@]+) +", "%1" )
p = p:gsub( " +([!%|&%-%+%*=%%/<>%?%~@]+)", "%1" )
local orig = p
while ( i <= maxlen ) do
local c = p:sub( i, i )
if c == " " or c == "," then -- do nothing
elseif c == "(" then depth = depth + 1
elseif c == ")" and depth > 0 then
depth = depth - 1
if depth == 0 then
local expr = p:sub( 1, i )
table.insert( results, {
s = expr:trim(),
t = "expr"
} )
if expr:find( "[&%|%-%+/%%%*]" ) ~= nil then results[#results].r = true end
p = p:sub( i + 1 )
i = 0
depth = 0
maxlen = p:len()
end
elseif depth == 0 and ops[c] then
if i > 1 then
local expr = p:sub( 1, i - 1 )
table.insert( results, {
s = expr:trim(),
t = "expr"
} )
if expr:find( "[&$|$-$+/$%%*]" ) ~= nil then results[#results].r = true end
end
c = p:sub( i ):match( "^([&%|%-%+*%%/><!%?=%~@][&%|%-%+*/><%?=%~]?)" )
table.insert( results, {
s = c,
t = "op",
a = c:trim() --sub(1,1)
} )
p = p:sub( i + c:len() )
i = 0
depth = 0
maxlen = p:len()
end
i = i + 1
end
p = p:trim()
if p:len() > 0 then
table.insert( results, {
s = p:trim(),
t = "expr",
l = true
} )
if p:find( "[!&%|%-%+/%%%*@]" ) ~= nil then results[#results].r = true end
end
local output = ""
-- So at this point, we've broken our string into all of its components. Now let's iterate through and fix it up.
i = 1
while( i <= #results ) do
local prev, piece, next = i > 1 and results[i-1] or nil, results[i], i < #results and results[i+1] or nil
local trimmed_prefix
-- If we get a math op (*) followed by a not (!) followed by an expression, we want to safely wrap up the !expr in safenum().
if prev and prev.t == "op" and math_ops[ prev.a ] and piece.t == "op" and piece.a == "!" and next and next.t == "expr" then
table.remove( results, i )
piece = results[ i ]
next = results[ i + 1 ]
piece.s = "(!" .. piece.s .. ")"
elseif piece.t == "expr" and piece.s:match( "^%s*[a-z0-9_]+%s*%(" ) then
trimmed_prefix = piece.s:match( "^%s*([a-z0-9_]+)%s*%(" )
piece.s = piece.s:gsub( "^%s*" .. trimmed_prefix .. "%s*", "" )
end
if piece and piece.t == "expr" then
if piece.r then
if piece.s == orig then
if bracketed then orig = "(" .. orig .. ")" end
if ands then orig = orig:gsub( "&", " and " ) end
if ors then orig = orig:gsub( "|", " or " ) end
if nots then orig = orig:gsub( "!", " not " ) end
if abss then orig = orig:gsub( "@", " abs " ) end
esDepth = esDepth - 1
return orig
end
piece.s = scripts:EmulateSyntax( piece.s, numeric )
end
if ( prev and prev.t == "op" and math_ops[ prev.a ] and not equality[ prev.a ] ) or ( next and next.t == "op" and math_ops[ next.a ] and not equality[ next.a ] ) then
-- This expression is getting mathed.
-- Lets see what it returns and wrap it in btoi if it is a boolean expr.
if piece.s:find("^variable%.") then
-- Let's wrap the variable just to be sure.
if piece.s:sub(1, 1) == "(" then piece.s = "safenum" .. piece.s
else piece.s = "safenum(" .. piece.s .. ")" end
else
local func, warn = Hekili:Loadstring( "return " .. ( SimToLua( piece.s ) or "" ) )
if func then
setfenv( func, state )
state:SetDefaultVariable( 1 )
local pass, val = pcall( func )
state:SetDefaultVariable( 0 )
if not pass and not piece.s:match("variable") then
local safepiece = piece.s:gsub( "%%", "%%%%" )
Hekili:Error( "Unable to compile '" .. safepiece:gsub("%%", "%%%%") .. "' - " .. val .. " (pcall-n)\n\nFrom: " .. esString:gsub( "%%", "%%%%" ) )
else
if trimmed_prefix ~= "safenum" and ( val == nil or type( val ) ~= "number" ) then piece.s = "safenum(" .. piece.s .. ")" end
end
else
Hekili:Error( "Unable to compile '" .. ( piece.s ):gsub("%%","%%%%") .. "' - " .. warn .. " (loadstring-n)\nFrom: " .. esString:gsub( "%%", "%%%%" ) )
end
end
piece.r = nil
elseif not numeric and ( not prev or ( prev.t == "op" and not math_ops[ prev.a ] and not equality[ prev.a ] ) ) and ( not next or ( next.t == "op" and not math_ops[ next.a ] and not equality[ next.a ] ) ) then
-- This expression is not having math operations performed on it.
-- Let's make sure it's a boolean.
if piece.s:find("^variable") then
if piece.s:sub(1, 1) == "(" then piece.s = "safebool" .. piece.s
else piece.s = "safebool(" .. piece.s .. ")" end
else
local func, warn = Hekili:Loadstring( "return " .. ( SimToLua( piece.s ) or "" ) )
if func then
setfenv( func, state )
state:SetDefaultVariable( 1 )
local pass, val = pcall( func )
state:SetDefaultVariable( 0 )
if not pass and not piece.s:match("variable") then
local safepiece = piece.s:gsub( "%%", "%%%%" )
Hekili:Error( "Unable to compile '" .. safepiece:gsub("%%", "%%%%") .. "' - " .. val .. " (pcall-b)\nFrom: " .. esString:gsub( "%%", "%%%%" ) )
else
if trimmed_prefix ~= "safebool" and ( val == nil or type( val ) == "number" ) then
piece.s = "safebool(" .. piece.s .. ")"
end
end
else
Hekili:Error( "Unable to compile '" .. ( piece.s ):gsub("%%","%%%%") .. "' - " .. warn .. " (loadstring-b)." )
end
end
piece.r = nil
end
end
if trimmed_prefix then
piece.s = trimmed_prefix .. piece.s
end
output = output .. piece.s
i = i + 1
end
if bracketed then output = "(" .. output .. ")" end
if ands then output = output:gsub( "&", " and " ) end
if ors then output = output:gsub( "|", " or " ) end
if nots then output = output:gsub( "!", " not " ) end
if abss then output = output:gsub( "@", " abs" ) end
-- output = output:gsub( " ", " " )
-- output = output:gsub( "not (safenum(", "safenum(not (" )
-- output = output:gsub( "not safebool(", "safebool(not " )
output = output:gsub( "!safenum(%b())", "safenum(!%1)" )
output = output:gsub( "@safebool", "@safenum" )
output = output:gsub( "!%((%b())%)", "!%1" )
esDepth = esDepth - 1
return output
end
end
-- Convert SimC syntax to Lua conditionals.
local function SimCToSnapshot( str, modifier )
-- If no conditions were provided, function should return true.
if not str or str == '' then return nil end
if type( str ) == 'number' then return str end
str = str:trim()
-- Strip comments.
str = str:gsub("^%-%-.-\n", "")
-- Replace '!' with ' not '.
-- str = forgetMeNots( str )
str = SimcWithResources( str )
-- Replace '%' for division with actual division operator '/'.
-- str = str:gsub("%%", "/")
-- Replace '&' with ' and '.
-- str = str:gsub("&", " and ")
-- Replace '|' with ' or '.
-- str = str:gsub("||", " or "):gsub("|", " or ")
--[[ if not modifier then
-- Replace assignment '=' with comparison '=='
str = str:gsub("([^=])=([^=])", "%1==%2" )
-- Fix any conditional '==' that got impacted by previous.
str = str:gsub("==+", "==")
str = str:gsub(">=+", ">=")
str = str:gsub("<=+", "<=")
str = str:gsub("!=+", "~=")
str = str:gsub("~=+", "~=")
end
-- Condense whitespace.
str = str:gsub("%s%s", " ")
-- Condense parenthetical spaces.
str = str:gsub("[(][%s+]", "("):gsub("[%s+][)]", ")") ]]
-- Address equipped.number => equipped[number]
str = str:gsub("equipped%.(%d+)", "equipped[%1]")
str = str:gsub("main_hand%.(%d[a-zA-Z0-9_]+)", "main_hand['%1']")
str = str:gsub("off_hand%.(%d[a-zA-Z0-9_]+)", "off_hand['%1']")
str = str:gsub("equipped%.(%d+)", "equipped[%1]")
str = str:gsub("lowest_vuln_within%.(%d+)", "lowest_vuln_within[%1]")
str = str:gsub("%.in([^a-zA-Z0-9_])", "['in']%1" )
str = str:gsub("%.in$", "['in']" )
str = str:gsub("imps_spawned_during%.([^<>=!&|]+)", "imps_spawned_during[%1]")
str = str:gsub("prev%.(%d+)", "prev[%1]")
str = str:gsub("prev_gcd%.(%d+)", "prev_gcd[%1]")
str = str:gsub("prev_off_gcd%.(%d+)", "prev_off_gcd[%1]")
str = str:gsub("time_to_sht%.(%d+)", "time_to_sht[%1]")
str = str:gsub("time_to_sht_plus%.(%d+)", "time_to_sht_plus[%1]")
return str
end
local function stripScript( str, thorough )
if not str then return 'true' end
if type( str ) == 'number' then return str end
-- Remove the 'return ' that was added during conversion.
str = str:gsub("^return ", "")
-- Remove min/max/safenum/safebool/abs.
-- str = str:gsub("([^%a%w_%.])min([^%a%w_)]+)%s?%(?", "%1%2 "):gsub("([^%a%w_%.])max([^%a%w_)]+)%s?%(?", "%1%2 "):gsub("([^%a%w_%.])safebool([^%a%w_)]+)%s?%(?", "%1%2 "):gsub("([^%a%w_%.])safenum([^%a%w_)]+)%s?%(?", "%1%2 ")
str = str:gsub( "abs(%b())", "%1" ):gsub( "min(%b())", "%1" ):gsub( "max(%b())", "%1" ):gsub( "safebool(%b())", "%1" ):gsub( "safenum(%b())", "%1" )
-- Remove comments.
str = str:gsub("%-%-.-\n", "")
-- Remove conjunctions.
str = str:gsub("[%s-]and[%s-]", " "):gsub("[%s-]or[%s-]", " "):gsub("%(-%s-not[%s-]", " ")
if not thorough then
-- Collapse whitespace around comparison operators.
str = str:gsub("[%s-]==[%s-]", "=="):gsub("[%s-]>=[%s-]", ">="):gsub("[%s-]<=[%s-]", "<="):gsub("[%s-]~=[%s-]", "~="):gsub("[%s-]<[%s-]", "<"):gsub("[%s-]>[%s-]", ">")
else
-- Strip operators and parentheses.
str = str:gsub("[=+]", " "):gsub("[><~]%??", " "):gsub("[%*//%-%+]", " "):gsub("[%(%)]", " ")
end
str = str:gsub( "([%a%w_])%.(%d+)", "%1[%2]" )
str = str:gsub( "%.in([ %.])", "['in']%1")
-- Collapse the rest of the whitespace.
str = str:gsub("[%s+]", " ")
return ( str )
end
scripts.stripScript = stripScript
function scripts:StoreValues( tbl, node, mod )
wipe( tbl )
if type( node ) == 'string' then node = self.DB[ node ] end
if not node then return end
local elems
if mod then elems = node.ModElements[ mod ]
else elems = node.Elements end
if not elems then return end
for k, v in pairs( elems ) do
local s, r = pcall( v )
if s then tbl[ k ] = r
elseif type( r ) == 'string' then tbl[ k ] = r:match( "lua:(%d+: .*)" ) or r end
if tbl[ k ] == nil then tbl[ k ] = 'nil' end
end
end
function scripts:StoreReadyValues( tbl, node )
self:StoreValues( tbl, node, "ready" )
end
function scripts:GetScript( scriptID )
return self.DB[ scriptID ]
end
local function GetScriptElements( script )
if type( script ) == 'number' then return end
local e, c = {}, stripScript( script, true )
for s in c:gmatch( "[^ ,]+" ) do
while s:match("^(%b())$" ) do s = s:sub( 2, -2 ) end
if not e[ s ] and not tonumber( s ) then
local ef = Hekili:Loadstring( "return ".. s )
if ef then
setfenv( ef, state )
local success, v = pcall( ef )
if success then
e[ s ] = ef
end
end
end
end
return e
end
scripts.GetScriptElements = GetScriptElements
-- newModifiers, key is the name of the element, value is whether to babyproof it or not.
local newModifiers = {
chain = 'bool',
early_chain_if = 'bool',
interrupt = 'bool',
interrupt_global = 'bool',
interrupt_if = 'bool',
interrupt_immediate = 'bool',
max_energy = 'bool',
moving = 'bool',
only_cwc = 'bool',
strict = 'bool',
target_if = 'bool',
use_off_gcd = 'bool',
use_while_casting = 'bool',
wait = 'bool',
-- Not necessarily a number, but not baby-proofed.
cycle_targets = 'raw',
default = 'raw',
empower_to = 'raw',
for_next = 'raw',
line_cd = 'raw',
max_cycle_targets = 'raw',
sec = 'raw',
value = 'raw',
value_else = 'raw',
sync = 'string', -- should be an ability's name.
action_name = 'string',
buff_name = 'string',
list_name = 'string',
op = 'string',
var_name = 'string',
}
local valueModifiers = {
sec = true,
value = true,
value_else = true,
line_cd = true,
max_cycle_targets = true,
empower_to = true,
}
--[[ local nameMap = {
call_action_list = "list_name",
run_action_list = "list_name",
variable = "var_name",
cancel_buff = "buff_name",
} ]]
local isString = {
op = true,
}
local debugArgTemplate = [[%s
local arg%d = debugformat( %s )]]
local debugPrintTemplate = [[-- %s %s
local prev_action = this_action
this_action = "%s"
%s
this_action = prev_action
return format( "%s", %s )]]
local function generateDebugPrint( node, condition, header, isRecheck )
local cleanPrint = SimcWithResources( condition:trim() ):gsub( "%%", "%%%%" )
local seen = {}
local argn = 0
local formatArgs, generateDebug, debugPrint = nil, "", nil
for token in cleanPrint:gmatch( "[a-zA-Z][A-Za-z0-9_%.]+" ) do
if not seen[ token ] then
argn = argn + 1
seen[ token ] = "arg" .. argn
generateDebug = format( debugArgTemplate, generateDebug, argn, token )
end
if not formatArgs then
formatArgs = "arg1"
else
formatArgs = format( "%s, %s", formatArgs, seen[ token ] )
end
end
if argn > 0 then
local replacements = {}
for k, v in pairs( seen ) do
insert( replacements, { k, "{" .. v .. "}" } )
end
sort( replacements, function( a, b ) return a[1]:len() > b[1]:len() end )
for _, replace in ipairs( replacements ) do
cleanPrint = cleanPrint:gsub( replace[1], replace[2] )
end
for _, replace in ipairs( replacements ) do
cleanPrint = cleanPrint:gsub( replace[2], replace[1] .. "[%%s]" )
end
generateDebug = format( debugPrintTemplate, header, isRecheck and "recheck debug" or "condition debug", node.action or "wait", generateDebug, cleanPrint, formatArgs )
generateDebug = HandleLanguageIncompatibilities( generateDebug )
debugPrint, formatArgs = Hekili:Loadstring( generateDebug )
if formatArgs then
Hekili:Error( "Unable to generate debug print for " .. header .. ": " .. formatArgs:gsub( "%%", "%%%%" ) .. "\n" .. generateDebug:gsub( "%%", "%%%%" ) )
return
end
return generateDebug, setfenv( debugPrint, state )
end
end
-- Need to convert all the appropriate scripts and store them safely...
local function ConvertScript( node, hasModifiers, header )
local previousScript = state.scriptID
state.scriptID = header
state.this_action = node.action
local t = node.criteria and node.criteria ~= "" and node.criteria
local clean = SimToLua( t )
t = scripts:EmulateSyntax( t )
local tPreSim = t
t = SimToLua( t )
local sf, e
if t then sf, e = Hekili:Loadstring( "-- " .. header .. "\nreturn safebool( " .. t .. " )" ) end
if sf then setfenv( sf, state ) end
--[[ if sf and not e then
local pass, val = pcall( sf )
if not pass then e = val end
end ]]
if sf and not e then
state:SetDefaultVariable( 1 )
local success, msg = pcall( sf )
if not success then e = msg end
state:SetDefaultVariable( 0 )
end
if e then e = e:match( ":(%d+: .*)" ) or e end
local se = clean and GetScriptElements( clean )
local varPool
local generateDebug, debugPrint
if se then
local hasElements = false
for k, v in pairs( se ) do
hasElements = true
if k:sub( 1, 8 ) == "variable" then
varPool = varPool or {}
table.insert( varPool, k:sub( 10 ) )
end
end
if hasElements then
generateDebug, debugPrint = generateDebugPrint( node, node.criteria, header )
end
end
-- autorecheck...
local rs, rc, erc, rEle
local recheckDebug, recheckPrint
if t and t ~= "" then
rs = scripts:BuildRecheck( node.criteria )
if rs then
local orig = rs
rc, erc = Hekili:Loadstring( "-- " .. header .. " recheck\nreturn " .. rs )
if rc then setfenv( rc, state ) end
rEle = GetScriptElements( orig )
--[[ if next( rEle ) ~= nil then
recheckDebug, recheckPrint = generateDebugPrint( node, rs, header, true )
end ]]
rEle.zzz = orig
if type( rc ) ~= "function" then
Hekili:Error( "Recheck function for " .. clean .. " ( " .. ( rs or "nil" ) .. ") was unsuccessful somehow." )
rc = nil
end
end
end
local output = {
Conditions = sf,
Error = e,
Elements = se,
Print = debugPrint,
Debug = generateDebug,
Recheck = rc,
RecheckScript = rs,
RecheckError = erc,
RecheckElements = rEle,
RecheckPrint = recheckPrint,
RecheckDebug = recheckDebug,
Modifiers = {},
ModElements = {},
ModEmulates = {},
ModSimC = {},
SpecialMods = "",
Variables = varPool,
Lua = clean and clean:trim() or nil,
Emulated = t and t:trim() or nil,
EmuPreSim = tPreSim and tPreSim:trim() or nil,
SimC = node.criteria and SimcWithResources( node.criteria:trim() ) or nil,
ID = header
}
if hasModifiers then
for m, value in pairs( newModifiers ) do
if node[ m ] then
local emulated
local o = SimToLua( node[ m ] )
output.SpecialMods = output.SpecialMods .. " - " .. m .. " : " .. o
local sf, e
if value == 'bool' then
emulated = SimToLua( scripts:EmulateSyntax( node[ m ] ) )
elseif value == 'raw' then
if m == "empower_to" and ( o == "max" or o == "maximum" ) then
emulated = SimToLua( scripts:EmulateSyntax( "max_empower", true ) )
else
emulated = SimToLua( scripts:EmulateSyntax( node[ m ], true ) )
end
else -- string
o = "'" .. o .. "'"
emulated = o
end
if node.action == "variable" then
--[[ local var_val, var_recheck, var_err
var_val = scripts:BuildRecheck( node[m] )
if var_val then
if var_val:match(",") then
end
var_val = scripts:EmulateSyntax( var_val )
var_val = SimToLua( var_val )
var_recheck, var_err = loadstring( "-- val " ..header .. " recheck\nreturn " .. var_val )
if var_recheck then setfenv( var_recheck, state ) end
if type( var_recheck ) ~= "function" then
Hekili:Error( "Variable recheck function for " .. node.criteria .. " ( " .. ( var_recheck or "nil" ) .. " ) was unsuccessful somehow." )
var_recheck = nil
end
output.VarRecheck = var_recheck
output.VarRecheckScript = var_val
output.VarRecheckError = var_err
end ]]
local rs, rc, erc
rs = scripts:BuildRecheck( node[m] )
if rs then
local orig = rs
rc, erc = Hekili:Loadstring( "-- var " .. header .. " recheck\nreturn " .. rs )
if rc then setfenv( rc, state ) end
--[[rEle = GetScriptElements( orig )
rEle.zzz = orig ]]
if type( rc ) ~= "function" then
Hekili:Error( "Variable recheck function for " .. o .. " ( " .. ( rs or "nil" ) .. ") was unsuccessful somehow." )
rc = nil
end
output.VarRecheck = rc
output.VarRecheckScript = rs
output.VarRecheckError = erc
end
end
sf, e = Hekili:Loadstring( "return " .. emulated )
if sf then
setfenv( sf, state )
output.Modifiers[ m ] = sf
output.ModElements[ m ] = GetScriptElements( o )
output.ModEmulates[ m ] = emulated
if type( node[ m ] ) == 'string' then output.ModSimC[ m ] = SimcWithResources( node[ m ]:trim() ) end
else
output.Modifiers[ m ] = e
end
end
end
end
state.scriptID = previousScript
return output
end
scripts.ConvertScript = ConvertScript
function scripts:CheckScript( scriptID, action, elem )
local prev_action = state.this_action
if action then state.this_action = action end
local script = self.DB[ scriptID ]
if not script then
state.this_action = prev_action
return false
end
if not elem then
if script.Error then
state.this_action = prev_action
return false, script.Error
elseif not script.Conditions then
state.this_action = prev_action
return true
end
state.this_action = prev_action
return script.Conditions()
else
if not script.Modifiers[ elem ] then
state.this_action = prev_action
return nil, elem .. " not set."
else
local success, value = pcall( script.Modifiers[ elem ] )
if success then
state.this_action = prev_action
return value
end
end
end
state.this_action = prev_action
return false
end
function scripts:CheckVariable( scriptID )
local script = self.DB[ scriptID ]
if not script then
return false, "no script"
elseif script.Error then
return false, script.Error
end
local mods = script.Modifiers
if mods.value then
local s, val = pcall( mods.value )
if s then return val end
end
return false, "no op or error"
end
function scripts:IsTimeSensitive( scriptID )
local s = self.DB[ scriptID ]
return s and s.TimeSensitive
end
function scripts:GetModifiers( scriptID, out )
out = out or {}
local script = self.DB[ scriptID ]
if not script then return out end
for k, v in pairs( script.Modifiers ) do
local success, value = pcall(v)
if success then out[ k ] = value end
end
return out
end
local scriptsLoaded = false
local function scriptLoader()
if not Hekili.LoadingScripts and not scriptsLoaded then scripts:LoadScripts() end
end
function Hekili:ScriptsLoaded()
return scriptsLoaded
end
local channelModifiers = {
interrupt = 1,
interrupt_if = 1,
interrupt_immediate = 1,
interrupt_global = 1,
chain = 1,
early_chain_if = 1,
}
function scripts:SwapScripts( s1, s2 )
local swap = scripts.DB[ s1 ]
scripts.DB[ s1 ] = scripts.DB[ s2 ]
scripts.DB[ s2 ] = swap
end
function scripts:LoadScripts()
if not Hekili.PLAYER_ENTERING_WORLD then
C_Timer.After( 1, scriptLoader )
return
end
local profile = Hekili.DB.profile
wipe( self.DB )
wipe( self.Channels )
wipe( self.PackInfo )
Hekili.LoadingScripts = true
state.reset()
for pack, pData in pairs( profile.packs ) do
local specData = pData.spec and class.specs[ pData.spec ]
if specData then
self.PackInfo[ pack ] = {
items = {},
essences = {},
auras = {},
hasOffGCD = false
}
for list, lData in pairs( pData.lists ) do
for action, data in ipairs( lData ) do
Hekili:Yield( "Loading " .. pack .. " - " .. list .. " - " .. action )
local scriptID = pack .. ":" .. list .. ":" .. action
local script = ConvertScript( data, true, scriptID )
if script.Error then
Hekili:Error( "Error in " .. scriptID .. " conditions: " .. script.Error )
end
script.action = data.action
local lua = script.Lua
if lua then
for aura in lua:gmatch( "d?e?buff%.([a-z_0-9]+)" ) do
self.PackInfo[ pack ].auras[ aura ] = true
end
for aura in lua:gmatch( "active_dot%.([a-z_0-9]+)" ) do
self.PackInfo[ pack ].auras[ aura ] = true
end
end
if data.use_off_gcd and data.use_off_gcd ~= 0 then
self.PackInfo[ pack ].hasOffGCD = true
end
if data.action == "call_action_list" or data.action == "run_action_list" then
-- Check for Time Sensitive conditions.
script.TimeSensitive = false
if lua then
-- If resources are checked, it's time-sensitive.
for k in pairs( GetResourceInfo() ) do
local resource = specData.resources[ k ]
resource = resource and resource.state
if resource and lua:find( k ) and ( resource.regenModel or resource.regen ~= 0.001 ) then
script.TimeSensitive = true
break
end
end
if not script.TimeSensitive then
-- Check for other time-sensitive variables.
if lua:find( "time" ) or lua:find( "cooldown" ) or lua:find( "charge" ) or lua:find( "remain" ) or lua:find( "up" ) or lua:find( "down" ) or lua:find( "ticking" ) or lua:find( "refreshable" ) or lua:find( "stealthed" ) or lua:find( "rune" ) then
script.TimeSensitive = true
end
end
end
end
local ability
if data.action then
ability = specData.abilities[ data.action ] or class.abilities[ data.action ]
end
if ability then
if ability.channeled then
if not self.Channels[ pack ] then self.Channels[ pack ] = {} end
if not self.Channels[ pack ][ data.action ] then
self.Channels[ pack ][ data.action ] = {}
end
local cInfo = self.Channels[ pack ][ data.action ]
-- This will load the channel criteria for the first entry for this ability in any of the action lists.
-- This seems OK as long as channel breakage criteria is based on the same logic for the same spell.
-- There's genuinely no way to know if a person is channeling Mind Flay because it was recommended, or just because they felt like it.
-- 2020-10-22: Modified this to decide that if any breakchannel logic is met, you break the channel.
-- TODO: Phase 3 would be only using channel-break logic for channel entries that you've passed in the current APL run.
for k in pairs( channelModifiers ) do
if script.Modifiers[ k ] then
local newfunc = script.Modifiers[ k ]
if newfunc and type( newfunc ) == "function" then
local oldfunc = cInfo[ k ]
if oldfunc then
local oldstr = cInfo[ "_" .. k ]
cInfo[ k ] = setfenv( function() return ( oldfunc() ) or ( newfunc() ) end, state )
cInfo[ "_" .. k ] = format( "( %s ) or ( %s )", oldstr or "nil", script.ModEmulates[ k ] )
else
cInfo[ "_" .. k ] = script.ModEmulates[ k ]
cInfo[ k ] = script.Modifiers[ k ]
end
end
end
end
end
if list ~= "precombat" and ( ability.item or data.action == "trinket1" or data.action == "trinket2" ) and data.enabled then
self.PackInfo[ pack ].items[ data.action ] = true
end
if list ~= "precombat" and ability.essence and data.enabled then
self.PackInfo[ pack ].essences[ data.action ] = true
end
end
self.DB[ scriptID ] = script
end
end
end
end
Hekili.LoadingScripts = false
scriptsLoaded = true
end
function Hekili:LoadScripts()
self.Scripts:LoadScripts()
self:UpdateUseItems()
-- self:UpdateDisplayVisibility()
end
function Hekili:IsEssenceScripted( token )
local pack = self:GetActivePack()
pack = pack and self.Scripts.PackInfo[ pack ]
if not pack then return false end
return pack.essences[ token ] or false
end
function Hekili:IsItemScripted( token, specific )
local pack = Hekili:GetActivePack()
if not pack then return false end
if not self.Scripts.PackInfo[ pack ] then return false end
if self.Scripts.PackInfo[ pack ].items[ token ] then return true end
if not specific and ( ( state.trinket.t1.is[ token ] and self.Scripts.PackInfo[ pack ].items.trinket1 ) or ( state.trinket.t2.is[ token ] and self.Scripts.PackInfo[ pack ].items.trinket2 ) ) then return true end
return false
end
function Hekili.Scripts:LoadItemScripts()
for k in pairs( self.DB ) do
if k:sub( 9 ) == "UseItems:" then
self.DB[ k ] = nil
end
end
local pack = "UseItems"
--[[ self.PackInfo[ pack ] = self.PackInfo[ pack ] or {
items = {}
} ]]
for list, lData in pairs( class.itemPack.lists ) do
for action, data in ipairs( lData ) do
local scriptID = pack .. ":" .. list .. ":" .. action
local script = ConvertScript( data, true, scriptID )
if data.action == "call_action_list" or data.action == "run_action_list" then
-- Check for Time Sensitive conditions.
script.TimeSensitive = false
local lua = script.Lua
if lua then
-- If resources are checked, it's time-sensitive.
for k in pairs( GetResourceInfo() ) do
if lua:find( k ) then script.TimeSensitive = true; break end
end
if lua:find( "rune" ) then script.TimeSensitive = true end
if not script.TimeSensitive then
-- Check for other time-sensitive variables.
if lua:find( "time" ) or lua:find( "cooldown" ) or lua:find( "charge" ) or lua:find( "remain" ) or lua:find( "up" ) or lua:find( "down" ) or lua:find( "ticking" ) or lua:find( "refreshable" ) then
script.TimeSensitive = true
end
end
end
end
local ability
if data.action then
ability = class.abilities[ data.action ] or class.specs[ 0 ].abilities[ data.action ]
end
if ability then
if ability.channeled then
if not self.Channels[ pack ] then self.Channels[ pack ] = {} end
if not self.Channels[ pack ][ data.action ] then
self.Channels[ pack ][ data.action ] = {}
end
local cInfo = self.Channels[ pack ][ data.action ]
-- This will load the channel criteria for the first entry for this ability in any of the action lists.
-- This seems OK as long as channel breakage criteria is based on the same logic for the same spell.
-- There's genuinely no way to know if a person is channeling Mind Flay because it was recommended, or just because they felt like it.
for k in pairs( channelModifiers ) do
if script.Modifiers[ k ] and not cInfo[ k ] then cInfo[ k ] = script.Modifiers[ k ] end
end
end
end
self.DB[ scriptID ] = script
end
end
end
function Hekili:LoadItemScripts()
self.Scripts:LoadItemScripts()
end
function Hekili:LoadScript( pack, list, id )
local data = self.DB.profile.packs[ pack ].lists[ list ][ id ]
local scriptID = pack .. ":" .. list .. ":" .. id
local script = ConvertScript( data, true, scriptID )
if script.Error then
Hekili:Error( "Error in " .. scriptID .. " conditions: " .. script.SimC .. "\n " .. script.Error )
end
if data.action == "call_action_list" or data.action == "run_action_list" then
-- Check for Time Sensitive conditions.
script.TimeSensitive = false
local lua = script.Lua
if lua then
-- If resources are checked, it's time-sensitive.
for k in pairs( GetResourceInfo() ) do
if lua:find( k ) then script.TimeSensitive = true; break end
end
if lua:find( "rune" ) then script.TimeSensitive = true end
if not script.TimeSensitive then
-- Check for other time-sensitive variables.
if lua:find( "time" ) or lua:find( "cooldown" ) or lua:find( "charge" ) or lua:find( "remain" ) or lua:find( "up" ) or lua:find( "down" ) or lua:find( "ticking" ) or lua:find( "refreshable" ) then
script.TimeSensitive = true
end
end
end
end
self.Scripts.DB[ scriptID ] = script
end
function scripts:ImplantDebugData( data )
local prev = state.this_action
state.this_action = data.actionName
if data.hook then
local s = self.DB[ data.hook ]
local pack, list, entry = data.hook:match( "^(.-):(.-):(.-)$" )
data.HookHeader = "Called from " .. pack .. ", " .. list .. ", " .. "#" .. entry .. "."
data.HookScript = s.SimC
data.HookElements = data.HookElements or {}
self:StoreValues( data.HookElements, s )
end
if data.script then
local s = self.DB[ data.script ]
data.ActScript = s.SimC
data.ActElements = data.ActElements or {}
self:StoreValues( data.ActElements, s )
end
state.this_action = prev
end
local key_cache = setmetatable( {}, {
__index = function( t, k )
t[k] = k:gsub( "(%S+)%[(%d+)]", "%1.%2" ):gsub( "(%S+)%['([%a%w_]+)']", "%1.%2" )
return t[k]
end
})
local checked = {}
local function embedConditionsAndValues( source, elements )
if source and source ~= "" then
local wasDebugging = Hekili.ActiveDebug
Hekili.ActiveDebug = false
if elements then
wipe( checked )
for k, v in pairs( elements ) do
if not checked[ k ] then
local key = key_cache[ k ]
local success, value = pcall( v, true )
-- if emsg then value = emsg end
if type( value ) == "number" then
if source == key then
source = source .. "[" .. tostring( value ) .. "]"
else
source = source:gsub( "([^a-z0-9_.[])("..key..")([^a-z0-9_.[])", format( "%%1%%2[%.2f]%%3", value ) )
source = source:gsub( "^("..key..")([^a-z0-9_.[])", format( "%%1[%.2f]%%2", value ) )
source = source:gsub( "([^a-z0-9_.[])("..key..")$", format( "%%1%%2[%.2f]", value ) )
end
-- source = source:gsub( "^("..key..")", format( "%%1[%.2f]", value ) )
elseif type( value ) == "boolean" then
if source == key then
source = source .. "[" .. tostring( value ) .. "]"
else
source = source:gsub( "([^a-z0-9_.[])("..key..")([^a-z0-9_.[])", format( "%%1%%2[%s]%%3", tostring( value ) ) )
source = source:gsub( "^("..key..")([^a-z0-9_.[])", format( "%%1[%s]%%2", tostring( value ) ) )
source = source:gsub( "([^a-z0-9_.[])("..key..")$", format( "%%1%%2[%s]", tostring( value ) ) )
end
end
checked[ k ] = true
end
end
end
if wasDebugging then Hekili.ActiveDebug = true end
return source
end
return "NONE"
end
do
local troubleshootingSnapshotTimes = false
function scripts:GetConditionsAndValues( scriptID, listName, actID, recheck )
if troubleshootingSnapshotTimes or not Hekili.ActiveDebug then return "[no data]" end
if listName and actID then
scriptID = scriptID .. ":" .. listName .. ":" .. actID
end
local script = self.DB[ scriptID ]
if recheck then
return embedConditionsAndValues( script.RecheckScript, script.RecheckElements )
end
if script.Print then return script.Print() end
return embedConditionsAndValues( script.SimC, script.Elements )
end
end
function scripts:GetModifierValues( modifier, scriptID, listName, actID )
if listName and actID then
scriptID = scriptID .. ":" .. listName .. ":" .. actID
end
local script = self.DB[ scriptID ]
if script and script.ModSimC[ modifier ] and script.ModSimC[ modifier ].SimC ~= "" then
local output = script.ModSimC[ modifier ]
wipe( checked )
for k, v in pairs( script.ModElements[ modifier ] ) do
if not checked[ k ] then
local key = key_cache[ k ]
local success, value = pcall( v )
-- if emsg then value = emsg end
if type( value ) == 'number' then
if output == key then
output = output .. "[" .. tostring( value ) .. "]"
else
output = output:gsub( "([^a-z0-9_.[])("..key..")([^a-z0-9_.[])", format( "%%1%%2[%.2f]%%3", value ) )
output = output:gsub( "^("..key..")([^a-z0-9_.[])", format( "%%1[%.2f]%%2", value ) )
output = output:gsub( "([^a-z0-9_.[])("..key..")$", format( "%%1%%2[%.2f]", value ) )
end
-- output = output:gsub( "^("..key..")", format( "%%1[%.2f]", value ) )
else
if output == key then
output = output .. "[" .. tostring( value ) .. "]"
else
output = output:gsub( "([^a-z0-9_.[])("..key..")([^a-z0-9_.[])", format( "%%1%%2[%s]%%3", tostring( value ) ) )
output = output:gsub( "^("..key..")([^a-z0-9_.[])", format( "%%1[%s]%%2", tostring( value ) ) )
output = output:gsub( "([^a-z0-9_.[])("..key..")$", format( "%%1%%2[%s]", tostring( value ) ) )
end
end
checked[ k ] = true
end
end
return output
end
return "NONE"
end
Hekili.dumpKeyCache = key_cache