WeakAura Patterns
Some of the patterns I use in or for my WeakAuras. This will grow over time.
Queueing & Custom Events
It's technically a basic pattern once you've dabbled with WeakAuras and custom code in general but absolutely worth documenting.
You may find yourself in a situation where:
- you need to listen to loads of events, but updating on every single of them is unperformant
- you need to listen to an unknown amount of events and you don't know when the last one occurs within a window
It's advisable to enqueue a custom event with your possibly aggregated data and then react to said event in a separate branch.
This would be a TSU trigger:
function (states, event, ...)
-- for the sake of a high-frequency example
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
-- do evil
-- whatever your criteria are
if matchesCriteria or aggregatedData then
aura_env.queue()
end
-- important to prevent these events from updating the aura
return false
end
if event == aura_env.customEventName then
local id = ...
-- make sure the event is actually from us
if id ~= aura_env.id then
return false
end
-- perform state update based on either the payload from the custom event or aggregated data
return true
end
-- ignore STATUS, OPTIONS etc.
return false
end
and in aura_env
:
aura_env.customEventName = "SOMETHING_DEFINITELY_UNIQUE"
aura_env.timer = nil
function aura_env.queue()
if aura_env.timer then
if not aura_env.timer:IsCancelled() then
aura_env.timer:Cancel()
end
aura_env.timer = nil
end
-- 3 is seconds here, whatever you need. 0 means next frame
aura_env.timer = C_Timer.NewTimer(3, function()
WeakAuras.ScanEvents(aura_env.customEventName, aura_env.id)
end)
end
Don't forget to include the custom event name in your events to trigger from!
Example: Breath of Eons
Breath of Eons applies a debuff with a dynamic duration on every target the casting Evoker is flying over, meaning we have a variable application time, duration and amount of affected targets.
Eventually, if at least one target lives for the duration of the debuff, it'll expire and do some damage.
Goal here is to announce the total damage done by the spell, no visual.
We'll need to listen to COMBAT_LOG_EVENT_UNFILTERED
filtering for SPELL_DAMAGE
and SPELL_MISSED
(to catch full absorbs) in the trigger:
--- CLEU:SPELL_DAMAGE:SPELL_MISSED, XEPHUI_BREATH_OF_EONS_ANNOUNCER
function (event, ...)
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
local _, subEvent = ...
local key = nil
local addition = 0
if subEvent == "SPELL_DAMAGE" then
local _, _, _, sourceGUID, _, _, _, _, _, _, _, spellId, _, _, amount, _, _, _, _, absorbed = ...
if spellId == 409632 then
key = sourceGUID
addition = amount + (absorbed or 0) -- to account for partial absorbs
end
elseif subEvent == "SPELL_MISSED" then
local _, _, _, sourceGUID, _, _, _, _, _, _, _, spellId, _, _, missType, _, amount = ...
if missType == "ABSORB" and spellId == 409632 then
key = sourceGUID
addition = amount
end
end
if addition > 0 and key then
aura_env.bySource[key] = aura_env.bySource[key] or 0
aura_env.bySource[key] = aura_env.bySource[key] + addition
aura_env.queue()
end
return false
end
if event == aura_env.customEventName then
local id = ...
if id ~= aura_env.id then
return false
end
aura_env.timer = nil
local message = aura_env.getMessage()
if aura_env.config.announce then
SendChatMessage(message, "SAY")
else
print(message)
end
return false
end
return false
end
and its accompanying Init Action:
aura_env.customEventName = "XEPHUI_BREATH_OF_EONS_ANNOUNCER"
aura_env.timer = nil
---@type table<string, number>
aura_env.bySource = {}
function aura_env.queue()
if aura_env.timer then
if not aura_env.timer:IsCancelled() then
aura_env.timer:Cancel()
end
aura_env.timer = nil
end
-- 3 seconds here to definitely catch all events which may be significantly delayed based on debuff application time
aura_env.timer = C_Timer.NewTimer(3, function()
WeakAuras.ScanEvents(aura_env.customEventName, aura_env.id)
end)
end
-- custom formatter for float precision purposes, Blizz provides a similar function but it truncates
---@param number number
---@return string
local function formatNumber(number)
if number > 1000000 then
return string.format("%1.2fM", number / 1000000)
end
if number > 1000 then
return string.format("%1.2fK", number / 1000)
end
return number
end
local baseBlueprint = C_Spell.GetSpellName(403631) .. " hit for %s!"
local maxContributorBlueprint = "Highest contributor: %s with %s (%s%%)"
---@return string
function aura_env.getMessage()
local total = 0
local individualMax = 0
local maxSource = nil
for sourceGUID, amount in pairs(aura_env.bySource) do
total = total + amount
if amount > individualMax then
individualMax = amount
maxSource = sourceGUID
end
end
table.wipe(aura_env.bySource)
local base = string.format(baseBlueprint, formatNumber(total))
if aura_env.config.includeContributor and individualMax > 0 and maxSource ~= nil then
local token = UnitTokenFromGUID(maxSource)
local name = token and UnitName(token) or nil
if name then
return string.format(
base .. " " .. maxContributorBlueprint,
name,
formatNumber(individualMax),
string.format("%.2f", individualMax / total * 100)
)
end
end
return base
end
Why does this work?
The order of events is as follows:
- multiple, slightly delayed CLEU from player 1 -> store values -> queue
- multiple, slightly delayed CLEU from player 2 -> store values -> kill timer -> queue again
- multiple, slightly delayed CLEU from player 3 -> store values -> kill timer -> queue again
- multiple, slightly delayed CLEU from player 4 -> store values -> kill timer -> queue again
- note these will very most likely also be in entirely variable orders
- eventually there's no new CLEU events
- custom event -> read values -> clear state -> announce
Write your code in some editor
Seriously. The amount of badly indented custom WA code out there that clearly wasn't written in an editor without formatting, autocompletion etc. is mind-boggling.
I understand there's obviously a gap both in expectation and experience between professional developers and hobbyists but in my opinion, especially as hobbyist the gains are even larger.
Some tooling I use:
- VS Code is obviously a good and easy choice for this
You'll avoid:
- accidental shadowing
- accidental global variables
- headaches from limited editing options ingame
- no shade to the WA team, but editing quality is naturally better in an environment made for it. WoW is not that environment!
You'll gain:
- general quality of life you gain from using a tool that's made for it
- versioning if you wanna use git
- an actual file system that you can search and copy from
- types, if you're into that (please be)
Finding Spell IDs
Regardless of whether you're working with custom code or just create some auras that should work across locales, you have to get your game ids from somewhere. For things like auras, talents, cast ids, anything that is immediately visible in game, addons fill that gap nicely - personally I'm just using idTip.
But what about damage/healing ids? Debuffs on players in a dungeon, fleeting things you either can't look at directly or effectively not in the heat of the moment? The answer at this point is obvious: use logs.
Any spell icon is a link to wowhead, e.g. this Eruption. The id is directly in the url: 395160
. This only leaves you with having to find a log but chances are, you're not the first one to use that spell or play the content it's used in so luckily that won't be an issue.
For fringe cases like early alpha/beta/ptr cycles, you can always log target dummies in some capital yourself.