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.

6495 lines
216 KiB

---@type string
local AddonName = ...
---@class Private
local Private = select(2, ...)
local internalVersion = 75
-- Lua APIs
local insert = table.insert
-- WoW APIs
local GetTalentInfo, InCombatLockdown = GetTalentInfo, InCombatLockdown
local UnitName, GetRealmName, UnitRace, UnitFactionGroup, IsInRaid
= UnitName, GetRealmName, UnitRace, UnitFactionGroup, IsInRaid
local UnitClass, UnitExists, UnitGUID, UnitAffectingCombat, GetInstanceInfo, IsInInstance
= UnitClass, UnitExists, UnitGUID, UnitAffectingCombat, GetInstanceInfo, IsInInstance
local UnitIsUnit, GetRaidRosterInfo, GetSpecialization, UnitInVehicle, UnitHasVehicleUI
= UnitIsUnit, GetRaidRosterInfo, GetSpecialization, UnitInVehicle, UnitHasVehicleUI
local SendChatMessage, UnitInBattleground, UnitInRaid, UnitInParty, GetTime
= SendChatMessage, UnitInBattleground, UnitInRaid, UnitInParty, GetTime
local CreateFrame, IsShiftKeyDown, GetScreenWidth, GetScreenHeight, GetCursorPosition, UpdateAddOnCPUUsage, GetFrameCPUUsage, debugprofilestop
= CreateFrame, IsShiftKeyDown, GetScreenWidth, GetScreenHeight, GetCursorPosition, UpdateAddOnCPUUsage, GetFrameCPUUsage, debugprofilestop
local debugstack = debugstack
local GetNumTalentTabs, GetNumTalents = GetNumTalentTabs, GetNumTalents
local MAX_NUM_TALENTS = MAX_NUM_TALENTS or 20
local ADDON_NAME = "WeakAuras"
---@class WeakAuras
local WeakAuras = WeakAuras
local L = WeakAuras.L
local versionString = WeakAuras.versionString
local prettyPrint = WeakAuras.prettyPrint
WeakAurasTimers = setmetatable({}, {__tostring=function() return "WeakAuras" end})
LibStub("AceTimer-3.0"):Embed(WeakAurasTimers)
Private.maxTimerDuration = 604800; -- A week, in seconds
local maxUpTime = 4294967; -- 2^32 / 1000
Private.watched_trigger_events = {}
-- The worlds simplest callback system.
-- That supports 1:N, but no de-registration and breaks if registering in a callback
--- @class callbacks
--- @field events table
--- @field RegisterCallback fun(self: callbacks, event: string, handler: function)
--- @field Fire fun(self: callbacks, event: string, ... : any)
Private.callbacks = {}
Private.callbacks.events = {}
function Private.callbacks:RegisterCallback(event, handler)
self.events[event] = self.events[event] or {}
tinsert(self.events[event], handler)
end
function Private.callbacks:Fire(event, ...)
if self.events[event] then
for index, f in ipairs(self.events[event]) do
f(event, ...)
end
end
end
function WeakAurasTimers:ScheduleTimerFixed(func, delay, ...)
if (delay < Private.maxTimerDuration) then
if delay + GetTime() > maxUpTime then
WeakAuras.prettyPrint(WeakAuras.L["Can't schedule timer with %i, due to a World of Warcraft bug with high computer uptime. (Uptime: %i). Please restart your computer."]:format(delay, GetTime()))
return
end
return self:ScheduleTimer(func, delay, ...)
end
end
local LDB = LibStub("LibDataBroker-1.1")
local LDBIcon = LibStub("LibDBIcon-1.0")
local LCG = LibStub("LibCustomGlow-1.0")
local LGF = LibStub("LibGetFrame-1.0")
local CustomNames = C_AddOns.IsAddOnLoaded("CustomNames") and LibStub("CustomNames") -- optional addon
if CustomNames then
WeakAuras.GetName = CustomNames.Get
WeakAuras.UnitName = CustomNames.UnitName
WeakAuras.GetUnitName = CustomNames.GetUnitName
WeakAuras.UnitFullName = CustomNames.UnitFullName
else
WeakAuras.GetName = function(name) return name end
WeakAuras.UnitName = UnitName
WeakAuras.GetUnitName = GetUnitName
WeakAuras.UnitFullName = UnitFullName
end
local timer = WeakAurasTimers
WeakAuras.timer = timer
local loginQueue = {}
local queueshowooc
function WeakAuras.InternalVersion()
return internalVersion;
end
do
local currentErrorHandlerId
local currentErrorHandlerUid
local currentErrorHandlerContext
local function waErrorHandler(errorMessage)
local juicedMessage = {}
local data
if currentErrorHandlerId then
data = WeakAuras.GetData(currentErrorHandlerId)
elseif currentErrorHandlerUid then
data = Private.GetDataByUID(currentErrorHandlerUid)
end
if data then
Private.AuraWarnings.UpdateWarning(data.uid, "LuaError", "error",
L["This aura has caused a Lua error."] .. "\n" .. L["Install the addons BugSack and BugGrabber for detailed error logs."], true)
table.insert(juicedMessage, L["Lua error in aura '%s': %s"]:format(data.id, currentErrorHandlerContext or L["unknown location"]))
else
table.insert(juicedMessage, L["Lua error"])
end
table.insert(juicedMessage, L["WeakAuras Version: %s"]:format(WeakAuras.versionString))
local version = data and (data.semver or data.version)
if version then
table.insert(juicedMessage, L["Aura Version: %s"]:format(version))
end
table.insert(juicedMessage, L["Stack trace:"])
table.insert(juicedMessage, errorMessage)
geterrorhandler()(table.concat(juicedMessage, "\n"))
end
function Private.GetErrorHandlerId(id, context)
currentErrorHandlerUid = nil
currentErrorHandlerId = id
currentErrorHandlerContext = context
return waErrorHandler
end
function Private.GetErrorHandlerUid(uid, context)
currentErrorHandlerUid = uid
currentErrorHandlerId = nil
currentErrorHandlerContext = context
return waErrorHandler
end
end
function Private.LoadOptions(msg)
if not(C_AddOns.IsAddOnLoaded("WeakAurasOptions")) then
if not WeakAuras.IsLoginFinished() then
prettyPrint(Private.LoginMessage())
loginQueue[#loginQueue + 1] = WeakAuras.OpenOptions
elseif InCombatLockdown() then
-- inform the user and queue ooc
prettyPrint(L["Options will finish loading after combat ends."])
queueshowooc = msg or "";
Private.frames["Addon Initialization Handler"]:RegisterEvent("PLAYER_REGEN_ENABLED")
return false;
else
local loaded, reason = C_AddOns.LoadAddOn("WeakAurasOptions");
if not(loaded) then
reason = string.lower("|cffff2020" .. _G["ADDON_" .. reason] .. "|r.")
WeakAuras.prettyPrint(string.format(L["Options could not be loaded, the addon is %s"], reason));
return false;
end
end
end
return true;
end
function WeakAuras.OpenOptions(msg)
if Private.NeedToRepairDatabase() then
StaticPopup_Show("WEAKAURAS_CONFIRM_REPAIR", nil, nil, {reason = "downgrade"})
elseif (WeakAuras.IsLoginFinished() and Private.LoadOptions(msg)) then
WeakAuras.ToggleOptions(msg, Private);
end
end
function Private.PrintHelp()
print(L["Usage:"])
print(L["/wa help - Show this message"])
print(L["/wa minimap - Toggle the minimap icon"])
print(L["/wa pstart - Start profiling. Optionally include a duration in seconds after which profiling automatically stops. To profile the next combat/encounter, pass a \"combat\" or \"encounter\" argument."])
print(L["/wa pstop - Finish profiling"])
print(L["/wa pprint - Show the results from the most recent profiling"])
print(L["/wa repair - Repair tool"])
print(L["If you require additional assistance, please open a ticket on GitHub or visit our Discord at https://discord.gg/weakauras!"])
end
SLASH_WEAKAURAS1, SLASH_WEAKAURAS2 = "/weakauras", "/wa";
function SlashCmdList.WEAKAURAS(input)
local args, msg = {}, nil
for v in string.gmatch(input, "%S+") do
if not msg then
msg = v:lower()
else
insert(args, v:lower())
end
end
if msg == "pstart" then
WeakAuras.StartProfile(args[1]);
elseif msg == "pstop" then
WeakAuras.StopProfile();
elseif msg == "pprint" then
WeakAuras.PrintProfile();
elseif msg == "pcancel" then
WeakAuras.CancelScheduledProfile()
elseif msg == "minimap" then
WeakAuras.ToggleMinimap();
elseif msg == "help" then
Private.PrintHelp();
elseif msg == "repair" then
StaticPopup_Show("WEAKAURAS_CONFIRM_REPAIR", nil, nil, {reason = "user"})
elseif msg == "ff" or msg == "feat" or msg == "feature" then
if #args < 2 then
local features = Private.Features:ListFeatures()
local summary = {}
for _, feature in ipairs(features) do
table.insert(summary, ("|c%s%s|r"):format(feature.enabled and "ff00ff00" or "ffff0000", feature.id))
end
prettyPrint(L["Syntax /wa feature <toggle|on|enable|disable|off> <feature>"])
prettyPrint(L["Available features: %s"]:format(table.concat(summary, ", ")))
else
local action = ({
toggle = "toggle",
on = "enable",
enable = "enable",
disable = "disable",
off = "disable"
})[args[1]]
if not action then
prettyPrint(L["Unknown action %q"]:format(args[1]))
else
local feature = args[2]
if not Private.Features:Exists(feature) then
prettyPrint(L["Unknown feature %q"]:format(feature))
elseif not Private.Features:Enabled(feature) then
if action ~= "disable" then
Private.Features:Enable(feature)
prettyPrint(L["Enabled feature %q"]:format(feature))
else
prettyPrint(L["Feature %q is already disabled"]:format(feature))
end
elseif Private.Features:Enabled(feature) then
if action ~= "enable" then
Private.Features:Disable(feature)
prettyPrint(L["Disabled feature %q"]:format(feature))
else
prettyPrint(L["Feature %q is already enabled"]:format(feature))
end
end
end
end
else
WeakAuras.OpenOptions(msg);
end
end
if not WeakAuras.IsLibsOK() then return end
function WeakAuras.ToggleMinimap()
WeakAurasSaved.minimap.hide = not WeakAurasSaved.minimap.hide
if WeakAurasSaved.minimap.hide then
LDBIcon:Hide("WeakAuras");
prettyPrint(L["Use /wa minimap to show the minimap icon again."])
else
LDBIcon:Show("WeakAuras");
end
end
BINDING_HEADER_WEAKAURAS = ADDON_NAME
BINDING_NAME_WEAKAURASTOGGLE = L["Toggle Options Window"]
BINDING_NAME_WEAKAURASPROFILINGTOGGLE = L["Toggle Performance Profiling Window"]
BINDING_NAME_WEAKAURASPRINTPROFILING = L["Print Profiling Results"]
-- An alias for WeakAurasSaved, the SavedVariables
-- Noteable properties:
-- debug: If set to true, WeakAura.debug() outputs messages to the chat frame
-- displays: All aura settings, keyed on their id
---@class WeakAurasSaved
local db;
-- While true no events are handled. E.g. WeakAuras is paused while the Options dialog is open
local paused = true;
local importing = false;
-- squelches actions and sounds from auras. is used e.g. to prevent lots of actions/sounds from triggering
-- on login or after closing the options dialog
local squelch_actions = true;
local in_loading_screen = false;
-- Load functions, keyed on id
local loadFuncs = {};
-- Load functions for the Options window that ignore various load options
local loadFuncsForOptions = {};
-- Mapping of events to ids, contains true if a aura should be checked for a certain event
local loadEvents = {}
-- All regions keyed on id, has properties: region, regionType, also see clones
Private.regions = {};
-- keyed on id, contains bool indicating whether the aura is loaded
Private.loaded = {};
local loaded = Private.loaded;
-- contains regions for clones
Private.clones = {};
local clones = Private.clones;
-- Unused regions that are kept around for clones
local clonePool = {}
-- One table per regionType, see RegisterRegionType, notable properties: create, modify and default
Private.regionTypes = {};
local regionTypes = Private.regionTypes;
Private.subRegionTypes = {}
local subRegionTypes = Private.subRegionTypes
-- One table per regionType, see RegisterRegionOptions
Private.regionOptions = {};
local regionOptions = Private.regionOptions;
Private.subRegionOptions = {}
local subRegionOptions = Private.subRegionOptions
-- Maps from trigger type to trigger system
Private.triggerTypes = {};
local triggerTypes = Private.triggerTypes;
-- Maps from trigger type to a function that can create options for the trigger
Private.triggerTypesOptions = {};
-- Trigger State, updated by trigger systems, then applied to regions by UpdatedTriggerState
-- keyed on id, triggernum, cloneid
-- cloneid can be a empty string
-- Noteable properties:
-- changed: Whether this trigger state was recently changed and its properties
-- need to be applied to a region. The glue code resets this
-- after syncing the region to the trigger state
-- show: Whether the region for this trigger state should be shown
-- progressType: Either "timed", "static"
-- duration: The duration if the progressType is timed
-- expirationTime: The expirationTime if the progressType is timed
-- autoHide: If the aura should be hidden on expiring
-- value: The value if the progressType is static
-- total: The total if the progressType is static
-- inverse: The static values should be interpreted inversely
-- name: The name information
-- icon: The icon information
-- texture: The texture information
-- stacks: The stacks information
-- index: The index of the buff/debuff for the buff trigger system, used to set the tooltip
-- spellId: spellId of the buff/debuff, used to set the tooltip
local triggerState = {}
-- Fallback states
local fallbacksStates = {};
-- List of all trigger systems, contains each system once
local triggerSystems = {}
local timers = {}; -- Timers for autohiding, keyed on id, triggernum, cloneid
WeakAuras.raidUnits = {};
WeakAuras.raidpetUnits = {};
WeakAuras.partyUnits = {};
WeakAuras.partypetUnits = {};
WeakAuras.petUnitToUnit = {
pet = "player"
}
WeakAuras.unitToPetUnit = {
player = "pet"
}
do
for i=1,40 do
WeakAuras.raidUnits[i] = "raid"..i
WeakAuras.raidpetUnits[i] = "raidpet"..i
WeakAuras.petUnitToUnit["raidpet"..i] = "raid"..i
WeakAuras.unitToPetUnit["raid"..i] = "raidpet"..i
end
for i=1,4 do
WeakAuras.partyUnits[i] = "party"..i
WeakAuras.partypetUnits[i] = "partypet"..i
WeakAuras.petUnitToUnit["partypet"..i] = "party"..i
WeakAuras.unitToPetUnit["party"..i] = "partypet"..i
end
end
---@param unit UnitToken
---@return boolean isPet
WeakAuras.UnitIsPet = function(unit)
return WeakAuras.petUnitToUnit[unit] ~= nil
end
local playerLevel = UnitLevel("player");
local currentInstanceType = "none"
-- Custom Action Functions, keyed on id, "init" / "start" / "finish"
Private.customActionsFunctions = {};
-- Custom Functions used in conditions, keyed on id, condition number, "changes", property number
Private.ExecEnv.customConditionsFunctions = {};
-- Text format functions for chat messages, keyed on id, condition number, changes, property number
Private.ExecEnv.conditionTextFormatters = {}
-- Helpers for conditions, that is custom run functions and preamble objects for built in checks
-- keyed on UID not on id!
Private.ExecEnv.conditionHelpers = {}
local load_prototype = Private.load_prototype;
function Private.validate(input, default)
for field, defaultValue in pairs(default) do
if(type(defaultValue) == "table" and type(input[field]) ~= "table") then
input[field] = {};
elseif(input[field] == nil) or (type(input[field]) ~= type(defaultValue)) then
input[field] = defaultValue;
end
if(type(input[field]) == "table") then
Private.validate(input[field], defaultValue);
end
end
end
---@diagnostic disable-next-line: duplicate-set-field
function Private.RegisterRegionType(name, createFunction, modifyFunction, default, properties, validate)
if not(name) then
error("Improper arguments to Private.RegisterRegionType - name is not defined", 2);
elseif(type(name) ~= "string") then
error("Improper arguments to Private.RegisterRegionType - name is not a string", 2);
elseif not(createFunction) then
error("Improper arguments to Private.RegisterRegionType - creation function is not defined", 2);
elseif(type(createFunction) ~= "function") then
error("Improper arguments to Private.RegisterRegionType - creation function is not a function", 2);
elseif not(modifyFunction) then
error("Improper arguments to Private.RegisterRegionType - modification function is not defined", 2);
elseif(type(modifyFunction) ~= "function") then
error("Improper arguments to Private.RegisterRegionType - modification function is not a function", 2)
elseif not(default) then
error("Improper arguments to Private.RegisterRegionType - default options are not defined", 2);
elseif(type(default) ~= "table") then
error("Improper arguments to Private.RegisterRegionType - default options are not a table", 2);
elseif(type(default) ~= "table" and type(default) ~= "nil") then
error("Improper arguments to Private.RegisterRegionType - properties options are not a table", 2);
elseif(regionTypes[name]) then
error("Improper arguments to Private.RegisterRegionType - region type \""..name.."\" already defined", 2);
else
regionTypes[name] = {
create = createFunction,
modify = modifyFunction,
default = default,
validate = validate,
properties = properties,
};
end
end
---@private
---@param name string
---@param displayName string
---@param supportFunction function
---@param createFunction function
---@param modifyFunction function
---@param onAcquire function
---@param onRelease function
---@param default table
---@param addDefaultsForNewAura function
---@param properties table
---@param supportsAdd? boolean
function WeakAuras.RegisterSubRegionType(name, displayName, supportFunction, createFunction, modifyFunction, onAcquire, onRelease, default, addDefaultsForNewAura, properties, supportsAdd)
if not(name) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - name is not defined", 2);
elseif(type(name) ~= "string") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - name is not a string", 2);
elseif not(displayName) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - display name is not defined".." "..name, 2);
elseif(type(displayName) ~= "string") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - display name is not a string", 2);
elseif not(supportFunction) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - support function is not defined", 2);
elseif(type(supportFunction) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - support function is not a function", 2);
elseif not(createFunction) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - creation function is not defined", 2);
elseif(type(createFunction) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - creation function is not a function", 2);
elseif not(modifyFunction) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - modification function is not defined", 2);
elseif(type(modifyFunction) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - modification function is not a function", 2)
elseif not(onAcquire) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - onAcquire function is not defined", 2);
elseif(type(onAcquire) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - onAcquire function is not a function", 2)
elseif not(onRelease) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - onRelease function is not defined", 2);
elseif(type(onRelease) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - onRelease function is not a function", 2)
elseif not(default) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - default options are not defined", 2);
elseif(type(default) ~= "table" and type(default) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - default options are not a table or a function", 2);
elseif(addDefaultsForNewAura and type(addDefaultsForNewAura) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionType - addDefaultsForNewAura function is not nil or a function", 2)
elseif(subRegionTypes[name]) then
error("Improper arguments to WeakAuras.RegisterSubRegionType - region type \""..name.."\" already defined", 2);
else
local pool = CreateObjectPool(createFunction)
subRegionTypes[name] = {
displayName = displayName,
supports = supportFunction,
modify = modifyFunction,
default = default,
addDefaultsForNewAura = addDefaultsForNewAura,
properties = properties,
supportsAdd = supportsAdd == nil or supportsAdd,
acquire = function()
local subRegion = pool:Acquire()
onAcquire(subRegion)
subRegion.type = name
return subRegion
end,
release = function(subRegion)
onRelease(subRegion)
pool:Release(subRegion)
end
};
end
end
---@diagnostic disable-next-line: duplicate-set-field
function Private.RegisterRegionOptions(name, createFunction, icon, displayName, createThumbnail, modifyThumbnail, description, templates, getAnchors)
if not(name) then
error("Improper arguments to Private.RegisterRegionOptions - name is not defined", 2);
elseif(type(name) ~= "string") then
error("Improper arguments to Private.RegisterRegionOptions - name is not a string", 2);
elseif not(createFunction) then
error("Improper arguments to Private.RegisterRegionOptions - creation function is not defined", 2);
elseif(type(createFunction) ~= "function") then
error("Improper arguments to Private.RegisterRegionOptions - creation function is not a function", 2);
elseif not(icon) then
error("Improper arguments to Private.RegisterRegionOptions - icon is not defined", 2);
elseif not(type(icon) == "string" or type(icon) == "function") then
error("Improper arguments to Private.RegisterRegionOptions - icon is not a string or a function", 2)
elseif not(displayName) then
error("Improper arguments to Private.RegisterRegionOptions - display name is not defined".." "..name, 2);
elseif(type(displayName) ~= "string") then
error("Improper arguments to Private.RegisterRegionOptions - display name is not a string", 2);
elseif (getAnchors and type(getAnchors) ~= "function") then
error("Improper arguments to Private.RegisterRegionOptions - anchors is not a function", 2);
elseif(regionOptions[name]) then
error("Improper arguments to Private.RegisterRegionOptions - region type \""..name.."\" already defined", 2);
else
local templateIcon
if (type(icon) == "function") then
-- We only want to create two icons and reparent it as needed
templateIcon = icon()
templateIcon:Hide()
icon = icon()
icon:Hide()
else
templateIcon = icon
end
local acquireThumbnail, releaseThumbnail
if createThumbnail and modifyThumbnail then
local thumbnailPool = CreateObjectPool(createThumbnail)
acquireThumbnail = function(parent, data)
local thumbnail, newObject = thumbnailPool:Acquire()
thumbnail:Show()
modifyThumbnail(parent, thumbnail, data)
return thumbnail
end
releaseThumbnail = function(thumbnail)
thumbnail:Hide()
thumbnailPool:Release(thumbnail)
end
end
regionOptions[name] = {
create = createFunction,
icon = icon,
templateIcon = templateIcon,
displayName = displayName,
createThumbnail = createThumbnail,
modifyThumbnail = modifyThumbnail,
acquireThumbnail = acquireThumbnail,
releaseThumbnail = releaseThumbnail,
description = description,
templates = templates,
getAnchors = getAnchors
};
end
end
---@private
---@param name string
---@param createFunction function
---@param description string
function WeakAuras.RegisterSubRegionOptions(name, createFunction, description)
if not(name) then
error("Improper arguments to WeakAuras.RegisterSubRegionOptions - name is not defined", 2);
elseif(type(name) ~= "string") then
error("Improper arguments to WeakAuras.RegisterSubRegionOptions - name is not a string", 2);
elseif not(createFunction) then
error("Improper arguments to WeakAuras.RegisterSubRegionOptions - creation function is not defined", 2);
elseif(type(createFunction) ~= "function") then
error("Improper arguments to WeakAuras.RegisterSubRegionOptions - creation function is not a function", 2);
elseif(subRegionOptions[name]) then
error("Improper arguments to WeakAuras.RegisterSubRegionOptions - region type \""..name.."\" already defined", 2);
else
subRegionOptions[name] = {
create = createFunction,
description = description,
};
end
end
---@diagnostic disable-next-line: duplicate-set-field (it's replaced in WeakAurasOptions.lua)
function WeakAuras.IsOptionsOpen()
return false;
end
function Private.ParseNumber(numString)
if not(numString and type(numString) == "string") then
if(type(numString) == "number") then
return numString, "notastring";
else
return nil;
end
elseif(numString:sub(-1) == "%") then
local percent = tonumber(numString:sub(1, -2));
if(percent) then
return percent / 100, "percent";
else
return nil;
end
else
-- Matches any string with two integers separated by a forward slash
-- Captures the two integers
local _, _, numerator, denominator = numString:find("(%d+)%s*/%s*(%d+)");
numerator, denominator = tonumber(numerator), tonumber(denominator);
if(numerator and denominator) then
if(denominator == 0) then
return nil;
else
return numerator / denominator, "fraction";
end
else
local num = tonumber(numString)
if(num) then
if(math.floor(num) ~= num) then
return num, "decimal";
else
return num, "whole";
end
else
return nil;
end
end
end
end
local function EvalBooleanArg(arg, trigger, default)
if(type(arg) == "function") then
return arg(trigger);
elseif type(arg) == "boolean" then
return arg
elseif type(arg) == "nil" then
return default
end
end
local function singleTest(arg, trigger, use, name, value, operator, use_exact, caseInsensitive)
local number = value and tonumber(value) or nil
if(arg.type == "tristate") then
if(use == false) then
return "(not "..name..")";
elseif(use) then
if(arg.test) then
return "("..arg.test:format(value)..")";
else
return name;
end
end
elseif(arg.type == "tristatestring") then
if(use == false) then
return "("..name.. "~=".. (number or string.format("%s", Private.QuotedString(value or ""))) .. ")"
elseif(use) then
return "("..name.. "==".. (number or string.format("%s", Private.QuotedString(value or ""))) .. ")"
end
elseif(arg.type == "multiselect") then
if arg.multiNoSingle then
-- convert single to multi
-- this is a lazy migration because multiNoSingle is not set for all game versions
if use == true then
trigger["use_"..name] = false
trigger[name] = trigger[name] or {}
trigger[name].multi = {};
if trigger[name].single ~= nil then
trigger[name].multi[trigger[name].single] = true;
trigger[name].single = nil
end
end
end
if(use == false) then -- multi selection
local any = false;
if (value and value.multi) then
local test = "(";
for value, positive in pairs(value.multi) do
local arg1 = tonumber(value) or ("[["..value.."]]")
local arg2
if arg.extraOption then
arg2 = trigger[name .. "_extraOption"] or 0
elseif arg.multiTristate then
arg2 = positive and 4 or 5
end
local testEnabled = true
if type(arg.enableTest) == "function" then
testEnabled = arg.enableTest(trigger, arg1, arg2)
end
if testEnabled then
local check
if not arg.test then
check = name.."=="..arg1
else
check = arg.test:format(arg1, arg2)
end
if arg.multiAll then
test = test..check.." and "
else
test = test..check.." or "
end
any = true;
end
end
if(any) then
test = test:sub(1, -6);
else
test = "(false";
end
test = test..")"
if arg.inverse then
if type(arg.inverse) == "boolean" then
test = "not " .. test
elseif type(arg.inverse) == "function" then
if arg.inverse(trigger) then
test = "not " .. test
end
end
end
return test
end
elseif(use) then -- single selection
local value = value and value.single or nil;
if not arg.test then
return value and "("..name.."=="..(tonumber(value) or ("[["..value.."]]"))..")";
else
return value and "("..arg.test:format(tonumber(value) or ("[["..value.."]]"))..")";
end
end
elseif(arg.type == "toggle") then
if(use) then
if(arg.test) then
return "("..arg.test:format(value)..")";
else
return name;
end
end
elseif (arg.type == "spell") then
if arg.showExactOption then
return "("..arg.test:format(value, tostring(use_exact) or "false") ..")";
else
return "("..arg.test:format(value)..")";
end
elseif(arg.test) then
return "("..arg.test:format(value)..")";
elseif(arg.type == "longstring" and operator) then
if(operator == "==") then
if caseInsensitive then
return ("(%s and %s:lower() == [[%s]]:lower())"):format(name, name, value)
else
return "("..name.."==[["..value.."]])";
end
else
if caseInsensitive then
local op = operator:format(value:lower())
return ("(%s:lower():%s)"):format(name, op)
else
return "("..name..":"..operator:format(value)..")";
end
end
elseif(arg.type == "number") then
if number then
return "("..name..(operator or "==").. number ..")";
end
else
if(type(value) == "table") then
value = "error";
end
return "("..name..(operator or "==")..(number or ("[["..(value or "").."]]"))..")";
end
end
-- Used for the load function, could be simplified a bit
-- It used to be also used for the generic trigger system
local function ConstructFunction(prototype, trigger, skipOptional)
local input = {"event"};
local required = {};
local tests = {};
local debug = {};
local events = {}
local init;
local preambles = ""
local orConjunctionGroups = {}
if(prototype.init) then
init = prototype.init(trigger);
else
init = "";
end
for index, arg in pairs(prototype.args) do
local enable = EvalBooleanArg(arg.enable, trigger, true)
local init = arg.init
local name = arg.name;
if(arg.init == "arg") then
tinsert(input, name);
end
if(enable) then
if (arg.optional and skipOptional) then
-- Do nothing
elseif arg.type == "tristate"
or arg.type == "toggle"
or arg.type == "tristatestring"
or (arg.type == "multiselect" and trigger["use_"..name] ~= nil)
or ((trigger["use_"..name] or arg.required) and trigger[name])
then
local test;
if arg.multiEntry then
if type(trigger[name]) == "table" and #trigger[name] > 0 then
test = ""
for i, value in ipairs(trigger[name]) do
local operator = name and type(trigger[name.."_operator"]) == "table" and trigger[name.."_operator"][i]
local caseInsensitive = name and arg.canBeCaseInsensitive and type(trigger[name.."_caseInsensitive"]) == "table" and trigger[name.."_caseInsensitive"][i]
local use_exact = name and type(trigger["use_exact_" .. name]) == "table" and trigger["use_exact_" .. name][i]
local use = name and trigger["use_"..name]
local single = singleTest(arg, trigger, use, name, value, operator, use_exact, caseInsensitive)
if single then
if test ~= "" then
test = test .. arg.multiEntry.operator
end
test = test .. single
end
end
if test == "" then
test = nil
else
test = "(" .. test .. ")"
end
end
else
local value = trigger[name]
local operator = name and trigger[name.."_operator"]
local caseInsensitive = name and trigger[name.."_caseInsensitive"]
local use_exact = name and trigger["use_exact_" .. name]
local use = name and trigger["use_"..name]
test = singleTest(arg, trigger, use, name, value, operator, use_exact, caseInsensitive)
end
if (arg.preamble) then
preambles = preambles .. arg.preamble:format(trigger[name]) .. "\n"
end
if test ~= "(test)" then
if(arg.required) then
tinsert(required, test);
elseif test ~= nil then
if arg.orConjunctionGroup then
orConjunctionGroups[arg.orConjunctionGroup ] = orConjunctionGroups[arg.orConjunctionGroup ] or {}
tinsert(orConjunctionGroups[arg.orConjunctionGroup ], test)
else
tinsert(tests, test);
end
end
end
if test and arg.events then
for index, event in ipairs(arg.events) do
events[event] = true
end
end
if(arg.debug) then
tinsert(debug, arg.debug:format(trigger[name]));
end
end
end
end
for _, orConjunctionGroup in pairs(orConjunctionGroups) do
tinsert(tests, "("..table.concat(orConjunctionGroup , " or ")..")")
end
local ret = {preambles .. "return function("..table.concat(input, ", ")..")\n"};
table.insert(ret, (init or ""));
table.insert(ret, (#debug > 0 and table.concat(debug, "\n") or ""));
table.insert(ret, "if(");
table.insert(ret, ((#required > 0) and table.concat(required, " and ").." and " or ""));
table.insert(ret, (#tests > 0 and table.concat(tests, " and ") or "true"));
table.insert(ret, ") then\n");
if(#debug > 0) then
table.insert(ret, "print('ret: true');\n");
end
table.insert(ret, "return true else return false end end");
return table.concat(ret), events;
end
function WeakAuras.GetActiveConditions(id, cloneId)
triggerState[id].activatedConditions[cloneId] = triggerState[id].activatedConditions[cloneId] or {};
return triggerState[id].activatedConditions[cloneId];
end
local function LoadCustomActionFunctions(data)
local id = data.id;
Private.customActionsFunctions[id] = {};
if (data.actions) then
if (data.actions.init and data.actions.init.do_custom and data.actions.init.custom) then
local func = WeakAuras.LoadFunction("return function() "..(data.actions.init.custom).."\n end");
Private.customActionsFunctions[id]["init"] = func;
end
if (data.actions.start) then
if (data.actions.start.do_custom and data.actions.start.custom) then
local func = WeakAuras.LoadFunction("return function() "..(data.actions.start.custom).."\n end");
Private.customActionsFunctions[id]["start"] = func;
end
if (data.actions.start.do_message and data.actions.start.message_custom) then
local func = WeakAuras.LoadFunction("return "..(data.actions.start.message_custom));
Private.customActionsFunctions[id]["start_message"] = func;
end
end
if (data.actions.finish) then
if (data.actions.finish.do_custom and data.actions.finish.custom) then
local func = WeakAuras.LoadFunction("return function() "..(data.actions.finish.custom).."\n end");
Private.customActionsFunctions[id]["finish"] = func;
end
if (data.actions.finish.do_message and data.actions.finish.message_custom) then
local func = WeakAuras.LoadFunction("return "..(data.actions.finish.message_custom));
Private.customActionsFunctions[id]["finish_message"] = func;
end
end
end
end
Private.talent_types_specific = {}
Private.pvp_talent_types_specific = {}
local function CreateTalentCache()
local _, player_class = UnitClass("player")
Private.talent_types_specific[player_class] = Private.talent_types_specific[player_class] or {};
if WeakAuras.IsClassicOrCata() then
for tab = 1, GetNumTalentTabs() do
for num_talent = 1, GetNumTalents(tab) do
local talentName, talentIcon = GetTalentInfo(tab, num_talent);
local talentId = (tab - 1) * MAX_NUM_TALENTS + num_talent
if (talentName and talentIcon) then
Private.talent_types_specific[player_class][talentId] = "|T"..talentIcon..":0|t "..talentName
end
end
end
else
local spec = GetSpecialization()
Private.talent_types_specific[player_class][spec] = Private.talent_types_specific[player_class][spec] or {};
for tier = 1, MAX_TALENT_TIERS do
for column = 1, NUM_TALENT_COLUMNS do
-- Get name and icon info for the current talent of the current class and save it
local _, talentName, talentIcon = GetTalentInfo(tier, column, 1)
local talentId = (tier-1)*3+column
-- Get the icon and name from the talent cache and record it in the table that will be used by WeakAurasOptions
if (talentName and talentIcon) then
Private.talent_types_specific[player_class][spec][talentId] = "|T"..talentIcon..":0|t "..talentName
end
end
end
end
end
local function CreatePvPTalentCache()
local _, player_class = UnitClass("player")
local spec = GetSpecialization()
if (not player_class or not spec) then
return;
end
Private.pvp_talent_types_specific[player_class] = Private.pvp_talent_types_specific[player_class] or {};
Private.pvp_talent_types_specific[player_class][spec] = Private.pvp_talent_types_specific[player_class][spec] or {};
--- @type fun(talentId: number): number, string
local function formatTalent(talentId)
local _, name, icon, _, _, spellId = GetPvpTalentInfoByID(talentId);
return spellId, "|T"..icon..":0|t "..name
end
local slotInfo = C_SpecializationInfo.GetPvpTalentSlotInfo(1);
if (slotInfo) then
Private.pvp_talent_types_specific[player_class][spec] = {};
local pvpSpecTalents = slotInfo.availableTalentIDs;
for _, talentId in ipairs(pvpSpecTalents) do
local index, displayText = formatTalent(talentId)
Private.pvp_talent_types_specific[player_class][spec][index] = displayText
end
end
end
Private.CompanionData = {}
-- use this function to not overwrite data from other companion compatible addons
-- when using this function, do not name your global data table "WeakAurasCompanion"
function WeakAuras.AddCompanionData(data)
WeakAuras.DeepMixin(Private.CompanionData, data)
end
-- add data from versions of companion compatible addon that does not use WeakAuras.AddCompanionData yet
local function AddLegacyCompanionData()
local CompanionData = WeakAurasCompanion and WeakAurasCompanion.WeakAuras or WeakAurasCompanion
if CompanionData then
WeakAuras.AddCompanionData(CompanionData)
end
end
function Private.PostAddCompanion()
-- add data from older version of companion addons
AddLegacyCompanionData()
-- nag if updates
local count = Private.CountWagoUpdates()
if count and count > 0 then
WeakAuras.prettyPrint(L["There are %i updates to your auras ready to be installed!"]:format(count))
end
-- nag if new installs
if Private.CompanionData.stash and next(Private.CompanionData.stash) then
WeakAuras.prettyPrint(L["You have new auras ready to be installed!"])
end
end
function Private.CountWagoUpdates()
if not (Private.CompanionData.slugs) then
return 0
end
local updatedSlugs, updatedSlugsCount = {}, 0
for id, aura in pairs(db.displays) do
if not aura.ignoreWagoUpdate and aura.url and aura.url ~= "" then
local slug, version = aura.url:match("wago.io/([^/]+)/([0-9]+)")
if not slug and not version then
slug = aura.url:match("wago.io/([^/]+)$")
version = 1
end
if slug and version then
local wago = Private.CompanionData.slugs[slug]
if wago and wago.wagoVersion and tonumber(wago.wagoVersion) > tonumber(version) then
if not updatedSlugs[slug] then
updatedSlugs[slug] = true
updatedSlugsCount = updatedSlugsCount + 1
end
end
end
end
end
return updatedSlugsCount
end
local function tooltip_draw(isAddonCompartment, blizzardTooltip)
local tooltip
if isAddonCompartment then
tooltip = blizzardTooltip
else
tooltip = GameTooltip
end
tooltip:ClearLines()
tooltip:AddDoubleLine("WeakAuras", versionString)
if Private.CompanionData.slugs then
local count = Private.CountWagoUpdates()
if count > 0 then
tooltip:AddLine(" ");
tooltip:AddLine((L["There are %i updates to your auras ready to be installed!"]):format(count));
end
end
tooltip:AddLine(" ");
tooltip:AddLine(L["|cffeda55fLeft-Click|r to toggle showing the main window."], 0.2, 1, 0.2);
if not WeakAuras.IsOptionsOpen() then
if paused then
tooltip:AddLine("|cFFFF0000"..L["Paused"].." - "..L["Shift-Click to resume addon execution."], 0.2, 1, 0.2);
else
tooltip:AddLine(L["|cffeda55fShift-Click|r to pause addon execution."], 0.2, 1, 0.2);
end
end
tooltip:AddLine(L["|cffeda55fRight-Click|r to toggle performance profiling window."], 0.2, 1, 0.2);
if not isAddonCompartment then
tooltip:AddLine(L["|cffeda55fMiddle-Click|r to toggle the minimap icon on or off."], 0.2, 1, 0.2);
end
tooltip:Show();
end
WeakAuras.GenerateTooltip = tooltip_draw;
local colorFrame = CreateFrame("Frame");
Private.frames["LDB Icon Recoloring"] = colorFrame;
local colorElapsed = 0;
local colorDelay = 2;
local r, g, b = 0.8, 0, 1;
local r2, g2, b2 = random(2)-1, random(2)-1, random(2)-1;
local tooltip_update_frame = CreateFrame("Frame");
Private.frames["LDB Tooltip Updater"] = tooltip_update_frame;
-- function copied from LibDBIcon-1.0.lua
local function getAnchors(frame)
local x, y = frame:GetCenter()
if not x or not y then return "CENTER" end
local hHalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or ""
local vHalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM"
return vHalf..hHalf, frame, (vHalf == "TOP" and "BOTTOM" or "TOP")..hHalf
end
local Broker_WeakAuras;
Broker_WeakAuras = LDB:NewDataObject("WeakAuras", {
type = "launcher",
text = "WeakAuras",
icon = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\icon.blp",
OnClick = function(self, button)
if button == 'LeftButton' then
if(IsShiftKeyDown()) then
if not(WeakAuras.IsOptionsOpen()) then
WeakAuras.Toggle();
end
else
WeakAuras.OpenOptions();
end
elseif(button == 'MiddleButton') then
WeakAuras.ToggleMinimap();
else
WeakAurasProfilingFrame:Toggle()
end
tooltip_draw()
end,
OnEnter = function(self)
colorFrame:SetScript("OnUpdate", function(self, elaps)
colorElapsed = colorElapsed + elaps;
if(colorElapsed > colorDelay) then
colorElapsed = colorElapsed - colorDelay;
r, g, b = r2, g2, b2;
r2, g2, b2 = random(2)-1, random(2)-1, random(2)-1;
end
Broker_WeakAuras.iconR = r + (r2 - r) * colorElapsed / colorDelay;
Broker_WeakAuras.iconG = g + (g2 - g) * colorElapsed / colorDelay;
Broker_WeakAuras.iconB = b + (b2 - b) * colorElapsed / colorDelay;
end);
local elapsed = 0;
local delay = 1;
tooltip_update_frame:SetScript("OnUpdate", function(self, elap)
elapsed = elapsed + elap;
if(elapsed > delay) then
elapsed = 0;
tooltip_draw();
end
end);
GameTooltip:SetOwner(self, "ANCHOR_NONE");
GameTooltip:SetPoint(getAnchors(self))
tooltip_draw();
end,
OnLeave = function(self)
colorFrame:SetScript("OnUpdate", nil);
tooltip_update_frame:SetScript("OnUpdate", nil);
GameTooltip:Hide();
end,
iconR = 0.6,
iconG = 0,
iconB = 1
});
do -- Archive stuff
local Archivist = select(2, ...).Archivist
local function OpenArchive()
if Archivist:IsInitialized() then
return Archivist
else
if not C_AddOns.IsAddOnLoaded("WeakAurasArchive") then
local ok, reason = C_AddOns.LoadAddOn("WeakAurasArchive")
if not ok then
reason = string.lower("|cffff2020" .. _G["ADDON_" .. reason] .. "|r.")
error(string.format(L["Could not load WeakAuras Archive, the addon is %s"], reason))
end
end
if type(WeakAurasArchive) ~= "table" then
WeakAurasArchive = {}
end
Archivist:Initialize(WeakAurasArchive)
end
return Archivist
end
function WeakAuras.LoadFromArchive(storeType, storeID)
local Archive = OpenArchive()
return Archive:Load(storeType, storeID)
end
end
local loginFinished, loginMessage = false, L["Options will open after the login process has completed."]
function WeakAuras.IsLoginFinished()
return loginFinished
end
function Private.LoginMessage()
return loginMessage
end
local function CheckForPreviousEncounter()
if (UnitAffectingCombat ("player") or InCombatLockdown()) then
for i = 1, 10 do
if (UnitExists ("boss" .. i)) then
local guid = UnitGUID ("boss" .. i)
if (guid and db.CurrentEncounter.boss_guids [guid]) then
-- we are in the same encounter
WeakAuras.CurrentEncounter = db.CurrentEncounter
return true
end
end
end
db.CurrentEncounter = nil
else
db.CurrentEncounter = nil
end
end
function Private.Login(takeNewSnapshots)
local loginThread = coroutine.create(function()
Private.Pause();
coroutine.yield(100)
if db.history then
local histRepo = WeakAuras.LoadFromArchive("Repository", "history")
local migrationRepo = WeakAuras.LoadFromArchive("Repository", "migration")
for uid, hist in pairs(db.history) do
local histStore = histRepo:Set(uid, hist.data)
local migrationStore = migrationRepo:Set(uid, hist.migration)
coroutine.yield(1000, "login move old history")
end
-- history is now in archive so we can shrink WeakAurasSaved
db.history = nil
end
Private.Features:Hydrate()
coroutine.yield(3000, "login check uid corruption")
local toAdd = {};
loginFinished = false
loginMessage = L["Options will open after the login process has completed."]
for id, data in pairs(db.displays) do
if(id ~= data.id) then
print("|cFF8800FFWeakAuras|r detected a corrupt entry in WeakAuras saved displays - '"..tostring(id).."' vs '"..tostring(data.id).."'" );
data.id = id;
end
tinsert(toAdd, data);
end
coroutine.yield(8000);
Private.AddMany(toAdd, takeNewSnapshots);
coroutine.yield(1000);
-- check in case of a disconnect during an encounter.
if (db.CurrentEncounter) then
CheckForPreviousEncounter()
end
coroutine.yield(1000);
Private.RegisterLoadEvents();
coroutine.yield(10000);
Private.Resume();
coroutine.yield(100);
local nextCallback = loginQueue[1];
while nextCallback do
tremove(loginQueue, 1);
if type(nextCallback) == 'table' then
nextCallback[1](unpack(nextCallback[2]))
else
nextCallback()
end
coroutine.yield(1000, "login post login callbacks");
nextCallback = loginQueue[1];
end
loginFinished = true
-- Tell Dynamic Groups that we are done with login
for _, region in pairs(Private.regions) do
if (region.region and region.region.RunDelayedActions) then
region.region:RunDelayedActions();
coroutine.yield(500, "login delayed region actions");
end
end
end)
Private.Threads:Immediate('login', loginThread, 15000, 1000)
end
local WeakAurasFrame = CreateFrame("Frame", "WeakAurasFrame", UIParent);
Private.frames["WeakAuras Main Frame"] = WeakAurasFrame;
WeakAurasFrame:SetAllPoints(UIParent);
local loadedFrame = CreateFrame("Frame");
Private.frames["Addon Initialization Handler"] = loadedFrame;
loadedFrame:RegisterEvent("ADDON_LOADED");
loadedFrame:RegisterEvent("PLAYER_LOGIN");
loadedFrame:RegisterEvent("PLAYER_LOGOUT")
loadedFrame:RegisterEvent("PLAYER_ENTERING_WORLD");
loadedFrame:RegisterEvent("LOADING_SCREEN_ENABLED");
loadedFrame:RegisterEvent("LOADING_SCREEN_DISABLED");
if WeakAuras.IsRetail() then
loadedFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED");
loadedFrame:RegisterEvent("PLAYER_PVP_TALENT_UPDATE");
else
loadedFrame:RegisterEvent("CHARACTER_POINTS_CHANGED");
loadedFrame:RegisterEvent("SPELLS_CHANGED");
end
loadedFrame:SetScript("OnEvent", function(self, event, addon)
if(event == "ADDON_LOADED") then
if(addon == ADDON_NAME) then
---@type WeakAurasSaved
WeakAurasSaved = WeakAurasSaved or {};
db = WeakAurasSaved;
Private.db = db
-- Defines the action squelch period after login
-- Stored in SavedVariables so it can be changed by the user if they find it necessary
db.login_squelch_time = db.login_squelch_time or 10;
-- Deprecated fields with *lots* of data, clear them out
db.iconCache = nil;
db.iconHash = nil;
db.tempIconCache = nil;
db.dynamicIconCache = db.dynamicIconCache or {};
db.displays = db.displays or {};
db.registered = db.registered or {};
db.features = db.features or {}
db.migrationCutoff = db.migrationCutoff or 730
db.historyCutoff = db.historyCutoff or 730
Private.UpdateCurrentInstanceType();
Private.SyncParentChildRelationships();
local isFirstUIDValidation = db.dbVersion == nil or db.dbVersion < 26;
Private.ValidateUniqueDataIds(isFirstUIDValidation);
if db.lastArchiveClear == nil then
db.lastArchiveClear = time();
elseif db.lastArchiveClear < time() - 86400 then
Private.CleanArchive(db.historyCutoff, db.migrationCutoff);
end
db.minimap = db.minimap or { hide = false };
LDBIcon:Register("WeakAuras", Broker_WeakAuras, db.minimap);
end
elseif(event == "PLAYER_LOGIN") then
local dbIsValid, takeNewSnapshots
if not db.dbVersion or db.dbVersion < internalVersion then
-- db is out of date, will run any necessary migrations in AddMany
db.dbVersion = internalVersion
db.lastUpgrade = time()
dbIsValid = true
takeNewSnapshots = true
elseif db.dbVersion > internalVersion then
-- user has downgraded past a forwards-incompatible migration
dbIsValid = false
else
-- db has same version as code, can commit to login
dbIsValid = true
end
if dbIsValid then
Private.Login(takeNewSnapshots)
else
-- db isn't valid. Request permission to run repair tool before logging in
StaticPopup_Show("WEAKAURAS_CONFIRM_REPAIR", nil, nil, {reason = "downgrade"})
end
elseif event == "PLAYER_LOGOUT" then
for id in pairs(db.displays) do
Private.ClearAuraEnvironment(id)
end
elseif(event == "LOADING_SCREEN_ENABLED") then
in_loading_screen = true;
elseif(event == "LOADING_SCREEN_DISABLED") then
in_loading_screen = false;
else
local callback
if(event == "PLAYER_ENTERING_WORLD") then
-- Schedule events that need to be handled some time after login
local now = GetTime()
callback = function()
local elapsed = GetTime() - now
local remainingSquelch = db.login_squelch_time - elapsed
if remainingSquelch > 0 then
timer:ScheduleTimer(function() squelch_actions = false; end, remainingSquelch); -- No sounds while loading
end
CreateTalentCache() -- It seems that GetTalentInfo might give info about whatever class was previously being played, until PLAYER_ENTERING_WORLD
Private.UpdateCurrentInstanceType();
Private.InitializeEncounterAndZoneLists()
end
Private.PostAddCompanion()
elseif(event == "PLAYER_PVP_TALENT_UPDATE") then
callback = CreatePvPTalentCache;
elseif(event == "ACTIVE_TALENT_GROUP_CHANGED" or event == "CHARACTER_POINTS_CHANGED" or event == "SPELLS_CHANGED") then
callback = CreateTalentCache;
elseif(event == "PLAYER_REGEN_ENABLED") then
callback = function()
if (queueshowooc) then
WeakAuras.OpenOptions(queueshowooc)
queueshowooc = nil
Private.frames["Addon Initialization Handler"]:UnregisterEvent("PLAYER_REGEN_ENABLED")
end
end
end
if WeakAuras.IsLoginFinished() then
callback()
else
loginQueue[#loginQueue + 1] = callback
end
end
end)
function Private.SetImporting(b)
importing = b;
end
function WeakAuras.IsImporting()
return importing;
end
function WeakAuras.IsPaused()
return paused;
end
function Private.Pause()
for id, states in pairs(triggerState) do
local changed
for triggernum in ipairs(states) do
changed = Private.SetAllStatesHidden(id, triggernum) or changed
end
if changed then
Private.UpdatedTriggerState(id)
end
end
paused = true;
end
function WeakAuras.Toggle()
if(paused) then
Private.Resume();
else
Private.Pause();
end
end
function Private.SquelchingActions()
return squelch_actions;
end
function WeakAuras.InLoadingScreen()
return in_loading_screen;
end
function Private.PauseAllDynamicGroups()
local suspended = {}
for id, region in pairs(Private.regions) do
if (region.region and region.region.Suspend) then
region.region:Suspend();
tinsert(suspended, id)
end
end
return suspended
end
function Private.ResumeAllDynamicGroups(suspended)
for _, id in ipairs(suspended) do
local region = WeakAuras.GetRegion(id)
if (region and region.Resume) then
region:Resume();
end
end
end
-- Encounter stuff
local function StoreBossGUIDs()
Private.StartProfileSystem("boss_guids")
if (WeakAuras.CurrentEncounter and WeakAuras.CurrentEncounter.boss_guids) then
for i = 1, 10 do
if (UnitExists ("boss" .. i)) then
local guid = UnitGUID ("boss" .. i)
if (guid) then
WeakAuras.CurrentEncounter.boss_guids [guid] = true
end
end
end
db.CurrentEncounter = WeakAuras.CurrentEncounter
end
Private.StopProfileSystem("boss_guids")
end
local function DestroyEncounterTable()
if (WeakAuras.CurrentEncounter) then
wipe(WeakAuras.CurrentEncounter)
end
WeakAuras.CurrentEncounter = nil
db.CurrentEncounter = nil
end
local function CreateEncounterTable(encounter_id)
local _, _, _, _, _, _, _, instanceId = GetInstanceInfo()
---@class CurrentEncounter
---@field encounterId number
---@field zone_id number
---@field boss_guids number[]
WeakAuras.CurrentEncounter = {
id = encounter_id,
zone_id = instanceId,
boss_guids = {},
}
timer:ScheduleTimer(StoreBossGUIDs, 2)
return WeakAuras.CurrentEncounter
end
local encounterScriptsDeferred = {}
local function LoadEncounterInitScriptsImpl(id)
if (currentInstanceType ~= "raid") then
return
end
if (id) then
local data = db.displays[id]
if (data and data.load.use_encounterid and not Private.IsEnvironmentInitialized(id) and data.actions.init and data.actions.init.do_custom) then
Private.ActivateAuraEnvironment(id)
Private.ActivateAuraEnvironment(nil)
end
encounterScriptsDeferred[id] = nil
else
for id, data in pairs(db.displays) do
if (data.load.use_encounterid and not Private.IsEnvironmentInitialized(id) and data.actions.init and data.actions.init.do_custom) then
Private.ActivateAuraEnvironment(id)
Private.ActivateAuraEnvironment(nil)
end
end
end
end
local function LoadEncounterInitScripts(id)
if not WeakAuras.IsLoginFinished() then
if encounterScriptsDeferred[id] then
return
end
loginQueue[#loginQueue + 1] = {LoadEncounterInitScriptsImpl, {id}}
encounterScriptsDeferred[id] = true
return
end
LoadEncounterInitScriptsImpl(id)
end
function Private.UpdateCurrentInstanceType(instanceType)
if (not IsInInstance()) then
currentInstanceType = "none"
else
currentInstanceType = instanceType or select (2, GetInstanceInfo())
end
end
local pausedOptionsProcessing = false;
function Private.pauseOptionsProcessing(enable)
pausedOptionsProcessing = enable;
end
function Private.IsOptionsProcessingPaused()
return pausedOptionsProcessing;
end
function Private.ExecEnv.GroupType()
if (IsInRaid()) then
return "raid";
end
if (IsInGroup()) then
return "group";
end
return "solo";
end
local function GetInstanceTypeAndSize()
local size, difficulty
local inInstance, Type = IsInInstance()
local _, instanceType, difficultyIndex, _, _, _, _, instanceId = GetInstanceInfo()
if (inInstance) then
size = Type
local difficultyInfo = Private.difficulty_info[difficultyIndex]
if difficultyInfo then
size, difficulty = difficultyInfo.size, difficultyInfo.difficulty
else
if WeakAuras.IsRetail() then
if size == "arena" then
if C_PvP.IsRatedArena() and not IsArenaSkirmish() then
size = "ratedarena"
end
elseif size == "pvp" then
if C_PvP.IsRatedBattleground() then
size = "ratedpvp"
end
end
end
end
return size, difficulty, instanceType, instanceId, difficultyIndex
end
return "none", "none", nil, nil, nil
end
---@return string instanceType
function WeakAuras.InstanceType()
return (GetInstanceTypeAndSize())
end
---@return string difficulty
function WeakAuras.InstanceDifficulty()
return select(2, GetInstanceTypeAndSize())
end
---@return number? difficultyID
function WeakAuras.InstanceTypeRaw()
return select(5, GetInstanceTypeAndSize())
end
local toLoad = {}
local toUnload = {};
local function scanForLoadsImpl(toCheck, event, arg1, ...)
if (Private.IsOptionsProcessingPaused()) then
return;
end
toCheck = toCheck or loadEvents[event or "SCAN_ALL"]
-- PET_BATTLE_CLOSE fires twice at the end of a pet battle. IsInBattle evaluates to TRUE during the
-- first firing, and FALSE during the second. I am not sure if this check is necessary, but the
-- following IF statement limits the impact of the PET_BATTLE_CLOSE event to the second one.
if (event == "PET_BATTLE_CLOSE" and C_PetBattles.IsInBattle()) then return end
if (event == "PLAYER_LEVEL_UP") then
playerLevel = arg1;
end
-- encounter id stuff, we are holding the current combat id to further load checks.
-- there is three ways to unload: encounter_end / zone changed (hearthstone used) / reload or disconnect
-- regen_enabled isn't good due to combat drop abilities such invisibility, vanish, fake death, etc.
local encounter_id = WeakAuras.CurrentEncounter and WeakAuras.CurrentEncounter.id or 0
if (event == "ENCOUNTER_START") then
encounter_id = tonumber(arg1)
CreateEncounterTable(encounter_id)
elseif (event == "ENCOUNTER_END") then
encounter_id = 0
DestroyEncounterTable()
end
if toCheck == nil or next(toCheck) == nil then
return
end
local player, realm, zone = UnitName("player"), GetRealmName(), GetRealZoneText()
--- @type boolean|number|nil, boolean|string|nil, boolean|string|nil, boolean|string
local specId, role, position, raidRole = false, false, false, false
--- @type boolean, boolean, boolean
local inPetBattle, vehicle, vehicleUi = false, false, false
--- @type boolean
local dragonriding
local zoneId = C_Map.GetBestMapForUnit("player")
local zonegroupId = zoneId and C_Map.GetMapGroupID(zoneId)
local minimapText = GetMinimapZoneText()
local _, race = UnitRace("player")
local faction = UnitFactionGroup("player")
local _, class = UnitClass("player")
local inCombat = UnitAffectingCombat("player")
--- @type boolean
local inEncounter = encounter_id ~= 0;
local alive = not UnitIsDeadOrGhost('player')
local raidMemberType = 0
if UnitIsGroupLeader("player") then
raidMemberType = 1
end
if UnitIsGroupAssistant("player") then
raidMemberType = raidMemberType + 2
end
if WeakAuras.IsClassicOrCata() then
local raidID = UnitInRaid("player")
if raidID then
raidRole = select(10, GetRaidRosterInfo(raidID))
end
role = "none"
if WeakAuras.IsCataClassic() then
vehicle = UnitInVehicle('player') or UnitOnTaxi('player') or false
vehicleUi = UnitHasVehicleUI('player') or HasOverrideActionBar() or HasVehicleActionBar() or false
else
vehicle = UnitOnTaxi('player')
end
else
dragonriding = Private.IsDragonriding()
inPetBattle = C_PetBattles.IsInBattle()
vehicle = UnitInVehicle('player') or UnitOnTaxi('player') or false
vehicleUi = UnitHasVehicleUI('player') or HasOverrideActionBar() or HasVehicleActionBar() or false
end
if WeakAuras.IsCataOrRetail() then
specId, role, position = Private.LibSpecWrapper.SpecRolePositionForUnit("player")
end
local size, difficulty, instanceType, instanceId, difficultyIndex= GetInstanceTypeAndSize()
Private.UpdateCurrentInstanceType(instanceType)
if (WeakAuras.CurrentEncounter) then
if (instanceId ~= WeakAuras.CurrentEncounter.zone_id and not inCombat) then
encounter_id = 0
DestroyEncounterTable()
end
end
if (event == "ZONE_CHANGED_NEW_AREA") then
LoadEncounterInitScripts();
end
local group = Private.ExecEnv.GroupType()
local groupSize = GetNumGroupMembers()
local affixes, warmodeActive, effectiveLevel = 0, false, 0
if WeakAuras.IsRetail() then
effectiveLevel = UnitEffectiveLevel("player")
affixes = C_ChallengeMode.IsChallengeModeActive() and select(2, C_ChallengeMode.GetActiveKeystoneInfo())
warmodeActive = C_PvP.IsWarModeDesired();
end
local changed = 0;
local shouldBeLoaded, couldBeLoaded;
local parentsToCheck = {}
wipe(toLoad);
wipe(toUnload);
for id in pairs(toCheck) do
local data = WeakAuras.GetData(id)
if (data and not data.controlledChildren) then
local loadFunc = loadFuncs[id];
local loadOpt = loadFuncsForOptions[id];
if WeakAuras.IsClassicEra() then
shouldBeLoaded = loadFunc and loadFunc("ScanForLoads_Auras", inCombat, alive, inEncounter, vehicle, class, player, realm, race, faction, playerLevel, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size)
couldBeLoaded = loadOpt and loadOpt("ScanForLoads_Auras", inCombat, alive, inEncounter, vehicle, class, player, realm, race, faction, playerLevel, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size)
elseif WeakAuras.IsCataClassic() then
shouldBeLoaded = loadFunc and loadFunc("ScanForLoads_Auras", inCombat, alive, inEncounter, vehicle, vehicleUi, class, specId, player, realm, race, faction, playerLevel, role, position, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex)
couldBeLoaded = loadOpt and loadOpt("ScanForLoads_Auras", inCombat, alive, inEncounter, vehicle, vehicleUi, class, specId, player, realm, race, faction, playerLevel, role, position, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex)
elseif WeakAuras.IsRetail() then
shouldBeLoaded = loadFunc and loadFunc("ScanForLoads_Auras", inCombat, alive, inEncounter, warmodeActive, inPetBattle, vehicle, vehicleUi, dragonriding, specId, player, realm, race, faction, playerLevel, effectiveLevel, role, position, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex, affixes)
couldBeLoaded = loadOpt and loadOpt("ScanForLoads_Auras", inCombat, alive, inEncounter, warmodeActive, inPetBattle, vehicle, vehicleUi, dragonriding, specId, player, realm, race, faction, playerLevel, effectiveLevel, role, position, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex, affixes)
end
if(shouldBeLoaded and not loaded[id]) then
changed = changed + 1;
toLoad[id] = true;
Private.EnsureRegion(id)
for parent in Private.TraverseParents(data) do
parentsToCheck[parent.id] = true
end
end
if(loaded[id] and not shouldBeLoaded) then
toUnload[id] = true;
changed = changed + 1;
for parent in Private.TraverseParents(data) do
parentsToCheck[parent.id] = true
end
end
if(shouldBeLoaded) then
loaded[id] = true;
elseif(couldBeLoaded) then
loaded[id] = false;
else
loaded[id] = nil;
end
end
end
if(changed > 0 and not paused) then
Private.LoadDisplays(toLoad, event, arg1, ...);
Private.UnloadDisplays(toUnload, event, arg1, ...);
Private.FinishLoadUnload();
end
Private.ScanForLoadsGroup(parentsToCheck)
Private.callbacks:Fire("ScanForLoads")
wipe(toLoad);
wipe(toUnload)
end
function Private.ScanForLoadsGroup(toCheck)
for id in pairs(toCheck) do
local data = WeakAuras.GetData(id)
if(data.controlledChildren) then
if(#data.controlledChildren > 0) then
---@type boolean?
local any_loaded = false;
for child in Private.TraverseLeafs(data) do
if(loaded[child.id] ~= nil) then
any_loaded = true;
break;
else
any_loaded = nil
end
end
if any_loaded then
Private.EnsureRegion(id)
end
loaded[id] = any_loaded;
else
Private.EnsureRegion(id)
loaded[id] = true;
end
end
end
end
function Private.ScanForLoads(toCheck, event, arg1, ...)
if not WeakAuras.IsLoginFinished() then
return
end
scanForLoadsImpl(toCheck, event, arg1, ...)
end
local loadFrame = CreateFrame("Frame");
Private.frames["Display Load Handling"] = loadFrame;
loadFrame:RegisterEvent("ENCOUNTER_START");
loadFrame:RegisterEvent("ENCOUNTER_END");
if WeakAuras.IsRetail() then
loadFrame:RegisterEvent("PLAYER_TALENT_UPDATE");
loadFrame:RegisterEvent("PLAYER_PVP_TALENT_UPDATE");
loadFrame:RegisterEvent("PLAYER_DIFFICULTY_CHANGED");
loadFrame:RegisterEvent("PET_BATTLE_OPENING_START");
loadFrame:RegisterEvent("PET_BATTLE_CLOSE");
loadFrame:RegisterEvent("VEHICLE_UPDATE");
loadFrame:RegisterEvent("UPDATE_VEHICLE_ACTIONBAR")
loadFrame:RegisterEvent("UPDATE_OVERRIDE_ACTIONBAR");
loadFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
loadFrame:RegisterEvent("CHALLENGE_MODE_START")
loadFrame:RegisterEvent("TRAIT_CONFIG_CREATED")
loadFrame:RegisterEvent("TRAIT_CONFIG_UPDATED")
else
loadFrame:RegisterEvent("CHARACTER_POINTS_CHANGED")
loadFrame:RegisterEvent("PLAYER_TALENT_UPDATE");
loadFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED");
end
if WeakAuras.IsCataClassic() then
loadFrame:RegisterEvent("VEHICLE_UPDATE");
loadFrame:RegisterEvent("UPDATE_VEHICLE_ACTIONBAR")
loadFrame:RegisterEvent("UPDATE_OVERRIDE_ACTIONBAR");
end
loadFrame:RegisterEvent("GROUP_ROSTER_UPDATE");
loadFrame:RegisterEvent("ZONE_CHANGED");
loadFrame:RegisterEvent("ZONE_CHANGED_INDOORS");
loadFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA");
loadFrame:RegisterEvent("PLAYER_LEVEL_UP");
loadFrame:RegisterEvent("PLAYER_REGEN_DISABLED");
loadFrame:RegisterEvent("PLAYER_REGEN_ENABLED");
loadFrame:RegisterEvent("PLAYER_ROLES_ASSIGNED");
loadFrame:RegisterEvent("SPELLS_CHANGED");
loadFrame:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player")
loadFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
loadFrame:RegisterEvent("PLAYER_DEAD")
loadFrame:RegisterEvent("PLAYER_ALIVE")
loadFrame:RegisterEvent("PLAYER_UNGHOST")
loadFrame:RegisterEvent("PARTY_LEADER_CHANGED")
if WeakAuras.IsRetail() then
Private.callbacks:RegisterCallback("WA_DRAGONRIDING_UPDATE", function ()
Private.StartProfileSystem("load");
Private.ScanForLoads(nil, "WA_DRAGONRIDING_UPDATE")
Private.StopProfileSystem("load");
end)
end
local unitLoadFrame = CreateFrame("Frame");
Private.frames["Display Load Handling 2"] = unitLoadFrame;
unitLoadFrame:RegisterUnitEvent("UNIT_FLAGS", "player");
if WeakAuras.IsCataOrRetail() then
unitLoadFrame:RegisterUnitEvent("UNIT_ENTERED_VEHICLE", "player");
unitLoadFrame:RegisterUnitEvent("UNIT_EXITED_VEHICLE", "player");
unitLoadFrame:RegisterUnitEvent("PLAYER_FLAGS_CHANGED", "player");
end
function Private.RegisterLoadEvents()
loadFrame:SetScript("OnEvent", function(frame, ...)
Private.StartProfileSystem("load");
Private.ScanForLoads(nil, ...)
Private.StopProfileSystem("load");
end);
C_Timer.NewTicker(0.5, function()
Private.StartProfileSystem("load");
local zoneId = C_Map.GetBestMapForUnit("player");
if loadFrame.zoneId ~= zoneId then
Private.ScanForLoads(nil, "ZONE_CHANGED")
loadFrame.zoneId = zoneId;
end
Private.StopProfileSystem("load");
end)
unitLoadFrame:SetScript("OnEvent", function(frame, e, arg1, ...)
Private.StartProfileSystem("load");
if (arg1 == "player") then
Private.ScanForLoads(nil, e, arg1, ...)
end
Private.StopProfileSystem("load");
end);
end
local function UnloadAll()
-- Even though auras are collapsed, their finish animation can be running
for id in pairs(loaded) do
if Private.regions[id] and Private.regions[id].region then
Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
end
if clones[id] then
for cloneId, region in pairs(clones[id]) do
Private.CancelAnimation(region, true, true, true, true, true, true)
end
end
end
for _, v in pairs(triggerState) do
for i = 1, v.numTriggers do
if (v[i]) then
wipe(v[i]);
end
end
end
for _, aura in pairs(timers) do
for _, trigger in pairs(aura) do
for _, record in pairs(trigger) do
if (record.handle) then
timer:CancelTimer(record.handle);
end
end
end
end
wipe(timers);
Private.UnloadAllConditions()
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.UnloadAll();
end
wipe(loaded);
end
function Private.Resume()
paused = false;
local suspended = Private.PauseAllDynamicGroups()
for id, region in pairs(Private.regions) do
if region.region then
region.region:Collapse();
end
end
for id, cloneList in pairs(clones) do
for cloneId, clone in pairs(cloneList) do
clone:Collapse();
end
end
UnloadAll();
scanForLoadsImpl();
if loadEvents["GROUP"] then
Private.ScanForLoadsGroup(loadEvents["GROUP"])
end
Private.ResumeAllDynamicGroups(suspended)
end
function Private.LoadDisplays(toLoad, ...)
for id in pairs(toLoad) do
local uid = WeakAuras.GetData(id).uid
Private.RegisterForGlobalConditions(uid);
triggerState[id].triggers = {};
triggerState[id].triggerCount = 0;
triggerState[id].show = false;
triggerState[id].activeTrigger = nil;
triggerState[id].activatedConditions = {};
if Private.DebugLog.IsEnabled(uid) then
WeakAuras.prettyPrint(L["Debug Logging enabled for '%s'"]:format(id))
Private.DebugLog.Print(uid, L["Aura loaded"])
end
end
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.LoadDisplays(toLoad, ...);
end
end
function Private.UnloadDisplays(toUnload, ...)
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.UnloadDisplays(toUnload, ...);
end
for id in pairs(toUnload) do
for i = 1, triggerState[id].numTriggers do
if (triggerState[id][i]) then
wipe(triggerState[id][i]);
end
end
triggerState[id].show = nil;
triggerState[id].activeTrigger = nil;
if (timers[id]) then
for _, trigger in pairs(timers[id]) do
for _, record in pairs(trigger) do
if (record.handle) then
timer:CancelTimer(record.handle);
end
end
end
timers[id] = nil;
end
local uid = WeakAuras.GetData(id).uid
Private.UnloadConditions(uid)
Private.regions[id].region:Collapse();
Private.CollapseAllClones(id);
-- Even though auras are collapsed, their finish animation can be running
Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
if clones[id] then
for _, region in pairs(clones[id]) do
Private.CancelAnimation(region, true, true, true, true, true, true)
end
end
end
end
function Private.FinishLoadUnload()
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.FinishLoadUnload();
end
end
-- transient cache of uid => id
-- eventually, the database will be migrated to index by uid
-- and this mapping will become redundant
-- this cache is loaded lazily via pAdd()
local UIDtoID = {}
function Private.GetDataByUID(uid)
return WeakAuras.GetData(UIDtoID[uid])
end
function Private.UIDtoID(uid)
return UIDtoID[uid]
end
---@private
function WeakAuras.Delete(data)
local id = data.id;
local uid = data.uid
local parentId = data.parent
local parentUid = data.parent and db.displays[data.parent].uid
Private.callbacks:Fire("AboutToDelete", uid, id, parentUid, parentId)
if(data.parent) then
local parentData = db.displays[data.parent];
if(parentData and parentData.controlledChildren) then
for index, childId in pairs(parentData.controlledChildren) do
if(childId == id) then
tremove(parentData.controlledChildren, index);
end
end
if parentData.sortHybridTable then
parentData.sortHybridTable[id] = nil
end
for parent in Private.TraverseParents(data) do
Private.ClearAuraEnvironment(parent.id);
end
end
end
UIDtoID[data.uid] = nil
if(data.controlledChildren) then
for _, childId in pairs(data.controlledChildren) do
local childData = db.displays[childId];
if(childData) then
childData.parent = nil;
WeakAuras.Add(childData);
end
end
end
if Private.regions[id] and Private.regions[id].region then
Private.regions[id].region:Collapse()
Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
-- Groups have a empty Collapse method so, we need to hide them here
Private.regions[id].region:Hide();
Private.regions[id].region = nil
Private.regions[id] = nil
end
if clones[id] then
for _, region in pairs(clones[id]) do
region:Collapse();
Private.CancelAnimation(region, true, true, true, true, true, true)
end
clones[id] = nil
end
db.registered[id] = nil;
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.Delete(id);
end
loaded[id] = nil;
loadFuncs[id] = nil;
loadFuncsForOptions[id] = nil;
for event, eventData in pairs(loadEvents) do
eventData[id] = nil
end
db.displays[id] = nil;
Private.DeleteAuraEnvironment(id)
triggerState[id] = nil;
if (Private.personalRessourceDisplayFrame) then
Private.personalRessourceDisplayFrame:delete(id);
end
if (Private.mouseFrame) then
Private.mouseFrame:delete(id);
end
Private.customActionsFunctions[id] = nil;
Private.ExecEnv.customConditionsFunctions[id] = nil;
Private.ExecEnv.conditionTextFormatters[id] = nil
Private.frameLevels[id] = nil;
Private.ExecEnv.conditionHelpers[data.uid] = nil
Private.RemoveHistory(data.uid)
Private.AddParents(data)
Private.callbacks:Fire("Delete", uid, id, parentUid, parentId)
end
function WeakAuras.Rename(data, newid)
local oldid = data.id
if(data.parent) then
local parentData = db.displays[data.parent];
if(parentData.controlledChildren) then
for index, childId in pairs(parentData.controlledChildren) do
if(childId == data.id) then
parentData.controlledChildren[index] = newid;
end
end
if parentData.sortHybridTable and parentData.sortHybridTable[oldid] then
parentData.sortHybridTable[newid] = true
parentData.sortHybridTable[oldid] = nil
end
end
local parentRegion = WeakAuras.GetRegion(data.parent)
if parentRegion and parentRegion.ReloadControlledChildren then
parentRegion:ReloadControlledChildren()
end
end
UIDtoID[data.uid] = newid
Private.regions[newid] = Private.regions[oldid];
Private.regions[oldid] = nil;
if Private.regions[newid] and Private.regions[newid].region then
Private.regions[newid].region.id = newid
end
if(clones[oldid]) then
clones[newid] = clones[oldid]
clones[oldid] = nil
for cloneid, clone in pairs(clones[newid]) do
clone.id = newid
end
end
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.Rename(oldid, newid);
end
loaded[newid] = loaded[oldid];
loaded[oldid] = nil;
loadFuncs[newid] = loadFuncs[oldid];
loadFuncs[oldid] = nil;
loadFuncsForOptions[newid] = loadFuncsForOptions[oldid]
loadFuncsForOptions[oldid] = nil;
for event, eventData in pairs(loadEvents) do
eventData[newid] = eventData[oldid]
eventData[oldid] = nil
end
timers[newid] = timers[oldid];
timers[oldid] = nil;
triggerState[newid] = triggerState[oldid];
triggerState[oldid] = nil;
Private.RenameAuraEnvironment(oldid, newid)
db.displays[newid] = db.displays[oldid];
db.displays[oldid] = nil;
db.displays[newid].id = newid;
if(data.controlledChildren) then
for index, childId in pairs(data.controlledChildren) do
local childData = db.displays[childId];
if(childData) then
childData.parent = data.id;
end
end
end
if (Private.personalRessourceDisplayFrame) then
Private.personalRessourceDisplayFrame:rename(oldid, newid);
end
if (Private.mouseFrame) then
Private.mouseFrame:rename(oldid, newid);
end
Private.customActionsFunctions[newid] = Private.customActionsFunctions[oldid];
Private.customActionsFunctions[oldid] = nil;
Private.ExecEnv.customConditionsFunctions[newid] = Private.ExecEnv.customConditionsFunctions[oldid];
Private.ExecEnv.customConditionsFunctions[oldid] = nil;
Private.ExecEnv.conditionTextFormatters[newid] = Private.ExecEnv.conditionTextFormatters[oldid]
Private.ExecEnv.conditionTextFormatters[oldid] = nil
Private.frameLevels[newid] = Private.frameLevels[oldid];
Private.frameLevels[oldid] = nil;
Private.ProfileRenameAura(oldid, newid);
-- TODO: This should not be necessary
WeakAuras.Add(data)
Private.callbacks:Fire("Rename", data.uid, oldid, newid)
end
function Private.Convert(data, newType)
local id = data.id;
Private.FakeStatesFor(id, false)
if Private.regions[id] then
Private.regions[id].region = nil
Private.regions[id] = nil
end
data.regionType = newType;
-- Clean up sub regions
if data.subRegions then
for index, subRegionData in ipairs_reverse(data.subRegions) do
local subType = subRegionData.type
local removeSubRegion = true
if subType and Private.subRegionTypes[subType] then
if Private.subRegionTypes[subType].supports(data.regionType) then
removeSubRegion = false
end
end
if removeSubRegion then
tremove(data.subRegions, index)
-- Adjust conditions!
if data.conditions then
for _, condition in ipairs(data.conditions) do
if type(condition.changes) == "table" then
for _, change in ipairs(condition.changes) do
if change.property then
local subRegionIndex, property = change.property:match("^sub%.(%d+)%.(.*)")
subRegionIndex = tonumber(subRegionIndex)
if subRegionIndex and property then
if subRegionIndex == index then
change.property = nil
elseif subRegionIndex > index then
change.property = "sub." .. subRegionIndex -1 .. "." .. property
end
end
end
end
end
end
end
end
end
end
WeakAuras.Add(data);
Private.FakeStatesFor(id, true)
local parentRegion = WeakAuras.GetRegion(data.parent)
if parentRegion and parentRegion.ReloadControlledChildren then
parentRegion:ReloadControlledChildren()
end
end
-- The default mixin doesn't recurse, this does
function WeakAuras.DeepMixin(dest, source)
local function recurse(source, dest)
for i,v in pairs(source) do
if(type(v) == "table") then
dest[i] = type(dest[i]) == "table" and dest[i] or {};
recurse(v, dest[i]);
else
dest[i] = v;
end
end
end
recurse(source, dest);
end
local function LastUpgrade()
return db.lastUpgrade and date(nil, db.lastUpgrade) or "unknown"
end
function Private.NeedToRepairDatabase()
return db.dbVersion and db.dbVersion > WeakAuras.InternalVersion()
end
local function RepairDatabase()
local coro = coroutine.create(function()
Private.SetImporting(true)
-- set db version to current code version
db.dbVersion = WeakAuras.InternalVersion()
-- reinstall snapshots from history
local newDB = Mixin({}, db.displays)
coroutine.yield(1000)
for id, data in pairs(db.displays) do
local snapshot = Private.GetMigrationSnapshot(data.uid)
if snapshot then
newDB[id] = nil
newDB[snapshot.id] = snapshot
coroutine.yield(1000, "repair get snapshot")
end
end
db.displays = newDB
Private.SetImporting(false)
-- finally, login
Private.Login()
end)
Private.Threads:Add("repair", coro, 'urgent')
end
StaticPopupDialogs["WEAKAURAS_CONFIRM_REPAIR"] = {
text = "",
button1 = L["Repair"],
button2 = L["Cancel"],
OnAccept = function(self)
RepairDatabase()
end,
OnShow = function(self)
local AutomaticRepairText = L["WeakAuras has detected that it has been downgraded.\nYour saved auras may no longer work properly.\nWould you like to run the |cffff0000EXPERIMENTAL|r repair tool? This will overwrite any changes you have made since the last database upgrade.\nLast upgrade: %s\n\n|cffff0000You should BACKUP your WTF folder BEFORE pressing this button.|r"]
local ManualRepairText = L["Are you sure you want to run the |cffff0000EXPERIMENTAL|r repair tool?\nThis will overwrite any changes you have made since the last database upgrade.\nLast upgrade: %s"]
if self.data.reason == "user" then
self.text:SetText(ManualRepairText:format(LastUpgrade()))
else
self.text:SetText(AutomaticRepairText:format(LastUpgrade()))
end
end,
OnCancel = function(self)
if self.data.reason ~= "user" then
Private.Login()
end
end,
whileDead = true,
showAlert = true,
timeout = 0,
preferredindex = STATICPOPUP_NUMDIALOGS
}
function Private.ValidateUniqueDataIds(silent)
-- ensure that there are no duplicated uids anywhere in the database
local seenUIDs = {}
for _, data in pairs(db.displays) do
if type(data.uid) == "string" then
if seenUIDs[data.uid] then
if not silent then
prettyPrint("Duplicate uid \""..data.uid.."\" detected in saved variables between \""..data.id.."\" and \""..seenUIDs[data.uid].id.."\".")
end
data.uid = WeakAuras.GenerateUniqueID()
seenUIDs[data.uid] = data
else
seenUIDs[data.uid] = data
end
elseif data.uid ~= nil then
if not silent then
prettyPrint("Invalid uid detected in saved variables for \""..data.id.."\"")
end
data.uid = WeakAuras.GenerateUniqueID()
seenUIDs[data.uid] = data
end
end
for uid, data in pairs(seenUIDs) do
UIDtoID[uid] = data.id
end
end
function Private.SyncParentChildRelationships(silent)
-- 1. Find all auras where data.parent ~= nil or data.controlledChildren ~= nil
-- If an aura has data.parent which doesn't exist, then remove data.parent
-- If an aura has data.parent which doesn't have data.controlledChildren, then remove data.parent
-- 2. For each aura with data.controlledChildren, iterate through the list of children and remove entries where:
-- The child doesn't exist in the database
-- The child ID is duplicated in data.controlledChildren (only the first will be kept)
-- The child's data.parent points to a different parent
-- The parent is a dynamic group and the child is a group/dynamic group
-- Otherwise, mark the child as having a valid parent relationship
-- 3. For each aura with data.parent, remove data.parent if it was not marked to have a valid relationship in 2.
local parents = {}
local children = {}
local childHasParent = {}
for id, data in pairs(db.displays) do
if data.parent then
if not db.displays[data.parent] then
if not(silent) then
prettyPrint("Detected corruption in saved variables: "..id.." has a nonexistent parent.")
end
data.parent = nil
elseif not db.displays[data.parent].controlledChildren then
if not silent then
prettyPrint("Detected corruption in saved variables: "..id.." thinks "..data.parent..
" controls it, but "..data.parent.." is not a group.")
end
data.parent = nil
else
children[id] = data
end
end
if data.controlledChildren then
parents[id] = data
end
end
for id, data in pairs(parents) do
local groupChildren = {}
local childrenToRemove = {}
local dynamicGroup = data.regionType == "dynamicgroup"
for index, childID in ipairs(data.controlledChildren) do
local child = children[childID]
if not child then
if not silent then
prettyPrint("Detected corruption in saved variables: "..id.." thinks it controls "..childID.." which doesn't exist.")
end
childrenToRemove[index] = true
elseif child.parent ~= id then
if not silent then
prettyPrint("Detected corruption in saved variables: "..id.." thinks it controls "..childID.." which it does not.")
end
childrenToRemove[index] = true
elseif dynamicGroup and child.controlledChildren then
if not silent then
prettyPrint("Detected corruption in saved variables: "..id.." is a dynamic group and controls "..childID.." which is a group/dynamicgroup.")
end
child.parent = nil
children[child.id] = nil
childrenToRemove[index] = true
elseif groupChildren[childID] then
if not silent then
prettyPrint("Detected corruption in saved variables: "..id.." has "..childID.." as a child in multiple positions.")
end
childrenToRemove[index] = true
else
groupChildren[childID] = index
childHasParent[childID] = true
end
end
if next(childrenToRemove) ~= nil then
for i = #data.controlledChildren, 1, -1 do
if childrenToRemove[i] then
tremove(data.controlledChildren, i)
end
end
end
end
for id, data in pairs(children) do
if not childHasParent[id] then
if not silent then
prettyPrint("Detected corruption in saved variables: "..id.." should be controlled by "..data.parent.." but isn't.")
end
local parent = parents[data.parent]
tinsert(parent.controlledChildren, id)
end
end
end
local function loadOrder(tbl, idtable)
local order = {}
local loaded = {};
local function load(id, depends)
local data = idtable[id];
if(data.parent) then
if(idtable[data.parent]) then
if depends[data.parent] then
error("Circular dependency in Private.AddMany between "..table.concat(depends, ", "));
else
if not(loaded[data.parent]) then
local dependsOut = CopyTable(depends)
dependsOut[data.parent] = true
coroutine.yield(100, "sort deps")
load(data.parent, dependsOut)
coroutine.yield(100, "sort deps")
end
end
else
data.parent = nil;
end
end
if not(loaded[id]) then
coroutine.yield(100, "sort deps");
loaded[id] = true;
tinsert(order, idtable[id])
end
end
for id, data in pairs(idtable) do
load(id, {});
coroutine.yield(100, "sort deps")
end
return order
end
---@type fun(data: auraData)
local pAdd
---@param tbl auraData[]
---@param takeSnapshots boolean
function Private.AddMany(tbl, takeSnapshots)
--- @type table<auraId, auraData>
local idtable = {};
--- @type table<auraId, auraId> The anchoring targets of other auras, key is the anchor, value is the aura that is anchoring
local anchorTargets = {}
for _, data in ipairs(tbl) do
-- There was an unfortunate bug in update.lua in 2022 that resulted
-- in auras having a circular dependencies
-- Fix one of the two known cases here
if data.id == data.parent then
data.parent = nil
tDeleteItem(data.controlledChildren, data.id)
end
idtable[data.id] = data;
if data.anchorFrameType == "SELECTFRAME" and data.anchorFrameFrame and data.anchorFrameFrame:sub(1, 10) == "WeakAuras:" then
anchorTargets[data.anchorFrameFrame:sub(11)] = data.id
end
end
-- Now fix up anchors, see #3971, where aura p was anchored to aura c and where c was a child of p, thus c was anchored to p
-- The game used to detect such anchoring circles. We can't detect all of them, but at least detect the one from the ticket.
for target, source in pairs(anchorTargets) do
-- We walk up the parent's of target, to check for source
local parent = target
if idtable[target] then
while(parent) do
if parent == source then
WeakAuras.prettyPrint(L["Warning: Anchoring to your own child '%s' in aura '%s' is imposssible."]:format(target, source))
idtable[source].anchorFrameType = "SCREEN"
end
parent = idtable[parent].parent
end
end
end
local order = loadOrder(tbl, idtable)
coroutine.yield(5000)
local oldSnapshots = {}
local copies = {}
if takeSnapshots then
for _, data in ipairs(order) do
if Private.ModernizeNeedsOldSnapshot(data) then
oldSnapshots[data.uid] = Private.GetMigrationSnapshot(data.uid)
end
copies[data.uid] = CopyTable(data)
coroutine.yield(200, "addmany prepare snapshot")
end
Private.Threads:Add("snapshot", coroutine.create(function()
prettyPrint(L["WeakAuras is creating a rollback snapshot of your auras. This snapshot will allow you to revert to the current state of your auras if something goes wrong. This process may cause your framerate to drop until it is complete."])
for uid, data in pairs(copies) do
Private.SetMigrationSnapshot(uid, data)
coroutine.yield(200, "snapshot")
end
prettyPrint(L["Rollback snapshot is complete. Thank you for your patience!"])
end), 'normal')
end
local groups = {}
local bads = {}
for _, data in ipairs(order) do
if data.parent and bads[data.parent] then
bads[data.id] = true
else
local oldSnapshot = oldSnapshots[data.uid] or nil
local ok = xpcall(WeakAuras.PreAdd, Private.GetErrorHandlerUid(data.uid, "PreAdd"), data, oldSnapshot)
if not ok then
prettyPrint(L["Unable to modernize aura '%s'. This is probably due to corrupt data or a bad migration, please report this to the WeakAuras team."]:format(data.id))
if data.regionType == "dynamicgroup" or data.regionType == "group" then
prettyPrint(L["All children of this aura will also not be loaded, to minimize the chance of further corruption."])
end
bads[data.id] = true
elseif data.regionType == "dynamicgroup" or data.regionType == "group" then
groups[data] = true
end
coroutine.yield(1000, "addmany modernize")
end
end
for _, data in ipairs(order) do
if not bads[data.id] then
if data.parent and bads[data.parent] then
bads[data.id] = true
else
local ok = xpcall(pAdd, Private.GetErrorHandlerUid(data.uid, "pAdd"), data)
if not ok then
bads[data.id] = true
end
end
end
coroutine.yield(2000, "addmany add")
end
for id in pairs(anchorTargets) do
local data = idtable[id]
if data and not bads[data.id] and (data.parent == nil or idtable[data.parent].regionType ~= "dynamicgroup") then
Private.EnsureRegion(id)
coroutine.yield(100, "addmany ensure anchor")
end
end
for data in pairs(groups) do
if not bads[data.id] then
if data.type == "dynamicgroup" then
if Private.regions[data.id] and Private.regions[data.id].region then
Private.regions[data.id].region:ReloadControlledChildren()
end
else
WeakAuras.Add(data)
end
end
coroutine.yield(1000, "addmany reload dynamic group");
end
end
local function customOptionIsValid(option)
if not option.type then
return false
elseif Private.author_option_classes[option.type] == "simple" then
if not option.key
or not option.name
or not option.default == nil then
return false
end
elseif Private.author_option_classes[option.type] == "group" then
if not option.key
or not option.name
or not option.default == nil
or not option.subOptions then
return false
end
end
return true
end
local function validateUserConfig(data, options, config)
local authorOptionKeys, corruptOptions = {}, {}
for index, option in ipairs(options) do
if not customOptionIsValid(option) or authorOptionKeys[option.key] then
prettyPrint(data.id .. " Custom Option #" .. index .. " in " .. data.id .. " has been detected as corrupt, and has been deleted.")
corruptOptions[index] = true
else
local optionClass = Private.author_option_classes[option.type]
if option.key then
authorOptionKeys[option.key] = index
end
if optionClass == "simple" then
if not option.key then
option.key = WeakAuras.GenerateUniqueID()
end
if config[option.key] == nil then
if type(option.default) ~= "table" then
config[option.key] = option.default
else
config[option.key] = CopyTable(option.default)
end
end
elseif optionClass == "group" then
local subOptions = option.subOptions
if type(config[option.key]) ~= "table" then
config[option.key] = {}
end
local subConfig = config[option.key]
if option.groupType == "array" then
for k, v in pairs(subConfig) do
if type(k) ~= "number" or type(v) ~= "table" then
-- if k was not a number, then this was a simple group before
-- if v is not a table, then this was likely a color option
wipe(subConfig) -- second iteration will fill table with defaults
break
end
end
if option.limitType == "fixed" then
for i = #subConfig + 1, option.size do
-- add missing entries
subConfig[i] = {}
end
end
if option.limitType ~= "none" then
for i = option.size + 1, #subConfig do
-- remove excess entries
subConfig[i] = nil
end
end
for _, toValidate in pairs(subConfig) do
validateUserConfig(data, subOptions, toValidate)
end
else
if type(next(subConfig)) ~= "string" then
-- either there are no sub options, in which case this is a noop
-- or this group was previously an array, in which case we need to wipe
wipe(subConfig)
end
validateUserConfig(data, subOptions, subConfig)
end
end
end
end
for key, value in pairs(config) do
if not authorOptionKeys[key] then
config[key] = nil
else
local option = options[authorOptionKeys[key]]
local optionClass = Private.author_option_classes[option.type]
if optionClass ~= "group" then
local option = options[authorOptionKeys[key]]
if option.type == "media" then
-- sounds can be number or string, other kinds of media can only be string
if type(value) ~= "string" and (type(value) ~= "number" or option.mediaType ~= "sound") then
config[key] = option.default
end
elseif type(value) ~= type(option.default) then
-- if type mismatch then we know that it can't be right
if type(option.default) ~= "table" then
config[key] = option.default
else
config[key] = CopyTable(option.default)
end
elseif option.type == "input" and option.useLength then
config[key] = config[key]:sub(1, option.length)
elseif option.type == "number" or option.type == "range" then
if (option.max and option.max < value) or (option.min and option.min > value) then
config[key] = option.default
else
if option.type == "number" and option.step then
local min = option.min or 0
config[key] = option.step * Round((value - min)/option.step) + min
end
end
elseif option.type == "select" then
if value < 1 or value > #option.values then
config[key] = option.default
end
elseif option.type == "multiselect" then
local multiselect = config[key]
for i, v in ipairs(multiselect) do
if option.default[i] ~= nil then
if type(v) ~= "boolean" then
multiselect[i] = option.default[i]
end
else
multiselect[i] = nil
end
end
for i, v in ipairs(option.default) do
if type(multiselect[i]) ~= "boolean" then
multiselect[i] = v
end
end
elseif option.type == "color" then
for i = 1, 4 do
local c = config[key][i]
if type(c) ~= "number" or c < 0 or c > 1 then
config[key] = option.default
break
end
end
end
end
end
end
for i = #options, 1, -1 do
if corruptOptions[i] then
tremove(options, i)
end
end
end
local oldDataStub = {
-- note: this is the minimal data stub which prevents false positives in diff upon reimporting an aura.
-- pending a refactor of other code which adds unnecessary fields, it is possible to shrink it
trigger = {
type = "aura",
names = {},
event = "Health",
subeventPrefix = "SPELL",
subeventSuffix = "_CAST_START",
spellIds = {},
unit = "player",
debuffType = "HELPFUL",
},
numTriggers = 1,
untrigger = {},
load = {
size = {
multi = {},
},
spec = {
multi = {},
},
class = {
multi = {},
},
},
actions = {
init = {},
start = {},
finish = {},
},
animation = {
start = {
type = "none",
duration_type = "seconds",
},
main = {
type = "none",
duration_type = "seconds",
},
finish = {
type = "none",
duration_type = "seconds",
},
},
conditions = {},
}
local oldDataStub2 = {
-- note: this is the minimal data stub which prevents false positives in diff upon reimporting an aura.
-- pending a refactor of other code which adds unnecessary fields, it is possible to shrink it
triggers = {
{
trigger = {
type = "aura",
names = {},
event = "Health",
subeventPrefix = "SPELL",
subeventSuffix = "_CAST_START",
spellIds = {},
unit = "player",
debuffType = "HELPFUL",
},
untrigger = {},
},
},
load = {
size = {
multi = {},
},
spec = {
multi = {},
},
class = {
multi = {},
},
},
actions = {
init = {},
start = {},
finish = {},
},
animation = {
start = {
type = "none",
duration_type = "seconds",
},
main = {
type = "none",
duration_type = "seconds",
},
finish = {
type = "none",
duration_type = "seconds",
},
},
conditions = {},
}
function Private.UpdateSoundIcon(data)
local function testConditions()
local sound, tts
if data.conditions then
for _, condition in ipairs(data.conditions) do
for changeIndex, change in ipairs(condition.changes) do
if change.property == "sound" then
sound = true
end
if change.property == "chat" and change.value and change.value.message_type == "TTS" then
tts = true
end
if sound and tts then break end
end
end
end
return sound, tts
end
local soundCondition, ttsCondition = testConditions()
-- sound
if data.actions.start.do_sound or data.actions.finish.do_sound then
Private.AuraWarnings.UpdateWarning(data.uid, "sound_action", "sound", L["This aura plays a sound via an action."])
else
Private.AuraWarnings.UpdateWarning(data.uid, "sound_action")
end
if soundCondition then
Private.AuraWarnings.UpdateWarning(data.uid, "sound_condition", "sound", L["This aura plays a sound via a condition."])
else
Private.AuraWarnings.UpdateWarning(data.uid, "sound_condition")
end
-- tts
if (data.actions.start.do_message and data.actions.start.message_type == "TTS")
or (data.actions.finish.do_message and data.actions.finish.message_type == "TTS")
then
Private.AuraWarnings.UpdateWarning(data.uid, "tts_action", "tts", L["This aura plays a Text To Speech via an action."])
else
Private.AuraWarnings.UpdateWarning(data.uid, "tts_action")
end
if ttsCondition then
Private.AuraWarnings.UpdateWarning(data.uid, "tts_condition", "tts", L["This aura plays a Text To Speech via a condition."])
else
Private.AuraWarnings.UpdateWarning(data.uid, "tts_condition")
end
end
function WeakAuras.PreAdd(data, snapshot)
if not data then return end
-- Readd what Compress removed before version 8
if (not data.internalVersion or data.internalVersion < 7) then
Private.validate(data, oldDataStub)
elseif (data.internalVersion < 8) then
Private.validate(data, oldDataStub2)
end
xpcall(Private.Modernize, Private.GetErrorHandlerId(data.id, L["Modernize"]), data, snapshot)
local default = data.regionType and Private.regionTypes[data.regionType] and Private.regionTypes[data.regionType].default
if default then
Private.validate(data, default)
end
local regionValidate = data.regionType and Private.regionTypes[data.regionType] and Private.regionTypes[data.regionType].validate
if regionValidate then
regionValidate(data)
end
Private.validate(data, Private.data_stub);
if data.subRegions then
for _, subRegionData in ipairs(data.subRegions) do
local subType = subRegionData.type
if subType and Private.subRegionTypes[subType] then
if Private.subRegionTypes[subType].supports(data.regionType) then
local default = Private.subRegionTypes[subType].default
if type(default) == "function" then
default = default(data.regionType)
end
if default then
Private.validate(subRegionData, default)
end
else
WeakAuras.prettyPrint(L["ERROR in '%s' unknown or incompatible sub element type '%s'"]:format(data.id, subType))
end
end
end
end
validateUserConfig(data, data.authorOptions, data.config)
data.init_started = nil
data.init_completed = nil
data.expanded = nil
end
function pAdd(data, simpleChange)
local id = data.id;
if not(id) then
error("Improper arguments to WeakAuras.Add - id not defined");
return;
end
data.uid = data.uid or WeakAuras.GenerateUniqueID()
if db.displays[id] and db.displays[id].uid ~= data.uid then
print("Improper? arguments to WeakAuras.Add - id", id, "is assigned to a different uid.", data.uid, db.displays[id].uid)
end
if UIDtoID[data.uid] and UIDtoID[data.uid] ~= id then
print("Improper? arguments to WeakAuras.Add - uid is assigned to a id. Uid:", data.uid, "assigned too:", UIDtoID[data.uid], "assigning now to", data.id)
end
local otherID = UIDtoID[data.uid]
if not otherID then
UIDtoID[data.uid] = id
elseif otherID ~= id then
-- duplicate uid
data.uid = WeakAuras.GenerateUniqueID()
UIDtoID[data.uid] = id
end
if simpleChange then
db.displays[id] = data
if WeakAuras.GetRegion(data.id) then
Private.SetRegion(data)
end
if clones[id] then
for cloneId, region in pairs(clones[id]) do
Private.SetRegion(data, cloneId)
end
end
Private.UpdatedTriggerState(id)
Private.callbacks:Fire("Add", data.uid, data.id, data, simpleChange)
else
Private.DebugLog.SetEnabled(data.uid, data.information.debugLog)
if Private.IsGroupType(data) then
Private.ClearAuraEnvironment(id);
for parent in Private.TraverseParents(data) do
Private.ClearAuraEnvironment(parent.id);
end
db.displays[id] = data;
if WeakAuras.GetRegion(data.id) then
Private.SetRegion(data)
end
Private.ScanForLoadsGroup({[id] = true});
loadEvents["GROUP"] = loadEvents["GROUP"] or {}
loadEvents["GROUP"][id] = true
else -- Non group aura
-- Make sure that we don't have a controlledChildren member.
data.controlledChildren = nil
local visible
if (WeakAuras.IsOptionsOpen()) then
visible = Private.FakeStatesFor(id, false)
else
if (Private.regions[id] and Private.regions[id].region) then
Private.regions[id].region:Collapse()
else
Private.CollapseAllClones(id)
end
end
-- If the aura has a onHide animation we need to cancel it to ensure it's truly hidden now
if Private.regions[id] then
Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
end
if clones[id] then
for _, region in pairs(clones[id]) do
Private.CancelAnimation(region, true, true, true, true, true, true)
end
end
Private.ClearAuraEnvironment(id);
for parent in Private.TraverseParents(data) do
Private.ClearAuraEnvironment(parent.id);
end
db.displays[id] = data;
if (not data.triggers.activeTriggerMode or data.triggers.activeTriggerMode > #data.triggers) then
data.triggers.activeTriggerMode = Private.trigger_modes.first_active;
end
for _, triggerSystem in pairs(triggerSystems) do
triggerSystem.Add(data);
end
local loadFuncStr, events = ConstructFunction(load_prototype, data.load);
for event, eventData in pairs(loadEvents) do
eventData[id] = nil
end
for event in pairs(events) do
loadEvents[event] = loadEvents[event] or {}
loadEvents[event][id] = true
end
loadEvents["SCAN_ALL"] = loadEvents["SCAN_ALL"] or {}
loadEvents["SCAN_ALL"][id] = true
local loadForOptionsFuncStr = ConstructFunction(load_prototype, data.load, true);
local loadFunc = Private.LoadFunction(loadFuncStr);
local loadForOptionsFunc = Private.LoadFunction(loadForOptionsFuncStr);
local triggerLogicFunc;
if data.triggers.disjunctive == "custom" then
triggerLogicFunc = WeakAuras.LoadFunction("return "..(data.triggers.customTriggerLogic or ""));
end
LoadCustomActionFunctions(data);
Private.LoadConditionPropertyFunctions(data);
Private.LoadConditionFunction(data)
loadFuncs[id] = loadFunc;
loadFuncsForOptions[id] = loadForOptionsFunc;
clones[id] = clones[id] or {};
if (timers[id]) then
for _, trigger in pairs(timers[id]) do
for _, record in pairs(trigger) do
if (record.handle) then
timer:CancelTimer(record.handle);
end
end
end
timers[id] = nil;
end
if WeakAuras.GetRegion(data.id) then
Private.SetRegion(data)
end
triggerState[id] = {
disjunctive = data.triggers.disjunctive or "all",
numTriggers = #data.triggers,
activeTriggerMode = data.triggers.activeTriggerMode or Private.trigger_modes.first_active,
triggerLogicFunc = triggerLogicFunc,
triggers = {},
triggerCount = 0,
activatedConditions = {},
};
LoadEncounterInitScripts(id);
if (WeakAuras.IsOptionsOpen()) then
Private.FakeStatesFor(id, visible)
end
if not(paused) then
Private.ScanForLoads({[id] = true});
end
end
Private.UpdateSoundIcon(data)
Private.callbacks:Fire("Add", data.uid, data.id, data, simpleChange)
end
end
---@private
function WeakAuras.Add(data, simpleChange)
local oldSnapshot
if Private.ModernizeNeedsOldSnapshot(data) then
oldSnapshot = Private.GetMigrationSnapshot(data.uid)
end
if (data.internalVersion or 0) < internalVersion then
Private.SetMigrationSnapshot(data.uid, data)
end
local ok = xpcall(WeakAuras.PreAdd, Private.GetErrorHandlerUid(data.uid, "PreAdd"), data, oldSnapshot)
if ok then
pAdd(data, simpleChange)
end
end
function Private.AddParents(data)
local parent = data.parent
if (parent) then
local parentData = WeakAuras.GetData(parent)
WeakAuras.Add(parentData)
Private.AddParents(parentData)
end
end
function Private.SetRegion(data, cloneId)
local regionType = data.regionType;
if not(regionType) then
error("Improper arguments to Private.SetRegion - regionType not defined in ".. data.id)
else
if(not regionTypes[regionType]) then
regionType = "fallback";
print("Improper arguments to WeakAuras.CreateRegion - regionType \""..data.regionType.."\" is not supported in ".. data.id)
end
local id = data.id;
if not(id) then
error("Improper arguments to Private.SetRegion - id not defined");
else
local region;
if(cloneId) then
region = clones[id][cloneId];
if (not region or region.regionType ~= data.regionType) then
if (region) then
clonePool[region.regionType] = clonePool[region.regionType] or {};
tinsert(clonePool[region.regionType], region);
region:Hide();
end
if(clonePool[data.regionType] and clonePool[data.regionType][1]) then
clones[id][cloneId] = tremove(clonePool[data.regionType]);
else
local clone = regionTypes[data.regionType].create(WeakAurasFrame, data);
clone.regionType = data.regionType;
clone:Hide();
clones[id][cloneId] = clone;
end
region = clones[id][cloneId];
end
else
if((not Private.regions[id]) or (not Private.regions[id].region) or Private.regions[id].regionType ~= regionType) then
region = regionTypes[regionType].create(WeakAurasFrame, data);
Private.regions[id] = {
regionType = regionType,
region = region
};
if regionType ~= "dynamicgroup" and regionType ~= "group" then
region.toShow = false
region:Hide()
else
region.toShow = true
end
else
region = Private.regions[id].region
end
end
region.id = id;
region.cloneId = cloneId or "";
Private.validate(data, regionTypes[regionType].default);
local parent = WeakAurasFrame;
if(data.parent) then
if WeakAuras.GetData(data.parent) then
parent = Private.EnsureRegion(data.parent)
else
data.parent = nil;
end
end
local loginFinished = WeakAuras.IsLoginFinished();
local anim_cancelled = loginFinished and Private.CancelAnimation(region, true, true, true, true, true, true);
regionTypes[regionType].modify(parent, region, data);
Private.regionPrototype.AddSetDurationInfo(region, data.uid)
Private.regionPrototype.AddExpandFunction(data, region, cloneId, parent, parent.regionType)
data.animation = data.animation or {};
data.animation.start = data.animation.start or {type = "none"};
data.animation.main = data.animation.main or {type = "none"};
data.animation.finish = data.animation.finish or {type = "none"};
data.animation.start.duration_type = data.animation.start.duration_type or "seconds"
data.animation.main.duration_type = data.animation.main.duration_type or "seconds"
data.animation.finish.duration_type = data.animation.finish.duration_type or "seconds"
if(cloneId) then
clonePool[regionType] = clonePool[regionType] or {};
end
if(anim_cancelled) then
Private.Animate("display", data.uid, "main", data.animation.main, region, false, nil, true, cloneId);
end
return region;
end
end
end
--- Ensures that a clone exists
---@param id auraId
---@param cloneId string
---@return table
local function EnsureClone(id, cloneId)
clones[id] = clones[id] or {}
if not(clones[id][cloneId]) then
local data = WeakAuras.GetData(id)
Private.SetRegion(data, cloneId)
end
return clones[id][cloneId]
end
local creatingRegions = false
function Private.CreatingRegions()
return creatingRegions
end
--- Ensures that a region exists
---@param id auraId
---@return table
local function EnsureRegion(id)
if not Private.regions[id] or not Private.regions[id].region then
Private.regions[id] = Private.regions[id] or {}
-- The region doesn't yet exist
-- But we must also ensure that our parents exists
-- So we go up the list of parents and collect auras that must be created
-- If we find a parent already exists, we can stop
--- @type auraId[]
local aurasToCreate = {}
while(id) do
local data = WeakAuras.GetData(id)
tinsert(aurasToCreate, data.id)
id = data.parent
end
for _, toCreateId in ipairs_reverse(aurasToCreate) do
local data = WeakAuras.GetData(toCreateId)
Private.SetRegion(data)
end
end
return Private.regions[id] and Private.regions[id].region
end
--- Ensures that a region/clone exists and returns it
function Private.EnsureRegion(id, cloneId)
-- Even if we are asked to only create a clone, we create the default region
-- too.
EnsureRegion(id)
if(cloneId and cloneId ~= "") then
return EnsureClone(id, cloneId);
end
return WeakAuras.GetRegion(id)
end
---returns the region, if it exists
---@param id auraId
---@param cloneId string|nil
---@return table|nil
function WeakAuras.GetRegion(id, cloneId)
if(cloneId and cloneId ~= "") then
return clones[id] and clones[id][cloneId]
end
return Private.regions[id] and Private.regions[id].region
end
-- Note, does not create a clone!
function Private.GetRegionByUID(uid, cloneId)
local id = Private.UIDtoID(uid)
if(cloneId and cloneId ~= "") then
return id and clones[id] and clones[id][cloneId];
end
return id and Private.regions[id] and Private.regions[id].region
end
function Private.CollapseAllClones(id, triggernum)
if(clones[id]) then
for i,v in pairs(clones[id]) do
v:Collapse();
end
end
end
function Private.SetAllStatesHidden(id, triggernum)
local triggerState = WeakAuras.GetTriggerStateForTrigger(id, triggernum);
local changed = false
for _, state in pairs(triggerState) do
changed = changed or state.show
state.show = false;
state.changed = true;
end
return changed
end
function Private.SetAllStatesHiddenExcept(id, triggernum, list)
local triggerState = WeakAuras.GetTriggerStateForTrigger(id, triggernum);
for cloneId, state in pairs(triggerState) do
if (not (list[cloneId])) then
state.show = false;
state.changed = true;
end
end
end
function Private.ReleaseClone(id, cloneId, regionType)
if (not clones[id]) then
return;
end
local region = clones[id][cloneId];
clones[id][cloneId] = nil;
if region:IsProtected() then
WeakAuras.prettyPrint(L["Error '%s' created a secure clone. We advise deleting the aura. For more information:\nhttps://github.com/WeakAuras/WeakAuras2/wiki/Protected-Frames"]:format(id))
else
clonePool[regionType][#clonePool[regionType] + 1] = region;
end
end
function Private.HandleChatAction(message_type, message, message_dest, message_dest_isunit, message_channel, r, g, b, region, customFunc, when, formatters, voice)
local useHiddenStates = when == "finish"
if (message:find('%%')) then
message = Private.ReplacePlaceHolders(message, region, customFunc, useHiddenStates, formatters);
end
if(message_type == "PRINT") then
DEFAULT_CHAT_FRAME:AddMessage(message, r or 1, g or 1, b or 1);
elseif message_type == "TTS" then
local validVoice = voice and Private.tts_voices[voice]
if not Private.SquelchingActions() then
pcall(function()
C_VoiceChat.SpeakText(
validVoice and voice or next(Private.tts_voices) or 0,
message,
1,
C_TTSSettings and C_TTSSettings.GetSpeechRate() or 0,
C_TTSSettings and C_TTSSettings.GetSpeechVolume() or 100
);
end)
end
elseif message_type == "ERROR" then
UIErrorsFrame:AddMessage(message, r or 1, g or 1, b or 1)
elseif(message_type == "COMBAT") then
if(CombatText_AddMessage) then
CombatText_AddMessage(message, COMBAT_TEXT_SCROLL_FUNCTION, r or 1, g or 1, b or 1);
end
elseif(message_type == "WHISPER") then
if(message_dest) then
if (message_dest:find('%%')) then
message_dest = Private.ReplacePlaceHolders(message_dest, region, customFunc, useHiddenStates, formatters);
end
if message_dest_isunit == true then
message_dest = GetUnitName(message_dest, true)
end
pcall(function() SendChatMessage(message, "WHISPER", nil, message_dest) end);
end
elseif(message_type == "SMARTRAID") then
local isInstanceGroup = IsInGroup(LE_PARTY_CATEGORY_INSTANCE)
if UnitInBattleground("player") then
pcall(function() SendChatMessage(message, "INSTANCE_CHAT") end)
elseif UnitInRaid("player") then
pcall(function() SendChatMessage(message, "RAID") end)
elseif UnitInParty("player") then
if isInstanceGroup then
pcall(function() SendChatMessage(message, "INSTANCE_CHAT") end)
else
pcall(function() SendChatMessage(message, "PARTY") end)
end
else
if IsInInstance() then
pcall(function() SendChatMessage(message, "SAY") end)
end
end
elseif(message_type == "SAY" or message_type == "YELL") then
if IsInInstance() then
pcall(function() SendChatMessage(message, message_type, nil, nil) end)
end
else
pcall(function() SendChatMessage(message, message_type, nil, nil) end);
end
end
local function actionGlowStop(actions, frame, id)
if not frame.__WAGlowFrame then return end
if actions.glow_type == "buttonOverlay" then
LCG.ButtonGlow_Stop(frame.__WAGlowFrame)
elseif actions.glow_type == "Pixel" then
LCG.PixelGlow_Stop(frame.__WAGlowFrame, id)
elseif actions.glow_type == "ACShine" then
LCG.AutoCastGlow_Stop(frame.__WAGlowFrame, id)
elseif actions.glow_type == "Proc" then
LCG.ProcGlow_Stop(frame.__WAGlowFrame, id)
end
end
local function actionGlowStart(actions, frame, id)
if not frame.__WAGlowFrame then
frame.__WAGlowFrame = CreateFrame("Frame", nil, frame)
frame.__WAGlowFrame:SetAllPoints(frame)
frame.__WAGlowFrame:SetSize(frame:GetSize())
end
local glow_frame = frame.__WAGlowFrame
if glow_frame:GetWidth() < 1 or glow_frame:GetHeight() < 1 then
actionGlowStop(actions, frame)
return
end
local color = actions.use_glow_color and actions.glow_color or nil
if actions.glow_type == "buttonOverlay" then
LCG.ButtonGlow_Start(glow_frame, color)
elseif actions.glow_type == "Pixel" then
LCG.PixelGlow_Start(
glow_frame,
color,
actions.glow_lines,
actions.glow_frequency,
actions.glow_length,
actions.glow_thickness,
actions.glow_XOffset,
actions.glow_YOffset,
actions.glow_border and true or false,
id
)
elseif actions.glow_type == "ACShine" then
LCG.AutoCastGlow_Start(
glow_frame,
color,
actions.glow_lines,
actions.glow_frequency,
actions.glow_scale,
actions.glow_XOffset,
actions.glow_YOffset,
id
)
elseif actions.glow_type == "Proc" then
LCG.ProcGlow_Start(glow_frame, {
color = color,
startAnim = actions.glow_startAnim and true or false,
xOffset = actions.glow_XOffset,
yOffset = actions.glow_YOffset,
duration = actions.glow_duration or 1,
key = id
})
end
end
local glow_frame_monitor
local anchor_unitframe_monitor
Private.dyngroup_unitframe_monitor = {}
do
local function frame_monitor_callback(event, frame, unit, previousUnit)
local new_frame
local FRAME_UNIT_UPDATE = event == "FRAME_UNIT_UPDATE"
local FRAME_UNIT_ADDED = event == "FRAME_UNIT_ADDED"
local FRAME_UNIT_REMOVED = event == "FRAME_UNIT_REMOVED"
local dynamicGroupsToUpdate = {}
if type(glow_frame_monitor) == "table" then
for region, data in pairs(glow_frame_monitor) do
if region.state and type(region.state.unit) == "string" and UnitIsUnit(region.state.unit, unit)
and ((data.frame ~= frame) and (FRAME_UNIT_ADDED or FRAME_UNIT_UPDATE))
or ((data.frame == frame) and FRAME_UNIT_REMOVED)
then
if not new_frame then
new_frame = WeakAuras.GetUnitFrame(unit)
end
if new_frame ~= data.frame then
local id = region.id .. (region.cloneId or "")
-- remove previous glow
if data.frame then
actionGlowStop(data.actions, data.frame, id)
end
data.frame = new_frame
if new_frame then
-- apply the glow to new_frame
actionGlowStart(data.actions, data.frame, id)
-- update hidefunc
local region = region
region.active_glows_hidefunc = region.active_glows_hidefunc or {}
region.active_glows_hidefunc[data.frame] = function()
actionGlowStop(data.actions, data.frame, id)
glow_frame_monitor[region] = nil
end
end
end
end
end
end
if type(anchor_unitframe_monitor) == "table" then
for region, data in pairs(anchor_unitframe_monitor) do
if region.state and type(region.state.unit) == "string" and UnitIsUnit(region.state.unit, unit)
and ((data.frame ~= frame) and (FRAME_UNIT_ADDED or FRAME_UNIT_UPDATE))
or ((data.frame == frame) and FRAME_UNIT_REMOVED)
then
if not new_frame then
new_frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
end
if new_frame ~= data.frame then
Private.AnchorFrame(data.data, region, data.parent)
end
end
end
end
for regionData, data_frame in pairs(Private.dyngroup_unitframe_monitor) do
if regionData.region.state and type(regionData.region.state.unit) == "string" and UnitIsUnit(regionData.region.state.unit, unit)
and ((data_frame ~= frame) and (FRAME_UNIT_ADDED or FRAME_UNIT_UPDATE))
or ((data_frame == frame) and FRAME_UNIT_REMOVED)
then
if not new_frame then
new_frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
end
if new_frame and new_frame ~= data_frame then
dynamicGroupsToUpdate[regionData.parent] = true
end
end
end
for frame in pairs(dynamicGroupsToUpdate) do
frame:DoPositionChildren()
end
end
LGF.RegisterCallback("WeakAuras", "FRAME_UNIT_UPDATE", frame_monitor_callback)
LGF.RegisterCallback("WeakAuras", "FRAME_UNIT_ADDED", frame_monitor_callback)
LGF.RegisterCallback("WeakAuras", "FRAME_UNIT_REMOVED", frame_monitor_callback)
end
function Private.HandleGlowAction(actions, region)
if actions.glow_action
and (
(
(actions.glow_frame_type == "UNITFRAME" or actions.glow_frame_type == "NAMEPLATE")
and region.state.unit
)
or (actions.glow_frame_type == "FRAMESELECTOR" and actions.glow_frame)
or (actions.glow_frame_type == "PARENTFRAME" and region:GetParent())
)
then
local glow_frame, should_glow_frame
if actions.glow_frame_type == "FRAMESELECTOR" then
if actions.glow_frame:sub(1, 10) == "WeakAuras:" then
local frame_name = actions.glow_frame:sub(11)
if WeakAuras.GetData(frame_name) then
Private.EnsureRegion(frame_name)
end
if Private.regions[frame_name] and Private.regions[frame_name].region then
glow_frame = Private.regions[frame_name].region
should_glow_frame = true
end
else
glow_frame = Private.GetSanitizedGlobal(actions.glow_frame)
should_glow_frame = true
end
elseif actions.glow_frame_type == "UNITFRAME" and region.state.unit then
glow_frame = WeakAuras.GetUnitFrame(region.state.unit)
should_glow_frame = true
elseif actions.glow_frame_type == "NAMEPLATE" and region.state.unit then
glow_frame = WeakAuras.GetUnitNameplate(region.state.unit)
should_glow_frame = true
elseif actions.glow_frame_type == "PARENTFRAME" then
glow_frame = region:GetParent()
should_glow_frame = true
end
if should_glow_frame then
local id = region.id .. (region.cloneId or "")
if actions.glow_action == "show" then
-- remove previous glow
if glow_frame then
if region.active_glows_hidefunc
and region.active_glows_hidefunc[glow_frame]
then
region.active_glows_hidefunc[glow_frame]()
end
-- start glow
actionGlowStart(actions, glow_frame, id)
-- make unglow function & monitor unitframe changes
region.active_glows_hidefunc = region.active_glows_hidefunc or {}
if actions.glow_frame_type == "UNITFRAME" then
region.active_glows_hidefunc[glow_frame] = function()
actionGlowStop(actions, glow_frame, id)
glow_frame_monitor[region] = nil
end
else
region.active_glows_hidefunc[glow_frame] = function()
actionGlowStop(actions, glow_frame, id)
end
end
end
if actions.glow_frame_type == "UNITFRAME" then
glow_frame_monitor = glow_frame_monitor or {}
glow_frame_monitor[region] = {
actions = actions,
frame = glow_frame
}
end
elseif actions.glow_action == "hide"
and glow_frame
and region.active_glows_hidefunc
and region.active_glows_hidefunc[glow_frame]
then
region.active_glows_hidefunc[glow_frame]()
region.active_glows_hidefunc[glow_frame] = nil
end
end
end
end
function Private.PerformActions(data, when, region)
if (paused or WeakAuras.IsOptionsOpen()) then
return;
end;
local actions;
local formatters
if(when == "start") then
actions = data.actions.start;
formatters = region.startFormatters
elseif(when == "finish") then
actions = data.actions.finish;
formatters = region.finishFormatters
else
return;
end
if(actions.do_message and actions.message_type and actions.message) then
local customFunc = Private.customActionsFunctions[data.id][when .. "_message"];
Private.HandleChatAction(actions.message_type, actions.message, actions.message_dest, actions.message_dest_isunit, actions.message_channel, actions.r, actions.g, actions.b, region, customFunc, when, formatters, actions.message_tts_voice);
end
if (actions.stop_sound) then
if (region.SoundStop) then
local fadeoutTime = actions.do_sound_fade and actions.stop_sound_fade and actions.stop_sound_fade * 1000 or 0
region:SoundStop(fadeoutTime);
end
end
if(actions.do_sound and actions.sound) then
if (region.SoundPlay) then
region:SoundPlay(actions);
end
end
if(actions.do_custom and actions.custom) then
local func = Private.customActionsFunctions[data.id][when]
if func then
Private.ActivateAuraEnvironment(region.id, region.cloneId, region.state, region.states);
xpcall(func, Private.GetErrorHandlerId(data.id, L["Custom Action"]));
Private.ActivateAuraEnvironment(nil);
end
end
-- Apply start glow actions even if squelch_actions is true, but don't apply finish glow actions
if actions.do_glow then
Private.HandleGlowAction(actions, region)
end
-- remove all glows on finish
if when == "finish" and actions.hide_all_glows and region.active_glows_hidefunc then
for _, hideFunc in pairs(region.active_glows_hidefunc) do
hideFunc()
end
wipe(region.active_glows_hidefunc)
end
if when == "finish" and type(anchor_unitframe_monitor) == "table" then
anchor_unitframe_monitor[region] = nil
end
end
--- @type fun(id: auraId): auraData?
function WeakAuras.GetData(id)
return id and db.displays[id];
end
local function GetTriggerSystem(data, triggernum)
local triggerType = data.triggers[triggernum] and data.triggers[triggernum].trigger.type
return triggerType and triggerTypes[triggerType]
end
local function wrapTriggerSystemFunction(functionName, mode)
local func;
func = function(data, triggernum, ...)
if (not triggernum) then
return func(data, data.triggers.activeTriggerMode or -1, ...);
elseif (triggernum < 0) then
local result;
if (mode == "or") then
result = false;
for i = 1, #data.triggers do
result = result or func(data, i);
end
elseif (mode == "and") then
result = true;
for i = 1, #data.triggers do
result = result and func(data, i);
end
elseif (mode == "table") then
result = {};
for i = 1, #data.triggers do
local tmp = func(data, i);
if (tmp) then
for k, v in pairs(tmp) do
result[k] = v;
end
end
end
elseif (mode == "call") then
for i = 1, #data.triggers do
func(data, i, ...);
end
elseif (mode == "firstValue") then
result = nil;
for i = 1, #data.triggers do
local tmp = func(data, i);
if (tmp) then
result = tmp;
break;
end
end
elseif (mode == "nameAndIcon") then
for i = 1, #data.triggers do
local tmp1, tmp2 = func(data, i);
if (tmp1) then
return tmp1, tmp2;
end
end
end
return result;
else -- triggernum >= 1
local triggerSystem = GetTriggerSystem(data, triggernum);
if (not triggerSystem) then
return false
end
return triggerSystem[functionName](data, triggernum, ...);
end
end
return func;
end
Private.CanHaveTooltip = wrapTriggerSystemFunction("CanHaveTooltip", "or");
-- This has to be in WeakAuras for now, because GetNameAndIcon can be called from the options
-- before the Options has access to Private
WeakAuras.GetNameAndIcon = wrapTriggerSystemFunction("GetNameAndIcon", "nameAndIcon");
Private.GetTriggerDescription = wrapTriggerSystemFunction("GetTriggerDescription", "call");
local wrappedGetOverlayInfo = wrapTriggerSystemFunction("GetOverlayInfo", "table");
Private.GetAdditionalProperties = function(data)
local props = {}
for child in Private.TraverseLeafsOrAura(data) do
for i, trigger in ipairs(child.triggers) do
local triggerSystem = GetTriggerSystem(child, i)
if triggerSystem then
local triggerProps = triggerSystem.GetAdditionalProperties(child, i)
if triggerProps and props[i] then
MergeTable(props[i], triggerProps)
elseif triggerProps then
props[i] = triggerProps
end
end
end
end
return props
end
Private.GetProgressSources = function(data)
local values = {}
if Private.IsGroupType(data) then
return values
end
for i = 1, #data.triggers do
local triggerSystem = GetTriggerSystem(data, i);
if (triggerSystem) then
triggerSystem.GetProgressSources(data, i, values)
end
end
return values
end
Private.GetProgressSourceFor = function(data, trigger, property)
local values = {}
local triggerSystem = GetTriggerSystem(data, trigger);
if (triggerSystem) then
triggerSystem.GetProgressSources(data, trigger, values)
for _, v in ipairs(values) do
if v.property == property then
return {trigger, v.type, v.property, v.total, v.modRate, v.inverse, v.paused, v.remaining}
end
end
end
return nil
end
-- In the aura data we only store trigger + property
-- But for the region we don't want to gather necessary meta data all the time
-- So we collect that in region:modify + on creation of the conditions function
Private.AddProgressSourceMetaData = function(data, progressSource)
if not progressSource then
return {}
end
local trigger = progressSource[1]
local property = progressSource[2]
if trigger == -2 then
return {-2, "auto", ""}
elseif trigger == -1 then
return {-1, "auto", ""}
elseif trigger == 0 then
return {0, "manual", progressSource[3], progressSource[4]}
else
return Private.GetProgressSourceFor(data, trigger, property)
end
end
-- ProgressSource values
-- For AceOptions to work correctly progress sources need to be comparable
-- via ==. We use a constants table so that identical tables use the same table
-- Additional while data.progressSource does contain additional data e.g. for manual progress
-- This is only for the progress source combobox, which only cares about the first or first two values
-- The greatness of the hacks knows no bounds
-- The constants table has weak keys
do
local function CompareProgressValueTables(a, b)
-- For auto/manual progress, only compare a[] with b[1]
if a[1] == -1 or a[1] == 0 then
return a[1] == b[1]
end
-- Only care about trigger + property
return a[1] == b[1] and a[2] == b[2]
end
local progressValueConstants = {}
setmetatable(progressValueConstants, {_mode = "v"})
function Private.GetProgressValueConstant(v)
if v == nil then
return v
end
-- This uses pairs because there could be empty slots
for _, constant in pairs(progressValueConstants) do
if CompareProgressValueTables(v, constant) then
return constant
end
end
-- And this inserts into the first empty slot for the array
tinsert(progressValueConstants, v)
return v
end
end
function Private.GetProgressSourcesForUi(data, subelement)
local values
if subelement then
-- Sub elements Automatic means to use the main auras' progress
values = {
[{-2, ""}] = L["Automatic"]
}
else
values = {
[{-1, ""}] = L["Automatic"],
[{0, ""}] = L["Manual"],
}
end
local triggerValues = Private.GetProgressSources(data)
for _, e in ipairs(triggerValues) do
if e.trigger and e.property then
values[{e.trigger, e.property}] = {L["Trigger %s"]:format(e.trigger), e.display}
end
end
local result = {}
for k, v in pairs(values) do
result[Private.GetProgressValueConstant(k)] = v
end
return result
end
function Private.GetOverlayInfo(data, triggernum)
local overlayInfo;
if (data.controlledChildren) then
overlayInfo = {};
for child in Private.TraverseLeafs(data) do
local tmp = wrappedGetOverlayInfo(child, triggernum);
if (tmp) then
for k, v in pairs(tmp) do
overlayInfo[k] = v;
end
end
end
else
overlayInfo = wrappedGetOverlayInfo(data, triggernum);
end
return overlayInfo;
end
function Private.GetTriggerConditions(data)
local conditions = {};
for i = 1, #data.triggers do
local triggerSystem = GetTriggerSystem(data, i);
if (triggerSystem) then
conditions[i] = triggerSystem.GetTriggerConditions(data, i);
conditions[i] = conditions[i] or {};
conditions[i].show = {
display = L["Active"],
type = "bool",
test = function(state, needle)
return (state and state.id and triggerState[state.id].triggers[i] or false) == (needle == 1);
end
}
end
end
return conditions;
end
local function CreateFallbackState(id, triggernum)
fallbacksStates[id] = fallbacksStates[id] or {};
fallbacksStates[id][triggernum] = fallbacksStates[id][triggernum] or {};
local states = fallbacksStates[id][triggernum];
states[""] = states[""] or {};
local state = states[""];
local data = db.displays[id];
local triggerSystem = GetTriggerSystem(data, triggernum);
if (triggerSystem) then
triggerSystem.CreateFallbackState(data, triggernum, state)
state.trigger = data.triggers[triggernum].trigger
state.triggernum = triggernum
else
state.show = true;
state.changed = true;
state.progressType = "timed";
state.duration = 0;
state.expirationTime = math.huge;
end
state.id = id
return states;
end
local currentTooltipRegion;
local currentTooltipOwner;
local function UpdateMouseoverTooltip(region)
if(region == currentTooltipRegion) then
Private.ShowMouseoverTooltip(currentTooltipRegion, currentTooltipOwner);
end
end
function Private.ShowMouseoverTooltip(region, owner)
currentTooltipRegion = region;
currentTooltipOwner = owner;
GameTooltip:SetOwner(owner, "ANCHOR_NONE");
GameTooltip:SetPoint("LEFT", owner, "RIGHT");
GameTooltip:ClearLines();
local triggerType;
if (region.state) then
triggerType = region.state.trigger.type;
end
local triggerSystem = triggerType and triggerTypes[triggerType];
if (not triggerSystem) then
GameTooltip:Hide();
return;
end
if (triggerSystem.SetToolTip(region.state.trigger, region.state)) then
GameTooltip:Show();
else
GameTooltip:Hide();
end
end
function Private.HideTooltip()
currentTooltipRegion = nil;
currentTooltipOwner = nil;
-- If a tooltip was shown for a "restricted" frame, that is e.g. for a aura
-- anchored to a nameplate, then that frame is no longer clamped to the screen,
-- because restricted frames can't be clamped. So dance to make the tooltip
-- unrestricted and then clamp it again.
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("RIGHT", UIParent, "LEFT");
GameTooltip:SetClampedToScreen(true)
GameTooltip:Hide()
end
do
local hiddenTooltip;
function WeakAuras.GetHiddenTooltip()
if not(hiddenTooltip) then
hiddenTooltip = CreateFrame("GameTooltip", "WeakAurasTooltip", nil, "GameTooltipTemplate");
hiddenTooltip:SetOwner(WorldFrame, "ANCHOR_NONE");
hiddenTooltip:AddFontStrings(
hiddenTooltip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText"),
hiddenTooltip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
);
end
return hiddenTooltip;
end
end
function WeakAuras.GetAuraInstanceTooltipInfo(unit, auraInstanceId, filter)
if WeakAuras.IsRetail() then
local tooltipText = ""
local tooltipData
if filter == "HELPFUL" then
tooltipData = C_TooltipInfo.GetUnitBuffByAuraInstanceID(unit, auraInstanceId, filter)
else
tooltipData = C_TooltipInfo.GetUnitDebuffByAuraInstanceID(unit, auraInstanceId, filter)
end
if not tooltipData then
return nil, "", "none", 0
end
local secondLine = tooltipData.lines[2] -- This is the line we want
if secondLine and secondLine.leftText then
tooltipText = secondLine.leftText
end
return tooltipData.dataInstanceID, Private.ParseTooltipText(tooltipText)
end
end
function Private.ParseTooltipText(tooltipText)
local debuffType = "none";
local tooltipSize = {};
if(tooltipText) then
for t in tooltipText:gmatch("(-?%d[%d%.,]*)") do
if (LARGE_NUMBER_SEPERATOR == ",") then
t = t:gsub(",", "");
else
t = t:gsub("%.", "");
t = t:gsub(",", ".");
end
tinsert(tooltipSize, tonumber(t));
end
end
if (#tooltipSize) then
return tooltipText, debuffType, unpack(tooltipSize);
else
return tooltipText, debuffType, 0;
end
end
function WeakAuras.GetAuraTooltipInfo(unit, index, filter)
local tooltipText = ""
if WeakAuras.IsRetail() then
local tooltipData = C_TooltipInfo.GetUnitAura(unit, index, filter)
local secondLine = tooltipData and tooltipData.lines[2] -- This is the line we want
if secondLine and secondLine.leftText then
tooltipText = secondLine.leftText
end
else
local tooltip = WeakAuras.GetHiddenTooltip();
tooltip:ClearLines();
tooltip:SetUnitAura(unit, index, filter);
local tooltipTextLine = select(5, tooltip:GetRegions())
tooltipText = tooltipTextLine and tooltipTextLine:GetObjectType() == "FontString" and tooltipTextLine:GetText() or "";
end
return Private.ParseTooltipText(tooltipText)
end
local FrameTimes = {};
function WeakAuras.ProfileFrames(all)
UpdateAddOnCPUUsage();
for name, frame in pairs(Private.frames) do
local FrameTime = GetFrameCPUUsage(frame);
FrameTimes[name] = FrameTimes[name] or 0;
if(all or FrameTime > FrameTimes[name]) then
print("|cFFFF0000"..name.."|r -", FrameTime, "-", FrameTime - FrameTimes[name]);
end
FrameTimes[name] = FrameTime;
end
end
local DisplayTimes = {};
function WeakAuras.ProfileDisplays(all)
UpdateAddOnCPUUsage();
for id, regionData in pairs(Private.regions) do
if regionData.region then
local DisplayTime = GetFrameCPUUsage(regionData.region, true);
DisplayTimes[id] = DisplayTimes[id] or 0;
if(all or DisplayTime > DisplayTimes[id]) then
print("|cFFFF0000"..id.."|r -", DisplayTime, "-", DisplayTime - DisplayTimes[id]);
end
DisplayTimes[id] = DisplayTime;
end
end
end
function Private.ValueFromPath(data, path)
if not data then
return nil
end
if (#path == 0) then
return data
elseif(#path == 1) then
return data[path[1]];
else
local reducedPath = {};
for i=2,#path do
reducedPath[i-1] = path[i];
end
return Private.ValueFromPath(data[path[1]], reducedPath);
end
end
function Private.ValueToPath(data, path, value)
if not data then
return
end
if(#path == 1) then
data[path[1]] = value;
else
local reducedPath = {};
for i=2,#path do
reducedPath[i-1] = path[i];
end
Private.ValueToPath(data[path[1]], reducedPath, value);
end
end
Private.frameLevels = {};
local function SetFrameLevel(id, frameLevel)
if (Private.frameLevels[id] == frameLevel) then
return;
end
if (Private.regions[id] and Private.regions[id].region) then
Private.ApplyFrameLevel(Private.regions[id].region, frameLevel)
end
if (clones[id]) then
for i,v in pairs(clones[id]) do
Private.ApplyFrameLevel(v, frameLevel)
end
end
Private.frameLevels[id] = frameLevel;
end
local function FixGroupChildrenOrderImpl(data, frameLevel)
SetFrameLevel(data.id, frameLevel)
local offset
if data.sharedFrameLevel then
offset = 0
else
offset = 4
end
for _, childId in ipairs(data.controlledChildren) do
local childData = WeakAuras.GetData(childId)
if childData.regionType ~= "group" and childData.regionType ~= "dynamicgroup" then
frameLevel = frameLevel + offset
SetFrameLevel(childId, frameLevel)
else
frameLevel = frameLevel + offset
local endFrameLevel = FixGroupChildrenOrderImpl(childData, frameLevel)
if not data.sharedFrameLevel then
frameLevel = endFrameLevel
end
end
end
return frameLevel
end
function Private.FixGroupChildrenOrderForGroup(data)
if data.parent then
return
end
FixGroupChildrenOrderImpl(data, 0)
end
local function GetFrameLevelFor(id)
return Private.frameLevels[id] or 5;
end
function Private.ApplyFrameLevel(region, frameLevel)
frameLevel = frameLevel or GetFrameLevelFor(region.id)
local setBackgroundFrameLevel = false
if region.subRegions then
for index, subRegion in pairs(region.subRegions) do
if subRegion.type == "subbackground" then
subRegion:SetFrameLevel(frameLevel + index)
setBackgroundFrameLevel = true
end
end
if not setBackgroundFrameLevel then
region:SetFrameLevel(frameLevel)
end
for index, subRegion in pairs(region.subRegions) do
if subRegion.type ~= "subbackground" then
subRegion:SetFrameLevel(frameLevel + index)
end
end
else
region:SetFrameLevel(frameLevel)
end
end
function WeakAuras.EnsureString(input)
if (input == nil) then
return "";
end
return tostring(input);
end
-- Handle coroutines
---@alias threadPriority 'urgent' | 'normal' | 'background' | 'instant'
---@alias threadPool table<string, threadData>
---@class threadData
---@field thread thread
---@field sequence table<string, number> to help debug problems in threads
---@class Threads
---@field pools table<threadPriority, threadPool>
local threads = {
frame = CreateFrame("Frame"),
size = 0,
---@type table<string, threadPriority>
prios = {},
pools = {
urgent = {},
normal = {},
background = {},
instant = {},
},
};
do
---@type table<threadPriority, true>
local validPriorities = {
urgent = true,
normal = true,
background = true,
instant = true,
}
-- Add an action to be resumed via OnUpdate
---@param name string
---@param thread thread | function
---@param prio threadPriority?
function threads:Add(name, thread, prio)
if not prio or not validPriorities[prio] then
prio = "normal"
end
if type(thread) == "function" then
thread = coroutine.create(thread)
end
if not self.prios[name] then
self.prios[name] = prio
self.pools[prio][name] = {
thread = thread,
sequence = {}
}
self.size = self.size + 1
self.frame:Show()
end
end
---@param name string
---@param prio threadPriority
function threads:SetPriority(name, prio)
local oldPrio = self.prios[name]
if oldPrio and oldPrio ~= prio then
self.pools[prio][name] = self.pools[oldPrio][name]
self.pools[oldPrio][name] = nil
self.prios[name] = prio
end
end
-- Remove an action from OnUpdate
---@param name string
function threads:Remove(name)
local prio = self.prios[name]
if prio then
local pool = self.pools[prio]
pool[name] = nil
self.prios[name] = nil
self.size = self.size - 1
if self.size == 0 then
self.frame:Hide()
end
end
end
---@param pool threadPool
---@param finish number
---@param defaultEstimate number
local function runThreadPool(pool, finish, defaultEstimate)
local start = debugprofilestop()
if finish <= start then return end
local estimates = {}
local ok, val1, val2
local continue = false
repeat
continue = false
for name, threadData in pairs(pool) do
local estimate = estimates[name] or defaultEstimate
if debugprofilestop() + estimate > finish then
break
else
continue = true
ok, val1, val2 = coroutine.resume(threadData.thread)
if not ok then
geterrorhandler()(val1 .. '\n' .. debugstack(threadData.thread))
elseif coroutine.status(threadData.thread) ~= "dead" then
estimates[name] = type(val1) == "number" and val1 or defaultEstimate
local sequence = val2 or "" --[[@as string]]
threadData.sequence[sequence] = (threadData.sequence[sequence] or 0) + 1
else
threads:Remove(name)
end
end
end
until not continue
end
---@param name string
---@param func thread
---@param limit number
---@param defaultEstimate number?
function threads:Immediate(name, func, limit, defaultEstimate)
self:Add(name, func, "instant")
runThreadPool(self.pools.instant, debugprofilestop() + limit, defaultEstimate or 1000)
if coroutine.status(func) ~= "dead" then
self:SetPriority(name, "urgent")
else
self:Remove(name)
end
end
-- Setup frame
threads.frame:Hide();
threads.frame:SetScript("OnUpdate", function()
local start = debugprofilestop();
runThreadPool(threads.pools.urgent, start + 15000, 1000)
runThreadPool(threads.pools.normal, start + 20, 1)
runThreadPool(threads.pools.background, start + 2, 0.5)
end);
threads.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
threads.frame:RegisterEvent("PLAYER_REGEN_DISABLED")
threads.frame:SetScript("OnEvent", function(self, event)
if event == "PLAYER_REGEN_ENABLED" and self:IsShown() then
self:Hide()
elseif event == "PLAYER_REGEN_DISABLED" and not self:IsShown() and threads.size > 0 then
self:Show()
end
end)
end
Private.Threads = threads;
function WeakAuras.RegisterTriggerSystem(types, triggerSystem)
for _, v in ipairs(types) do
triggerTypes[v] = triggerSystem;
end
tinsert(triggerSystems, triggerSystem);
end
function WeakAuras.RegisterTriggerSystemOptions(types, func)
for _, v in ipairs(types) do
Private.triggerTypesOptions[v] = func;
end
end
function WeakAuras.GetTriggerStateForTrigger(id, triggernum)
if (triggernum == -1) then
return Private.GetGlobalConditionState();
end
triggerState[id][triggernum] = triggerState[id][triggernum] or {}
return triggerState[id][triggernum];
end
function WeakAuras.GetActiveStates(id)
return triggerState[id].activeStates
end
function WeakAuras.GetActiveTriggers(id)
return triggerState[id].triggers
end
do
--- @type table<auraId, boolean>
local visibleFakeStates = {}
--- @type fun(_: any, uid: uid, id: auraId)
local function OnDelete(_, uid, id)
visibleFakeStates[id] = nil
end
--- @type fun(_: any, uid: uid, oldId: auraId, newId: auraId)
local function OnRename(_, uid, oldId, newId)
visibleFakeStates[newId] = visibleFakeStates[oldId]
visibleFakeStates[oldId] = nil
end
Private.callbacks:RegisterCallback("Delete", OnDelete)
Private.callbacks:RegisterCallback("Rename", OnRename)
local UpdateFakeTimesHandle
local function UpdateFakeTimers()
local suspended = Private.PauseAllDynamicGroups()
local t = GetTime()
for id, triggers in pairs(triggerState) do
local changed = false
for triggernum, triggerData in ipairs(triggers) do
for id, state in pairs(triggerData) do
if state.progressType == "timed" and state.expirationTime and state.expirationTime < t and state.duration and state.duration > 0 then
state.expirationTime = t + state.duration
state.changed = true
changed = true
end
end
end
if changed then
Private.UpdatedTriggerState(id)
end
end
Private.ResumeAllDynamicGroups(suspended)
end
function Private.SetFakeStates()
if UpdateFakeTimesHandle then
return
end
for id, states in pairs(triggerState) do
local changed
for triggernum in ipairs(states) do
changed = Private.SetAllStatesHidden(id, triggernum) or changed
end
if changed then
Private.UpdatedTriggerState(id)
end
end
UpdateFakeTimesHandle = timer:ScheduleRepeatingTimer(UpdateFakeTimers, 1)
end
function Private.ClearFakeStates()
timer:CancelTimer(UpdateFakeTimesHandle)
for id in pairs(triggerState) do
Private.FakeStatesFor(id, false)
end
end
function Private.FakeStatesFor(id, visible)
if visibleFakeStates[id] == visible then
return visibleFakeStates[id]
end
if visible then
visibleFakeStates[id] = true
Private.UpdateFakeStatesFor(id)
else
visibleFakeStates[id] = false
if triggerState[id] then
local changed = false
for triggernum in ipairs(triggerState[id]) do
changed = Private.SetAllStatesHidden(id, triggernum) or changed
end
if changed then
Private.UpdatedTriggerState(id)
end
end
end
return not visibleFakeStates[id]
end
function Private.UpdateFakeStatesFor(id)
if (WeakAuras.IsOptionsOpen() and visibleFakeStates[id]) then
local data = WeakAuras.GetData(id)
if (data) then
for triggernum in ipairs(data.triggers) do
Private.SetAllStatesHidden(id, triggernum)
local triggerSystem = GetTriggerSystem(data, triggernum)
if triggerSystem and triggerSystem.CreateFakeStates then
triggerSystem.CreateFakeStates(id, triggernum)
end
end
Private.UpdatedTriggerState(id)
if WeakAuras.GetMoverSizerId() == id then
WeakAuras.SetMoverSizer(id)
end
end
end
end
end
--- @type fun(id: auraId, triggernum: integer, cloneId: string)
local function stopAutoHideTimer(id, triggernum, cloneId)
if(timers[id] and timers[id][triggernum] and timers[id][triggernum][cloneId]) then
local record = timers[id][triggernum][cloneId];
if (record.handle) then
timer:CancelTimer(record.handle);
end
record.handle = nil;
record.expirationTime = nil;
record.state = nil
end
end
--- @type fun(id: auraId, triggernum: integer, cloneId: string, state: state)
local function startStopTimers(id, cloneId, triggernum, state)
if not state.show or not state.autoHide then
stopAutoHideTimer(id, triggernum, cloneId)
return
end
-- state.autoHide can be a timer, or a boolean
-- if it's a bool, for backwards compability we look at paused
local expirationTime
if type(state.autoHide) == "boolean" then
if state.paused then
stopAutoHideTimer(id, triggernum, cloneId)
return
else
if state.expirationTime == nil and type(state.duration) == "number" then
-- Set the expiration time, because users rely on that, even though it's wrong to do
state.expirationTime = GetTime() + state.duration
end
expirationTime = state.expirationTime
end
elseif type(state.autoHide) == "number" then
expirationTime = state.autoHide
end
timers[id] = timers[id] or {};
timers[id][triggernum] = timers[id][triggernum] or {};
timers[id][triggernum][cloneId] = timers[id][triggernum][cloneId] or {};
local record = timers[id][triggernum][cloneId];
if (record.expirationTime ~= expirationTime or record.state ~= state) then
if (record.handle ~= nil) then
timer:CancelTimer(record.handle);
end
if expirationTime then
record.handle = timer:ScheduleTimerFixed(
function()
if (state.show ~= false and state.show ~= nil) then
state.show = false;
state.changed = true;
-- if the trigger has updated then check to see if it is flagged for WatchedTrigger and send to queue if it is
if Private.watched_trigger_events[id] and Private.watched_trigger_events[id][triggernum] then
Private.AddToWatchedTriggerDelay(id, triggernum)
end
Private.UpdatedTriggerState(id);
end
end,
expirationTime - GetTime());
record.expirationTime = expirationTime;
record.state = state
end
end
end
local function ApplyStateToRegion(id, cloneId, region, parent)
-- Force custom text function to be run again
region.values.lastCustomTextUpdate = nil
region:Update();
region.subRegionEvents:Notify("Update", region.state, region.states)
UpdateMouseoverTooltip(region);
region:Expand();
if parent and parent.ActivateChild then
parent:ActivateChild(id, cloneId)
end
end
-- Fallbacks if the states are empty
local emptyState = {};
emptyState[""] = {};
local function applyToTriggerStateTriggers(stateShown, id, triggernum)
if (stateShown and not triggerState[id].triggers[triggernum]) then
triggerState[id].triggers[triggernum] = true;
triggerState[id].triggerCount = triggerState[id].triggerCount + 1;
return true;
elseif (not stateShown and triggerState[id].triggers[triggernum]) then
triggerState[id].triggers[triggernum] = false;
triggerState[id].triggerCount = triggerState[id].triggerCount - 1;
return true;
end
return false;
end
local function evaluateTriggerStateTriggers(id)
local result = false;
if WeakAuras.IsOptionsOpen() then
-- While the options are open ignore the combination function
return triggerState[id].triggerCount > 0
end
if (triggerState[id].disjunctive == "any" and triggerState[id].triggerCount > 0) then
result = true;
elseif(triggerState[id].disjunctive == "all" and triggerState[id].triggerCount == triggerState[id].numTriggers) then
result = true;
else
if (triggerState[id].disjunctive == "custom" and triggerState[id].triggerLogicFunc) then
Private.ActivateAuraEnvironment(id)
local ok, returnValue = xpcall(triggerState[id].triggerLogicFunc, Private.GetErrorHandlerId(id, L["Custom Trigger Combination"]), triggerState[id].triggers);
Private.ActivateAuraEnvironment()
result = ok and returnValue;
end
end
return result;
end
local function ApplyStatesToRegions(id, activeTrigger, states)
-- Show new clones
local data = WeakAuras.GetData(id)
local parent
if data and data.parent then
parent = Private.EnsureRegion(data.parent)
end
if parent and parent.Suspend then
parent:Suspend()
end
for cloneId, state in pairs(states) do
if (state.show) then
local region = Private.EnsureRegion(id, cloneId);
local applyChanges = not region.toShow or state.changed or region.state ~= state
region.state = state
region.states = region.states or {}
for triggernum = -1, triggerState[id].numTriggers do
local triggerState
if triggernum == activeTrigger then
triggerState = state
else
local triggerStates = WeakAuras.GetTriggerStateForTrigger(id, triggernum)
triggerState = triggerStates[cloneId] or triggerStates[""] or {}
end
if triggernum > 0 then
applyChanges = applyChanges or region.states[triggernum] ~= triggerState or (triggerState and triggerState.changed)
or region.states[triggernum] ~= triggerState
or (triggerState and triggerState.changed)
end
region.states[triggernum] = triggerState
end
if (applyChanges) then
ApplyStateToRegion(id, cloneId, region, parent);
Private.RunConditions(region, data.uid, not state.show)
end
end
end
if parent and parent.Resume then
parent:Resume()
end
end
-- handle trigger updates that have been requested to be sent into custom
-- we need the id and triggernum that's changing, but can't send the ScanEvents to the custom trigger until after UpdatedTriggerState has fired
local delayed_watched_trigger = {}
function Private.AddToWatchedTriggerDelay(id, triggernum)
delayed_watched_trigger[id] = delayed_watched_trigger[id] or {}
tinsert(delayed_watched_trigger[id], triggernum)
end
Private.callbacks:RegisterCallback("Delete", function(_, uid, id)
delayed_watched_trigger[id] = nil
end)
Private.callbacks:RegisterCallback("Rename", function(_, uid, oldId, newId)
delayed_watched_trigger[newId] = delayed_watched_trigger[oldId]
delayed_watched_trigger[oldId] = nil
end)
function Private.SendDelayedWatchedTriggers()
for id in pairs(delayed_watched_trigger) do
local watched = delayed_watched_trigger[id]
-- Since the observers are themselves observable, we set the list of observers to
-- empty here.
delayed_watched_trigger[id] = {}
Private.ScanEventsWatchedTrigger(id, watched)
end
end
function Private.UpdatedTriggerState(id)
if (not triggerState[id]) then
return;
end
local changed = false;
for triggernum = 1, triggerState[id].numTriggers do
triggerState[id][triggernum] = triggerState[id][triggernum] or {};
local anyStateShown = false;
for cloneId, state in pairs(triggerState[id][triggernum]) do
state.trigger = db.displays[id].triggers[triggernum] and db.displays[id].triggers[triggernum].trigger;
state.triggernum = triggernum;
state.id = id;
if (state.changed) then
startStopTimers(id, cloneId, triggernum, state);
end
anyStateShown = anyStateShown or state.show;
end
-- Update triggerState.triggers
changed = applyToTriggerStateTriggers(anyStateShown, id, triggernum) or changed;
end
-- Figure out whether we should be shown or not
local show = triggerState[id].show;
if (changed or show == nil) then
show = evaluateTriggerStateTriggers(id);
end
-- Figure out which subtrigger is active, and if it changed
local newActiveTrigger = triggerState[id].activeTriggerMode;
if (newActiveTrigger == Private.trigger_modes.first_active) then
-- Mode is first active trigger, so find a active trigger
for i = 1, triggerState[id].numTriggers do
if (triggerState[id].triggers[i]) then
newActiveTrigger = i;
break;
end
end
end
local oldShow = triggerState[id].show;
triggerState[id].activeTrigger = newActiveTrigger;
triggerState[id].show = show;
triggerState[id].fallbackStates = nil
local activeTriggerState = WeakAuras.GetTriggerStateForTrigger(id, newActiveTrigger);
if (not next(activeTriggerState)) then
if (show) then
activeTriggerState = CreateFallbackState(id, newActiveTrigger)
else
activeTriggerState = emptyState;
end
elseif (show) then
local needsFallback = true;
for _, state in pairs(activeTriggerState) do
if (state.show) then
needsFallback = false;
break;
end
end
if (needsFallback) then
activeTriggerState = CreateFallbackState(id, newActiveTrigger)
end
end
triggerState[id].activeStates = activeTriggerState
local region;
-- Now apply
if (show and not oldShow) then -- Hide => Show
ApplyStatesToRegions(id, newActiveTrigger, activeTriggerState);
elseif (not show and oldShow) then -- Show => Hide
for _, clone in pairs(clones[id]) do
clone:Collapse()
end
if Private.regions[id] and Private.regions[id].region then
Private.regions[id].region:Collapse()
end
elseif (show and oldShow) then -- Already shown, update regions
-- Hide old clones
for cloneId, clone in pairs(clones[id]) do
if (not activeTriggerState[cloneId] or not activeTriggerState[cloneId].show) then
clone:Collapse()
end
end
if (not activeTriggerState[""] or not activeTriggerState[""].show) then
if Private.regions[id] and Private.regions[id].region then
Private.regions[id].region:Collapse()
end
end
-- Show new states
ApplyStatesToRegions(id, newActiveTrigger, activeTriggerState);
end
for triggernum = 1, triggerState[id].numTriggers do
triggerState[id][triggernum] = triggerState[id][triggernum] or {};
for cloneId, state in pairs(triggerState[id][triggernum]) do
if (not state.show) then
triggerState[id][triggernum][cloneId] = nil;
end
state.changed = false;
end
end
-- once updatedTriggerStates is complete, and empty states removed, etc., then check for queued watched triggers update
Private.SendDelayedWatchedTriggers()
end
function Private.RunCustomTextFunc(region, customFunc)
if not customFunc then
return nil
end
local state = region.state
Private.ActivateAuraEnvironment(region.id, region.cloneId, region.state, region.states);
local progress = Private.dynamic_texts.p.func(Private.dynamic_texts.p.get(state), state, 1)
local dur = Private.dynamic_texts.t.func(Private.dynamic_texts.t.get(state), state, 1)
local name = Private.dynamic_texts.n.func(Private.dynamic_texts.n.get(state))
local icon = Private.dynamic_texts.i.func(Private.dynamic_texts.i.get(state))
local stacks = Private.dynamic_texts.s.func(Private.dynamic_texts.s.get(state))
local expirationTime
local duration
if state then
if state.progressType == "timed" then
expirationTime = state.expirationTime
duration = state.duration
else
expirationTime = state.total
duration = state.value
end
end
local custom = {select(2, xpcall(customFunc, Private.GetErrorHandlerId(region.id, L["Custom Text Function"]), expirationTime or math.huge, duration or 0, progress, dur, name, icon, stacks))}
Private.ActivateAuraEnvironment(nil)
return custom
end
local function ReplaceValuePlaceHolders(textStr, region, customFunc, state, formatter, trigger)
local value;
if string.sub(textStr, 1, 1) == "c" then
local custom
if customFunc then
custom = Private.RunCustomTextFunc(region, customFunc)
else
custom = region.values.custom
end
local index = tonumber(textStr:match("^c(%d+)$") or 1)
if custom then
value = custom[index]
end
if value == nil then value = "" end
if formatter then
value = formatter(value, state)
end
if custom then
value = WeakAuras.EnsureString(value)
end
else
local variable = Private.dynamic_texts[textStr];
if (not variable) then
return nil;
end
value = variable.get(state)
if formatter then
value = formatter(value, state, trigger)
elseif variable.func then
value = variable.func(value)
end
end
return type(value) ~= "table" and value or ""
end
-- States:
-- 0 Normal state, text is just appended to result. Can transition to percent start state 1 via %
-- 1 Percent start state, entered via %. Can transition to via { to braced, via % to normal, AZaz09 to percent rest state
-- 2 Percent rest state, stay in it via AZaz09, transition to normal on anything else
-- 3 Braced state, } transitions to normal, everything else stay in braced state
local function nextState(char, state)
if state == 0 then -- Normal State
if char == 37 then -- % sign
return 1 -- Enter Percent state
end
return 0
elseif state == 1 then -- Percent Start State
if char == 37 then -- % sign
return 0 -- Return to normal state
elseif char == 123 then -- { sign
return 3 -- Enter Braced state
elseif (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
-- 0-9a-zA-Z or dot character
return 2 -- Enter Percent rest state
end
return 0 -- % followed by non alpha-numeric. Back to normal state
elseif state == 2 then
if (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
return 2 -- Continue in same state
end
if char == 37 then
return 1 -- End of %, but also start of new %
end
return 0 -- Back to normal
elseif state == 3 then
if char == 125 then -- } closing brace
return 0 -- Back to normal
end
return 3
end
-- Shouldn't happen
return state
end
local function ContainsPlaceHolders(textStr, symbolFunc, checkDoublePercent)
if not textStr then
return false
end
local endPos = textStr:len();
local state = 0
local currentPos = 1
local start = 1
local containsDoublePercent = false
while currentPos <= endPos do
local char = string.byte(textStr, currentPos);
local nextState = nextState(char, state)
if state == 1 then -- Last char was a %
if char == 123 then -- {
start = currentPos + 1
elseif char == 37 then -- %
containsDoublePercent = true
start = currentPos
else
start = currentPos
end
elseif state == 2 or state == 3 then
if nextState == 0 or nextState == 1 then
local symbol = string.sub(textStr, start, currentPos - 1)
if symbolFunc(symbol) then
return true
end
end
end
state = nextState
currentPos = currentPos + 1
end
if state == 2 then
local symbol = string.sub(textStr, start, currentPos - 1)
if symbolFunc(symbol) then
return true
end
end
if checkDoublePercent then
return containsDoublePercent
end
return false
end
function Private.ContainsCustomPlaceHolder(textStr)
return ContainsPlaceHolders(textStr, function(symbol)
return string.match(symbol, "^c%d*$")
end)
end
function Private.ContainsPlaceHolders(textStr, toCheck)
return ContainsPlaceHolders(textStr, function(symbol)
if symbol:len() == 1 and toCheck:find(symbol, 1, true) then
return true
end
local _, last = symbol:find("^%d+%.")
if not last then
return false
end
symbol = symbol:sub(last + 1)
if symbol:len() == 1 and toCheck:find(symbol, 1, true) then
return true
end
end)
end
function Private.ContainsAnyPlaceHolders(textStr)
return ContainsPlaceHolders(textStr, function(symbol) return true end, true)
end
Private.ContainsPlaceHoldersPredicate = ContainsPlaceHolders
local function ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
local triggerNum, sym = string.match(symbol, "(.+)%.(.+)")
triggerNum = triggerNum and tonumber(triggerNum)
if triggerNum and sym then
if regionStates[triggerNum] then
if (useHiddenStates or regionStates[triggerNum].show) then
if regionStates[triggerNum][sym] then
local value = regionStates[triggerNum][sym]
if formatters[symbol] then
return tostring(formatters[symbol](value, regionStates[triggerNum], triggerNum) or "") or ""
else
return tostring(value) or ""
end
else
local value = ReplaceValuePlaceHolders(sym, region, customFunc, regionStates[triggerNum], formatters[symbol], triggerNum);
return value or ""
end
end
end
return ""
elseif regionState[symbol] then
if(useHiddenStates or regionState.show) then
local value = regionState[symbol]
if formatters[symbol] then
return tostring(formatters[symbol](value, regionState, triggerState[regionState.id].activeTrigger) or "") or ""
else
return tostring(value) or ""
end
end
return ""
else
local activeTrigger = triggerState[regionState.id].activeTrigger
local value = (useHiddenStates or regionState.show)
and ReplaceValuePlaceHolders(symbol, region, customFunc, regionState, formatters[symbol], activeTrigger)
return value or ""
end
end
function Private.ReplacePlaceHolders(textStr, region, customFunc, useHiddenStates, formatters)
local regionValues = region.values;
local regionState = region.state or {};
local regionStates = region.states or {};
if (not regionState and not regionValues) then
return;
end
local endPos = textStr:len();
if (endPos < 2) then
textStr = textStr:gsub("\\n", "\n");
return textStr;
end
if (endPos == 2) then
if string.byte(textStr, 1) == 37 then
local symbol = string.sub(textStr, 2)
if symbol == "%" then
return "%" -- Double % input
end
local value = ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters);
if (value) then
textStr = tostring(value);
end
end
textStr = textStr:gsub("\\n", "\n");
return textStr;
end
local result = ""
local currentPos = 1 -- Position of the "cursor"
local state = 0
local start = 1 -- Start of whatever "word" we are currently considering, doesn't include % or {} symbols
while currentPos <= endPos do
local char = string.byte(textStr, currentPos);
if state == 0 then -- Normal State
if char == 37 then -- % sign
if currentPos > start then
result = result .. string.sub(textStr, start, currentPos - 1)
end
end
elseif state == 1 then -- Percent Start State
if char == 123 then -- { sign
start = currentPos + 1
else
start = currentPos
end
elseif state == 2 then -- Percent Rest State
if (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
-- 0-9a-zA-Z or dot character
else -- End of variable
local symbol = string.sub(textStr, start, currentPos - 1)
result = result .. ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
if char == 37 then
-- Do nothing
else
start = currentPos
end
end
elseif state == 3 then
if char == 125 then -- } closing brace
local symbol = string.sub(textStr, start, currentPos - 1)
result = result .. ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
start = currentPos + 1
end
end
state = nextState(char, state)
currentPos = currentPos + 1
end
if state == 0 and currentPos > start then
result = result .. string.sub(textStr, start, currentPos - 1)
elseif state == 2 and currentPos > start then
local symbol = string.sub(textStr, start, currentPos - 1)
result = result .. ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
elseif state == 1 then
result = result .. "%"
end
textStr = result:gsub("\\n", "\n");
return textStr;
end
function Private.ParseTextStr(textStr, symbolCallback)
if not textStr then
return
end
local endPos = textStr:len();
local currentPos = 1 -- Position of the "cursor"
local state = 0
local start = 1 -- Start of whatever "word" we are currently considering, doesn't include % or {} symbols
while currentPos <= endPos do
local char = string.byte(textStr, currentPos);
if state == 0 then -- Normal State
elseif state == 1 then -- Percent Start State
if char == 123 then
start = currentPos + 1
else
start = currentPos
end
elseif state == 2 then -- Percent Rest State
if (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
-- 0-9a-zA-Z or dot character
else -- End of variable
local symbol = string.sub(textStr, start, currentPos - 1)
symbolCallback(symbol)
if char == 37 then
-- Do nothing
else
start = currentPos
end
end
elseif state == 3 then
if char == 125 then -- } closing brace
local symbol = string.sub(textStr, start, currentPos - 1)
symbolCallback(symbol)
start = currentPos + 1
end
end
state = nextState(char, state)
currentPos = currentPos + 1
end
if state == 2 and currentPos > start then
local symbol = string.sub(textStr, start, currentPos - 1)
symbolCallback(symbol)
end
end
function Private.CreateFormatters(input, getter, withoutColor, data)
local seenSymbols = {}
local formatters = {}
local everyFrameFormatters = {}
local parseFn = function(symbol)
if not seenSymbols[symbol] then
local _, sym = string.match(symbol, "(.+)%.(.+)")
sym = sym or symbol
if sym == "i" then
-- Do nothing
else
local default = (sym == "p" or sym == "t") and "timed" or "none"
local selectedFormat = getter(symbol .. "_format", default)
if (Private.format_types[selectedFormat]) then
formatters[symbol], everyFrameFormatters[symbol] = Private.format_types[selectedFormat].CreateFormatter(symbol, getter, withoutColor, data)
end
end
end
seenSymbols[symbol] = true
end
if type(input) == "string" then
Private.ParseTextStr(input, parseFn)
elseif type(input) == "table" then
for _, v in ipairs(input) do
Private.ParseTextStr(v, parseFn)
end
end
return formatters, everyFrameFormatters
end
function Private.AnyEveryFrameFormatters(textStr, everyFrameFormatters)
if next(everyFrameFormatters) then
local function predicate(symbol)
if everyFrameFormatters[symbol] then
return true
end
end
return Private.ContainsPlaceHoldersPredicate(textStr, predicate)
end
end
function Private.IsAuraActive(uid)
local id = Private.UIDtoID(uid)
local active = triggerState[id];
return active and active.show;
end
function WeakAuras.IsAuraActive(id)
local active = triggerState[id]
return active and active.show
end
function Private.ActiveTrigger(uid)
local id = Private.UIDtoID(uid)
return triggerState[id] and triggerState[id].activeTrigger
end
-- Attach to Cursor/Frames code
-- Very simple function to convert a hsv angle to a color with
-- value hardcoded to 1 and saturation hardcoded to 0.75
local function colorWheel(angle)
local hh = angle / 60;
local i = floor(hh);
local ff = hh - i;
local p = 0;
local q = 0.75 * (1.0 - ff);
local t = 0.75 * ff;
if (i == 0) then
return 0.75, t, p;
elseif (i == 1) then
return q, 0.75, p;
elseif (i == 2) then
return p, 0.75, t;
elseif (i == 3) then
return p, q, 0.75;
elseif (i == 4) then
return t, p, 0.75;
else
return 0.75, p, q;
end
end
local function xPositionNextToOptions()
local xOffset;
local optionsFrame = Private.OptionsFrame();
local centerX = (optionsFrame:GetLeft() + optionsFrame:GetRight()) / 2;
if (centerX > GetScreenWidth() / 2) then
if (optionsFrame:GetLeft() > 400) then
xOffset = optionsFrame:GetLeft() - 200;
else
xOffset = optionsFrame:GetLeft() / 2;
end
else
if (GetScreenWidth() - optionsFrame:GetRight() > 400 ) then
xOffset = optionsFrame:GetRight() + 200;
else
xOffset = (GetScreenWidth() + optionsFrame:GetRight()) / 2;
end
end
return xOffset;
end
local mouseFrame;
local function ensureMouseFrame()
if (mouseFrame) then
return;
end
---@class Frame
mouseFrame = CreateFrame("Frame", "WeakAurasAttachToMouseFrame", UIParent);
mouseFrame.attachedVisibleFrames = {};
mouseFrame:SetWidth(1);
mouseFrame:SetHeight(1);
local moverFrame = CreateFrame("Frame", "WeakAurasMousePointerFrame", mouseFrame);
mouseFrame.moverFrame = moverFrame;
moverFrame:SetPoint("TOPLEFT", mouseFrame, "CENTER");
moverFrame:SetWidth(32);
moverFrame:SetHeight(32);
moverFrame:SetFrameStrata("FULLSCREEN"); -- above settings dialog
moverFrame:EnableMouse(true)
moverFrame:SetScript("OnMouseDown", function()
mouseFrame:SetMovable(true);
mouseFrame:StartMoving()
end);
moverFrame:SetScript("OnMouseUp", function()
mouseFrame:StopMovingOrSizing();
mouseFrame:SetMovable(false);
local xOffset = mouseFrame:GetRight() - GetScreenWidth();
local yOffset = mouseFrame:GetTop() - GetScreenHeight();
db.mousePointerFrame = db.mousePointerFrame or {};
db.mousePointerFrame.xOffset = xOffset;
db.mousePointerFrame.yOffset = yOffset;
end);
moverFrame.colorWheelAnimation = function()
local angle = ((GetTime() - moverFrame.startTime) % 5) / 5 * 360;
moverFrame.texture:SetVertexColor(colorWheel(angle));
end;
local texture = moverFrame:CreateTexture(nil, "BACKGROUND");
moverFrame.texture = texture;
texture:SetAllPoints(moverFrame);
texture:SetTexture("Interface\\Cursor\\Point");
local label = moverFrame:CreateFontString(nil, "BACKGROUND", "GameFontHighlightSmall")
label:SetJustifyH("LEFT")
label:SetJustifyV("TOP")
label:SetPoint("TOPLEFT", moverFrame, "BOTTOMLEFT");
label:SetText("WeakAuras Anchor");
moverFrame:Hide();
mouseFrame.OptionsOpened = function()
if (db.mousePointerFrame) then
-- Restore from settings
mouseFrame:ClearAllPoints();
mouseFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", db.mousePointerFrame.xOffset, db.mousePointerFrame.yOffset);
else
-- Fnd a suitable position
local optionsFrame = Private.OptionsFrame();
local yOffset = (optionsFrame:GetTop() + optionsFrame:GetBottom()) / 2;
local xOffset = xPositionNextToOptions();
-- We use the top right, because the main frame uses the top right as the reference too
mouseFrame:ClearAllPoints();
mouseFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", xOffset - GetScreenWidth(), yOffset - GetScreenHeight());
end
-- Change the color of the mouse cursor
moverFrame.startTime = GetTime();
moverFrame:SetScript("OnUpdate", moverFrame.colorWheelAnimation);
mouseFrame:SetScript("OnUpdate", nil);
end
mouseFrame.moveWithMouse = function()
local scale = 1 / UIParent:GetEffectiveScale();
local x, y = GetCursorPosition();
mouseFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x * scale, y * scale);
end
mouseFrame.OptionsClosed = function()
moverFrame:Hide();
mouseFrame:ClearAllPoints();
mouseFrame:SetScript("OnUpdate", mouseFrame.moveWithMouse);
moverFrame:SetScript("OnUpdate", nil);
wipe(mouseFrame.attachedVisibleFrames);
end
mouseFrame.expand = function(self, id)
local data = WeakAuras.GetData(id);
if (data.anchorFrameType == "MOUSE") then
self.attachedVisibleFrames[id] = true;
self:updateVisible();
end
end
mouseFrame.collapse = function(self, id)
self.attachedVisibleFrames[id] = nil;
self:updateVisible();
end
mouseFrame.rename = function(self, oldid, newid)
self.attachedVisibleFrames[newid] = self.attachedVisibleFrames[oldid];
self.attachedVisibleFrames[oldid] = nil;
self:updateVisible();
end
mouseFrame.delete = function(self, id)
self.attachedVisibleFrames[id] = nil;
self:updateVisible();
end
mouseFrame.anchorFrame = function(self, id, anchorFrameType)
if (anchorFrameType == "MOUSE") then
self.attachedVisibleFrames[id] = true;
else
self.attachedVisibleFrames[id] = nil;
end
self:updateVisible();
end
mouseFrame.updateVisible = function(self)
if (not WeakAuras.IsOptionsOpen()) then
return;
end
if (next(self.attachedVisibleFrames)) then
mouseFrame.moverFrame:Show();
else
mouseFrame.moverFrame:Hide();
end
end
if (WeakAuras.IsOptionsOpen()) then
mouseFrame:OptionsOpened();
else
mouseFrame:OptionsClosed();
end
Private.mouseFrame = mouseFrame;
end
local personalRessourceDisplayFrame;
function Private.ensurePRDFrame()
if (personalRessourceDisplayFrame) then
return;
end
personalRessourceDisplayFrame = CreateFrame("Frame", "WeakAurasAttachToPRD", UIParent);
personalRessourceDisplayFrame:Hide();
personalRessourceDisplayFrame.attachedVisibleFrames = {};
-- force an early frame draw; otherwise this frame won't be drawn until the next frame,
-- and any attached auras won't have a valid rect
personalRessourceDisplayFrame:SetPoint("CENTER", UIParent, "CENTER");
personalRessourceDisplayFrame:SetSize(16, 16)
personalRessourceDisplayFrame:GetSize()
Private.personalRessourceDisplayFrame = personalRessourceDisplayFrame;
local moverFrame = CreateFrame("Frame", "WeakAurasPRDMoverFrame", personalRessourceDisplayFrame);
personalRessourceDisplayFrame.moverFrame = moverFrame;
moverFrame:SetPoint("TOPLEFT", personalRessourceDisplayFrame, "TOPLEFT", -2, 2);
moverFrame:SetPoint("BOTTOMRIGHT", personalRessourceDisplayFrame, "BOTTOMRIGHT", 2, -2);
moverFrame:SetFrameStrata("FULLSCREEN"); -- above settings dialog
moverFrame:EnableMouse(true)
moverFrame:SetScript("OnMouseDown", function()
personalRessourceDisplayFrame:SetMovable(true);
personalRessourceDisplayFrame:StartMoving()
end);
moverFrame:SetScript("OnMouseUp", function()
personalRessourceDisplayFrame:StopMovingOrSizing();
personalRessourceDisplayFrame:SetMovable(false);
local xOffset = personalRessourceDisplayFrame:GetRight();
local yOffset = personalRessourceDisplayFrame:GetTop();
db.personalRessourceDisplayFrame = db.personalRessourceDisplayFrame or {};
local scale = UIParent:GetEffectiveScale() / personalRessourceDisplayFrame:GetEffectiveScale();
db.personalRessourceDisplayFrame.xOffset = xOffset / scale - GetScreenWidth();
db.personalRessourceDisplayFrame.yOffset = yOffset / scale - GetScreenHeight();
end);
moverFrame:Hide();
local texture = moverFrame:CreateTexture(nil, "BACKGROUND");
personalRessourceDisplayFrame.texture = texture;
texture:SetAllPoints(moverFrame);
texture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\PRDFrame");
local label = moverFrame:CreateFontString(nil, "BACKGROUND", "GameFontHighlight")
label:SetPoint("CENTER", moverFrame, "CENTER");
label:SetText("WeakAuras Anchor");
personalRessourceDisplayFrame:RegisterEvent('NAME_PLATE_UNIT_ADDED');
personalRessourceDisplayFrame:RegisterEvent('NAME_PLATE_UNIT_REMOVED');
personalRessourceDisplayFrame.Attach = function(self, frame, frameTL, frameBR)
self:SetParent(frame);
self:ClearAllPoints();
self:SetPoint("TOPLEFT", frameTL, "TOPLEFT");
self:SetPoint("BOTTOMRIGHT", frameBR, "BOTTOMRIGHT");
self:Show()
end
personalRessourceDisplayFrame.Detach = function(self, frame)
self:ClearAllPoints();
self:Hide()
self:SetParent(UIParent)
end
personalRessourceDisplayFrame.OptionsOpened = function()
personalRessourceDisplayFrame:Detach();
personalRessourceDisplayFrame:SetScript("OnEvent", nil);
personalRessourceDisplayFrame:ClearAllPoints();
personalRessourceDisplayFrame:Show()
local xOffset, yOffset;
if (db.personalRessourceDisplayFrame) then
xOffset = db.personalRessourceDisplayFrame.xOffset;
yOffset = db.personalRessourceDisplayFrame.yOffset;
end
-- Calculate size of self nameplate
local prdWidth;
local prdHeight;
if (KuiNameplatesCore and KuiNameplatesCore.profile) then
prdWidth = KuiNameplatesCore.profile.frame_width_personal;
prdHeight = KuiNameplatesCore.profile.frame_height_personal;
if (KuiNameplatesCore.profile.ignore_uiscale) then
local _, screenWidth = GetPhysicalScreenSize();
local uiScale = 1;
if (screenWidth) then
uiScale = 768 / screenWidth;
end
personalRessourceDisplayFrame:SetScale(uiScale / UIParent:GetEffectiveScale());
else
personalRessourceDisplayFrame:SetScale(1);
end
personalRessourceDisplayFrame.texture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\PRDFrameKui");
else
local namePlateVerticalScale = tonumber(GetCVar("NamePlateVerticalScale"));
local zeroBasedScale = namePlateVerticalScale - 1.0;
local clampedZeroBasedScale = Saturate(zeroBasedScale);
local horizontalScale = tonumber(GetCVar("NamePlateHorizontalScale"));
local baseNamePlateWidth = NamePlateDriverFrame.baseNamePlateWidth;
prdWidth = baseNamePlateWidth * horizontalScale * Lerp(1.1, 1.0, clampedZeroBasedScale) - 24;
prdHeight = 4 * namePlateVerticalScale * Lerp(1.2, 1.0, clampedZeroBasedScale) * 2 + 1;
personalRessourceDisplayFrame:SetScale(1 / UIParent:GetEffectiveScale());
personalRessourceDisplayFrame.texture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\PRDFrame");
end
local scale = UIParent:GetEffectiveScale() / personalRessourceDisplayFrame:GetEffectiveScale();
if (not xOffset or not yOffset) then
local optionsFrame = Private.OptionsFrame();
yOffset = optionsFrame:GetBottom() + prdHeight / scale - GetScreenHeight();
xOffset = xPositionNextToOptions() + prdWidth / 2 / scale - GetScreenWidth();
end
xOffset = xOffset * scale;
yOffset = yOffset * scale;
personalRessourceDisplayFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", xOffset, yOffset);
personalRessourceDisplayFrame:SetPoint("BOTTOMLEFT", UIParent, "TOPRIGHT", xOffset - prdWidth, yOffset - prdHeight);
end
personalRessourceDisplayFrame.OptionsClosed = function()
personalRessourceDisplayFrame:SetScale(1);
local frame = C_NamePlate.GetNamePlateForUnit("player");
if (frame) then
if (Plater and frame.unitFrame.PlaterOnScreen) then
personalRessourceDisplayFrame:Attach(frame, frame.unitFrame.healthBar, frame.unitFrame.powerBar);
elseif (frame.kui and frame.kui.bg and frame.kui:IsShown()) then
personalRessourceDisplayFrame:Attach(frame.kui, frame.kui.bg, frame.kui.bg);
elseif (ElvUIPlayerNamePlateAnchor) then
personalRessourceDisplayFrame:Attach(ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor);
else
personalRessourceDisplayFrame:Attach(frame, frame.UnitFrame.healthBar, NamePlateDriverFrame.classNamePlatePowerBar);
end
else
personalRessourceDisplayFrame:Detach();
personalRessourceDisplayFrame:Hide();
end
personalRessourceDisplayFrame:SetScript("OnEvent", personalRessourceDisplayFrame.eventHandler);
personalRessourceDisplayFrame.texture:Hide();
personalRessourceDisplayFrame.moverFrame:Hide();
wipe(personalRessourceDisplayFrame.attachedVisibleFrames);
end
personalRessourceDisplayFrame.eventHandler = function(self, event, nameplate)
Private.StartProfileSystem("prd");
if (event == "NAME_PLATE_UNIT_ADDED") then
if (UnitIsUnit(nameplate, "player")) then
local frame = C_NamePlate.GetNamePlateForUnit("player");
if (frame) then
if (Plater and frame.unitFrame.PlaterOnScreen) then
personalRessourceDisplayFrame:Attach(frame, frame.unitFrame.healthBar, frame.unitFrame.powerBar);
elseif (frame.kui and frame.kui.bg and frame.kui:IsShown()) then
personalRessourceDisplayFrame:Attach(frame.kui, KuiNameplatesPlayerAnchor, KuiNameplatesPlayerAnchor);
elseif (ElvUIPlayerNamePlateAnchor) then
personalRessourceDisplayFrame:Attach(ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor);
else
personalRessourceDisplayFrame:Attach(frame, frame.UnitFrame.healthBar, NamePlateDriverFrame.classNamePlatePowerBar);
end
personalRessourceDisplayFrame:Show();
db.personalRessourceDisplayFrame = db.personalRessourceDisplayFrame or {};
else
personalRessourceDisplayFrame:Detach();
personalRessourceDisplayFrame:Hide();
end
end
elseif (event == "NAME_PLATE_UNIT_REMOVED") then
if (UnitIsUnit(nameplate, "player")) then
personalRessourceDisplayFrame:Detach();
personalRessourceDisplayFrame:Hide();
end
end
Private.StopProfileSystem("prd");
end
personalRessourceDisplayFrame.expand = function(self, id)
local data = WeakAuras.GetData(id);
if (data.anchorFrameType == "PRD") then
self.attachedVisibleFrames[id] = true;
self:updateVisible();
end
end
personalRessourceDisplayFrame.collapse = function(self, id)
self.attachedVisibleFrames[id] = nil;
self:updateVisible();
end
personalRessourceDisplayFrame.rename = function(self, oldid, newid)
self.attachedVisibleFrames[newid] = self.attachedVisibleFrames[oldid];
self.attachedVisibleFrames[oldid] = nil;
self:updateVisible();
end
personalRessourceDisplayFrame.delete = function(self, id)
self.attachedVisibleFrames[id] = nil;
self:updateVisible();
end
personalRessourceDisplayFrame.anchorFrame = function(self, id, anchorFrameType)
if (anchorFrameType == "PRD" or anchorFrameType == "NAMEPLATE") then
self.attachedVisibleFrames[id] = true;
else
self.attachedVisibleFrames[id] = nil;
end
self:updateVisible();
end
personalRessourceDisplayFrame.updateVisible = function(self)
if (not WeakAuras.IsOptionsOpen()) then
return;
end
if (next(self.attachedVisibleFrames)) then
personalRessourceDisplayFrame.texture:Show();
personalRessourceDisplayFrame.moverFrame:Show();
personalRessourceDisplayFrame:Show();
else
personalRessourceDisplayFrame.texture:Hide();
personalRessourceDisplayFrame.moverFrame:Hide();
personalRessourceDisplayFrame:Hide();
end
end
if (WeakAuras.IsOptionsOpen()) then
personalRessourceDisplayFrame.OptionsOpened();
else
personalRessourceDisplayFrame.OptionsClosed();
end
Private.personalRessourceDisplayFrame = personalRessourceDisplayFrame
end
local postPonedAnchors = {};
local anchorTimer
local function tryAnchorAgain()
local delayed = postPonedAnchors;
postPonedAnchors = {};
anchorTimer = nil;
for id, _ in pairs(delayed) do
local data = WeakAuras.GetData(id);
local region = WeakAuras.GetRegion(id);
if (data and region) then
local parent = WeakAurasFrame;
local parentData
if data.parent then
parentData = WeakAuras.GetData(data.parent)
if parentData and Private.EnsureRegion(data.parent) then
parent = Private.regions[data.parent].region
end
end
if not parentData or parentData.regionType ~= "dynamicgroup" then
Private.AnchorFrame(data, region, parent)
end
end
end
end
local function postponeAnchor(id)
postPonedAnchors[id] = true;
if (not anchorTimer) then
anchorTimer = timer:ScheduleTimer(tryAnchorAgain, 1);
end
end
local HiddenFrames = CreateFrame("Frame", "WeakAurasHiddenFrames")
HiddenFrames:Hide()
WeakAuras.HiddenFrames = HiddenFrames
local function GetAnchorFrame(data, region, parent)
local id = region.id
local anchorFrameType = data.anchorFrameType
local anchorFrameFrame = data.anchorFrameFrame
if not id then return end
if (personalRessourceDisplayFrame) then
personalRessourceDisplayFrame:anchorFrame(id, anchorFrameType);
end
if (mouseFrame) then
mouseFrame:anchorFrame(id, anchorFrameType);
end
if (anchorFrameType == "SCREEN") then
return parent;
end
if (anchorFrameType == "UIPARENT") then
return UIParent;
end
if (anchorFrameType == "PRD") then
Private.ensurePRDFrame();
personalRessourceDisplayFrame:anchorFrame(id, anchorFrameType);
return personalRessourceDisplayFrame;
end
if (anchorFrameType == "MOUSE") then
ensureMouseFrame();
mouseFrame:anchorFrame(id, anchorFrameType);
return mouseFrame;
end
if (anchorFrameType == "NAMEPLATE") then
local unit = region.state and region.state.unit
if unit then
local frame = unit and WeakAuras.GetUnitNameplate(unit)
if frame then return frame end
end
if WeakAuras.IsOptionsOpen() then
Private.ensurePRDFrame()
personalRessourceDisplayFrame:anchorFrame(id, anchorFrameType)
return personalRessourceDisplayFrame
end
end
if (anchorFrameType == "UNITFRAME") then
local unit = region.state and region.state.unit
if unit then
local frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
if frame then
anchor_unitframe_monitor = anchor_unitframe_monitor or {}
anchor_unitframe_monitor[region] = {
data = data,
parent = parent,
frame = frame
}
return frame
end
end
end
if (anchorFrameType == "SELECTFRAME" and anchorFrameFrame) then
if(anchorFrameFrame:sub(1, 10) == "WeakAuras:") then
local frame_name = anchorFrameFrame:sub(11);
if (frame_name == id) then
return parent;
end
local targetData = WeakAuras.GetData(frame_name)
if targetData then
for parentData in Private.TraverseParents(targetData) do
if parentData.id == data.id then
WeakAuras.prettyPrint(L["Warning: Anchoring to your own child '%s' in aura '%s' is imposssible."]:format(frame_name, data.id))
return parent
end
end
end
if Private.regions[frame_name] and Private.regions[frame_name].region then
return Private.regions[frame_name].region;
end
postponeAnchor(id);
else
if (Private.GetSanitizedGlobal(anchorFrameFrame)) then
return Private.GetSanitizedGlobal(anchorFrameFrame);
end
postponeAnchor(id);
return parent;
end
end
if (anchorFrameType == "CUSTOM" and region.customAnchorFunc) then
Private.StartProfileSystem("custom region anchor")
Private.StartProfileAura(region.id)
Private.ActivateAuraEnvironment(region.id, region.cloneId, region.state)
local ok, frame = xpcall(region.customAnchorFunc, Private.GetErrorHandlerId(region.id, L["Custom Anchor"]))
Private.ActivateAuraEnvironment()
Private.StopProfileSystem("custom region anchor")
Private.StopProfileAura(region.id)
if ok and frame then
return frame
elseif WeakAuras.IsOptionsOpen() then
return parent
else
return HiddenFrames
end
end
-- Fallback
return parent;
end
local anchorFrameDeferred = {}
function Private.AnchorFrame(data, region, parent, force)
if data.anchorFrameType == "CUSTOM"
and (data.regionType == "group" or data.regionType == "dynamicgroup")
and not WeakAuras.IsLoginFinished()
and not force
then
if not anchorFrameDeferred[data.id] then
loginQueue[#loginQueue + 1] = {Private.AnchorFrame, {data, region, parent, true}}
anchorFrameDeferred[data.id] = true
end
else
local anchorParent = GetAnchorFrame(data, region, parent);
if not anchorParent then return end
if (data.anchorFrameParent or data.anchorFrameParent == nil
or data.anchorFrameType == "SCREEN" or data.anchorFrameType == "UIPARENT" or data.anchorFrameType == "MOUSE") then
xpcall(region.SetParent, Private.GetErrorHandlerId(data.id, L["Anchoring"]), region, anchorParent);
else
region:SetParent(parent or WeakAurasFrame);
end
local anchorPoint = data.anchorPoint
if data.parent then
if data.anchorFrameType == "SCREEN" or data.anchorFrameType == "MOUSE" then
anchorPoint = "CENTER"
end
else
if data.anchorFrameType == "MOUSE" then
anchorPoint = "CENTER"
end
end
region:SetAnchor(data.selfPoint, anchorParent, anchorPoint);
if(data.frameStrata == 1) then
region:SetFrameStrata(region:GetParent():GetFrameStrata());
else
region:SetFrameStrata(Private.frame_strata_types[data.frameStrata]);
end
Private.ApplyFrameLevel(region)
anchorFrameDeferred[data.id] = nil
end
end
function Private.FindUnusedId(prefix)
prefix = prefix or "New"
local num = 2;
local id = prefix
while(db.displays[id]) do
id = prefix .. " " .. num;
num = num + 1;
end
return id
end
function WeakAuras.SetModel(frame, unused, model_fileId, isUnit, isDisplayInfo)
if isDisplayInfo then
pcall(frame.SetDisplayInfo, frame, tonumber(model_fileId))
elseif isUnit then
pcall(frame.SetUnit, frame, model_fileId)
else
pcall(frame.SetModel, frame, tonumber(model_fileId))
end
end
function Private.IsCLEUSubevent(subevent)
if Private.subevent_prefix_types[subevent] then
return true
else
for prefix in pairs(Private.subevent_prefix_types) do
if subevent:match(prefix) then
local suffix = subevent:sub(#prefix + 1)
if Private.subevent_suffix_types[suffix] then
return true
end
end
end
end
return false
end
--- SafeToNumber converts a string to number, but only if it fits into a unsigned 32bit integer
--- The C api often takes only 32bit values, and complains if passed a value outside
---@param input any
---@return number|nil number
function WeakAuras.SafeToNumber(input)
local nr = tonumber(input)
return nr and (nr < 2147483648 and nr > -2147483649) and nr or nil
end
local textSymbols = {
["{rt1}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_1:0|t",
["{rt2}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_2:0|t",
["{rt3}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_3:0|t",
["{rt4}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_4:0|t",
["{rt5}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_5:0|t",
["{rt6}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_6:0|t",
["{rt7}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_7:0|t",
["{rt8}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_8:0|t"
}
---@param txt string
---@return string result
function WeakAuras.ReplaceRaidMarkerSymbols(txt)
local start = 1
while true do
local firstChar = txt:find("{", start, true)
if not firstChar then
return txt
end
local lastChar = txt:find("}", firstChar, true)
if not lastChar then
return txt
end
local replace = textSymbols[txt:sub(firstChar, lastChar)]
if replace then
txt = txt:sub(1, firstChar - 1) .. replace .. txt:sub(lastChar + 1)
start = firstChar + #replace
else
start = lastChar
end
end
end
function Private.ReplaceLocalizedRaidMarkers(txt)
local start = 1
while true do
local firstChar = txt:find("{", start, true)
if not firstChar then
return txt
end
local lastChar = txt:find("}", firstChar, true)
if not lastChar then
return txt
end
local symbol = strlower(txt:sub(firstChar + 1, lastChar - 1))
if ICON_TAG_LIST[symbol] then
local replace = "rt" .. ICON_TAG_LIST[symbol]
if replace then
txt = txt:sub(1, firstChar) .. replace .. txt:sub(lastChar)
start = firstChar + #replace
else
start = lastChar
end
else
start = lastChar
end
end
end
-- WORKAROUND
-- UnitPlayerControlled doesn't work if the target is "too" far away
--- @return boolean?
function Private.UnitPlayerControlledFixed(unit)
local guid = UnitGUID(unit)
return guid and guid:sub(1, 6) == "Player"
end
do
local trackableUnits = {}
trackableUnits["player"] = true
trackableUnits["target"] = true
trackableUnits["focus"] = true
trackableUnits["pet"] = true
trackableUnits["vehicle"] = true
trackableUnits["softenemy"] = true
trackableUnits["softfriend"] = true
for i = 1, 5 do
trackableUnits["arena" .. i] = true
trackableUnits["arenapet" .. i] = true
end
for i = 1, 4 do
trackableUnits["party" .. i] = true
trackableUnits["partypet" .. i] = true
end
for i = 1, 10 do
trackableUnits["boss" .. i] = true
end
for i = 1, 40 do
trackableUnits["raid" .. i] = true
trackableUnits["raidpet" .. i] = true
trackableUnits["nameplate" .. i] = true
end
---@param unit UnitToken
---@return boolean? result
function WeakAuras.IsUntrackableSoftTarget(unit)
if not Private.soft_target_cvars[unit] then return end
-- technically this is incorrect if user doesn't have KBM and sets CVar to "2" (KBM only)
-- but, there doesn't seem to be a way to detect 'user lacks KBM'
-- anyways, the intersection of people who know how to set cvars and also don't have KBM for WoW is probably nil
-- that might change if WoW ever ends up on playstation & friends, but also hell might freeze over so who knows
local threshold = C_GamePad.IsEnabled() and 1 or 2
return (tonumber(C_CVar.GetCVar(Private.soft_target_cvars[unit])) or 0) < threshold
end
---@param unit UnitToken
---@return boolean result
function WeakAuras.UntrackableUnit(unit)
return not trackableUnits[unit]
end
end
do
local ownRealm = select(2, UnitFullName("player"))
---@param unit UnitToken
---@return string name
---@return string realm
function WeakAuras.UnitNameWithRealm(unit)
ownRealm = ownRealm or select(2, UnitFullName("player"))
local name, realm = UnitFullName(unit)
return name or "", realm or ownRealm or ""
end
function WeakAuras.UnitNameWithRealmCustomName(unit)
ownRealm = ownRealm or select(2, UnitFullName("player"))
local name, realm = WeakAuras.UnitFullName(unit)
return name or "", realm or ownRealm or ""
end
end
function Private.ExecEnv.ParseNameCheck(name)
local matches = {
name = {},
realm = {},
full = {},
AddMatch = function(self, input, start, last)
local match = strtrim(input:sub(start, last))
-- state: 1: In name
-- state: 2: In Realm
-- state: -1: Escape Name
-- state: -2: In Escape Realm
local state = 1
local name = ""
local realm = ""
for index = 1, #match do
local c = match:sub(index, index)
if state == -1 then
name = name .. c
state = 1
elseif state == -2 then
realm = realm .. c
state = 2
elseif state == 1 then
if c == "\\" then
state = -1
elseif c == "-" then
state = 2
else
name = name .. c
end
elseif state == 2 then
if c == "\\" then
state = -2
else
realm = realm .. c
end
end
end
if name == "" then
if realm == "" then
-- Do nothing
else
self.realm[realm] = true
end
else
if realm == "" then
self.name[name] = true
else
self.full[name .. "-" .. realm] = true
end
end
end,
Check = function(self, name, realm)
if not name or not realm then
return false
end
return self.name[name] or self.realm[realm] or self.full[name .. "-" .. realm]
end
}
if not name then return end
local start = 1
local last = name:find(',', start, true)
while (last) do
matches:AddMatch(name, start, last - 1)
start = last + 1
last = name:find(',', start, true)
end
last = #name
matches:AddMatch(name, start, last)
return matches
end
function Private.ExecEnv.ParseZoneCheck(input)
if not input then return end
local matcher = {
Check = function(self)
return false
end,
CheckBoth = function(self, zoneId, zonegroupId, instanceId, minimapZoneText)
return self:CheckPositive(zoneId, zonegroupId, instanceId, minimapZoneText)
and self:CheckNegative(zoneId, zonegroupId, instanceId, minimapZoneText)
end,
CheckPositive = function(self, zoneId, zonegroupId, instanceId, minimapZoneText)
return self.zoneIds[zoneId] or self.zoneGroupIds[zonegroupId] or (instanceId and self.instanceIds[instanceId]) or self.areaNames[minimapZoneText]
end,
CheckNegative = function(self, zoneId, zonegroupId, instanceId, minimapZoneText)
return not (self.negZoneIds[zoneId]
or self.negZoneGroupIds[zonegroupId]
or (instanceId and self.negInstanceIds[instanceId])
or self.negAreaNames[minimapZoneText])
end,
AddId = function(self, input, start, last)
local id = tonumber(strtrim(input:sub(start, last)))
if id then
local prevChar = input:sub(start - 1, start - 1)
local prevPrevchar = input:sub(start - 2, start - 2)
if prevChar == 'g' or prevChar == 'G' then
if prevPrevchar == "-" then
self.negZoneGroupIds[id] = true
else
self.zoneGroupIds[id] = true
end
elseif prevChar == 'c' or prevChar == 'C' then
local addTo = self.zoneIds
if prevPrevchar == "-" then
addTo = self.negZoneIds
end
addTo[id] = true
local info = C_Map.GetMapChildrenInfo(id, nil, true)
if info then
for _,childInfo in pairs(info) do
addTo[childInfo.mapID] = true
end
end
elseif prevChar == 'a' or prevChar == 'A' then
local areaName = C_Map.GetAreaInfo(id)
if areaName then
if prevPrevchar == "-" then
self.negAreaNames[areaName] = true
else
self.areaNames[areaName] = true
end
end
elseif prevChar == 'i' or prevChar == 'I' then
if prevPrevchar == "-" then
self.negInstanceIds[id] = true
else
self.instanceIds[id] = true
end
else
if prevChar == "-" then
self.negZoneIds[id] = true
else
self.zoneIds[id] = true
end
end
end
end,
zoneIds = {},
zoneGroupIds = {},
instanceIds = {},
areaNames = {},
negZoneIds = {},
negZoneGroupIds = {},
negInstanceIds = {},
negAreaNames = {},
}
local start = input:find('%d', 1)
if start then
local last = input:find('%D', start)
while (last) do
matcher:AddId(input, start, last - 1)
start = input:find('%d', last + 1) or #input + 1
last = input:find('%D', start)
end
last = #input
matcher:AddId(input, start, last)
end
local hasPositive = next(matcher.zoneIds) or next(matcher.zoneGroupIds) or next(matcher.instanceIds) or next(matcher.areaNames)
local hasNegative = next(matcher.negZoneIds) or next(matcher.negZoneGroupIds) or next(matcher.negInstanceIds) or next(matcher.negAreaNames)
if hasPositive and hasNegative then
matcher.Check = matcher.CheckBoth
elseif hasPositive then
matcher.Check = matcher.CheckPositive
elseif hasNegative then
matcher.Check = matcher.CheckNegative
end
return matcher
end
function WeakAuras.IsAuraLoaded(id)
return Private.loaded[id]
end
function Private.ExecEnv.CreateSpellChecker()
local matcher = {
names = {},
spellIds = {},
AddName = function(self, name)
local spellId = tonumber(name)
if spellId then
name = Private.ExecEnv.GetSpellName(spellId)
if name then
self.names[name] = true
end
else
self.names[name] = true
end
end,
AddExact = function(self, spellId)
spellId = tonumber(spellId)
self.spellIds[spellId] = true
end,
Check = function(self, spellId)
if spellId then
return self.spellIds[spellId] or self.names[Private.ExecEnv.GetSpellName(spellId)]
end
end,
CheckName = function(self, name)
return self.names[name]
end
}
return matcher
end
function Private.IconSources(data)
local values = {
[-1] = L["Automatic"],
[0] = L["Manual Icon"],
}
for i = 1, #data.triggers do
values[i] = string.format(L["Trigger %i"], i)
end
return values
end
-- This should be used instead of string.format("...%q...", input)
-- e.g. string.format("...%s...", Private.QuotedString(input))
-- If the string is passed to loadstring.
-- It escapes --, which loadstring would otherwise interpret as comment starts
function Private.QuotedString(input)
local str = string.format("%q", input)
return (str:gsub("%-%-", "-\\-"))
end
-- Helper function to make the templates not care, how the generic triggers
-- are categorized
---@private
function WeakAuras.GetTriggerCategoryFor(triggerType)
local prototype = Private.event_prototypes[triggerType]
return prototype and prototype.type
end
function Private.SortOrderForValues(values)
local sortOrder = {}
for key, value in pairs(values) do
tinsert(sortOrder, key)
end
table.sort(sortOrder, function(aKey, bKey)
local aValue = values[aKey]
local bValue = values[bKey]
if aValue:sub(1, #WeakAuras.newFeatureString) == WeakAuras.newFeatureString then
aValue = aValue:sub(#WeakAuras.newFeatureString + 1)
end
if bValue:sub(1, #WeakAuras.newFeatureString) == WeakAuras.newFeatureString then
bValue = bValue:sub(#WeakAuras.newFeatureString + 1)
end
return aValue < bValue
end)
return sortOrder
end
do
local function shouldInclude(data, includeGroups, includeLeafs)
if data.controlledChildren then
return includeGroups
else
return includeLeafs
end
end
local function Traverse(data, includeSelf, includeGroups, includeLeafs)
if includeSelf and shouldInclude(data, includeGroups, includeLeafs) then
coroutine.yield(data)
end
if data.controlledChildren then
for _, child in ipairs(data.controlledChildren) do
Traverse(WeakAuras.GetData(child), true, includeGroups, includeLeafs)
end
end
end
local function TraverseLeafs(data)
return Traverse(data, false, false, true)
end
local function TraverseLeafsOrAura(data)
return Traverse(data, true, false, true)
end
local function TraverseGroups(data)
return Traverse(data, true, true, false)
end
local function TraverseSubGroups(data)
return Traverse(data, false, true, false)
end
local function TraverseAllChildren(data)
return Traverse(data, false, true, true)
end
local function TraverseAll(data)
return Traverse(data, true, true, true)
end
local function TraverseParents(data)
while data.parent do
local parentData = WeakAuras.GetData(data.parent)
coroutine.yield(parentData)
data = parentData
end
end
-- Only non-group auras, not include self
function Private.TraverseLeafs(data)
return coroutine.wrap(TraverseLeafs), data
end
-- The root if it is a non-group, otherwise non-group children
function Private.TraverseLeafsOrAura(data)
return coroutine.wrap(TraverseLeafsOrAura), data
end
-- All groups, includes self
function Private.TraverseGroups(data)
return coroutine.wrap(TraverseGroups), data
end
-- All groups, excludes self
function Private.TraverseSubGroups(data)
return coroutine.wrap(TraverseSubGroups), data
end
-- All Children, excludes self
function Private.TraverseAllChildren(data)
return coroutine.wrap(TraverseAllChildren), data
end
-- All Children and self
function Private.TraverseAll(data)
return coroutine.wrap(TraverseAll), data
end
function Private.TraverseParents(data)
return coroutine.wrap(TraverseParents), data
end
--- Returns whether the data is a group or dynamicgroup
---@param data auraData
---@return boolean
function Private.IsGroupType(data)
return data.regionType == "group" or data.regionType == "dynamicgroup"
end
end