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