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
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 or 0
|
|
|
|
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 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
|
|
|