--- @type string, Private
local AddonName , Private = ...
local internalVersion = 69
-- Lua APIs
local insert = table.insert
-- WoW APIs
local GetTalentInfo , IsAddOnLoaded , InCombatLockdown = GetTalentInfo , IsAddOnLoaded , InCombatLockdown
local LoadAddOn , UnitName , GetRealmName , UnitRace , UnitFactionGroup , IsInRaid
= LoadAddOn , UnitName , GetRealmName , UnitRace , UnitFactionGroup , IsInRaid
local UnitClass , UnitExists , UnitGUID , UnitAffectingCombat , GetInstanceInfo , IsInInstance
= UnitClass , UnitExists , UnitGUID , UnitAffectingCombat , GetInstanceInfo , IsInInstance
local UnitIsUnit , GetRaidRosterInfo , GetSpecialization , UnitInVehicle , UnitHasVehicleUI , GetSpellInfo
= UnitIsUnit , GetRaidRosterInfo , GetSpecialization , UnitInVehicle , UnitHasVehicleUI , GetSpellInfo
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 "
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 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 prefix = " "
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 )
prefix = L [ " Lua error in aura '%s': %s " ] : format ( data.id , currentErrorHandlerContext or L [ " unknown location " ] ) .. " \n "
else
prefix = L [ " Lua error " ] .. " \n "
end
prefix = prefix .. L [ " WeakAuras Version: %s " ] : format ( WeakAuras.versionString ) .. " \n "
local version = data and ( data.semver or data.version )
if version then
prefix = prefix .. L [ " Aura Version: %s " ] : format ( version ) .. " \n "
end
geterrorhandler ( ) ( prefix .. errorMessage )
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 ( 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 = 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
else
insert ( args , v )
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 " } )
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
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
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 ;
local levelColors = {
[ 0 ] = " |cFFFFFFFF " ,
[ 1 ] = " |cFF40FF40 " ,
[ 2 ] = " |cFF6060FF " ,
[ 3 ] = " |cFFFF4040 "
} ;
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
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
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
-- This function is 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 )
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
return " ( " .. name .. " ==[[ " .. value .. " ]]) " ;
else
return " ( " .. name .. " : " .. operator : format ( value ) .. " ) " ;
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 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 )
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 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 )
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 " ;
ret = ret .. ( init or " " ) ;
-- Enable to debug load function inputs
--ret = ret .. "print('INPUTS')"
--for _, i in ipairs(input) do
-- ret = ret .. "print('" .. i .. "'," .. i .. ")"
--end
ret = ret .. ( # debug > 0 and table.concat ( debug , " \n " ) or " " ) ;
ret = ret .. " if( " ;
ret = ret .. ( ( # required > 0 ) and table.concat ( required , " and " ) .. " and " or " " ) ;
ret = ret .. ( # tests > 0 and table.concat ( tests , " and " ) or " true " ) ;
ret = ret .. " ) then \n " ;
if ( # debug > 0 ) then
ret = ret .. " print('ret: true'); \n " ;
end
ret = ret .. " return true else return false end end " ;
return 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.IsClassicEraOrWrath ( ) 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 { } ;
local function formatTalent ( talentId )
local _ , name , icon = GetPvpTalentInfoByID ( talentId ) ;
return " |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 i , talentId in ipairs ( pvpSpecTalents ) do
Private.pvp_talent_types_specific [ player_class ] [ spec ] [ i ] = formatTalent ( talentId ) ;
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 )
local tooltip = GameTooltip ;
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
WeakAuras.RealTimeProfilingWindow : 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 IsAddOnLoaded ( " WeakAurasArchive " ) then
local ok , reason = 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
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 ( initialTime , takeNewSnapshots )
local loginThread = coroutine.create ( function ( )
Private.Pause ( ) ;
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 ( )
end
-- history is now in archive so we can shrink WeakAurasSaved
db.history = nil
coroutine.yield ( ) ;
end
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 ( ) ;
Private.AddMany ( toAdd , takeNewSnapshots ) ;
coroutine.yield ( ) ;
-- check in case of a disconnect during an encounter.
if ( db.CurrentEncounter ) then
CheckForPreviousEncounter ( )
end
coroutine.yield ( ) ;
Private.RegisterLoadEvents ( ) ;
Private.Resume ( ) ;
coroutine.yield ( ) ;
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 ( ) ;
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 ( ) ;
end
end
end )
if initialTime then
local startTime = debugprofilestop ( )
local finishTime = debugprofilestop ( )
local ok , msg
-- hard limit seems to be 19 seconds. We'll do 15 for now.
while coroutine.status ( loginThread ) ~= ' dead ' and finishTime - startTime < 15000 do
ok , msg = coroutine.resume ( loginThread )
finishTime = debugprofilestop ( )
end
if coroutine.status ( loginThread ) ~= ' dead ' then
Private.dynFrame : AddAction ( ' login ' , loginThread )
end
if not ok then
loginMessage = L [ " WeakAuras has encountered an error during the login process. Please report this issue at https://github.com/WeakAuras/Weakauras2/issues/new. " ]
.. " \n Message: " .. msg
geterrorhandler ( ) ( msg .. ' \n ' .. debugstack ( loginThread ) )
end
else
Private.dynFrame : AddAction ( ' login ' , loginThread )
end
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
WeakAurasSaved = WeakAurasSaved or { } ;
db = WeakAurasSaved ;
-- 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 { } ;
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
-- run login thread for up to 15 seconds, then defer to dynFrame
Private.Login ( 15000 , 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 _ , _ , _ , _ , _ , _ , _ , ZoneMapID = GetInstanceInfo ( )
WeakAuras.CurrentEncounter = {
id = encounter_id ,
zone_id = ZoneMapID ,
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 , _ , _ , _ , _ , ZoneMapID = 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 , ZoneMapID , difficultyIndex
end
return " none " , " none " , nil , nil , nil
end
function WeakAuras . InstanceType ( )
return GetInstanceTypeAndSize ( ) , nil
end
function WeakAuras . InstanceDifficulty ( )
return select ( 2 , GetInstanceTypeAndSize ( ) )
end
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 _ , 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.IsClassicEraOrWrath ( ) then
local raidID = UnitInRaid ( " player " )
if raidID then
raidRole = select ( 10 , GetRaidRosterInfo ( raidID ) )
end
role = " none "
if WeakAuras.IsWrathClassic ( ) then
role = UnitGroupRolesAssigned ( " player " )
if role == " NONE " then
role = GetTalentGroupRole ( GetActiveTalentGroup ( ) ) or " NONE "
end
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 ( )
specId , role , position = Private.LibSpecWrapper . SpecRolePositionForUnit ( " player " )
inPetBattle = C_PetBattles.IsInBattle ( )
vehicle = UnitInVehicle ( ' player ' ) or UnitOnTaxi ( ' player ' ) or false
vehicleUi = UnitHasVehicleUI ( ' player ' ) or HasOverrideActionBar ( ) or HasVehicleActionBar ( ) or false
end
local size , difficulty , instanceType , ZoneMapID , difficultyIndex = GetInstanceTypeAndSize ( )
Private.UpdateCurrentInstanceType ( instanceType )
if ( WeakAuras.CurrentEncounter ) then
if ( ZoneMapID ~= 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 , 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 , encounter_id , size )
elseif WeakAuras.IsWrathClassic ( ) then
shouldBeLoaded = loadFunc and loadFunc ( " ScanForLoads_Auras " , inCombat , alive , inEncounter , vehicle , vehicleUi , class , player , realm , race , faction , playerLevel , role , raidRole , group , groupSize , raidMemberType , zone , zoneId , zonegroupId , encounter_id , size , difficulty , difficultyIndex )
couldBeLoaded = loadOpt and loadOpt ( " ScanForLoads_Auras " , inCombat , alive , inEncounter , vehicle , vehicleUi , class , player , realm , race , faction , playerLevel , role , raidRole , group , groupSize , raidMemberType , zone , zoneId , zonegroupId , 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 , 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 , 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 " )
end
if WeakAuras.IsWrathClassic ( ) then
loadFrame : RegisterEvent ( " PLAYER_TALENT_UPDATE " ) ;
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 : RegisterEvent ( " UNIT_INVENTORY_CHANGED " )
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.IsWrathOrRetail ( ) 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
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 ( loginAfter )
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 ( )
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 ( )
end
end
db.displays = newDB
Private.SetImporting ( false )
-- finally, login
Private.Login ( )
end )
Private.dynFrame : AddAction ( " repair " , coro )
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. \n Your saved auras may no longer work properly. \n Would you like to run the |cffff0000EXPERIMENTAL|r repair tool? This will overwrite any changes you have made since the last database upgrade. \n Last 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? \n This will overwrite any changes you have made since the last database upgrade. \n Last 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
-- 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
-- We can probably remove this code in 2023 again
for d in pairs ( depends ) do
local uid = idtable [ d ] . uid
if uid == " fjtz3A6LwBW " then -- Fojji - Deathknight UI, need to fixup a lot
local cycleRoot = d
idtable [ cycleRoot ] . parent = nil
for d in pairs ( depends ) do
tDeleteItem ( idtable [ d ] . controlledChildren , cycleRoot )
end
return loadOrder ( tbl , idtable )
end
coroutine.yield ( )
end
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 ( )
load ( data.parent , dependsOut )
coroutine.yield ( )
end
end
else
data.parent = nil ;
end
end
if not ( loaded [ id ] ) then
coroutine.yield ( ) ;
loaded [ id ] = true ;
tinsert ( order , idtable [ id ] )
end
end
for id , data in pairs ( idtable ) do
load ( id , { } ) ;
coroutine.yield ( )
end
return order
end
---@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 ( )
local groups = { }
for _ , data in ipairs ( order ) do
WeakAuras.Add ( data , takeSnapshots ) ;
coroutine.yield ( )
if data.regionType == " dynamicgroup " or data.regionType == " group " then
groups [ data ] = true
end
end
for id in pairs ( anchorTargets ) do
local data = idtable [ id ]
if data and ( data.parent == nil or idtable [ data.parent ] . regionType ~= " dynamicgroup " ) then
Private.EnsureRegion ( id )
end
end
for data in pairs ( groups ) do
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
coroutine.yield ( ) ;
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 function removeSpellNames ( data )
local trigger
for i = 1 , # data.triggers do
trigger = data.triggers [ i ] . trigger
if trigger and trigger.type == " aura " then
if type ( trigger.spellName ) == " number " then
trigger.realSpellName = GetSpellInfo ( trigger.spellName ) or trigger.realSpellName
end
if ( trigger.spellId ) then
trigger.name = GetSpellInfo ( trigger.spellId ) or trigger.name ;
end
if ( trigger.spellIds ) then
for i = 1 , 10 do
if ( trigger.spellIds [ i ] ) then
trigger.names = trigger.names or { } ;
trigger.names [ i ] = GetSpellInfo ( trigger.spellIds [ i ] ) or trigger.names [ i ] ;
end
end
end
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 anySoundCondition ( )
if data.conditions then
for _ , condition in ipairs ( data.conditions ) do
for changeIndex , change in ipairs ( condition.changes ) do
if change.property == " sound " then
return true
end
end
end
end
end
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 anySoundCondition ( ) 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
end
function WeakAuras . PreAdd ( data )
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
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.Modernize ( data ) ;
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 )
removeSpellNames ( data )
data.init_started = nil
data.init_completed = nil
data.expanded = nil
end
local 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
function WeakAuras . Add ( data , takeSnapshot , simpleChange )
local snapshot
if takeSnapshot or ( data.internalVersion or 0 ) < internalVersion then
snapshot = CopyTable ( data )
end
if takeSnapshot then
Private.SetMigrationSnapshot ( data.uid , snapshot )
end
local ok = xpcall ( WeakAuras.PreAdd , Private.GetErrorHandlerUid ( data.uid , " PreAdd " ) , data )
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 " ) ;
else
if ( not regionTypes [ regionType ] ) then
regionType = " fallback " ;
print ( " Improper arguments to WeakAuras.CreateRegion - regionType \" " .. data.regionType .. " \" is not supported " ) ;
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 ) ;
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 " } ;
if ( Private.CanHaveDuration ( data ) ) then
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 " ;
else
data.animation . start.duration_type = " seconds " ;
data.animation . main.duration_type = " seconds " ;
data.animation . finish.duration_type = " seconds " ;
end
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
-- and as an additional wrinkle, for dynamic groups, all children must exist!
-- and we have to call ReloadControlledChildren at the end
-- 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
-- And dynamic groups require creating all children, thus we don't need
-- to care which path we came to them
--- @type auraId[]
local aurasToCreate = { }
--- @type auraId[]
local dynamicGroups = { }
creatingRegions = true
while ( id ) do
local data = WeakAuras.GetData ( id )
if ( data.regionType == " dynamicgroup " ) then
wipe ( aurasToCreate )
tinsert ( aurasToCreate , data.id )
tinsert ( dynamicGroups , data.id )
else
tinsert ( aurasToCreate , data.id )
end
id = data.parent
end
for _ , toCreateId in ipairs_reverse ( aurasToCreate ) do
local data = WeakAuras.GetData ( toCreateId )
Private.SetRegion ( data )
if ( data.regionType == " dynamicgroup " ) then
for child in Private.TraverseAllChildren ( data ) do
Private.SetRegion ( child )
end
end
end
creatingRegions = false
for _ , dynamicGroupId in ipairs_reverse ( dynamicGroups ) do
local dgRegion = Private.regions [ dynamicGroupId ] . region
dgRegion : ReloadControlledChildren ( )
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: \n https://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 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 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 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 )
)
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
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
region : SoundStop ( ) ;
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.CanHaveDuration = wrapTriggerSystemFunction ( " CanHaveDuration " , " firstValue " ) ;
Private.CanHaveClones = wrapTriggerSystemFunction ( " CanHaveClones " , " or " ) ;
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 , triggernum , ... )
local additionalProperties = " "
for i = 1 , # data.triggers do
local triggerSystem = GetTriggerSystem ( data , i ) ;
if ( triggerSystem ) then
local add = triggerSystem.GetAdditionalProperties ( data , i )
if ( add and add ~= " " ) then
if additionalProperties ~= " " then
additionalProperties = additionalProperties .. " \n "
end
additionalProperties = additionalProperties .. add ;
end
end
end
if additionalProperties ~= " " then
additionalProperties = " \n \n "
.. L [ " Additional Trigger Replacements " ] .. " \n "
.. additionalProperties .. " \n \n "
.. L [ " The trigger number is optional, and uses the trigger providing dynamic information if not specified. " ]
end
return additionalProperties
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
function Private . FixGroupChildrenOrderForGroup ( data )
local frameLevel = 1 ;
if data.parent == nil then
for child in Private.TraverseAll ( data ) do
SetFrameLevel ( child.id , frameLevel ) ;
frameLevel = frameLevel + 4 ;
end
end
end
local function GetFrameLevelFor ( id )
return Private.frameLevels [ id ] or 5 ;
end
function Private . ApplyFrameLevel ( region , frameLevel )
frameLevel = frameLevel or GetFrameLevelFor ( region.id )
if region.subRegions then
for index , subRegion in pairs ( region.subRegions ) do
if subRegion.type == " subbackground " then
subRegion : SetFrameLevel ( frameLevel + index )
end
end
for index , subRegion in pairs ( region.subRegions ) do
if subRegion.type ~= " subbackground " then
subRegion : SetFrameLevel ( frameLevel + index )
end
end
end
end
function WeakAuras . EnsureString ( input )
if ( input == nil ) then
return " " ;
end
return tostring ( input ) ;
end
-- Handle coroutines
local dynFrame = { } ;
do
-- Internal data
dynFrame.frame = CreateFrame ( " Frame " ) ;
dynFrame.update = { } ;
dynFrame.size = 0 ;
-- Add an action to be resumed via OnUpdate
function dynFrame . AddAction ( self , name , func )
if not name then
name = string.format ( " NIL " , dynFrame.size + 1 ) ;
end
if not dynFrame.update [ name ] then
dynFrame.update [ name ] = func ;
dynFrame.size = dynFrame.size + 1
dynFrame.frame : Show ( ) ;
end
end
-- Remove an action from OnUpdate
function dynFrame . RemoveAction ( self , name )
if dynFrame.update [ name ] then
dynFrame.update [ name ] = nil ;
dynFrame.size = dynFrame.size - 1
if dynFrame.size == 0 then
dynFrame.frame : Hide ( ) ;
end
end
end
-- Setup frame
dynFrame.frame : Hide ( ) ;
dynFrame.frame : SetScript ( " OnUpdate " , function ( self , elapsed )
-- Start timing
local start = debugprofilestop ( ) ;
local hasData = true ;
-- Resume as often as possible (Limit to 16ms per frame -> 60 FPS)
while ( debugprofilestop ( ) - start < 16 and hasData ) do
-- Stop loop without data
hasData = false ;
-- Resume all coroutines
for name , func in pairs ( dynFrame.update ) do
-- Loop has data
hasData = true ;
-- Resume or remove
if coroutine.status ( func ) ~= " dead " then
local ok , msg = coroutine.resume ( func )
if not ok then
geterrorhandler ( ) ( msg .. ' \n ' .. debugstack ( func ) )
end
else
dynFrame : RemoveAction ( name ) ;
end
end
end
end ) ;
end
Private.dynFrame = dynFrame ;
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
local function startStopTimers ( id , cloneId , triggernum , state )
if ( state.show ) then
if ( state.autoHide and state.duration and state.duration > 0 and not state.paused ) then -- autohide, update timer
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 ( state.expirationTime == nil ) then
state.expirationTime = GetTime ( ) + state.duration ;
end
if ( record.expirationTime ~= state.expirationTime or record.state ~= state ) then
if ( record.handle ~= nil ) then
timer : CancelTimer ( record.handle ) ;
end
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 ,
state.expirationTime - GetTime ( ) ) ;
record.expirationTime = state.expirationTime ;
record.state = state
end
else -- no auto hide, delete timer
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
else -- not shown
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
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 { }
local needsTimerTick = false
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
needsTimerTick = needsTimerTick or ( triggerState and triggerState.show and triggerState.progressType == " timed " )
end
region : SetTriggerProvidesTimer ( needsTimerTick )
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 )
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 )
elseif variable.func then
value = variable.func ( value )
end
end
return 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
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 ] ) or " " ) or " "
else
return tostring ( value ) or " "
end
else
local value = ReplaceValuePlaceHolders ( sym , region , customFunc , regionStates [ triggerNum ] , formatters [ symbol ] ) ;
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 ) or " " ) or " "
else
return tostring ( value ) or " "
end
end
return " "
else
local value = ( useHiddenStates or regionState.show ) and ReplaceValuePlaceHolders ( symbol , region , customFunc , regionState , formatters [ symbol ] ) ;
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 )
local seenSymbols = { }
local formatters = { }
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 ] = Private.format_types [ selectedFormat ] . CreateFormatter ( symbol , getter , withoutColor )
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
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
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 = { } ;
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 )
if data.anchorFrameType == " CUSTOM "
and ( data.regionType == " group " or data.regionType == " dynamicgroup " )
and not WeakAuras.IsLoginFinished ( )
and not anchorFrameDeferred [ data.id ]
then
loginQueue [ # loginQueue + 1 ] = { Private.AnchorFrame , { data , region , parent } }
anchorFrameDeferred [ data.id ] = true
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 , model_path , model_fileId , isUnit , isDisplayInfo )
if WeakAuras.IsClassicEra ( ) then
if isDisplayInfo then
pcall ( frame.SetDisplayInfo , frame , tonumber ( model_path ) )
elseif isUnit then
pcall ( frame.SetUnit , frame , model_path )
else
pcall ( frame.SetModel , frame , model_path )
end
else
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
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
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 "
}
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
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
function WeakAuras . UntrackableUnit ( unit )
return not trackableUnits [ unit ]
end
end
do
local ownRealm = select ( 2 , UnitFullName ( " player " ) )
function WeakAuras . UnitNameWithRealm ( unit )
ownRealm = ownRealm or select ( 2 , UnitFullName ( " player " ) )
local name , realm = 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 , zoneId , zonegroupId )
return self.zoneIds [ zoneId ] or self.zoneGroupIds [ zonegroupId ]
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 )
if prevChar == ' g ' or prevChar == ' G ' then
self.zoneGroupIds [ id ] = true
else
self.zoneIds [ id ] = true
end
end
end ,
zoneIds = { } ,
zoneGroupIds = { }
}
local start = input : find ( ' %d ' , 1 )
local last = input : find ( ' %D ' , start )
while ( last ) do
matcher : AddId ( input , start , last - 1 )
start = input : find ( ' %d ' , last + 1 )
last = input : find ( ' %D ' , start )
end
last = # input
matcher : AddId ( input , start , last )
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 = GetSpellInfo ( 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 )
return self.spellIds [ spellId ] or self.names [ GetSpellInfo ( spellId ) ]
end ,
CheckName = function ( self , name )
return self.names [ name ]
end
}
return matcher
end
function Private . IconSources ( data )
local values = {
[ - 1 ] = L [ " Dynamic Information " ] ,
[ 0 ] = L [ " Fallback 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
function WeakAuras . GetTriggerCategoryFor ( triggerType )
local prototype = Private.event_prototypes [ triggerType ]
return prototype and prototype.type
end
function WeakAuras . UnitStagger ( unit )
return UnitStagger ( unit ) or 0
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 ]
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