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.

659 lines
22 KiB

-- Hekili.lua
-- July 2024
local addon, ns = ...
Hekili = LibStub("AceAddon-3.0"):NewAddon( "Hekili", "AceConsole-3.0", "AceSerializer-3.0" )
Hekili.Version = C_AddOns.GetAddOnMetadata( "Hekili", "Version" )
Hekili.Flavor = C_AddOns.GetAddOnMetadata( "Hekili", "X-Flavor" ) or "Retail"
local format = string.format
local insert, concat = table.insert, table.concat
local GetBuffDataByIndex, GetDebuffDataByIndex = C_UnitAuras.GetBuffDataByIndex, C_UnitAuras.GetDebuffDataByIndex
local UnpackAuraData = AuraUtil.UnpackAuraData
local buildStr, _, _, buildNum = GetBuildInfo()
Hekili.CurrentBuild = buildNum
if Hekili.Version == ( "@" .. "project-version" .. "@" ) then
Hekili.Version = format( "Dev-%s (%s)", buildStr, date( "%Y%m%d" ) )
Hekili.IsDev = true
end
Hekili.AllowSimCImports = true
Hekili.IsRetail = function()
return Hekili.Flavor == "Retail"
end
Hekili.IsWrath = function()
return Hekili.Flavor == "Wrath"
end
Hekili.IsClassic = function()
return Hekili.IsWrath()
end
Hekili.IsDragonflight = function()
return buildNum >= 100000
end
5 months ago
Hekili.BuiltFor = 110100
Hekili.GameBuild = buildStr
5 months ago
ns.PTR = buildNum > 110100
Hekili.IsPTR = ns.PTR
ns.Patrons = "|cFFFFD100Current Status|r\n\n"
.. "All existing specializations are currently supported, though healer priorities are experimental and focused on rotational DPS only.\n\n"
5 months ago
.. "If you find odd recommendations or other issues, please follow the |cFFFFD100Report Issue|r link below and submit all the necessary information to have your issue investigated.\n\n"
.. "Please |cffff0000do not|r submit tickets for routine priority updates (i.e., from SimulationCraft). They are routinely updated."
do
local cpuProfileDB = {}
function Hekili:ProfileCPU( name, func )
cpuProfileDB[ name ] = func
end
ns.cpuProfile = cpuProfileDB
local frameProfileDB = {}
function Hekili:ProfileFrame( name, f )
frameProfileDB[ name ] = f
end
ns.frameProfile = frameProfileDB
end
ns.lib = {
Format = {}
}
-- 04072017: Let's go ahead and cache aura information to reduce overhead.
ns.auras = {
target = {
buff = {},
debuff = {}
},
player = {
buff = {},
debuff = {}
}
}
Hekili.Class = {
specs = {},
num = 0,
file = "NONE",
initialized = false,
resources = {},
resourceAuras = {},
talents = {},
pvptalents = {},
auras = {},
auraList = {},
powers = {},
gear = {},
setBonuses = {},
knownAuraAttributes = {},
stateExprs = {},
stateFuncs = {},
stateTables = {},
abilities = {},
abilityByName = {},
abilityList = {},
itemList = {},
itemMap = {},
itemPack = {
lists = {
items = {}
}
},
packs = {},
pets = {},
totems = {},
potions = {},
potionList = {},
hooks = {},
range = 8,
settings = {},
stances = {},
toggles = {},
variables = {},
}
5 months ago
local class = Hekili.Class
Hekili.Scripts = {
DB = {},
Channels = {},
PackInfo = {},
}
Hekili.State = {}
ns.hotkeys = {}
ns.keys = {}
ns.queue = {}
ns.targets = {}
ns.TTD = {}
ns.UI = {
Displays = {},
Buttons = {}
}
ns.debug = {}
ns.snapshots = {}
function Hekili:Query( ... )
local output = ns
for i = 1, select( '#', ... ) do
output = output[ select( i, ... ) ]
end
return output
end
function Hekili:Run( ... )
local n = select( "#", ... )
local fn = select( n, ... )
local func = ns
for i = 1, fn - 1 do
func = func[ select( i, ... ) ]
end
return func( select( fn, ... ) )
end
local debug = ns.debug
local active_debug
local current_display
local lastIndent = 0
function Hekili:SetupDebug( display )
if not self.ActiveDebug then return end
if not display then return end
current_display = display
debug[ current_display ] = debug[ current_display ] or {
log = {},
index = 1
}
active_debug = debug[ current_display ]
active_debug.index = 1
lastIndent = 0
local pack = self.State.system.packName
if not pack then return end
self:Debug( "New Recommendations for [ %s ] requested at %s ( %.2f ); using %s( %s ) priority.", display, date( "%H:%M:%S"), GetTime(), self.DB.profile.packs[ pack ].builtIn and "built-in " or "", pack )
end
function Hekili:Debug( ... )
if not self.ActiveDebug then return end
if not active_debug then return end
local indent, text = ...
local start
if type( indent ) ~= "number" then
indent = lastIndent
text = ...
start = 2
else
lastIndent = indent
start = 3
end
local prepend = format( indent > 0 and ( "%" .. ( indent * 4 ) .. "s" ) or "%s", "" )
text = text:gsub("\n", "\n" .. prepend )
text = format( "%" .. ( indent > 0 and ( 4 * indent ) or "" ) .. "s", "" ) .. text
if select( start, ... ) ~= nil then
active_debug.log[ active_debug.index ] = format( text, select( start, ... ) )
else
active_debug.log[ active_debug.index ] = text
end
active_debug.index = active_debug.index + 1
end
local snapshots = ns.snapshots
local hasScreenshotted = false
function Hekili:SaveDebugSnapshot( dispName )
local snapped = false
local formatKey = ns.formatKey
local state = Hekili.State
for k, v in pairs( debug ) do
if not dispName or dispName == k then
for i = #v.log, v.index, -1 do
v.log[ i ] = nil
end
5 months ago
-- Store previous spell data.
local prevString = "\nprevious_spells:"
-- Skip over the actions in the "prev" table that were added to computed the next recommended ability in the queue.
local i, j = ( #state.predictions + 1 ), 1
local spell = state.prev[i].spell or "no_action"
if spell == "no_action" then
prevString = prevString .. " no history available"
else
local numHistory = #state.prev.history
while i <= numHistory and spell ~= "no_action" do
prevString = format( "%s\n %d - %s", prevString, j, spell )
i, j = i + 1, j + 1
spell = state.prev[i].spell or "no_action"
end
end
prevString = prevString .. "\n\n"
insert( v.log, 1, prevString )
-- Store aura data.
5 months ago
local auraString = "\n### Auras ###\n"
local now = GetTime()
5 months ago
local playerBuffs = {}
local pbOrder = {}
local longestKey, longestName = 0, 0
5 months ago
AuraUtil.ForEachAura( "player", "HELPFUL", nil, function( aura )
if aura.isFromPlayerOrPlayerPet then
local model = class.auras[ aura.spellId ]
local key = model and model.key or formatKey( aura.name )
local offset = 0
local newKey = key
while( playerBuffs[ newKey ] ) do
offset = offset + 1
newKey = format( "%s_%d", key, offset )
end
5 months ago
if newKey ~= key then key = newKey end
5 months ago
pbOrder[ #pbOrder + 1 ] = key
longestKey = max( longestKey, key:len() )
longestName = max( longestName, aura.name:len() )
5 months ago
playerBuffs[ key ] = {}
local elem = playerBuffs[ key ]
5 months ago
elem.spellId = aura.spellId
elem.key = key
elem.name = aura.name
elem.count = aura.applications > 0 and aura.applications or 1
elem.remains = aura.expirationTime > 0 and ( aura.expirationTime - now ) or 3600
local scraped = state.auras.player.buff[ model and model.key or key ]
if scraped and scraped.applied > 0 then
elem.sCount = scraped.count > 0 and scraped.count or 1
elem.sRemains = scraped.expires > 0 and ( scraped.expires - now ) or 3600
end
end
end, true )
for token, caught in pairs( state.auras.player.buff ) do
if not playerBuffs[ token ] and caught.expires > 0 then
playerBuffs[ token ] = {
spellId = caught.id,
key = caught.key,
name = "",
count = 0,
remains = 0,
sCount = caught.count > 0 and caught.count or 1,
sRemains = caught.expires > 0 and ( caught.expires - now ) or 3600
}
pbOrder[ #pbOrder + 1 ] = token
longestKey = max( longestKey, token:len() )
end
end
5 months ago
sort( pbOrder )
5 months ago
local playerDebuffs = {}
local pdOrder = {}
5 months ago
AuraUtil.ForEachAura( "player", "HARMFUL", nil, function( aura )
local model = class.auras[ aura.spellId ]
local key = model and model.key or formatKey( aura.name )
local offset = 0
local newKey = key
while( playerDebuffs[ newKey ] ) do
offset = offset + 1
newKey = format( "%s_%d", key, offset )
end
if newKey ~= key then key = newKey end
5 months ago
pdOrder[ #pdOrder + 1 ] = key
longestKey = max( longestKey, key:len() )
longestName = max( longestName, aura.name:len() )
playerDebuffs[ key ] = {}
local elem = playerDebuffs[ key ]
elem.spellId = aura.spellId
elem.key = key
elem.name = aura.name
elem.count = aura.applications > 0 and aura.applications or 1
elem.remains = aura.expirationTime > 0 and ( aura.expirationTime - now ) or 3600
local scraped = state.auras.player.debuff[ model and model.key or key ]
if scraped and scraped.applied > 0 then
elem.sCount = scraped.count > 0 and scraped.count or 1
elem.sRemains = scraped.expires > 0 and ( scraped.expires - now ) or 3600
end
end, true )
for token, caught in pairs( state.auras.player.debuff ) do
if not playerDebuffs[ token ] and caught.expires > 0 then
playerDebuffs[ token ] = {
spellId = caught.id,
key = caught.key,
name = "",
count = 0,
remains = 0,
sCount = caught.count > 0 and caught.count or 1,
sRemains = caught.expires > 0 and ( caught.expires - now ) or 3600
}
pdOrder[ #pdOrder + 1 ] = token
longestKey = max( longestKey, token:len() )
end
end
5 months ago
sort( pdOrder )
5 months ago
local targetBuffs = {}
local tbOrder = {}
5 months ago
AuraUtil.ForEachAura( "target", "HELPFUL", nil, function( aura )
local model = class.auras[ aura.spellId ]
local key = model and model.key or formatKey( aura.name )
5 months ago
local offset = 0
local newKey = key
5 months ago
while( targetBuffs[ newKey ] ) do
offset = offset + 1
newKey = format( "%s_%d", key, offset )
end
5 months ago
if newKey ~= key then key = newKey end
tbOrder[ #tbOrder + 1 ] = key
longestKey = max( longestKey, key:len() )
longestName = max( longestName, aura.name:len() )
5 months ago
targetBuffs[ key ] = {}
local elem = targetBuffs[ key ]
5 months ago
elem.spellId = aura.spellId
elem.key = key
elem.name = aura.name
5 months ago
elem.count = aura.applications > 0 and aura.applications or 1
elem.remains = aura.expirationTime > 0 and ( aura.expirationTime - now ) or 3600
5 months ago
local scraped = state.auras.target.buff[ model and model.key or key ]
if scraped and scraped.applied > 0 then
elem.sCount = scraped.count > 0 and scraped.count or 1
elem.sRemains = scraped.expires > 0 and ( scraped.expires - now ) or 3600
end
end, true )
for token, caught in pairs( state.auras.target.buff ) do
if not targetBuffs[ token ] and caught.expires > 0 then
targetBuffs[ token ] = {
spellId = caught.id,
key = caught.key,
name = "",
count = 0,
remains = 0,
5 months ago
sCount = caught.count > 0 and caught.count or 1,
sRemains = caught.expires > 0 and ( caught.expires - now ) or 3600
}
tbOrder[ #tbOrder + 1 ] = token
longestKey = max( longestKey, token:len() )
end
end
5 months ago
sort( tbOrder )
local targetDebuffs = {}
local tdOrder = {}
AuraUtil.ForEachAura( "target", "HARMFUL", nil, function( aura )
if aura.isFromPlayerOrPlayerPet then
local model = class.auras[ aura.spellId ]
local key = model and model.key or formatKey( aura.name )
local offset = 0
local newKey = key
while( targetDebuffs[ newKey ] ) do
offset = offset + 1
newKey = format( "%s_%d", key, offset )
end
if newKey ~= key then key = newKey end
tdOrder[ #tdOrder + 1 ] = key
longestKey = max( longestKey, key:len() )
longestName = max( longestName, aura.name:len() )
targetDebuffs[ key ] = {}
local elem = targetDebuffs[ key ]
elem.spellId = aura.spellId
elem.key = key
elem.name = aura.name
elem.count = aura.applications > 0 and aura.applications or 1
elem.remains = aura.expirationTime > 0 and ( aura.expirationTime - now ) or 3600
local scraped = state.auras.target.debuff[ model and model.key or key ]
if scraped and scraped.applied > 0 then
elem.sCount = scraped.count > 0 and scraped.count or 1
elem.sRemains = scraped.expires > 0 and ( scraped.expires - now ) or 3600
end
end
end, true )
for token, caught in pairs( state.auras.target.debuff ) do
if not targetDebuffs[ token ] and caught.expires > 0 then
targetDebuffs[ token ] = {
spellId = caught.id,
key = caught.key,
name = "",
count = 0,
remains = 0,
sCount = caught.count > 0 and caught.count or 1,
sRemains = caught.expires > 0 and ( caught.expires - now ) or 3600
}
tdOrder[ #tdOrder + 1 ] = token
longestKey = max( longestKey, token:len() )
end
end
sort( tdOrder )
local header = " n | ID | Token" .. string.rep( " ", longestKey - 4 ) .. " | Name" .. string.rep( " ", longestName - 4 ) .. " | A. Count | A. Remains | S. Count | S. Remains\n"
.. " --- | ------- | " .. string.rep( "-", longestKey + 1 ) .. " | " .. string.rep( "-", longestName ) .. " | -------- | ---------- | -------- | ----------"
if #pbOrder > 0 then
auraString = auraString .. "\nplayer_buffs:\n" .. header
for i, token in ipairs( pbOrder ) do
local aura = playerBuffs[ token ]
auraString = format( "%s\n %-2d | %7d | %s%-" .. longestKey .. "s | %-" .. longestName .. "s | %8d | %10.2f | %8d | %10.2f",
auraString, i, class.auras[ token ] and class.auras[ token ].id or -1, ( class.auras[ token ] and " " or "*" ), token, aura.name, aura.count, aura.remains, aura.sCount or -1, aura.sRemains or - 1 )
end
else
auraString = auraString .. "\nplayer_buffs: none"
end
if #pdOrder > 0 then
auraString = auraString .. "\n\nplayer_debuffs:\n" .. header
for i, token in ipairs( pdOrder ) do
local aura = playerDebuffs[ token ]
auraString = format( "%s\n %-2d | %7d | %s%-" .. longestKey .. "s | %-" .. longestName .. "s | %8d | %10.2f | %8d | %10.2f",
auraString, i, class.auras[ token ] and class.auras[ token ].id or -1, ( class.auras[ token ] and " " or "*" ), token, aura.name, aura.count, aura.remains, aura.sCount or -1, aura.sRemains or - 1 )
end
else
auraString = auraString .. "\n\nplayer_debuffs: none"
end
if #tbOrder > 0 then
auraString = auraString .. "\n\ntarget_buffs:\n" .. header
for i, token in ipairs( tbOrder ) do
local aura = targetBuffs[ token ]
local model = class.auras[ token ]
auraString = format( "%s\n %-2d | %7d | %s%-" .. longestKey .. "s | %-" .. longestName .. "s | %8d | %10.2f | %8d | %10.2f",
auraString, i, model and model.id or -1, model and " " or "*", token, aura.name, aura.count, aura.remains, aura.sCount or -1, aura.sRemains or - 1 )
end
else
auraString = auraString .. "\n\ntarget_buffs: none"
end
if #tdOrder > 0 then
auraString = auraString .. "\n\ntarget_debuffs:\n" .. header
for i, token in ipairs( tdOrder ) do
local aura = targetDebuffs[ token ]
auraString = format( "%s\n %-2d | %7d | %s%-" .. longestKey .. "s | %-" .. longestName .. "s | %8d | %10.2f | %8d | %10.2f",
auraString, i, class.auras[ token ] and class.auras[ token ].id or -1, ( class.auras[ token ] and " " or "*" ), token, aura.name, aura.count, aura.remains, aura.sCount or -1, aura.sRemains or - 1 )
end
else
auraString = auraString .. "\n\ntarget_debuffs: none"
end
insert( v.log, 1, auraString )
5 months ago
insert( v.log, 1, "\n### Targets ###\n\ndetected_targets: " .. ( Hekili.TargetDebug or "no data" ) )
insert( v.log, 1, self:GenerateProfile() )
5 months ago
local performance
local pInfo = HekiliEngine.threadUpdates
-- TODO: Include # of active displays, number of icons displayed.
if pInfo then
performance = string.format( "\n\nPerformance\n"
.. "|| Updates || Updates / sec || Avg. Work || Avg. Time || Avg. Frames || Peak Work || Peak Time || Peak Frames || FPS || Work Cap ||\n"
.. "|| %7d || %13.2f || %9.2f || %9.2f || %11.2f || %9.2f || %9.2f || %11.2f || %3d || %8.2f ||",
pInfo.updates, pInfo.updatesPerSec, pInfo.meanWorkTime, pInfo.meanClockTime, pInfo.meanFrames, pInfo.peakWorkTime, pInfo.peakClockTime, pInfo.peakFrames, GetFramerate() or 0, Hekili.maxFrameTime or 0 )
end
if performance then insert( v.log, performance ) end
local custom = ""
local pack = self.DB.profile.packs[ state.system.packName ]
if not pack.builtIn then
5 months ago
custom = format( " |cFFFFA700(*%s[%d])|r", state.spec.name, state.spec.id )
end
local overview = format( "%s%s; %s|r", state.system.packName, custom, dispName or state.display )
local recs = Hekili.DisplayPool[ dispName or state.display ].Recommendations
for i, rec in ipairs( recs ) do
if not rec.actionName then
if i == 1 then
overview = format( "%s - |cFF666666N/A|r", overview )
end
break
end
5 months ago
overview = format( "%s%s%s|cFFFFD100(%0.2f)|r", overview, ( i == 1 and " - " or ", " ), rec.actionName, rec.time )
end
insert( v.log, 1, overview )
local snap = {
header = "|cFFFFD100[" .. date( "%H:%M:%S" ) .. "]|r " .. overview,
log = concat( v.log, "\n" ),
data = ns.tableCopy( v.log ),
recs = {}
}
insert( snapshots, snap )
snapped = true
end
end
-- Limit screenshot to once per login.
if snapped then
5 months ago
if Hekili.DB.profile.screenshot and ( not hasScreenshotted or Hekili.ManualSnapshot ) then
Screenshot()
hasScreenshotted = true
end
return true
end
return false
end
Hekili.Snapshots = ns.snapshots
ns.Tooltip = CreateFrame( "GameTooltip", "HekiliTooltip", UIParent, "GameTooltipTemplate" )
Hekili:ProfileFrame( "HekiliTooltip", ns.Tooltip )