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.

2111 lines
103 KiB

-- Hekili.lua
-- April 2014
local addon, ns = ...
local Hekili = _G[ addon ]
local class = Hekili.Class
local state = Hekili.State
local scripts = Hekili.Scripts
local callHook = ns.callHook
local clashOffset = ns.clashOffset
local formatKey = ns.formatKey
local getSpecializationID = ns.getSpecializationID
local getResourceName = ns.getResourceName
local orderedPairs = ns.orderedPairs
local tableCopy = ns.tableCopy
local timeToReady = ns.timeToReady
local GetItemInfo = ns.CachedGetItemInfo
local trim = string.trim
local tcopy = ns.tableCopy
local tinsert, tremove, twipe = table.insert, table.remove, table.wipe
-- checkImports()
-- Remove any displays or action lists that were unsuccessfully imported.
local function checkImports()
end
ns.checkImports = checkImports
local function EmbedBlizOptions()
local panel = CreateFrame( "Frame", "HekiliDummyPanel", UIParent )
panel.name = "Hekili"
local open = CreateFrame( "Button", "HekiliOptionsButton", panel, "UIPanelButtonTemplate" )
open:SetPoint( "CENTER", panel, "CENTER", 0, 0 )
open:SetWidth( 250 )
open:SetHeight( 25 )
open:SetText( "Open Hekili Options Panel" )
open:SetScript( "OnClick", function ()
ns.StartConfiguration()
end )
Hekili:ProfileFrame( "OptionsEmbedFrame", open )
InterfaceOptions_AddCategory( panel )
end
-- OnInitialize()
-- Addon has been loaded by the WoW client (1x).
function Hekili:OnInitialize()
self.DB = LibStub( "AceDB-3.0" ):New( "HekiliDB", self:GetDefaults(), true )
self.Options = self:GetOptions()
self.Options.args.profiles = LibStub( "AceDBOptions-3.0" ):GetOptionsTable( self.DB )
-- Reimplement LibDualSpec; some folks want different layouts w/ specs of the same class.
local LDS = LibStub( "LibDualSpec-1.0" )
LDS:EnhanceDatabase( self.DB, "Hekili" )
LDS:EnhanceOptions( self.Options.args.profiles, self.DB )
self.DB.RegisterCallback( self, "OnProfileChanged", "TotalRefresh" )
self.DB.RegisterCallback( self, "OnProfileCopied", "TotalRefresh" )
self.DB.RegisterCallback( self, "OnProfileReset", "TotalRefresh" )
local AceConfig = LibStub( "AceConfig-3.0" )
AceConfig:RegisterOptionsTable( "Hekili", self.Options )
local AceConfigDialog = LibStub( "AceConfigDialog-3.0" )
-- EmbedBlizOptions()
self:RegisterChatCommand( "hekili", "CmdLine" )
self:RegisterChatCommand( "hek", "CmdLine" )
local LDB = LibStub( "LibDataBroker-1.1", true )
local LDBIcon = LDB and LibStub( "LibDBIcon-1.0", true )
Hekili_OnAddonCompartmentClick = function( addonName, button )
if button == "RightButton" then ns.StartConfiguration()
else
ToggleDropDownMenu( 1, nil, ns.UI.Menu, "cursor", 8, 5 )
end
GameTooltip:Hide()
end
local function GetDataText()
local p = Hekili.DB.profile
local m = p.toggles.mode.value
local color = "FFFFD100"
if p.toggles.essences.override then
-- Don't show Essences here if it's overridden by CDs anyway?
return format( "|c%s%s|r %sCD|r %sInt|r %sDef|r", color,
m == "single" and "ST" or ( m == "aoe" and "AOE" or ( m == "dual" and "Dual" or ( m == "reactive" and "React" or "Auto" ) ) ),
p.toggles.cooldowns.value and "|cFF00FF00" or "|cFFFF0000",
p.toggles.interrupts.value and "|cFF00FF00" or "|cFFFF0000",
p.toggles.defensives.value and "|cFF00FF00" or "|cFFFF0000" )
else
return format( "|c%s%s|r %sCD|r %smCD|r %sInt|r",
color,
m == "single" and "ST" or ( m == "aoe" and "AOE" or ( m == "dual" and "Dual" or ( m == "reactive" and "React" or "Auto" ) ) ),
p.toggles.cooldowns.value and "|cFF00FF00" or "|cFFFF0000",
p.toggles.essences.value and "|cFF00FF00" or "|cFFFF0000",
p.toggles.interrupts.value and "|cFF00FF00" or "|cFFFF0000" )
end
end
Hekili_OnAddonCompartmentEnter = function( addonName, button )
GameTooltip:SetOwner( AddonCompartmentFrame )
GameTooltip:AddDoubleLine( "Hekili", GetDataText() )
GameTooltip:AddLine( "|cFFFFFFFFLeft-click to make quick adjustments.|r" )
GameTooltip:AddLine( "|cFFFFFFFFRight-click to open the options interface.|r" )
GameTooltip:Show()
end
Hekili_OnAddonCompartmentLeave = function( addonName, button )
GameTooltip:Hide()
end
if LDB then
ns.UI.Minimap = ns.UI.Minimap or LDB:NewDataObject( "Hekili", {
type = "data source",
text = "Hekili",
icon = "Interface\\ICONS\\spell_nature_bloodlust",
OnClick = Hekili_OnAddonCompartmentClick,
OnEnter = function( self )
GameTooltip:SetOwner( self )
GameTooltip:AddDoubleLine( "Hekili", ns.UI.Minimap.text )
GameTooltip:AddLine( "|cFFFFFFFFLeft-click to make quick adjustments.|r" )
GameTooltip:AddLine( "|cFFFFFFFFRight-click to open the options interface.|r" )
GameTooltip:Show()
end,
OnLeave = Hekili_OnAddonCompartmentLeave
} )
function ns.UI.Minimap:RefreshDataText()
self.text = GetDataText()
end
ns.UI.Minimap:RefreshDataText()
if LDBIcon then
LDBIcon:Register( "Hekili", ns.UI.Minimap, self.DB.profile.iconStore )
end
end
self:RestoreDefaults()
self:RunOneTimeFixes()
checkImports()
ns.primeTooltipColors()
self.PendingSpecializationChange = true
callHook( "onInitialize" )
end
function Hekili:ReInitialize()
self:OverrideBinds()
self:RestoreDefaults()
checkImports()
self:RunOneTimeFixes()
self.PendingSpecializationChange = true
callHook( "onInitialize" )
if self.DB.profile.enabled == false and self.DB.profile.AutoDisabled then
self.DB.profile.AutoDisabled = nil
self.DB.profile.enabled = true
self:Enable()
end
end
function Hekili:OnEnable()
ns.StartEventHandler()
self:TotalRefresh( true )
ns.ReadKeybindings()
self.PendingSpecializationChange = true
self:ForceUpdate( "ADDON_ENABLED" )
if self.BuiltFor > self.CurrentBuild then
self:Notify( "|cFFFF0000WARNING|r: This version of Hekili is for a future version of WoW; you should reinstall for " .. self.GameBuild .. "." )
end
end
Hekili:ProfileCPU( "StartEventHandler", ns.StartEventHandler )
Hekili:ProfileCPU( "BuildUI", Hekili.BuildUI )
Hekili:ProfileCPU( "SpecializationChanged", Hekili.SpecializationChanged )
Hekili:ProfileCPU( "OverrideBinds", Hekili.OverrideBinds )
Hekili:ProfileCPU( "TotalRefresh", Hekili.TotalRefresh )
function Hekili:OnDisable()
self:UpdateDisplayVisibility()
self:BuildUI()
ns.StopEventHandler()
end
function Hekili:Toggle()
self.DB.profile.enabled = not self.DB.profile.enabled
if self.DB.profile.enabled then
self:Enable()
else
self:Disable()
end
self:UpdateDisplayVisibility()
end
local z_PVP = {
arena = true,
pvp = true
}
local listStack = {} -- listStack for a given index returns the scriptID of its caller (or 0 if called by a display).
local listCache = {} -- listCache is a table of return values for a given scriptID at various times.
local listValue = {} -- listValue shows the cached values from the listCache.
local lcPool = {}
local lvPool = {}
local Stack = {}
local Block = {}
local InUse = {}
local StackPool = {}
function Hekili:AddToStack( script, list, parent, run )
local entry = tremove( StackPool ) or {}
entry.script = script
entry.list = list
entry.parent = parent
entry.run = run
entry.priorMin = nil
tinsert( Stack, entry )
if self.ActiveDebug then
local path = "+"
for n, entry in ipairs( Stack ) do
if entry.run then
path = format( "%s%s [%s]", path, ( n > 1 and "," or "" ), entry.list )
else
path = format( "%s%s %s", path,( n > 1 and "," or "" ), entry.list )
end
end
self:Debug( path )
end
-- if self.ActiveDebug then self:Debug( "Adding " .. list .. " to stack, parent is " .. ( parent or "(none)" ) .. " (RAL = " .. tostring( run ) .. ".") end
InUse[ list ] = true
end
local blockValues = {}
local inTable = {}
local function blockHelper( ... )
local n = select( "#", ... )
twipe( inTable )
for i = 1, n do
local val = select( i, ... )
if val > 0 and val >= state.delayMin and not inTable[ val ] then
blockValues[ #blockValues + 1 ] = val
inTable[ val ] = true
end
end
table.sort( blockValues )
end
function Hekili:PopStack()
local x = tremove( Stack, #Stack )
if not x then return end
if self.ActiveDebug then
if x.run then
self:Debug( "- [%s]", x.list )
else
self:Debug( "- %s", x.list )
end
end
-- if self.ActiveDebug then self:Debug( "Removed " .. x.list .. " from stack." ) end
if x.priorMin then
if self.ActiveDebug then Hekili:Debug( "Resetting delayMin to %.2f from %.2f.", x.priorMin, state.delayMin ) end
state.delayMin = x.priorMin
end
for i = #Block, 1, -1 do
if Block[ i ].parent == x.script then
if self.ActiveDebug then self:Debug( "Removed " .. Block[ i ].list .. " from blocklist as " .. x.list .. " was its parent." ) end
tinsert( StackPool, tremove( Block, i ) )
end
end
if x.run then
-- This was called via Run Action List; we have to make sure it DOESN'T PASS until we exit this list.
if self.ActiveDebug then self:Debug( "Added " .. x.list .. " to blocklist as it was called via RAL." ) end
state:PurgeListVariables( x.list )
tinsert( Block, x )
-- Set up new delayMin.
x.priorMin = state.delayMin
local actualDelay = state.delay
-- If the script would block at the present time, find when it wouldn't block.
if scripts:CheckScript( x.script ) then
local script = scripts:GetScript( x.script )
if script.Recheck then
if #blockValues > 0 then twipe( blockValues ) end
blockHelper( script.Recheck() )
local firstFail
if Hekili.ActiveDebug then Hekili:Debug( " - blocking script did not immediately block; will attempt to tune it." ) end
for i, check in ipairs( blockValues ) do
state.delay = actualDelay + check
if not scripts:CheckScript( x.script ) then
firstFail = check
break
end
end
if firstFail and firstFail > 0 then
state.delayMin = actualDelay + firstFail
local subFail
-- May want to try to tune even better?
for i = 1, 10 do
if subFail then subFail = firstFail - ( firstFail - subFail ) / 2
else subFail = firstFail / 2 end
state.delay = actualDelay + subFail
if not scripts:CheckScript( x.script ) then
firstFail = subFail
subFail = nil
end
end
state.delayMin = actualDelay + firstFail
if Hekili.ActiveDebug then Hekili:Debug( " - setting delayMin to " .. state.delayMin .. " based on recheck and brute force." ) end
else
state.delayMin = x.priorMin
-- Leave it alone.
if Hekili.ActiveDebug then Hekili:Debug( " - leaving delayMin at " .. state.delayMin .. "." ) end
end
end
end
state.delay = actualDelay
end
InUse[ x.list ] = nil
end
function Hekili:CheckStack()
local t = state.query_time
for i, b in ipairs( Block ) do
listCache[ b.script ] = listCache[ b.script ] or tremove( lcPool ) or {}
local cache = listCache[ b.script ]
if cache[ t ] == nil then cache[ t ] = scripts:CheckScript( b.script ) end
if self.ActiveDebug then
listValue[ b.script ] = listValue[ b.script ] or tremove( lvPool ) or {}
local values = listValue[ b.script ]
values[ t ] = values[ t ] or scripts:GetConditionsAndValues( b.script )
self:Debug( "Blocking list ( %s ) called from ( %s ) would %s at %.2f.", b.list, b.script, cache[ t ] and "BLOCK" or "NOT BLOCK", state.delay )
self:Debug( values[ t ] )
end
if cache[ t ] then
return false
end
end
for i, s in ipairs( Stack ) do
listCache[ s.script ] = listCache[ s.script ] or tremove( lcPool ) or {}
local cache = listCache[ s.script ]
if cache[ t ] == nil then cache[ t ] = scripts:CheckScript( s.script ) end
if self.ActiveDebug then
listValue[ s.script ] = listValue[ s.script ] or tremove( lvPool ) or {}
local values = listValue[ s.script ]
values[ t ] = values[ t ] or scripts:GetConditionsAndValues( s.script )
self:Debug( "List ( %s ) called from ( %s ) would %s at %.2f.", s.list, s.script, cache[ t ] and "PASS" or "FAIL", state.delay )
self:Debug( values[ t ]:gsub( "%%", "%%%%" ) )
end
if not cache[ t ] then
return false
end
end
return true
end
local function return_false() return false end
local default_modifiers = {
early_chain_if = return_false,
chain = return_false,
interrupt_if = return_false,
interrupt = return_false
}
function Hekili:CheckChannel( ability, prio )
if not state.channeling then
if self.ActiveDebug then self:Debug( "CC: We aren't channeling; CheckChannel is false." ) end
return false
end
local channel = state.buff.casting.up and ( state.buff.casting.v3 == 1 ) and state.buff.casting.v1 or nil
if not channel then
if self.ActiveDebug then self:Debug( "CC: We are not channeling per buff.casting.v3; CheckChannel is false." ) end
return false
end
local a = class.abilities[ channel ]
if not a then
if self.ActiveDebug then self:Debug( "CC: We don't recognize the channeled spell; CheckChannel is false." ) end
return false
end
channel = a.key
local aura = class.auras[ a.aura or channel ]
if a.break_any and channel ~= ability then
if self.ActiveDebug then self:Debug( "CC: %s.break_any is true; break it.", channel ) end
return true
end
if not a.tick_time and ( not aura or not aura.tick_time ) then
if self.ActiveDebug then self:Debug( "CC: No aura / no aura.tick_time to forecast channel breaktimes; don't break it." ) end
return false
end
local modifiers = scripts.Channels[ state.system.packName ]
modifiers = modifiers and modifiers[ channel ] or default_modifiers
--[[ if self.ActiveDebug then
if default_modifiers == modifiers then
self:Debug( "Using default modifiers." )
else
local vals = ""
for k, v in pairs( modifiers ) do
vals = format( "%s%s = %s - ", vals, tostring( k ), tostring( type(v) == "function" and v() or v ) )
end
self:Debug( "Channel modifiers: %s", vals )
end
end ]]
local tick_time = a.tick_time or aura.tick_time
local remains = state.channel_remains
if channel == ability then
if prio <= remains + 0.01 then
if self.ActiveDebug then self:Debug( "CC: ...looks like chaining, not breaking channel.", ability ) end
return false
end
if modifiers.early_chain_if then
local eci = state.cooldown.global_cooldown.ready and ( remains < tick_time or ( ( remains - state.delay ) / tick_time ) % 1 <= 0.5 ) and modifiers.early_chain_if()
if self.ActiveDebug then self:Debug( "CC: early_chain_if returns %s...", tostring( eci ) ) end
return eci
end
if modifiers.chain then
local chain = state.cooldown.global_cooldown.ready and ( remains < tick_time ) and modifiers.chain()
if self.ActiveDebug then self:Debug( "CC: chain returns %s...", tostring( chain ) ) end
return chain
end
if self.ActiveDebug then self:Debug( "CC: channel == ability, not breaking." ) end
return false
else
-- If interrupt_global is flagged, we interrupt for any potential cast. Don't bother with additional testing.
-- REVISIT THIS: Interrupt Global allows entries from any action list rather than just the current (sub) list.
-- That means interrupt / interrupt_if should narrow their scope to the current APL (at some point, anyway).
--[[ if modifiers.interrupt_global and modifiers.interrupt_global() then
if self.ActiveDebug then self:Debug( "CC: Interrupt Global is true." ) end
return true
end ]]
local act = state.this_action
state.this_action = channel
-- We are concerned with chain and early_chain_if.
if modifiers.interrupt_if and modifiers.interrupt_if() then
local imm = modifiers.interrupt_immediate and modifiers.interrupt_immediate() or nil
local val = state.cooldown.global_cooldown.ready and ( imm or remains < tick_time or ( state.query_time - state.buff.casting.applied ) % tick_time < 0.25 )
if self.ActiveDebug then
self:Debug( "CC: Interrupt_If is %s.", tostring( val ) )
end
state.this_action = act
return val
end
if modifiers.interrupt and modifiers.interrupt() then
local val = state.cooldown.global_cooldown.ready and ( remains < tick_time or ( ( remains - state.delay ) / tick_time ) % 1 <= 0.5 )
if self.ActiveDebug then self:Debug( "CC: Interrupt is %s.", tostring( val ) ) end
state.this_action = act
return val
end
state.this_action = act
end
if self.ActiveDebug then self:Debug( "CC: No result; defaulting to false." ) end
return false
end
do
local knownCache = {}
local reasonCache = {}
function Hekili:IsSpellKnown( spell )
return state:IsKnown( spell )
--[[ local id = class.abilities[ spell ] and class.abilities[ spell ].id or spell
if knownCache[ id ] ~= nil then return knownCache[ id ], reasonCache[ id ] end
knownCache[ id ], reasonCache[ id ] = state:IsKnown( spell )
return knownCache[ id ], reasonCache[ id ] ]]
end
local disabledCache = {}
local disabledReasonCache = {}
function Hekili:IsSpellEnabled( spell )
local disabled, reason = state:IsDisabled( spell )
return not disabled, reason
end
function Hekili:ResetSpellCaches()
twipe( knownCache )
twipe( reasonCache )
twipe( disabledCache )
twipe( disabledReasonCache )
end
end
local Timer = {
start = 0,
n = {},
v = {},
Reset = function( self )
if not Hekili.ActiveDebug then return end
twipe( self.n )
twipe( self.v )
self.start = debugprofilestop()
self.n[1] = "Start"
self.v[1] = self.start
end,
Track = function( self, key )
if not Hekili.ActiveDebug then return end
tinsert( self.n, key )
tinsert( self.v, debugprofilestop() )
end,
Output = function( self )
if not Hekili.ActiveDebug then return "" end
local o = ""
for i = 2, #self.n do
o = string.format( "%s:%s(%.2f)", o, self.n[i], ( self.v[i] - self.v[i-1] ) )
end
return o
end,
Total = function( self )
if not Hekili.ActiveDebug then return "" end
return string.format("%.2f", self.v[#self.v] - self.start )
end,
}
do
local function DoYield( self, msg, time, force )
if not coroutine.running() then return end
time = time or debugprofilestop()
if msg then
self.Engine.lastYieldReason = msg
end
if force or self.maxFrameTime > 0 and time - self.activeFrameStart > self.maxFrameTime then
coroutine.yield()
end
end
local function FirstYield( self, msg, time )
if Hekili.PLAYER_ENTERING_WORLD and not Hekili.LoadingScripts then
self.Yield = DoYield
end
end
Hekili.Yield = FirstYield
end
local invalidActionWarnings = {}
function Hekili:GetPredictionFromAPL( dispName, packName, listName, slot, action, wait, depth, caller )
local specID = state.spec.id
local spec = rawget( self.DB.profile.specs, specID )
local module = class.specs[ specID ]
packName = packName or spec and spec.package
if not packName then return end
local pack
if ( packName == "UseItems" ) then pack = class.itemPack
else pack = self.DB.profile.packs[ packName ] end
local packInfo = scripts.PackInfo[ spec.package ]
local list = pack.lists[ listName ]
local debug = self.ActiveDebug
if debug then self:Debug( "Current recommendation was %s at +%.2fs.", action or "NO ACTION", wait or state.delayMax ) end
-- if debug then self:Debug( "ListCheck: Success(%s-%s)", packName, listName ) end
local precombatFilter = listName == "precombat" and state.time > 0
local rAction = action
local rWait = wait or 15
local rDepth = depth or 0
local strict = false -- disabled for now.
local force_channel = false
local stop = false
if self:IsListActive( packName, listName ) then
local actID = 1
while actID <= #list do
self:Yield( "GetPrediction... " .. dispName .. "-" .. packName .. ":" .. actID )
if rWait < state.delayMax then state.delayMax = rWait end
--[[ Watch this section, may impact usage of off-GCD abilities.
if rWait <= state.cooldown.global_cooldown.remains and not state.spec.can_dual_cast then
if debug then self:Debug( "The recommended action (%s) would be ready before the next GCD (%.2f < %.2f); exiting list (%s).", rAction, rWait, state.cooldown.global_cooldown.remains, listName ) end
break
else ]]
if rWait <= 0.2 then
if debug then self:Debug( "The recommended action (%s) is ready in less than 0.2s; exiting list (%s).", rAction, listName ) end
break
elseif state.delayMin > state.delayMax then
if debug then self:Debug( "The current minimum delay (%.2f) is greater than the current maximum delay (%.2f). Exiting list (%s).", state.delayMin, state.delayMax, listName ) end
break
elseif rAction and not packInfo.hasOffGCD and rWait <= state.cooldown.global_cooldown.remains then -- and state.settings.gcdSync then
if debug then self:Debug( "The recommended action (%s) is ready within the active GCD; exiting list (%s).", rAction, listName ) end
break
elseif stop then
if debug then self:Debug( "The action list reached a stopping point; exiting list (%s).", listName ) end
break
end
Timer:Reset()
local entry = list[ actID ]
if self:IsActionActive( packName, listName, actID ) then
-- Check for commands before checking actual actions.
local scriptID = packName .. ":" .. listName .. ":" .. actID
local action = entry.action
state.this_action = action
state.delay = nil
local ability = class.abilities[ action ]
if not ability then
if not invalidActionWarnings[ scriptID ] then
Hekili:Error( "Priority '%s' uses action '%s' ( %s - %d ) that is not found in the abilities table.", packName, action or "unknown", listName, actID )
invalidActionWarnings[ scriptID ] = true
end
elseif state.whitelist and not state.whitelist[ action ] and ( ability.id < -99 or ability.id > 0 ) then
-- if debug then self:Debug( "[---] %s ( %s - %d) not castable while casting a spell; skipping...", action, listName, actID ) end
elseif rWait <= state.cooldown.global_cooldown.remains and not state.spec.can_dual_cast and ability.gcd ~= "off" then
-- if debug then self:Debug( "Only off-GCD abilities would be usable before the currently selected ability; skipping..." ) end
else
local entryReplaced = false
if action == "heart_essence" and class.essence_unscripted and class.active_essence then
action = class.active_essence
ability = class.abilities[ action ]
state.this_action = action
entryReplaced = true
elseif action == "trinket1" then
if state.trinket.t1.usable and state.trinket.t1.ability and not Hekili:IsItemScripted( state.trinket.t1.ability, true ) then
action = state.trinket.t1.ability
ability = class.abilities[ action ]
state.this_action = action
entryReplaced = true
else
if debug then
self:Debug( "\nBypassing 'trinket1' action because %s.", state.trinket.t1.usable and state.trinket.t1.ability and ( state.trinket.t1.ability .. " is used elsewhere in this priority" ) or "the equipped trinket #1 is not usable" )
end
ability = nil
end
elseif action == "trinket2" then
if state.trinket.t2.usable and state.trinket.t2.ability and not Hekili:IsItemScripted( state.trinket.t2.ability, true ) then
action = state.trinket.t2.ability
ability = class.abilities[ action ]
state.this_action = action
entryReplaced = true
else
if debug then
self:Debug( "\nBypassing 'trinket2' action because %s.", state.trinket.t2.usable and state.trinket.t2.ability and ( state.trinket.t2.ability .. " is used elsewhere in this priority" ) or "the equipped trinket #2 is not usable" )
end
ability = nil
end
end
rDepth = rDepth + 1
-- if debug then self:Debug( "[%03d] %s ( %s - %d )", rDepth, action, listName, actID ) end
local wait_time = state.delayMax or 15
local clash = 0
local known, reason = self:IsSpellKnown( action )
local enabled, enReason = self:IsSpellEnabled( action )
local scriptID = packName .. ":" .. listName .. ":" .. actID
state.scriptID = scriptID
if debug then
local d = ""
if entryReplaced then d = format( "\nSubstituting %s for %s action; it is otherwise not included in the priority.", action, class.abilities[ entry.action ].name ) end
if action == "call_action_list" or action == "run_action_list" then
d = d .. format( "\n%-4s %s ( %s - %d )", rDepth .. ".", ( action .. ":" .. ( state.args.list_name or "unknown" ) ), listName, actID )
elseif action == "cancel_buff" then
d = d .. format( "\n%-4s %s ( %s - %d )", rDepth .. ".", ( action .. ":" .. ( state.args.buff_name or "unknown" ) ), listName, actID )
elseif action == "cancel_action" then
d = d .. format( "\n%-4s %s ( %s - %d )", rDepth .. ".", ( action .. ":" .. ( state.args.action_name or "unknown" ) ), listName, actID )
else
d = d .. format( "\n%-4s %s ( %s - %d )", rDepth .. ".", action, listName, actID )
end
if not known then d = d .. " - " .. ( reason or "ability unknown" )
elseif not enabled then d = d .. " - ability disabled ( " .. ( enReason or "unknown" ) .. " )" end
self:Debug( d )
end
Timer:Track( "Ability Known, Enabled" )
if ability and known and enabled then
local script = scripts:GetScript( scriptID )
wait_time = state:TimeToReady()
clash = state.ClashOffset()
state.delay = wait_time
if not script then
if debug then self:Debug( "There is no script ( " .. scriptID .. " ). Skipping." ) end
elseif script.Error then
if debug then self:Debug( "The conditions for this entry contain an error. Skipping." ) end
elseif wait_time > state.delayMax then
if debug then self:Debug( "The action is not ready ( %.2f ) before our maximum delay window ( %.2f ) for this query.", wait_time, state.delayMax ) end
elseif ( rWait - state.ClashOffset( rAction ) ) - ( wait_time - clash ) <= 0.05 then
if debug then self:Debug( "The action is not ready in time ( %.2f vs. %.2f ) [ Clash: %.2f vs. %.2f ] - padded by 0.05s.", wait_time, rWait, clash, state.ClashOffset( rAction ) ) end
else
-- APL checks.
if precombatFilter and not ability.essential then
if debug then self:Debug( "We are already in-combat and this pre-combat action is not essential. Skipping." ) end
else
Timer:Track("Post-TTR and Essential")
if action == "call_action_list" or action == "run_action_list" or action == "use_items" then
-- We handle these here to avoid early forking between starkly different APLs.
local aScriptPass = true
local ts = not strict and entry.strict ~= 1 and scripts:IsTimeSensitive( scriptID )
if not entry.criteria or entry.criteria == "" then
if debug then self:Debug( "There is no criteria for %s.", action == "use_items" and "Use Items" or state.args.list_name or "this action list" ) end
-- aScriptPass = ts or self:CheckStack()
else
aScriptPass = scripts:CheckScript( scriptID ) -- and self:CheckStack() -- we'll check the stack with the list's entries.
if not aScriptPass and ts then
-- Time-sensitive criteria, let's see if we have rechecks that would pass.
state.recheck( action, script, Stack, Block )
if #state.recheckTimes == 0 then
if debug then self:Debug( "Time-sensitive Criteria FAIL at +%.2f with no valid rechecks - %s", state.offset, scripts:GetConditionsAndValues( scriptID ) ) end
ts = false
elseif state.delayMax and state.recheckTimes[ 1 ] > state.delayMax then
if debug then self:Debug( "Time-sensitive Criteria FAIL at +%.2f with rechecks outside of max delay ( %.2f > %.2f ) - %s", state.offset, state.recheckTimes[ 1 ], state.delayMax, scripts:GetConditionsAndValues( scriptID ) ) end
ts = false
elseif state.recheckTimes[ 1 ] > rWait then
if debug then self:Debug( "Time-sensitive Criteria FAIL at +%.2f with rechecks greater than wait time ( %.2f > %.2f ) - %s", state.offset, state.recheckTimes[ 1 ], rWait, scripts:GetConditionsAndValues( scriptID ) ) end
ts = false
end
else
if debug then
self:Debug( "%sCriteria for %s %s at +%.2f - %s", ts and "Time-sensitive " or "", state.args.list_name or "???", ts and "deferred" or ( aScriptPass and "PASS" or "FAIL" ), state.offset, scripts:GetConditionsAndValues( scriptID ) )
end
end
aScriptPass = ts or aScriptPass
end
if aScriptPass then
if action == "use_items" then
self:AddToStack( scriptID, "items", caller )
rAction, rWait, rDepth = self:GetPredictionFromAPL( dispName, "UseItems", "items", slot, rAction, rWait, rDepth, scriptID )
if debug then self:Debug( "Returned from Use Items; current recommendation is %s (+%.2f).", rAction or "NO ACTION", rWait ) end
self:PopStack()
else
local name = state.args.list_name
if InUse[ name ] then
if debug then self:Debug( "Action list (%s) was found, but would cause a loop.", name ) end
elseif name and pack.lists[ name ] then
if debug then self:Debug( "Action list (%s) was found.", name ) end
self:AddToStack( scriptID, name, caller, action == "run_action_list" )
rAction, rWait, rDepth = self:GetPredictionFromAPL( dispName, packName, name, slot, rAction, rWait, rDepth, scriptID )
if debug then self:Debug( "Returned from list (%s), current recommendation is %s (+%.2f).", name, rAction or "NO ACTION", rWait ) end
self:PopStack()
-- REVISIT THIS: IF A RUN_ACTION_LIST CALLER IS NOT TIME SENSITIVE, DON'T BOTHER LOOPING THROUGH IT IF ITS CONDITIONS DON'T PASS.
-- if action == "run_action_list" and not ts then
-- if debug then self:Debug( "This entry was not time-sensitive; exiting loop." ) end
-- break
-- end
else
if debug then self:Debug( "Action list (%s) not found. Skipping.", name or "no name" ) end
end
end
end
elseif action == "variable" then
local name = state.args.var_name
if class.variables[ name ] then
if debug then self:Debug( " - variable.%s references a hardcoded variable and this entry will be ignored.", name ) end
elseif name ~= nil then
state:RegisterVariable( name, scriptID, listName, Stack )
if debug then self:Debug( " - variable.%s[%s] will check this script entry ( %s )", name, tostring( state.variable[ name ] ), scriptID ) end
else
if debug then self:Debug( " - variable name not provided, skipping." ) end
end
else
-- Target Cycling.
-- We have to determine *here* whether the ability would be used on the current target or a different target.
if state.args.cycle_targets == 1 and state.settings.cycle and state.active_enemies > 1 then
state.SetupCycle( ability )
if state.cycle_enemies == 1 then
if debug then Hekili:Debug( "There is only 1 valid enemy for target cycling; canceling cycle." ) end
state.ClearCycle()
end
else
state.ClearCycle()
end
Timer:Track("Post Cycle")
local usable, why = state:IsUsable()
Timer:Track("Post Usable")
if debug then
if usable then
local cost = state.action[ action ].cost
local costType = state.action[ action ].cost_type
if cost and cost > 0 then
self:Debug( "The action (%s) is usable at (%.2f + %.2f) with cost of %d %s (have %d).", action, state.offset, state.delay, cost or 0, costType or "unknown", costType and state[ costType ] and state[ costType ].current or -1 )
else
self:Debug( "The action (%s) is usable at (%.2f + %.2f).", action, state.offset, state.delay )
end
else
self:Debug( "The action (%s) is unusable at (%.2f + %.2f) because %s.", action, state.offset, state.delay, why or "IsUsable returned false" )
end
end
if usable then
local waitValue = max( 0, rWait - state:ClashOffset( rAction ) )
local readyFirst = state.delay - clash < waitValue
if debug then self:Debug( " - the action is %sready before the current recommendation (at +%.2f vs. +%.2f).", readyFirst and "" or "NOT ", state.delay, waitValue ) end
if readyFirst then
local hasResources = true
Timer:Track("Post Ready/Clash")
if hasResources then
local channelPass = not state.channeling or ( action ~= state.channel ) or self:CheckChannel( action, rWait )
local aScriptPass = channelPass and self:CheckStack()
Timer:Track("Post Stack")
if not channelPass then
if debug then self:Debug( " - this entry cannot break the channeled spell." ) end
if action == state.channel then
stop = scripts:CheckScript( scriptID )
end
elseif not aScriptPass then
if debug then self:Debug( " - this entry would not be reached at the current time via the current action list path (%.2f).", state.delay ) end
else
if not entry.criteria or entry.criteria == '' then
if debug then
self:Debug( " - this entry has no criteria to test." )
if not channelPass then self:Debug( " - however, criteria not met to break current channeled spell." ) end
end
else
Timer:Track("Pre-Script")
aScriptPass = scripts:CheckScript( scriptID )
Timer:Track("Post-Script")
if debug then
self:Debug( " - this entry's criteria %s: %s", aScriptPass and "PASSES" or "FAILS", scripts:GetConditionsAndValues( scriptID ) )
end
end
end
Timer:Track("Pre-Recheck")
-- NEW: If the ability's conditions didn't pass, but the ability can report on times when it should recheck, let's try that now.
if not aScriptPass then
state.recheck( action, script, Stack, Block )
Timer:Track("Post-Recheck Times")
if #state.recheckTimes == 0 then
if debug then self:Debug( "There were no recheck events to check." ) end
else
local base_delay = state.delay
if debug then self:Debug( "There are " .. #state.recheckTimes .. " recheck events." ) end
local first_rechannel = 0
Timer:Track("Pre-Recheck Loop")
for i, step in pairs( state.recheckTimes ) do
local new_wait = base_delay + step
Timer:Track("Recheck Loop Start")
if new_wait >= 10 then
if debug then self:Debug( "Rechecking stopped at step #%d. The recheck ( %.2f ) isn't ready within a reasonable time frame ( 10s ).", i, new_wait ) end
break
elseif ( action ~= state.channel ) and waitValue <= base_delay + step + 0.05 then
if debug then self:Debug( "Rechecking stopped at step #%d. The previously chosen ability is ready before this recheck would occur ( %.2f <= %.2f + 0.05 ).", i, waitValue, new_wait ) end
break
end
state.delay = base_delay + step
local usable, why = state:IsUsable()
if debug then
if not usable then
self:Debug( "The action (%s) is no longer usable at (%.2f + %.2f) because %s.", action, state.offset, state.delay, why or "IsUsable returned false" )
state.delay = base_delay
break
end
end
Timer:Track("Recheck Post-Usable")
if self:CheckStack() then
Timer:Track("Recheck Post-Stack")
aScriptPass = scripts:CheckScript( scriptID )
Timer:Track("Recheck Post-Script")
channelPass = not state.channeling or ( action ~= state.channel ) or self:CheckChannel( action, rWait )
Timer:Track("Recheck Post-Channel")
if debug then
self:Debug( "Recheck #%d ( +%.2f ) %s: %s", i, state.delay, aScriptPass and "MET" or "NOT MET", scripts:GetConditionsAndValues( scriptID ) )
if not channelPass then self:Debug( " - however, criteria not met to break current channeled spell." ) end
end
aScriptPass = aScriptPass and channelPass
else
if debug then self:Debug( "Unable to recheck #%d at %.2f, as APL conditions would not pass.", i, state.delay ) end
end
Timer:Track("Recheck Loop End")
if aScriptPass then
if first_rechannel == 0 and state.channel and action == state.channel then
first_rechannel = state.delay
if debug then self:Debug( "This is the currently channeled spell; it would be rechanneled at this time, will check end of channel. " .. state.channel_remains ) end
elseif first_rechannel > 0 and ( not state.channel or state.channel_remains < 0.05 ) then
if debug then self:Debug( "Appears that the ability would be cast again at the end of the channel, stepping back to first rechannel point. " .. state.channel_remains ) end
state.delay = first_rechannel
waitValue = first_rechannel
break
else break end
else state.delay = base_delay end
end
Timer:Track("Post Recheck Loop")
end
end
Timer:Track("Post Recheck")
if aScriptPass then
if action == "potion" then
local item = class.abilities.potion.item
slot.scriptType = "simc"
slot.script = scriptID
slot.hook = caller
slot.display = dispName
slot.pack = packName
slot.list = listName
slot.listName = listName
slot.action = actID
slot.actionName = state.this_action
slot.actionID = -1 * item
slot.texture = select( 10, GetItemInfo( item ) )
slot.caption = ability.caption or entry.caption
slot.item = item
slot.wait = state.delay
slot.resource = state.GetResourceType( rAction )
rAction = state.this_action
rWait = state.delay
if debug then
-- scripts:ImplantDebugData( slot )
self:Debug( "Action chosen: %s at %.2f!", rAction, rWait )
end
-- slot.indicator = ( entry.Indicator and entry.Indicator ~= "none" ) and entry.Indicator
state.selection_time = state.delay
state.selected_action = rAction
elseif action == "wait" then
local sec = state.args.sec or 0.5
if sec <= 0 then
if debug then self:Debug( "Invalid wait value ( %.2f ); skipping...", sec ) end
else
slot.scriptType = "simc"
slot.script = scriptID
slot.hook = caller
slot.display = dispName
slot.pack = packName
slot.list = listName
slot.listName = listName
slot.action = actID
slot.actionName = state.this_action
slot.actionID = ability.id
slot.caption = ability.caption or entry.caption
slot.texture = ability.texture
slot.indicator = ability.indicator
if ability.interrupt and state.buff.casting.up then
slot.interrupt = true
slot.castStart = state.buff.casting.applied
else
slot.interrupt = nil
slot.castStart = nil
end
slot.wait = state.delay
slot.waitSec = sec
slot.resource = state.GetResourceType( rAction )
rAction = state.this_action
rWait = state.delay
state.selection_time = state.delay
state.selected_action = rAction
if debug then
self:Debug( "Action chosen: %s at %.2f!", rAction, state.delay )
end
end
elseif action == "cancel_action" then
if state.args.action_name and state:IsChanneling( state.args.action_name ) then state.channel_breakable = true end
elseif action == "pool_resource" then
if state.args.for_next == 1 then
-- Pooling for the next entry in the list.
local next_entry = list[ actID + 1 ]
local next_action = next_entry and next_entry.action
local next_id = next_action and class.abilities[ next_action ] and class.abilities[ next_action ].id
local extra_amt = state.args.extra_amount or 0
local next_known = next_action and state:IsKnown( next_action )
local next_usable, next_why = next_action and state:IsUsable( next_action )
local next_cost = next_action and state.action[ next_action ] and state.action[ next_action ].cost or 0
local next_res = next_action and state.GetResourceType( next_action ) or class.primaryResource
if not next_entry then
if debug then self:Debug( "Attempted to Pool Resources for non-existent next entry in the APL. Skipping." ) end
elseif not next_action or not next_id or next_id < 0 then
if debug then self:Debug( "Attempted to Pool Resources for invalid next entry in the APL. Skipping." ) end
elseif not next_known then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but the next entry is not known. Skipping.", next_action ) end
elseif not next_usable then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but the next entry is not usable because %s. Skipping.", next_action, next_why or "of an unknown reason" ) end
elseif state.cooldown[ next_action ].remains > 0 then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but the next entry is on cooldown. Skipping.", next_action ) end
elseif state[ next_res ].current >= next_cost + extra_amt then
if debut then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but we already have all the resources needed ( %.2f > %.2f + %.2f ). Skipping.", next_ation, state[ next_res ].current, next_cost, extra_amt ) end
else
-- Oops. We only want to wait if
local next_wait = state[ next_res ][ "time_to_" .. ( next_cost + extra_amt ) ]
--[[ if next_wait > 0 then
if debug then self:Debug( "Next Wait: %.2f; TTR: %.2f, Resource(%.2f): %.2f", next_wait, state:TimeToReady( next_action, true ), next_cost + extra_amt, state[ next_res ][ "time_to_" .. ( next_cost + extra_amt ) ] ) end
end ]]
if next_wait <= 0 then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but there is no need to wait. Skipping.", next_action ) end
elseif next_wait >= rWait then
if debug then self:Debug( "The currently chosen action ( %s ) is ready at or before the next action ( %.2fs <= %.2fs ). Skipping.", ( rAction or "???" ), rWait, next_wait ) end
elseif state.delayMax and next_wait >= state.delayMax then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but we would exceed our time ceiling in %.2fs. Skipping.", next_action, next_wait ) end
elseif next_wait >= 10 then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but we'd have to wait much too long ( %.2f ). Skipping.", next_action, next_wait ) end
else
-- Pad the wait value slightly, to make sure the resource is actually generated.
next_wait = next_wait + 0.01
state.offset = state.offset + next_wait
state.this_action = next_action
aScriptPass = not next_entry.criteria or next_entry.criteria == '' or scripts:CheckScript( packName .. ':' .. listName .. ':' .. ( actID + 1 ) )
state.this_action = "pool_resource"
if not aScriptPass then
if debug then self:Debug( "Attempted to Pool Resources for Next Entry ( %s ), but its conditions would not be met. Skipping.", next_action ) end
state.offset = state.offset - next_wait
else
if debug then self:Debug( "Pooling Resources for Next Entry ( %s ), delaying by %.2f ( extra %d ).", next_action, next_wait, extra_amt ) end
state.offset = state.offset - next_wait
state.advance( next_wait )
end
end
end
else
-- Pooling for a Wait Value.
-- NYI.
-- if debug then self:Debug( "Pooling for a specified period of time is not supported yet. Skipping." ) end
if debug then self:Debug( "pool_resource is disabled as pooling is automatically accounted for by the forecasting engine." ) end
end
-- if entry.PoolForNext or state.args.for_next == 1 then
-- if debug then self:Debug( "Pool Resource is not used in the Predictive Engine; ignored." ) end
-- end
else
slot.scriptType = "simc"
slot.script = scriptID
slot.hook = caller
slot.display = dispName
slot.pack = packName
slot.list = listName
slot.listName = listName
slot.action = actID
slot.actionName = ability.key
slot.actionID = ability.id
slot.caption = ability.caption or entry.caption
slot.texture = ability.texture
slot.indicator = ability.indicator
if ability.interrupt and state.buff.casting.up then
slot.interrupt = true
slot.castStart = state.buff.casting.applied
else
slot.interrupt = nil
slot.castStart = nil
end
slot.wait = state.delay
slot.waitSec = nil
slot.resource = state.GetResourceType( rAction )
rAction = state.this_action
rWait = state.delay
state.selection_time = state.delay
state.selected_action = rAction
slot.empower_to = ability.empowered and ( state.args.empower_to or state.max_empower ) or nil
if debug then
-- scripts:ImplantDebugData( slot )
self:Debug( "Action chosen: %s at %.2f!", rAction, state.delay )
end
if state.IsCycling( nil, true ) then
slot.indicator = "cycle"
elseif module and module.cycle then
slot.indicator = module.cycle()
end
Timer:Track( "Action Stored" )
end
end
state.ClearCycle()
end
end
end
if rWait == 0 or force_channel then break end
end
end
end
end
if debug and action ~= "call_action_list" and action ~= "run_action_list" and action ~= "use_items" then
self:Debug( "Time spent on this action: %.2fms\nTimeData:%s-%s-%d:%s:%.2f%s", Timer:Total(), packName, listName, actID, action, Timer:Total(), Timer:Output() )
end
end
else
if debug then self:Debug( "\nEntry #%d in list ( %s ) is not set or not enabled. Skipping.", actID, listName ) end
end
actID = actID + 1
end
else
if debug then self:Debug( "ListActive: N (%s-%s)", packName, listName ) end
end
if debug then self:Debug( "Exiting %s with recommendation of %s at +%.2fs.", listName or "UNKNOWN", action or "NO ACTION", wait or state.delayMax ) end
local scriptID = listStack[ listName ]
listStack[ listName ] = nil
if listCache[ scriptID ] then twipe( listCache[ scriptID ] ) end
if listValue[ scriptID ] then twipe( listValue[ scriptID ] ) end
return rAction, rWait, rDepth
end
Hekili:ProfileCPU( "GetPredictionFromAPL", Hekili.GetPredictionFromAPL )
function Hekili:GetNextPrediction( dispName, packName, slot )
local debug = self.ActiveDebug
-- This is the entry point for the prediction engine.
-- Any cache-wiping should happen here.
twipe( Stack )
twipe( Block )
twipe( InUse )
twipe( listStack )
for k, v in pairs( listCache ) do tinsert( lcPool, v ); twipe( v ); listCache[ k ] = nil end
for k, v in pairs( listValue ) do tinsert( lvPool, v ); twipe( v ); listValue[ k ] = nil end
self:ResetSpellCaches()
state:ResetVariables()
local display = rawget( self.DB.profile.displays, dispName )
local pack = rawget( self.DB.profile.packs, packName )
if not pack then return end
local action, wait, depth = nil, 10, 0
state.this_action = nil
state.selection_time = 10
state.selected_action = nil
if self.ActiveDebug then
self:Debug( "Checking if I'm casting ( %s ) and if it is a channel ( %s ).", state.buff.casting.up and "Yes" or "No", state.buff.casting.v3 == 1 and "Yes" or "No" )
if state.buff.casting.up then
if state.buff.casting.v3 == 1 then self:Debug( " - Is criteria met to break channel? %s.", state.channel_breakable and "Yes" or "No" ) end
self:Debug( " - Can I cast while casting/channeling? %s.", state.spec.can_dual_cast and "Yes" or "No" )
end
end
if not state.channel_breakable and state.buff.casting.up and state.spec.can_dual_cast then
self:Debug( "Whitelist of castable-while-casting spells applied [ %d, %.2f ]", state.buff.casting.v1, state.buff.casting.remains )
state:SetWhitelist( state.spec.dual_cast )
else
self:Debug( "No whitelist." )
state:SetWhitelist( nil )
end
if pack.lists.precombat then
local listName = "precombat"
if debug then self:Debug( 1, "\nProcessing precombat action list [ %s - %s ].", packName, listName ); self:Debug( 2, "" ) end
action, wait, depth = self:GetPredictionFromAPL( dispName, packName, "precombat", slot, action, wait, depth )
if debug then self:Debug( 1, "\nCompleted precombat action list [ %s - %s ].", packName, listName ) end
else
if debug then
if state.time > 0 then
self:Debug( "Precombat APL not processed because combat time is %.2f.", state.time )
end
end
end
if pack.lists.default and wait > 0 then
local list = pack.lists.default
local listName = "default"
if debug then self:Debug( 1, "\nProcessing default action list [ %s - %s ].", packName, listName ); self:Debug( 2, "" ) end
action, wait, depth = self:GetPredictionFromAPL( dispName, packName, "default", slot, action, wait, depth )
if debug then self:Debug( 1, "\nCompleted default action list [ %s - %s ].", packName, listName ) end
end
if debug then self:Debug( "Recommendation is %s at %.2f + %.2f.", action or "NO ACTION", state.offset, wait ) end
return action, wait, depth
end
Hekili:ProfileCPU( "GetNextPrediction", Hekili.GetNextPrediction )
local pvpZones = {
arena = true,
pvp = true
}
function Hekili:GetDisplayByName( name )
return rawget( self.DB.profile.displays, name ) and name or nil
end
local aoeDisplayRule = function( p )
local spec = rawget( p.specs, state.spec.id )
if not spec or not class.specs[ state.spec.id ] then return false end
if Hekili:GetToggleState( "mode" ) == "reactive" and ns.getNumberTargets() < ( spec.aoe or 3 ) then
if HekiliDisplayAOE.RecommendationsStr then
HekiliDisplayAOE.RecommendationsStr = nil
HekiliDisplayAOE.NewRecommendations = true
end
return false
end
return true
end
local displayRules = {
Interrupts = { function( p ) return p.toggles.interrupts.value and p.toggles.interrupts.separate end, false, "Defensives" },
Defensives = { function( p ) return p.toggles.defensives.value and p.toggles.defensives.separate end, false, "Cooldowns" },
Cooldowns = { function( p ) return p.toggles.cooldowns.value and p.toggles.cooldowns.separate end, false, "Primary" },
Primary = { function( ) return true end, true , "AOE" },
AOE = { aoeDisplayRule , false, "Interrupts" }
}
local lastDisplay = "AOE"
local hasSnapped
function Hekili.Update( initial )
if not Hekili:ScriptsLoaded() then
Hekili:LoadScripts()
return
end
if not Hekili:IsValidSpec() then
return
end
local profile = Hekili.DB.profile
local specID = state.spec.id
if not specID then return end
local spec = rawget( profile.specs, specID )
if not spec then return end
local packName = spec.package
if not packName then return end
local pack = rawget( profile.packs, packName )
if not pack then return end
local debug = Hekili.ActiveDebug
Hekili:GetNumTargets( true )
local snaps
local dispName = initial or displayRules[ lastDisplay ][ 3 ]
state.display = dispName
for round = 1, 5 do
local rule, fullReset, nextDisplay = unpack( displayRules[ dispName ] )
local display = rawget( profile.displays, dispName )
fullReset = fullReset or state.offset > 0
if debug then
Hekili:SetupDebug( dispName )
Hekili:Debug( "*** START OF NEW DISPLAY: %s ***", dispName )
end
local UI = ns.UI.Displays[ dispName ]
local Queue = UI.Recommendations
UI:SetThreadLocked( true )
if Queue then
for k, v in pairs( Queue ) do
for l, w in pairs( v ) do
if type( Queue[ k ][ l ] ) ~= "table" then
Queue[ k ][ l ] = nil
end
end
end
end
local checkstr = ""
if UI.Active and UI.alpha > 0 and rule( profile ) then
for i = #Stack, 1, -1 do tinsert( StackPool, tremove( Stack, i ) ) end
for i = #Block, 1, -1 do tinsert( StackPool, tremove( Block, i ) ) end
state.reset( dispName, fullReset )
-- Clear the stack in case we interrupted ourselves.
wipe( InUse )
state.system.specID = specID
state.system.specInfo = spec
state.system.packName = packName
state.system.packInfo = pack
state.system.display = dispName
state.system.dispInfo = display
local actualStartTime = debugprofilestop()
local numRecs = display.numIcons or 4
if display.flash.enabled and display.flash.suppress then
numRecs = 1
end
for i = 1, numRecs do
local chosen_depth = 0
Queue[ i ] = Queue[ i ] or {}
local slot = Queue[ i ]
slot.index = i
state.index = i
if debug then Hekili:Debug( 0, "\nRECOMMENDATION #%d ( Offset: %.2f, GCD: %.2f, %s: %.2f ).\n", i, state.offset, state.cooldown.global_cooldown.remains, ( state.buff.casting.v3 == 1 and "Channeling" or "Casting" ), state.buff.casting.remains ) end
local action, wait, depth
state.delay = 0
state.delayMin = 0
state.delayMax = dispName ~= "Primary" and dispName ~= "AOE" and display.forecastPeriod or 15
local hadProj = false
local events = state:GetQueue()
local event = events[ 1 ]
local n = 1
if debug and #events > 0 then
Hekili:Debug( 1, "There are %d queued events to review.", #events )
end
while( event ) do
local eStart
if debug then
eStart = debugprofilestop()
local resources
for k in orderedPairs( class.resources ) do
resources = ( resources and ( resources .. ", " ) or "" ) .. string.format( "%s[ %.2f / %.2f ]", k, state[ k ].current, state[ k ].max )
end
Hekili:Debug( 1, "Resources: %s\n", resources )
if state.channeling then
Hekili:Debug( 1, "Currently channeling ( %s ) until ( %.2f ).\n", state.channel, state.channel_remains )
end
end
ns.callHook( "step" )
local t = event.time - state.now - state.offset
if t < 0 then
state.offset = state.offset - t
if debug then Hekili:Debug( 1, "Finishing queued event #%d ( %s of %s ) due at %.2f because the event should've already occurred.\n", n, event.type, event.action, t ) end
state:HandleEvent( event )
state.offset = state.offset + t
event = events[ 1 ]
elseif t < 0.2 then
if debug then Hekili:Debug( 1, "Finishing queued event #%d ( %s of %s ) due at %.2f because the event occurs w/in 0.2 seconds.\n", n, event.type, event.action, t ) end
state.advance( t )
if event == events[ 1 ] then
-- Event did not get handled due to rounding.
state:HandleEvent( event )
-- state:RemoveEvent( event )
end
event = events[ 1 ]
else
--[[
Okay, new paradigm. We're checking whether we should break channeled spells before we worry about casting while casting.
Are we channeling?
a. If yes, check whether conditions are met to break the channel.
i. If yes, allow the channel to be broken by anything but the channeled spell itself.
If we get a condition-pass for the channeled spell, stop seeking recommendations and move on.
ii. If no, move on to checking whether we can cast while casting (old code).
b. If no, move on to checking whether we can cast while casting (old code).
]]
local channeling, shouldBreak = state:IsChanneling(), false
if channeling then
if debug then Hekili:Debug( "We are channeling, checking if we should break the channel..." ) end
shouldBreak = Hekili:CheckChannel( nil, 0 )
state.channel_breakable = shouldBreak
else
state.channel_breakable = false
end
local casting, shouldCheck = state:IsCasting(), false
if ( casting or ( channeling and not shouldBreak ) ) and state.spec.can_dual_cast then
shouldCheck = false
for spell in pairs( state.spec.dual_cast ) do
if debug then Hekili:Debug( "CWC: %s | %s | %s | %s | %.2f | %s | %.2f | %.2f", spell, tostring( state:IsKnown( spell ) ), tostring( state:IsUsable( spell ) ), tostring( class.abilities[ spell ].dual_cast ), state:TimeToReady( spell ), tostring( state:TimeToReady( spell ) <= t ), state.offset, state.delay ) end
if class.abilities[ spell ].dual_cast and state:IsKnown( spell ) and state:IsUsable( spell ) and state:TimeToReady( spell ) <= t then
shouldCheck = true
break
end
end
end
local overrideIndex, overrideAction, overrideType, overrideTime
if channeling and ( shouldBreak or shouldCheck ) and event.type == "CHANNEL_TICK" then
local eventAbility = class.abilities[ event.action ]
if eventAbility and not eventAbility.tick then
-- The ability doesn't actually do anything at any tick times, so let's use the time of the next non-channel tick event instead.
for i = 1, #events do
local e = events[ i ]
if e.type ~= "CHANNEL_TICK" then
overrideIndex = i
overrideAction = e.action
overrideType = e.type
overrideTime = e.time - state.now - state.offset
if debug then Hekili:Debug( "As %s's channel has no tick function, we will check between now and %s's %s event in %.2f seconds.", event.action, overrideAction, overrideType, overrideTime ) end
break
end
end
end
end
if ( casting or channeling ) and not shouldBreak and not shouldCheck then
if debug then Hekili:Debug( 1, "Finishing queued event #%d ( %s of %s ) due at %.2f as player is casting and castable spells are not ready.\nCasting: %s, Channeling: %s, Break: %s, Check: %s", n, event.type, event.action, t, casting and "Yes" or "No", channeling and "Yes" or "No", shouldBreak and "Yes" or "No", shouldCheck and "Yes" or "No" ) end
if t > 0 then
state.advance( t )
local resources
for k in orderedPairs( class.resources ) do
resources = ( resources and ( resources .. ", " ) or "" ) .. string.format( "%s[ %.2f / %.2f ]", k, state[ k ].current, state[ k ].max )
end
Hekili:Debug( 1, "Resources: %s\n", resources )
end
event = events[ 1 ]
else
state:SetConstraint( 0, ( overrideTime or t ) - 0.01 )
hadProj = true
if debug then Hekili:Debug( 1, "Queued event #%d (%s %s) due at %.2f; checking pre-event recommendations.\n", overrideIndex or n, overrideAction or event.action, overrideType or event.type, overrideTime or t ) end
if casting or channeling then
state:ApplyCastingAuraFromQueue()
if debug then Hekili:Debug( 2, "Player is casting for %.2f seconds. %s.", state.buff.casting.remains, shouldBreak and "We can break the channel" or "Only spells castable while casting will be used" ) end
else
state.removeBuff( "casting" )
end
local waitLoop = 0
repeat
action, wait, depth = Hekili:GetNextPrediction( dispName, packName, slot )
if action == "wait" then
if debug then Hekili:Debug( "EXECUTING WAIT ( %.2f ) EVENT AT ( +%.2f ) AND RECHECKING RECOMMENDATIONS...", slot.waitSec, wait ) end
state.advance( wait + slot.waitSec )
slot.action = nil
slot.actionName = nil
slot.actionID = nil
state.delay = 0
state.delayMin = 0
state.delayMax = dispName ~= "Primary" and dispName ~= "AOE" and display.forecastPeriod or 15
action, wait = nil, 10
action, wait, depth = Hekili:GetNextPrediction( dispName, packName, slot )
end
waitLoop = waitLoop + 1
if waitLoop > 2 then
if debug then Hekili:Debug( "BREAKING WAIT LOOP!" ) end
slot.action = nil
slot.actionName = nil
slot.actionID = nil
state.delay = 0
state.delayMin = 0
state.delayMax = dispName ~= "Primary" and dispName ~= "AOE" and display.forecastPeriod or 15
action, wait = nil, 10
break
end
until action ~= "wait"
if action == "wait" then
action, wait = nil, 10
end
if not action then
if debug then Hekili:Debug( "Time spent on event #%d PREADVANCE: %.2fms...", n, debugprofilestop() - eStart ) end
if debug then Hekili:Debug( 1, "No recommendation found before event #%d (%s %s) at %.2f; triggering event and continuing ( %.2f ).\n", n, event.action or "NO ACTION", event.type or "NO TYPE", t, state.offset + state.delay ) end
state.advance( overrideTime or t )
if debug then Hekili:Debug( "Time spent on event #%d POSTADVANCE: %.2fms...", n, debugprofilestop() - eStart ) end
event = events[ 1 ]
else
break
end
end
end
n = n + 1
if n > 10 then
if debug then Hekili:Debug( "WARNING: Attempted to process 10+ events; breaking to avoid CPU wastage." ) end
break
end
Hekili.ThreadStatus = "Processed event #" .. n .. " for " .. dispName .. "."
end
if not action then
state.delay = 0
state.delayMin = 0
state.delayMax = dispName ~= "Primary" and dispName ~= "AOE" and display.forecastPeriod or 15
if class.file == "DEATHKNIGHT" then
state:SetConstraint( 0, min( state.delayMax, max( 0.01 + state.rune.cooldown * 2, 10 ) ) )
else
state:SetConstraint( 0, min( state.delayMax, 10 ) )
end
if hadProj and debug then Hekili:Debug( "[ ** ] No recommendation before queued event(s), checking recommendations after %.2f.", state.offset ) end
if debug then
local resources
for k in orderedPairs( class.resources ) do
resources = ( resources and ( resources .. ", " ) or "" ) .. string.format( "%s[ %.2f / %.2f ]", k, state[ k ].current, state[ k ].max )
end
Hekili:Debug( 1, "Resources: %s", resources or "none" )
ns.callHook( "step" )
if state.channeling then
Hekili:Debug( " - Channeling ( %s ) until ( %.2f ).", state.channel, state.channel_remains )
end
end
local waitLoop = 0
repeat
action, wait, depth = Hekili:GetNextPrediction( dispName, packName, slot )
if action == "wait" then
if debug then Hekili:Debug( "EXECUTING WAIT ( %.2f ) EVENT AT ( +%.2f ) AND RECHECKING RECOMMENDATIONS...", slot.waitSec, wait ) end
state.advance( wait + slot.waitSec )
slot.action = nil
slot.actionName = nil
slot.actionID = nil
state.delay = 0
state.delayMin = 0
state.delayMax = dispName ~= "Primary" and dispName ~= "AOE" and display.forecastPeriod or 15
action, wait = nil, 10
action, wait, depth = Hekili:GetNextPrediction( dispName, packName, slot )
end
waitLoop = waitLoop + 1
if waitLoop > 2 then
if debug then Hekili:Debug( "BREAKING WAIT LOOP!" ) end
slot.action = nil
slot.actionName = nil
slot.actionID = nil
state.delay = 0
state.delayMin = 0
state.delayMax = dispName ~= "Primary" and dispName ~= "AOE" and display.forecastPeriod or 15
action, wait = nil, 10
break
end
until action ~= "wait"
if action == "wait" then
action, wait = nil, 10
end
end
state.delay = wait
if debug then
Hekili:Debug( "Recommendation #%d is %s at %.2fs (%.2fs).", i, action or "NO ACTION", wait or state.delayMax, state.offset + state.delay )
end
if action then
slot.time = state.offset + wait
slot.exact_time = state.now + state.offset + wait
slot.delay = i > 1 and wait or ( state.offset + wait )
slot.since = i > 1 and slot.time - Queue[ i - 1 ].time or 0
slot.resources = slot.resources or {}
slot.depth = chosen_depth
state.scriptID = slot.script
local ability = class.abilities[ action ]
local cast_target = i == 1 and state.cast_target ~= "nobody" and state.cast_target or state.target.unit
if slot.indicator == "cycle" then
state.SetupCycle( ability )
cast_target = cast_target .. "c"
end
if debug then scripts:ImplantDebugData( slot ) end
checkstr = checkstr and ( checkstr .. ':' .. action ) or action
slot.keybind, slot.keybindFrom = Hekili:GetBindingForAction( action, display, i )
slot.resource_type = state.GetResourceType( action )
for k,v in pairs( class.resources ) do
slot.resources[ k ] = state[ k ].current
end
if i < display.numIcons then
-- Advance through the wait time.
state.this_action = action
if state.delay > 0 then state.advance( state.delay ) end
local cast = ability.cast
if ability.gcd ~= "off" and state.cooldown.global_cooldown.remains == 0 then
state.setCooldown( "global_cooldown", state.gcd.execute )
end
if state.buff.casting.up and not ability.dual_cast then
state.stopChanneling( false, action )
state.removeBuff( "casting" )
end
if ability.cast > 0 then
if not ability.channeled then
if debug then Hekili:Debug( "Queueing %s cast finish at %.2f [+%.2f] on %s.", action, state.query_time + cast, state.offset + cast, cast_target ) end
state.applyBuff( "casting", ability.cast, nil, ability.id, nil, false )
state:QueueEvent( action, state.query_time, state.query_time + cast, "CAST_FINISH", cast_target )
else
if ability.charges and ability.charges > 1 and ability.recharge > 0 then
state.spendCharges( action, 1 )
elseif action ~= "global_cooldown" and ability.cooldown > 0 then
state.setCooldown( action, ability.cooldown )
end
if debug then Hekili:Debug( "Queueing %s channel finish at %.2f [%.2f+%.2f].", action, state.query_time + cast, state.offset, cast, cast_target ) end
state:QueueEvent( action, state.query_time, state.query_time + cast, "CHANNEL_FINISH", cast_target )
-- Queue ticks because we may not have an ability.tick function, but may have resources tied to an aura.
if ability.tick_time then
local ticks = floor( cast / ability.tick_time )
for i = 1, ticks do
state:QueueEvent( action, state.query_time, state.query_time + ( i * ability.tick_time ), "CHANNEL_TICK", cast_target )
end
if debug then Hekili:Debug( "Queued %d ticks of channel %s.", ticks, action ) end
end
if Hekili.Scripts.Channels and Hekili.Scripts.Channels[ packName ] and Hekili.Scripts.Channels[ packName ][ action ] then
state:QueueEvent( action, state.query_time, state.cooldown.global_cooldown.expires, "GCD_FINISH", cast_target )
end
state:RunHandler( action )
ns.spendResources( action )
end
else
-- Instants.
if ability.charges and ability.charges > 1 and ability.recharge > 0 then
state.spendCharges( action, 1 )
elseif action ~= "global_cooldown" and ability.cooldown > 0 then
state.setCooldown( action, ability.cooldown )
end
ns.spendResources( action )
state:RunHandler( action )
end
-- Projectile spells have two handlers, effectively. A handler (run on cast/channel finish), and then an impact handler.
if ability.isProjectile then
state:QueueEvent( action, state.query_time + cast, nil, "PROJECTILE_IMPACT", cast_target )
end
if state.trinket.t1.is[ action ] and state.trinket.t2.has_cooldown then
local t2 = state.trinket.t2.cooldown.key
local duration = ability.cooldown / 6
if state.cooldown[ t2 ].remains < duration then
state.setCooldown( t2, duration )
end
elseif state.trinket.t2.is[ action ] and state.trinket.t1.has_cooldown then
local t1 = state.trinket.t1.cooldown.key
local duration = ability.cooldown / 6
if state.cooldown[ t1 ].remains < duration then
state.setCooldown( t1, duration )
end
end
end
else
for s = i, numRecs do
action = action or ''
checkstr = checkstr and ( checkstr .. ':' .. action ) or action
slot[s] = nil
end
state.delay = 0
if debug then
local resInfo
for k in orderedPairs( class.resources ) do
local res = rawget( state, k )
if res then
local forecast = res.forecast and res.fcount and res.forecast[ res.fcount ]
local final = "N/A"
if forecast then
final = string.format( "%.2f @ [%d - %s] %.2f", forecast.v, res.fcount, forecast.e or "none", forecast.t - state.now - state.offset )
end
resInfo = ( resInfo and ( resInfo .. ", " ) or "" ) .. string.format( "%s[ %.2f / %.2f || %s ]", k, res.current, res.max, final )
end
if resInfo then resInfo = "Resources: " .. resInfo end
end
if resInfo then
Hekili:Debug( resInfo )
end
else
if not hasSnapped and profile.autoSnapshot and InCombatLockdown() and state.level >= 50 and ( dispName == "Primary" or dispName == "AOE" ) then
Hekili:Print( "Unable to make recommendation for " .. dispName .. " #" .. i .. "; triggering auto-snapshot..." )
hasSnapped = dispName
UI:SetThreadLocked( false )
return "AutoSnapshot"
end
end
break
end
end
UI.NewRecommendations = true
UI.RecommendationsStr = checkstr
UI:SetThreadLocked( false )
if WeakAuras and WeakAuras.ScanEvents then
if not UI.EventPayload then
UI.EventPayload = {
{}, -- [1]
}
setmetatable( UI.EventPayload, {
__index = UI.EventPayload[ 1 ],
__mode = "kv"
} )
end
for i = 1, numRecs do
if UI.EventPayload[ i ] then wipe( UI.EventPayload[ i ] )
else UI.EventPayload[ i ] = {} end
for k, v in pairs( Queue[ i ] ) do
UI.EventPayload[ i ][ k ] = v
end
end
WeakAuras.ScanEvents( "HEKILI_RECOMMENDATION_UPDATE", dispName, Queue[ 1 ].actionID, Queue[ 1 ].indicator, Queue[ 1 ].empower_to, UI.EventPayload )
end
if debug then
Hekili:Debug( "Time spent generating recommendations: %.2fms", debugprofilestop() - actualStartTime )
if Hekili:SaveDebugSnapshot( dispName ) then
if snaps then
snaps = snaps .. ", " .. dispName
else
snaps = dispName
end
if Hekili.Config then LibStub( "AceConfigDialog-3.0" ):SelectGroup( "Hekili", "snapshots" ) end
end
end
lastDisplay = dispName
dispName = nextDisplay
state.display = dispName
if round < 5 then Hekili:Yield( "Recommendations finished for " .. lastDisplay .. ".", nil, true ) end
else
if UI.RecommendationsStr then
UI.RecommendationsStr = nil
UI.NewRecommendations = true
end
lastDisplay = dispName
dispName = nextDisplay
state.display = dispName
end
end
if snaps then
Hekili:Print( "Snapshots saved: " .. snaps .. "." )
end
end
Hekili:ProfileCPU( "ThreadedUpdate", Hekili.Update )
function Hekili_GetRecommendedAbility( display, entry )
entry = entry or 1
if not rawget( Hekili.DB.profile.displays, display ) then
return nil, "Display not found."
end
if not ns.queue[ display ] then
return nil, "No queue for that display."
end
local slot = ns.queue[ display ][ entry ]
if not slot or not slot.actionID then
return nil, "No entry #" .. entry .. " for that display."
end
local payload = Hekili.DisplayPool[ display ].EventPayload
return slot.actionID, slot.empower_to, payload and payload[ entry ]
end
local usedCPU = {}
function Hekili:DumpFrameInfo()
wipe( usedCPU )
for k, v in orderedPairs( ns.frameProfile ) do
local usage, calls = GetFrameCPUUsage( v, true )
-- calls = self.ECount[ k ] or calls
if usage and calls > 0 then
local db = {}
db.name = k or v:GetName()
db.calls = calls
db.usage = usage
db.average = usage / calls
db.peak = v.peakUsage
table.insert( usedCPU, db )
end
end
table.sort( usedCPU, function( a, b ) return a.usage < b.usage end )
print( "Frame CPU Usage Data" )
for i, v in ipairs( usedCPU ) do
if v.peak and type( v.peak ) == "number" then
print( format( "%-40s %6.2fms (%6d calls, %6.2fms average, %6.2fms peak)", v.name, v.usage, v.calls, v.average, v.peak ) )
else
print( format( "%-40s %6.2fms (%6d calls, %6.2fms average)", v.name, v.usage, v.calls, v.average ) )
if v.peak then
for k, info in pairs( v.peak ) do
print( " - " .. k .. ": " .. info )
end
end
end
end
end