Files
github-actions[bot] fc92506470 WeakestAuras 0.18.1
Fewer freezes

Fixes
- Fixed frequent freezes on characters with auras for another class or for items
  not carried: those auras no longer make every aura recompile whenever spell or
  item data arrives.
- Trigger State Updater auras no longer lose their clones when an unrelated aura
  names a spell or item the client cannot look up.

Performance
- Much less garbage during play from dynamic groups, health, power and cast bars
  in raids, spell cooldowns, progress bars and looping animations.

- /wa prof report shows memory allocated per entry, WeakestAuras' share of the
  client's memory churn, and garbage collections with how long their frame took.
  /wa prof report kb sorts by memory.
2026-10-31 18:45:49 +00:00

377 lines
16 KiB
Lua

-- WeakestAuras -- the load system: decides whether a display is *active at all*
-- (registered with its trigger systems, allowed to show) based on data.load --
-- class(es)/race/faction/level/zone/combat/group-size-or-type/stance/alive/
-- mounted/on-taxi/name/realm/guild/spell-known/item-equipped. Mirrors WA2's
-- load system (§11), trimmed to the subset with a real 1.12+ClassicAPI
-- data source: encounter, warmode, spec role and talent introspection have no
-- API on this client.
-- Upstream section refs (§N) point at design/architecture/weakauras2-reference.md
--
-- One deliberate divergence, matching Conditions.lua's: upstream compiles
-- data.load to a loadFunc via ConstructFunction; we *interpret* a small fixed
-- prototype instead. The load args are few and simple (a handful of AND'd
-- constraints), so an interpreter is clearer and lower-risk than assembling Lua
-- source with no offline linter.
--
-- The transition machinery lives in StateMachine.lua (WA.SetDisplayLoaded), which
-- owns the trigger systems and regions; this file only *evaluates* load state
-- (WA.EvalLoad) and drives a re-scan on load-relevant events (WA.ScanForLoads).
if WeakestAuras.disabled then return end
local WA = WeakestAuras
local function numCmp(op, a, b)
if a == nil or b == nil then return false end
if op == "==" then return a == b
elseif op == "~=" then return a ~= b
elseif op == ">" then return a > b
elseif op == ">=" then return a >= b
elseif op == "<" then return a < b
elseif op == "<=" then return a <= b end
return false
end
-- Members in the current group (raid count, else party members + self). Drives
-- the "group size" load constraint.
local function groupSize()
if IsInRaid() then return GetNumRaidMembers() end
return GetNumPartyMembers() + 1
end
-- "solo"/"group"/"raid" -- mirrors WA2's Private.ExecEnv.GroupType().
local function groupType()
if IsInRaid() then return "raid" end
if IsInGroup() then return "group" end
return "solo"
end
-- Case-insensitive substring match, shared by the zone/player/realm/guild
-- text constraints (all "does this contain what I typed" checks).
local function substrMatch(hay, needle)
if not needle or needle == "" then return true end
return string.find(string.lower(hay or ""), string.lower(needle), 1, true) ~= nil
end
-- The instance-size bucket of the current moment. GetInstanceInfo gives the
-- instance type, but its maxPlayers is a per-type default (40 for every raid,
-- ZG and Turtle's custom raids included), so raid head-count stands in for the
-- raid sizes. Head-count is an estimate by construction: a 22-strong Molten
-- Core run reads "twentyfive" until three more join. Bucket keys are
-- upstream's, so an import lands without renaming.
local function instanceSizeBucket()
local _, instanceType = GetInstanceInfo()
if instanceType == "pvp" or instanceType == "arena" then return "pvp" end
if instanceType ~= "raid" then return instanceType end -- "none", "party"
local raid = GetNumRaidMembers()
if raid > 25 then return "fortyman" end
if raid > 20 then return "twentyfive" end
if raid > 10 then return "twenty" end
if raid > 5 then return "ten" end
return "party"
end
WA.CLASS_TOKENS = { "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "SHAMAN", "MAGE", "WARLOCK", "DRUID" }
-- The class picker's labels: localized class names in their class colours.
-- `|c...|r` escapes render in any plain FontString, so the dropdown needs no
-- widget-level change.
local classNames = FillLocalizedClassList({})
WA.CLASS_COLOR_LABELS = {}
for _, token in ipairs(WA.CLASS_TOKENS) do
WA.CLASS_COLOR_LABELS[token] = C_ClassColor.GetClassColor(token):WrapTextInColorCode(classNames[token])
end
local COMBAT_VALUES = { "incombat", "outofcombat" }
local COMBAT_LABELS = { incombat = "In Combat", outofcombat = "Out of Combat" }
local ALIVE_VALUES = { "alive", "dead" }
local ALIVE_LABELS = { alive = "Alive", dead = "Dead or Ghost" }
local MOUNTED_VALUES = { "mounted", "notmounted" }
local MOUNTED_LABELS = { mounted = "Mounted", notmounted = "Not Mounted" }
local TAXI_VALUES = { "ontaxi", "nottaxi" }
local TAXI_LABELS = { ontaxi = "On Taxi", nottaxi = "Not On Taxi" }
-- Mirrors WA2's Private.group_types (solo / party / raid) -- see groupType().
local INGROUP_VALUES = { "solo", "group", "raid" }
local INGROUP_LABELS = { solo = "Not in Group", group = "In Party", raid = "In Raid" }
-- WA2's Private.instance_types, minus the flavors this client cannot be in
-- (scenario, flexible, arena, rated anything) -- see instanceSizeBucket() for
-- how each is decided.
local INSTANCE_SIZE_VALUES = { "none", "party", "ten", "twenty", "twentyfive", "fortyman", "pvp" }
local INSTANCE_SIZE_LABELS = {
none = "No Instance", party = "Dungeon", ten = "Raid (up to 10)",
twenty = "Raid (11-20)", twentyfive = "Raid (21-25)", fortyman = "Raid (26-40)",
pvp = "Battleground",
}
-- UnitRaceBase's locale-independent tokens (ClassicAPI). "Scourge" is the
-- internal token for Undead, relabeled for the picker.
local RACE_TOKENS = { "Human", "Orc", "Dwarf", "NightElf", "Scourge", "Tauren", "Gnome", "Troll" }
local RACE_LABELS = {
Human = "Human", Orc = "Orc", Dwarf = "Dwarf", NightElf = "Night Elf",
Scourge = "Undead", Tauren = "Tauren", Gnome = "Gnome", Troll = "Troll",
}
local FACTION_VALUES = { "Horde", "Alliance" }
local FACTION_LABELS = { Horde = "Horde", Alliance = "Alliance" }
-- The load prototype: one descriptor per optional constraint (§11's
-- load_prototype args). Each has a `use_<name>` gate in data.load; when set, its
-- eval(L) must pass. OptionsFrame.lua's Load tab builds its editor from this same
-- list, so adding a constraint is one entry here (the args economy of §4.1/§11).
-- `optional = true` marks a constraint the *current moment* decides rather than
-- the character (combat, zone, group, stance, alive, mounted, taxi). Skipping
-- exactly those is what separates "this aura isn't for this character" from
-- "this aura is mine but the moment doesn't qualify" -- see WA.EvalLoadStatic.
-- `never` is handled outside this list (a plain gate-less kill switch).
-- A `multiselect` widget is the one gate that is not a plain on/off toggle:
-- `use_<name>` holds a mode string, and the constraint passes if the character's
-- value is the one chosen or in the set chosen. The convention and its editor
-- live in WeakestAuras.lua; these entries only say which constraints take one.
-- Its domain is `values`/`labels`, or a `valuesFunc` returning that pair where
-- the domain is per-character rather than fixed (the shapeshift bar).
WA.loadPrototype = {
{
name = "combat", display = "Combat", widget = "select", optional = true,
values = COMBAT_VALUES, labels = COMBAT_LABELS, default = "incombat",
eval = function(L)
local inCombat = UnitAffectingCombat("player") and true or false
if L.combat == "outofcombat" then return not inCombat end
return inCombat
end,
},
{
-- isActive is required on every multiselect: WA.EvalLoad's default gate
-- treats *any* truthy `use_<name>` as "constraint on", but a stray
-- non-"single"/"multi" value (a leftover "off" from an older save, say) is
-- truthy in Lua and would permanently fail eval() below, silently
-- hard-blocking the aura while the Load tab still displays "Ignored".
name = "class", display = "Class", widget = "multiselect",
values = WA.CLASS_TOKENS, labels = WA.CLASS_COLOR_LABELS,
isActive = function(L) return WA.MultiSelectMode(L, "class") ~= "off" end,
eval = function(L)
local _, cls = UnitClass("player")
return WA.MultiSelectMatches(L, "class", cls)
end,
},
{
name = "race", display = "Race", widget = "multiselect",
values = RACE_TOKENS, labels = RACE_LABELS, default = "Human",
isActive = function(L) return WA.MultiSelectMode(L, "race") ~= "off" end,
eval = function(L)
local token = UnitRaceBase("player")
return WA.MultiSelectMatches(L, "race", token)
end,
},
{
name = "faction", display = "Faction", widget = "multiselect",
values = FACTION_VALUES, labels = FACTION_LABELS, default = "Alliance",
isActive = function(L) return WA.MultiSelectMode(L, "faction") ~= "off" end,
eval = function(L)
return WA.MultiSelectMatches(L, "faction", UnitFactionGroup("player"))
end,
},
{
name = "level", display = "Level", widget = "opnumber", operator = ">=", default = 1,
eval = function(L)
return numCmp(L.level_operator or ">=", UnitLevel("player"), L.level or 1)
end,
},
{
name = "zone", display = "Zone (substring)", widget = "input", default = "", optional = true,
eval = function(L) return substrMatch(GetRealZoneText(), L.zone) end,
},
{
name = "size", display = "Group Size", widget = "opnumber", operator = ">=", default = 0, optional = true,
eval = function(L)
return numCmp(L.size_operator or ">=", groupSize(), L.size or 0)
end,
},
{
name = "ingroup", display = "Group Type", widget = "multiselect", optional = true,
values = INGROUP_VALUES, labels = INGROUP_LABELS, default = "solo",
isActive = function(L) return WA.MultiSelectMode(L, "ingroup") ~= "off" end,
eval = function(L) return WA.MultiSelectMatches(L, "ingroup", groupType()) end,
},
{
name = "instancesize", display = "Instance Size (estimated)", widget = "multiselect", optional = true,
values = INSTANCE_SIZE_VALUES, labels = INSTANCE_SIZE_LABELS, default = "none",
isActive = function(L) return WA.MultiSelectMode(L, "instancesize") ~= "off" end,
eval = function(L) return WA.MultiSelectMatches(L, "instancesize", instanceSizeBucket()) end,
},
{
-- The bar's occupied slots, named per character rather than a bare number:
-- the picker comes from the walk the Stance/Form trigger filters on, so
-- slot 2 reads "2 - Defensive Stance" on a warrior and "2 - Aquatic Form"
-- on a druid. Positions, not SpellShapeshiftForm ids -- see
-- WA.ShapeshiftBarIndex for why the active slot is read off each slot's
-- own `active` return.
name = "stance", display = "Stance/Form", widget = "multiselect", optional = true,
valuesFunc = function() return WA.ShapeshiftForms() end, default = 0,
isActive = function(L) return WA.MultiSelectMode(L, "stance") ~= "off" end,
eval = function(L) return WA.MultiSelectMatches(L, "stance", WA.ShapeshiftBarIndex()) end,
},
{
name = "alive", display = "Alive", widget = "select", optional = true,
values = ALIVE_VALUES, labels = ALIVE_LABELS, default = "alive",
eval = function(L)
local dead = UnitIsDeadOrGhost("player") and true or false
if L.alive == "dead" then return dead end
return not dead
end,
},
{
name = "mounted", display = "Mounted", widget = "select", optional = true,
values = MOUNTED_VALUES, labels = MOUNTED_LABELS, default = "mounted",
eval = function(L)
local m = IsMounted() and true or false
if L.mounted == "notmounted" then return not m end
return m
end,
},
{
name = "vehicle", display = "On Taxi", widget = "select", optional = true,
values = TAXI_VALUES, labels = TAXI_LABELS, default = "ontaxi",
eval = function(L)
local t = UnitOnTaxi("player") and true or false
if L.vehicle == "nottaxi" then return not t end
return t
end,
},
{
name = "player", display = "Character Name (substring)", widget = "input", default = "",
eval = function(L) return substrMatch(UnitName("player"), L.player) end,
},
{
name = "realm", display = "Realm (substring)", widget = "input", default = "",
eval = function(L) return substrMatch(GetRealmName(), L.realm) end,
},
{
name = "guild", display = "Guild (substring)", widget = "input", default = "",
eval = function(L) return substrMatch(GetGuildInfo("player"), L.guild) end,
},
{
name = "spellknown", display = "Spell Known", widget = "spell", default = "",
eval = function(L)
local id = WA.ResolveSpellID(L.spellknown)
return id ~= nil and IsSpellKnown(id) and true or false
end,
},
{
name = "not_spellknown", display = "Spell NOT Known", widget = "spell", default = "",
eval = function(L)
local id = WA.ResolveSpellID(L.not_spellknown)
return id == nil or not IsSpellKnown(id)
end,
},
{
name = "itemequiped", display = "Item Equipped (item ID)", widget = "input", default = "",
eval = function(L)
local id = tonumber(L.itemequiped)
if not id then return false end
for slot = 1, 19 do
if GetInventoryItemID("player", slot) == id then return true end
end
return false
end,
},
{
name = "not_itemequiped", display = "Item NOT Equipped (item ID)", widget = "input", default = "",
eval = function(L)
local id = tonumber(L.not_itemequiped)
if not id then return true end
for slot = 1, 19 do
if GetInventoryItemID("player", slot) == id then return false end
end
return true
end,
},
}
-- Each entry's use_<name> key, built once: a scan evaluates every entry of every
-- display, and the concatenation was a fresh string each time.
local useKeys = setmetatable({}, { __index = function(t, arg)
local key = "use_" .. arg.name
t[arg] = key
return key
end })
-- All enabled constraints AND together (§11); `never` short-circuits to
-- unloaded. skipOptional drops the moment-dependent entries, leaving only what
-- the character decides. An empty data.load loads unconditionally, preserving
-- the "loaded = added" default every earlier phase assumed.
local function evalLoad(data, skipOptional)
local L = data.load or {}
if L.never then return false end
for i = 1, #WA.loadPrototype do
local arg = WA.loadPrototype[i]
if not (skipOptional and arg.optional) then
-- Most entries gate on a plain boolean use_<name>; a few (class's
-- "off"/"single"/"multi" tier) need their own isActive since a stray
-- truthy-but-not-a-real-mode value would otherwise read as "enabled".
-- isActive must be branched on, not `and`/`or`-ed with the default
-- gate: `isActive(L) or default` can't express a false isActive, and
-- would fall back to the very gate isActive exists to override.
local active
if arg.isActive then
active = arg.isActive(L) and true or false
else
active = L[useKeys[arg]] and true or false
end
if active and not arg.eval(L) then
return false
end
end
end
return true
end
-- Overrides Data.lua's stub: the load state the engine acts on.
function WA.EvalLoad(data)
return evalLoad(data, false)
end
-- The same check with the transient constraints skipped: true for a display
-- that belongs to this character even when the moment doesn't qualify. Drives
-- the Standby label only -- what loads is WA.EvalLoad alone (§11).
function WA.EvalLoadStatic(data)
return evalLoad(data, true)
end
-- Re-evaluates every leaf display's load state and applies any transitions (ref
-- §11 ScanForLoads). No loadEvents->candidate-ids map: at tens of auras a full
-- re-scan on a load-relevant event is cheap, same "unconditional, cheap at this
-- scale" choice Conditions.lua's global-event handler makes. SetDisplayLoaded is
-- itself a no-op on displays whose state didn't change.
function WA.ScanForLoads()
if not WA.SetDisplayLoaded then return end
for id, data in pairs(WeakestAurasDB.displays) do
if not WA.IsGroup(data) then
WA.safecall(id, WA.SetDisplayLoaded, data, WA.EvalLoad(data))
end
end
end
-- Load-relevant events: any of these can flip a load constraint, so re-scan on
-- all of them. PLAYER_ENTERING_WORLD is the initial settle (zone/level/group are
-- only reliable once the world is loaded -- AddAllDisplays runs before that).
-- Guarded RegisterEvent for names that vary by build (the shapeshift events),
-- same defense GenericTrigger's ensureEventRegistered uses.
local LOAD_EVENTS = {
"PLAYER_ENTERING_WORLD", "PLAYER_REGEN_ENABLED", "PLAYER_REGEN_DISABLED",
"PLAYER_LEVEL_UP", "ZONE_CHANGED_NEW_AREA", "ZONE_CHANGED", "ZONE_CHANGED_INDOORS",
"RAID_ROSTER_UPDATE", "PARTY_MEMBERS_CHANGED", "UPDATE_SHAPESHIFT_FORM", "UPDATE_SHAPESHIFT_FORMS",
"PLAYER_DEAD", "PLAYER_ALIVE", "PLAYER_UNGHOST", "UNIT_FLAGS",
"PLAYER_GUILD_UPDATE", "SPELLS_CHANGED", "PLAYER_EQUIPMENT_CHANGED",
"UPDATE_BATTLEFIELD_STATUS",
}
local loadFrame = CreateFrame("Frame")
for i = 1, #LOAD_EVENTS do
pcall(loadFrame.RegisterEvent, loadFrame, LOAD_EVENTS[i])
end
-- UNIT_AURA and UNIT_FLAGS fire for every visible unit, once per token (very
-- chatty in a raid, and UNIT_FLAGS bursts as a whole raid enters combat), but
-- only the player's own -- the mount buff, the taxi flag -- matter to any load
-- constraint here, so nobody else's forces a full re-scan of every display.
loadFrame:RegisterEvent("UNIT_AURA")
loadFrame:SetScript("OnEvent", function()
if (event == "UNIT_AURA" or event == "UNIT_FLAGS") and arg1 ~= "player" then return end
WA.ScanForLoads()
end)