-- 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 Hekili.BuiltFor = 110100 Hekili.GameBuild = buildStr 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" .. "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 = {}, } 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 -- 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. local auraString = "\n### Auras ###\n" local now = GetTime() local playerBuffs = {} local pbOrder = {} local longestKey, longestName = 0, 0 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 if newKey ~= key then key = newKey end pbOrder[ #pbOrder + 1 ] = key longestKey = max( longestKey, key:len() ) longestName = max( longestName, aura.name:len() ) playerBuffs[ key ] = {} local elem = playerBuffs[ 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.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 sort( pbOrder ) local playerDebuffs = {} local pdOrder = {} 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 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 sort( pdOrder ) local targetBuffs = {} local tbOrder = {} AuraUtil.ForEachAura( "target", "HELPFUL", 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( targetBuffs[ newKey ] ) do offset = offset + 1 newKey = format( "%s_%d", key, offset ) end if newKey ~= key then key = newKey end tbOrder[ #tbOrder + 1 ] = key longestKey = max( longestKey, key:len() ) longestName = max( longestName, aura.name:len() ) targetBuffs[ key ] = {} local elem = targetBuffs[ 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.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, 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 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 ) insert( v.log, 1, "\n### Targets ###\n\ndetected_targets: " .. ( Hekili.TargetDebug or "no data" ) ) insert( v.log, 1, self:GenerateProfile() ) 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 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 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 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 )