mirror of
https://github.com/fuffc/WeakestAuras.git
synced 2026-11-10 20:07:35 +00:00
ClassicAPI 1.18 is now the only requirement Requirements - Needs ClassicAPI 1.18.0 or newer. SuperWoW is no longer used, and Nampower is optional. - On an older ClassicAPI the addon stays off, and a popup links the download. - Custom triggers on SuperWoW or Nampower events only fire while that mod is installed. Triggers - New Combat Log trigger, with lists of spell IDs, spell names and NPC IDs. WeakAuras2 auras that use one now import. - Custom triggers can listen to the combat log with CLEU or CLEU:SUBEVENT. - Crowd Control matches a specific type (stun, fear, root, silence, disarm and more) or a school lockout, with the effect's real timer. - Saved Crowd Control auras: Root, Silence and Disarm filters carry over; CC and Snare become "any type", and snares are no longer detected. - Cast bars follow pushback and channel updates. Displays - Icon cooldown swipes are drawn by the client's own cooldown frame. - Sound actions play on a chosen channel, and a new Stop sound type ends a looping sound. Fixes - Cooldown auras no longer sweep on every wand shot. - The Global Cooldown trigger ignores wand and Auto Shot shots.
1336 lines
53 KiB
Lua
1336 lines
53 KiB
Lua
-- WeakestAuras -- data model: the saved aura shape, region/trigger type
|
|
-- registries (RegisterRegionType/RegisterTriggerType), and aura CRUD.
|
|
-- Upstream section refs (§N) point at design/architecture/weakauras2-reference.md
|
|
|
|
if WeakestAuras.disabled then return end
|
|
|
|
local WA = WeakestAuras
|
|
|
|
WA.EnsureDB()
|
|
|
|
-- Region types (Regions.lua) and trigger types (Triggers.lua) register themselves
|
|
-- here by name; the options window looks a data's regionType/trigger.type up in
|
|
-- these tables rather than switching on a fixed list, so adding a new display or
|
|
-- trigger kind never touches OptionsFrame.lua.
|
|
WA.regionTypes = {}
|
|
WA.triggerTypes = {}
|
|
-- Sub-region types (SubText.lua) register here; a display's data.subRegions is
|
|
-- an ordered array of instances of these (§8). Same
|
|
-- options/runtime split as region types: the spec carries defaults +
|
|
-- create/modify + a supports(regionType) gate + condition properties.
|
|
WA.subRegionTypes = {}
|
|
|
|
-- spec = { defaults = { field = value, ... }, options = function(data) return {...} end,
|
|
-- icon = "Interface\\Icons\\..." -- list-row fallback texture, used only
|
|
-- when this type registers no createThumbnail, or WA.AcquireThumbnail
|
|
-- returns nil (see OptionsFrame.lua's refreshList),
|
|
-- createThumbnail = function(parent) ... return frame end, -- optional
|
|
-- modifyThumbnail = function(frame, data) ... end } -- optional
|
|
-- createThumbnail/modifyThumbnail are the real per-region-type preview: a small
|
|
-- frame built once and repainted per aura, pooled below rather than created per
|
|
-- row since frames on this client can't be destroyed. modifyThumbnail runs after
|
|
-- the caller has already sized the frame (WA.AcquireThumbnail), so it can read
|
|
-- back frame:GetWidth()/GetHeight() for anything that scales with the box.
|
|
function WA.RegisterRegionType(name, spec)
|
|
WA.regionTypes[name] = spec
|
|
end
|
|
|
|
WA.thumbnailPools = {}
|
|
local function thumbnailPool(regionType)
|
|
local pool = WA.thumbnailPools[regionType]
|
|
if not pool then
|
|
pool = {}
|
|
WA.thumbnailPools[regionType] = pool
|
|
end
|
|
return pool
|
|
end
|
|
|
|
-- Returns a frame parented to `parent`, sized size x size, modified for `data`
|
|
-- and Shown; nil when the type registers no createThumbnail. The caller anchors
|
|
-- it. Order matters: the frame is sized BEFORE modifyThumbnail runs, since a
|
|
-- mock bar/glyph derives its fill length from the box it's already been given.
|
|
function WA.AcquireThumbnail(regionType, parent, data, size)
|
|
local spec = WA.regionTypes[regionType]
|
|
if not spec or not spec.createThumbnail then return nil end
|
|
local pool = thumbnailPool(regionType)
|
|
local frame = table.remove(pool)
|
|
if not frame then
|
|
frame = spec.createThumbnail(parent)
|
|
if not frame then return nil end
|
|
frame.thumbType = regionType
|
|
end
|
|
frame:SetParent(parent)
|
|
-- A pooled frame still carries the anchor of the row it last sat on, whose
|
|
-- frame is a live object -- the caller's own SetPoint would otherwise be
|
|
-- laid over a point pinned to somebody else's row.
|
|
frame:ClearAllPoints()
|
|
frame:SetWidth(size)
|
|
frame:SetHeight(size)
|
|
if spec.modifyThumbnail then spec.modifyThumbnail(frame, data) end
|
|
frame:Show()
|
|
return frame
|
|
end
|
|
|
|
-- Hides `frame` and returns it to its own type's pool. Safe on nil. Guards
|
|
-- against inserting the same frame twice, in case a caller ever releases
|
|
-- without having reacquired in between.
|
|
function WA.ReleaseThumbnail(frame)
|
|
if not frame then return end
|
|
frame:Hide()
|
|
local pool = thumbnailPool(frame.thumbType)
|
|
for i = 1, #pool do
|
|
if pool[i] == frame then return end
|
|
end
|
|
table.insert(pool, frame)
|
|
end
|
|
|
|
-- Re-paints an already-acquired thumbnail in place, for a caller that knows
|
|
-- the frame's type hasn't changed and only needs the display's fields
|
|
-- refreshed (OptionsFrame.lua's refreshList) -- the acquire/release dance is
|
|
-- for a type change only. No-op when the type registers no modifyThumbnail or
|
|
-- frame is nil.
|
|
function WA.ModifyThumbnail(regionType, frame, data)
|
|
if not frame then return end
|
|
local spec = WA.regionTypes[regionType]
|
|
if spec and spec.modifyThumbnail then spec.modifyThumbnail(frame, data) end
|
|
end
|
|
|
|
-- Read-only: how many released frames of this type are sitting in the pool
|
|
-- right now. Exists for the headless harness to assert the acquire/release
|
|
-- accounting never leaks a frame past what the visible rows need.
|
|
function WA.ThumbnailPoolSize(regionType)
|
|
local pool = WA.thumbnailPools[regionType]
|
|
return pool and #pool or 0
|
|
end
|
|
|
|
-- spec = { defaults = ..., options = ..., summary = function(data) return "..." end,
|
|
-- migrate = function(trigger) ... end }
|
|
-- summary: one-line "what this watches" text for the aura list row's second line.
|
|
-- migrate: optional, in-place field-rename fixup for a saved trigger table of
|
|
-- this type (e.g. auraType -> debuffType); must be idempotent, MergeDefaults
|
|
-- runs it on every touch. This is the *options-side* registry (defaults +
|
|
-- options tab + summary per trigger.type); the *runtime* layer that actually
|
|
-- produces states is a trigger system registered via WA.RegisterTriggerSystem
|
|
-- (StateMachine.lua), mirroring upstream's triggerTypesOptions vs triggerSystems
|
|
-- split.
|
|
function WA.RegisterTriggerType(name, spec)
|
|
WA.triggerTypes[name] = spec
|
|
end
|
|
|
|
-- spec = { displayName, supports = function(regionType) return bool end,
|
|
-- default = {...}, create = function(parent) end,
|
|
-- modify = function(parent, subRegion, parentData, subRegionData) end,
|
|
-- addDefaultsForNewAura = function(data) end, properties = {...},
|
|
-- enforced = bool }
|
|
-- enforced: MergeDefaults keeps exactly one on every aura this type supports,
|
|
-- and the options list offers no add, delete or duplicate for it. subbackground
|
|
-- is the case -- a placeholder standing for the region's own art inside the
|
|
-- draw-order list, which is meaningless in any count but one.
|
|
function WA.RegisterSubRegionType(name, spec)
|
|
WA.subRegionTypes[name] = spec
|
|
end
|
|
|
|
-- Seeds a leaf display's data.subRegions with each supporting sub-region type's
|
|
-- defaults (§8: icon gets a %s stacks text, bar gets %n + %p). Run once
|
|
-- per display via the internalVersion gate in MergeDefaults, so it both
|
|
-- initializes new auras and preserves the (formerly hardcoded) text on auras
|
|
-- saved before subRegions existed, without re-adding on every load.
|
|
function WA.AddDefaultsForNewAura(data)
|
|
if WA.IsGroup(data) then return end
|
|
data.subRegions = data.subRegions or {}
|
|
for _, spec in pairs(WA.subRegionTypes) do
|
|
if spec.addDefaultsForNewAura and (not spec.supports or spec.supports(data.regionType)) then
|
|
spec.addDefaultsForNewAura(data)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The trigger config table for triggernum n (data.triggers[n].trigger), or nil.
|
|
-- The options UI and summary read this instead of reaching into the array shape
|
|
-- directly.
|
|
function WA.GetTrigger(data, n)
|
|
if not data.triggers then return nil end
|
|
local entry = data.triggers[n]
|
|
return entry and entry.trigger
|
|
end
|
|
|
|
-- onlyGroups: if provided (true/false), lists only types whose isGroup flag
|
|
-- matches -- used to keep an existing aura's own Region type selector from
|
|
-- offering to convert across the leaf/group boundary (see OptionsFrame.lua's
|
|
-- getInfoOptions), matching upstream WeakAuras2's own behavior: its
|
|
-- "Convert to..." menu is simply never offered for a group (always has
|
|
-- controlledChildren) and never lists group/dynamicgroup as a target for a
|
|
-- leaf, rather than handling the conversion's fallout. Omitted (nil), lists
|
|
-- every registered type regardless of category (e.g. the "+ New" picker,
|
|
-- which is free to create either kind from scratch).
|
|
-- `internal` types are never listed: `fallback` exists to stand in for a region
|
|
-- this addon does not have, so offering it as something to create or convert to
|
|
-- would be offering the absence itself.
|
|
function WA.RegionTypeList(onlyGroups)
|
|
local list = {}
|
|
for name, spec in pairs(WA.regionTypes) do
|
|
if not spec.internal and (onlyGroups == nil or (spec.isGroup == true) == onlyGroups) then
|
|
table.insert(list, name)
|
|
end
|
|
end
|
|
table.sort(list)
|
|
return list
|
|
end
|
|
|
|
-- The region spec that will actually build this display: its own type's, or the
|
|
-- `fallback` type's when the addon has no such region. An aura can name a
|
|
-- regionType we do not have -- upstream ships progresstexture, model and
|
|
-- stopmotion, all of which exist in the wild -- and nothing rejects one on
|
|
-- import, so without this it gets a row in the list and is then simply never
|
|
-- drawn, with nothing on screen to say why.
|
|
--
|
|
-- Resolved wherever a region is built rather than rewritten at import, so an aura
|
|
-- whose type we later gain starts working on its own, with no second migration.
|
|
function WA.RegionSpecFor(data)
|
|
return (data and WA.regionTypes[data.regionType]) or WA.regionTypes["fallback"]
|
|
end
|
|
|
|
-- True if this aura's regionType is a container (group/dynamicgroup) rather
|
|
-- than a leaf display: it has no trigger of its own, and is addressed by id
|
|
-- in another aura's controlledChildren instead of (or as well as) WA.GetOrder().
|
|
function WA.IsGroup(data)
|
|
local region = WA.regionTypes[data.regionType]
|
|
return region ~= nil and region.isGroup == true
|
|
end
|
|
|
|
-- A texture value this client can actually draw, or nil for one it cannot --
|
|
-- every caller already has a placeholder for nil, so an unusable value falls
|
|
-- back to it instead of painting the engine's missing-texture block.
|
|
--
|
|
-- Two forms reach us from a WeakAuras export and neither is a file here:
|
|
--
|
|
-- * A **fileID** (135274, or the string "135274" -- upstream writes both).
|
|
-- Modern clients keep a numeric handle per texture and `SetTexture` accepts
|
|
-- it; this one has no such table, so WA2FileIDs.lua ships the offline half of
|
|
-- one -- the icons the client's own MPQs contain, keyed by the fileID retail
|
|
-- gave them. An ID outside that table names an icon added after 1.12 and is
|
|
-- genuinely undrawable.
|
|
-- * An **atlas name** (`Legionfall_BarSpark`, `GarrMission_EncounterBar-Spark`).
|
|
-- Atlases are a retail concept with no 1.12 equivalent, and they are
|
|
-- recognisable by having no path separator -- a real texture path here always
|
|
-- has one.
|
|
--
|
|
-- A path we *can* form may still be missing (someone else's addon folder), and
|
|
-- nothing offline can tell: `SetTexture` reports nothing and `GetTexture` hands
|
|
-- back whatever string it was given. That case still draws the block.
|
|
--
|
|
-- This is on the paint path -- Regions.lua calls it for icon, texture,
|
|
-- progresstexture, spark and group art -- so the fileID table is built on the
|
|
-- first numeric value and never again. A session that pastes no import string
|
|
-- pays for WA2FileIDs.lua's string constant and nothing more.
|
|
local fileIDs
|
|
local function fileIDPath(id)
|
|
if not fileIDs then
|
|
-- WA2FileIDs.lua loads after this file, so the string can legitimately
|
|
-- be absent -- caching an empty table for the session would be wrong.
|
|
if not WA.wa2FileIDs then return nil end
|
|
fileIDs = {}
|
|
for num, name in string.gmatch(WA.wa2FileIDs, "(%d+),([^;]+)") do
|
|
fileIDs[tonumber(num)] = name
|
|
end
|
|
end
|
|
local name = fileIDs[id]
|
|
return name and ("Interface\\Icons\\" .. name) or nil
|
|
end
|
|
|
|
function WA.DrawableTexture(value)
|
|
if type(value) == "number" then return fileIDPath(value) end
|
|
if type(value) ~= "string" or value == "" then return nil end
|
|
if not string.find(value, "\\", 1, true) and not string.find(value, "/", 1, true) then
|
|
-- Upstream writes a fileID as a number or as its digits, and the digit
|
|
-- form is unambiguous here: no texture path lacks a separator, and no
|
|
-- atlas name is all digits.
|
|
if string.find(value, "^%d+$") then return fileIDPath(tonumber(value)) end
|
|
return nil
|
|
end
|
|
return value
|
|
end
|
|
|
|
-- A model path this client's SetModel can load, or nil. Takes the fileDataID a
|
|
-- WeakAuras export stores (number or digit string); WA2ModelIDs.lua ships the
|
|
-- resolvable subset, ";fileID,path;"-delimited. Unlike the icon table this one
|
|
-- is searched in place per lookup rather than parsed whole: model auras are
|
|
-- rare, and the string is several times the icon table's size. Hits and misses
|
|
-- are memoised, so a shown model costs one scan per session.
|
|
--
|
|
-- The '.mdx' spelling is deliberate: it is the form this client's SetModel
|
|
-- provably takes (pfUI), and the engine maps it onto the archives' .m2 itself.
|
|
local function packedLookup(s, key)
|
|
if not s then return nil end
|
|
local _, stop = string.find(s, ";" .. key .. ",", 1, true)
|
|
if not stop then return nil end
|
|
local semi = string.find(s, ";", stop + 1, true)
|
|
if not semi then return nil end
|
|
return string.sub(s, stop + 1, semi - 1)
|
|
end
|
|
|
|
local modelFiles
|
|
function WA.ResolveModelFile(value)
|
|
local id = tonumber(value)
|
|
if not id then return nil end
|
|
modelFiles = modelFiles or {}
|
|
local hit = modelFiles[id]
|
|
if hit ~= nil then
|
|
if hit == false then return nil end
|
|
return hit
|
|
end
|
|
local path = packedLookup(WA.wa2ModelIDs, id)
|
|
if path then path = string.gsub(path, "/", "\\") .. ".mdx" end
|
|
modelFiles[id] = path or false
|
|
return path
|
|
end
|
|
|
|
-- A CreatureDisplayInfo id that dresses this model path, or nil --
|
|
-- SetDisplayInfo with it renders the model textured where SetModel renders it
|
|
-- white (skins live in CreatureDisplayInfo, which SetModel never consults).
|
|
-- Table shipped by tools/mkmodeldisplays.py from this client's own DBCs.
|
|
local modelDisplays
|
|
function WA.ResolveModelDisplay(path)
|
|
if type(path) ~= "string" or path == "" then return nil end
|
|
modelDisplays = modelDisplays or {}
|
|
local hit = modelDisplays[path]
|
|
if hit ~= nil then
|
|
if hit == false then return nil end
|
|
return hit
|
|
end
|
|
local key = string.lower(string.gsub(path, "\\", "/"))
|
|
key = string.gsub(key, "%.md[xl]$", "")
|
|
key = string.gsub(key, "%.m2$", "")
|
|
local id = tonumber(packedLookup(WA.modelDisplayIDs, key))
|
|
modelDisplays[path] = id or false
|
|
return id
|
|
end
|
|
|
|
-- Resolves the icon a leaf display currently shows: Manual mode uses
|
|
-- data.displayIcon outright; Automatic asks the first trigger that can name
|
|
-- one (WA.GetTriggerSystem's GetNameAndIcon), falling back to displayIcon.
|
|
-- nil when nothing resolves -- every caller applies its own placeholder.
|
|
-- Same rule region:UpdateIcon applies at runtime (Regions.lua); shared here so
|
|
-- the aura list row and the icon/progressbar thumbnails can't drift apart.
|
|
function WA.ResolveDisplayIcon(data)
|
|
if WA.IsGroup(data) then return nil end
|
|
if data.iconSource == 0 then
|
|
return WA.DrawableTexture(data.displayIcon)
|
|
end
|
|
for n = 1, #(data.triggers or {}) do
|
|
local system = WA.GetTriggerSystem and WA.GetTriggerSystem(data, n)
|
|
if system and system.GetNameAndIcon then
|
|
local _, icon = system.GetNameAndIcon(data, n)
|
|
icon = WA.DrawableTexture(icon)
|
|
if icon then return icon end
|
|
end
|
|
end
|
|
return WA.DrawableTexture(data.displayIcon)
|
|
end
|
|
|
|
-- A table-valued default (e.g. a group's controlledChildren = {}) must be
|
|
-- copied per-instance, not handed out by reference -- mergeMissing used to
|
|
-- assign the defaults table itself, which would leave every new aura of that
|
|
-- type sharing one physical controlledChildren array. The same trap catches
|
|
-- anything copying a whole display, so the exporter and WA.DuplicateAura share
|
|
-- this rather than each carrying their own six lines.
|
|
function WA.DeepCopy(v)
|
|
if type(v) ~= "table" then return v end
|
|
local copy = {}
|
|
for k2, v2 in pairs(v) do copy[k2] = WA.DeepCopy(v2) end
|
|
return copy
|
|
end
|
|
|
|
local function mergeMissing(target, defaults)
|
|
if not defaults then return end
|
|
for k, v in pairs(defaults) do
|
|
if target[k] == nil then target[k] = WA.DeepCopy(v) end
|
|
end
|
|
end
|
|
|
|
local function renameField(target, oldKey, newKey)
|
|
if target[oldKey] ~= nil then
|
|
target[newKey] = target[oldKey]
|
|
target[oldKey] = nil
|
|
end
|
|
end
|
|
|
|
local TEXT_REGION_RENAMES = {
|
|
text_font = "font",
|
|
text_size = "fontSize",
|
|
text_flags = "outline",
|
|
text_color = "color",
|
|
text_justifyH = "justify",
|
|
text_justifyV = "justifyV",
|
|
text_spacing = "spacing",
|
|
text_shadowColor = "shadowColor",
|
|
text_shadowX = "shadowXOffset",
|
|
text_shadowY = "shadowYOffset",
|
|
}
|
|
local SUBTEXT_RENAMES = {
|
|
text_size = "text_fontSize",
|
|
text_flags = "text_fontType",
|
|
text_justifyH = "text_justify",
|
|
text_shadowX = "text_shadowXOffset",
|
|
text_shadowY = "text_shadowYOffset",
|
|
text_anchorPoint = "anchor_point",
|
|
text_x = "anchorXOffset",
|
|
text_y = "anchorYOffset",
|
|
}
|
|
|
|
local function migrateConditionProperties(data)
|
|
local conditions = data.conditions or {}
|
|
for i = 1, #conditions do
|
|
local changes = conditions[i].changes or {}
|
|
for j = 1, #changes do
|
|
local property = changes[j].property
|
|
if property == "text_size" then
|
|
changes[j].property = "fontSize"
|
|
elseif property == "text_color" then
|
|
changes[j].property = "color"
|
|
else
|
|
local _, _, prefix, suffix = string.find(property or "", "^(sub%.[0-9]+%.)(.*)$")
|
|
if suffix == "text_size" then
|
|
changes[j].property = prefix .. "text_fontSize"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Rewrites the index inside every `sub.<n>.<key>` condition property against a
|
|
-- map of old index -> new index, or -> false where the sub-region is gone (the
|
|
-- change's property is cleared, which the editor renders as an unset row rather
|
|
-- than erroring).
|
|
--
|
|
-- Every structural edit to data.subRegions must call this. The index is the only
|
|
-- thing tying a condition to the element it targets, so a shift that skips it
|
|
-- does not error -- it silently retargets the condition at whatever moved into
|
|
-- the slot. Conditions are the sole holder of these references; anchor targets
|
|
-- are region-provided keys ("region", "bar", "icon"), never `sub.<n>`.
|
|
function WA.RemapSubRegionRefs(data, replacements)
|
|
local conditions = data.conditions or {}
|
|
for i = 1, #conditions do
|
|
local changes = conditions[i].changes or {}
|
|
for j = 1, #changes do
|
|
local _, _, index, suffix = string.find(changes[j].property or "", "^sub%.([0-9]+)%.(.*)$")
|
|
if index then
|
|
-- Present-but-false is "deleted" and nil is "not in the map, leave
|
|
-- alone", so this cannot collapse into a truthiness test.
|
|
local mapped = replacements[tonumber(index)]
|
|
if mapped == false then
|
|
changes[j].property = nil
|
|
elseif mapped then
|
|
changes[j].property = "sub." .. mapped .. "." .. suffix
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Keeps exactly one entry of `kind` in data.subRegions: inserts one at index 1
|
|
-- when there is none, drops the extras when there is more than one. Mirrors
|
|
-- upstream's Private.EnforceSubregionExists, condition remap included -- both
|
|
-- edits renumber, and a renumber no map describes silently retargets whatever
|
|
-- conditions point past it.
|
|
--
|
|
-- Index 1 is what makes the insert invisible: a row standing for the region's
|
|
-- own art, placed under everything the aura already carries, leaves every
|
|
-- effect drawing exactly where it drew before.
|
|
function WA.EnforceSubregionExists(data, kind)
|
|
local subs = data.subRegions or {}
|
|
data.subRegions = subs
|
|
local found = {}
|
|
for i = 1, #subs do
|
|
if subs[i].type == kind then table.insert(found, i) end
|
|
end
|
|
local n = #found
|
|
if n == 0 then
|
|
local map = {}
|
|
for i = 1, #subs do map[i] = i + 1 end
|
|
table.insert(subs, 1, { type = kind })
|
|
WA.RemapSubRegionRefs(data, map)
|
|
elseif n > 1 then
|
|
-- Backwards, so each removal's index is still the one the map is built
|
|
-- against: taking the earliest first would renumber the rest under it.
|
|
for i = n, 2, -1 do
|
|
local at = found[i]
|
|
local map = { [at] = false }
|
|
for j = at + 1, #subs do map[j] = j - 1 end
|
|
table.remove(subs, at)
|
|
WA.RemapSubRegionRefs(data, map)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- What each sub-region type's draw level used to be a fixed constant of, before
|
|
-- the level came off the entry's position in data.subRegions. A type owning no
|
|
-- frame at all -- subtick, which draws on the parent's bar and takes no level --
|
|
-- takes the floor, since where it sits in the list cannot change what it looks
|
|
-- like either way.
|
|
local LEGACY_SUB_LEVELS = { subtext = 1, subborder = 2, subglow = 3 }
|
|
local LEGACY_SUB_FLOOR = 1
|
|
|
|
-- Draw order was per-type and list order meant nothing, so a saved aura can
|
|
-- carry a border ahead of a text in the list while drawing behind it. Sorting by
|
|
-- the old constants makes list order agree with what is already on screen;
|
|
-- skipping it would restack every such aura the first time it loads, silently.
|
|
local function migrateSchemaV5(data)
|
|
local subs = data.subRegions
|
|
local n = #(subs or {})
|
|
if n < 2 then return end
|
|
local order = {}
|
|
for i = 1, n do
|
|
order[i] = { index = i, rank = LEGACY_SUB_LEVELS[subs[i].type] or LEGACY_SUB_FLOOR }
|
|
end
|
|
-- table.sort is not stable, so equal ranks fall through to the original
|
|
-- index: two texts must keep the order the user put them in.
|
|
table.sort(order, function(a, b)
|
|
if a.rank ~= b.rank then return a.rank < b.rank end
|
|
return a.index < b.index
|
|
end)
|
|
local sorted, map, moved = {}, {}, false
|
|
for i = 1, n do
|
|
sorted[i] = subs[order[i].index]
|
|
map[order[i].index] = i
|
|
if order[i].index ~= i then moved = true end
|
|
end
|
|
if not moved then return end
|
|
for i = 1, n do subs[i] = sorted[i] end
|
|
WA.RemapSubRegionRefs(data, map)
|
|
end
|
|
|
|
-- The progresstexture crop moved onto upstream's meaning: a sampling scale
|
|
-- about the texture centre, where the sqrt(2) growth in the shared transform
|
|
-- makes 0.41 the no-crop value, rather than a per-edge inset where 0 was. A
|
|
-- WeakAuras2 aura's crop_x now travels here untranslated, and the two
|
|
-- orientation families share one formula.
|
|
--
|
|
-- Whether the aura carried a crop of its own is decided before MergeDefaults
|
|
-- seeds one, and passed in: afterwards a seeded default is indistinguishable
|
|
-- from a stored value, and rescaling a seeded 0.41 would zoom a new aura's art.
|
|
-- The value itself is read here rather than snapshotted, because schema 3's
|
|
-- camelCase fold writes `crop_x` after that point.
|
|
--
|
|
-- The mapping preserves what each aura already drew -- an old inset of c
|
|
-- sampled 1 - 2c of the art, the new scale samples sqrt(2) / (1 + crop) -- so
|
|
-- it cannot change how anything looks, whichever convention the stored value
|
|
-- was written under. user_x flips with it: upstream negates that key on the way
|
|
-- to the texture, and a seeded 0 is left alone by the same arithmetic.
|
|
local function migrateSchemaV6(data, hadCropX, hadCropY)
|
|
if data.regionType ~= "progresstexture" then return end
|
|
local function scaleFor(crop)
|
|
local span = 1 - 2 * (crop or 0)
|
|
if span < 0.01 then span = 0.01 end
|
|
return 1.4142 / span - 1
|
|
end
|
|
if hadCropX then data.crop_x = scaleFor(data.crop_x) end
|
|
if hadCropY then data.crop_y = scaleFor(data.crop_y) end
|
|
if data.user_x then data.user_x = -data.user_x end
|
|
end
|
|
|
|
-- The class load constraint was the only multi-select in the addon and stored
|
|
-- its set under a name of its own (`classes`). Every multi-select now spells the
|
|
-- set `<name>_multi`, so the one-off name folds into the convention.
|
|
local function migrateSchemaV7(data)
|
|
local L = data.load
|
|
if type(L) ~= "table" then return end
|
|
if L.classes ~= nil and L.class_multi == nil then
|
|
L.class_multi = L.classes
|
|
end
|
|
L.classes = nil
|
|
-- The three constraints that gained the tier were plain on/off gates.
|
|
for _, name in ipairs({ "race", "faction", "ingroup" }) do
|
|
WA.MultiSelectMigrateGate(L, name)
|
|
end
|
|
end
|
|
|
|
-- The stance load constraint was a bare bar position with an on/off gate; it
|
|
-- takes the multi-select tier, where the position it held is the "one of" value.
|
|
local function migrateSchemaV8(data)
|
|
local L = data.load
|
|
if type(L) ~= "table" then return end
|
|
WA.MultiSelectMigrateGate(L, "stance")
|
|
end
|
|
|
|
-- A sub-region imported from upstream before the combined anchor was split
|
|
-- carries the anchored part where a bare point belongs ("OUTER_TOPLEFT"), which
|
|
-- SetPoint rejects outright.
|
|
local function migrateSchemaV4(data)
|
|
local proto = WA.regionPrototype
|
|
for i = 1, #(data.subRegions or {}) do
|
|
local subData = data.subRegions[i]
|
|
local anchor = subData and subData.anchor_point
|
|
if type(anchor) == "string" and not proto.IsAnchorPoint(anchor) then
|
|
local point = proto.ResolveAnchorPoint(anchor, "CENTER")
|
|
subData.anchor_target = subData.anchor_target or anchor
|
|
subData.anchor_point = point
|
|
if not proto.IsAnchorPoint(subData.self_point) then
|
|
subData.self_point = proto.AutoSelfPoint(anchor, point, data.regionType)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function migrateSchemaV3(data)
|
|
if data.regionType == "text" then
|
|
for oldKey, newKey in pairs(TEXT_REGION_RENAMES) do
|
|
renameField(data, oldKey, newKey)
|
|
end
|
|
end
|
|
for i = 1, #(data.subRegions or {}) do
|
|
local subData = data.subRegions[i]
|
|
if subData then
|
|
for oldKey, newKey in pairs(SUBTEXT_RENAMES) do
|
|
renameField(subData, oldKey, newKey)
|
|
end
|
|
if subData.tick_placement ~= nil then
|
|
subData.tick_placements = { subData.tick_placement }
|
|
subData.tick_placement = nil
|
|
end
|
|
end
|
|
end
|
|
if data.regionType == "progresstexture" then
|
|
renameField(data, "cropX", "crop_x")
|
|
renameField(data, "cropY", "crop_y")
|
|
renameField(data, "userX", "user_x")
|
|
renameField(data, "userY", "user_y")
|
|
end
|
|
migrateConditionProperties(data)
|
|
end
|
|
|
|
-- The version the migration gates in MergeDefaults end on. The two must move
|
|
-- together: a new migration adds a gate and bumps this in the same change.
|
|
-- Data that is current-schema by construction rather than by migration
|
|
-- (WA2Translate's output) is stamped with it directly.
|
|
WA.SCHEMA_VERSION = 8
|
|
|
|
-- Fills in missing fields from the aura's regionType/trigger.type defaults, and
|
|
-- migrates saved data through the local schema versions (upstream's `triggers`
|
|
-- array + combination metadata, §1). Safe to call repeatedly (e.g. after
|
|
-- switching regionType, or normalizing older saved data) -- never overwrites a
|
|
-- field the user already set.
|
|
function WA.MergeDefaults(data)
|
|
local region = WA.regionTypes[data.regionType]
|
|
-- Asked before the merge seeds them: schema 6 rescales a stored crop, and a
|
|
-- seeded default is indistinguishable from one once mergeMissing has run.
|
|
-- Both spellings, since schema 3's camelCase fold has not run yet either.
|
|
local hadCropX = data.crop_x ~= nil or data.cropX ~= nil
|
|
local hadCropY = data.crop_y ~= nil or data.cropY ~= nil
|
|
mergeMissing(data, region and region.defaults)
|
|
|
|
if WA.IsGroup(data) then
|
|
-- Groups combine other displays; they have no trigger of their own.
|
|
-- Old NewAura seeded every aura (groups included) with a stray
|
|
-- trigger/triggers table -- clear it so a group never carries one.
|
|
data.trigger = nil
|
|
data.triggers = nil
|
|
else
|
|
-- Collapse the old single `data.trigger` into triggers[1].trigger,
|
|
-- unchanged inside (§1).
|
|
if data.trigger then
|
|
data.triggers = { { trigger = data.trigger } }
|
|
data.trigger = nil
|
|
end
|
|
data.triggers = data.triggers or {}
|
|
if not data.triggers[1] then data.triggers[1] = { trigger = {} } end
|
|
if data.triggers.disjunctive == nil then data.triggers.disjunctive = "all" end
|
|
if data.triggers.activeTriggerMode == nil then
|
|
data.triggers.activeTriggerMode = WA.trigger_modes.first_active
|
|
end
|
|
|
|
for i = 1, #data.triggers do
|
|
local entry = data.triggers[i]
|
|
entry.trigger = entry.trigger or {}
|
|
entry.untrigger = entry.untrigger or {}
|
|
entry.trigger.type = entry.trigger.type or "aura"
|
|
local ttype = WA.triggerTypes[entry.trigger.type]
|
|
if ttype and ttype.migrate then ttype.migrate(entry.trigger) end
|
|
mergeMissing(entry.trigger, ttype and ttype.defaults)
|
|
end
|
|
|
|
-- A fixed activeTriggerMode left pointing past the end (a trigger the
|
|
-- user deleted) self-heals back to first_active, so saved data never
|
|
-- resolves a shown-but-nonexistent trigger -- same self-repair spirit as
|
|
-- GetOrder (upstream clamps here too).
|
|
local atm = data.triggers.activeTriggerMode
|
|
if type(atm) == "number" and atm ~= WA.trigger_modes.first_active
|
|
and (atm < 1 or atm > #data.triggers) then
|
|
data.triggers.activeTriggerMode = WA.trigger_modes.first_active
|
|
end
|
|
end
|
|
|
|
-- Always present, even on an aura that uses none, so adding one never needs
|
|
-- a migration pass.
|
|
data.subRegions = data.subRegions or {}
|
|
-- Upstream enforces its placeholders from each region type's `validate`;
|
|
-- driving it off the registry instead means a type declaring itself enforced
|
|
-- needs no new call site here. No internalVersion gate, and upstream has none
|
|
-- either: the call is idempotent, so a saved aura gains its row on the next
|
|
-- merge and keeps it. It runs ahead of the migrations below on purpose --
|
|
-- schema 5's legacy sort ranks an unknown type at the floor and breaks ties
|
|
-- by original index, so the row it inserts at 1 stays at 1 through that pass.
|
|
for kind, spec in pairs(WA.subRegionTypes) do
|
|
if spec.enforced and (not spec.supports or spec.supports(data.regionType)) then
|
|
WA.EnforceSubregionExists(data, kind)
|
|
end
|
|
end
|
|
data.conditions = data.conditions or {}
|
|
data.load = data.load or {}
|
|
data.authorOptions = data.authorOptions or {}
|
|
data.config = data.config or {}
|
|
WA.ValidateUserConfig(data)
|
|
data.actions = data.actions or {}
|
|
data.actions.init = data.actions.init or {}
|
|
data.actions.start = data.actions.start or {}
|
|
data.actions.finish = data.actions.finish or {}
|
|
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"
|
|
|
|
-- Second migration (upstream's internalVersion/Modernize pattern): seed the
|
|
-- default sub-region texts exactly once. internalVersion < 1 covers both a
|
|
-- brand-new aura (NewAura routes through here) and one predating sub-regions,
|
|
-- whose countdown/stacks text was hardcoded in the region -- both get the
|
|
-- defaults, and the bump keeps them from being re-added if the user later
|
|
-- clears every text. Depends on every RegisterSubRegionType having run, same
|
|
-- ordering constraint NormalizeAll already relies on (see OptionsFrame.lua).
|
|
data.internalVersion = data.internalVersion or 0
|
|
if not WA.IsGroup(data) and data.internalVersion < 1 then
|
|
WA.AddDefaultsForNewAura(data)
|
|
data.internalVersion = 1
|
|
end
|
|
|
|
-- Third migration: position moved from a fixed
|
|
-- CENTER/UIParent x/y to the full anchor tuple (selfPoint/anchorPoint/
|
|
-- xOffset/yOffset). mergeMissing above already seeded the tuple defaults;
|
|
-- carry a pre-tuple aura's old x/y onto the offsets before dropping them.
|
|
-- A brand-new aura has no x/y, so it keeps the seeded defaults untouched.
|
|
if not WA.IsGroup(data) and data.internalVersion < 2 then
|
|
if data.x ~= nil then data.xOffset = data.x end
|
|
if data.y ~= nil then data.yOffset = data.y end
|
|
data.x = nil
|
|
data.y = nil
|
|
data.internalVersion = 2
|
|
end
|
|
|
|
if data.internalVersion < 3 then
|
|
migrateSchemaV3(data)
|
|
data.internalVersion = 3
|
|
end
|
|
|
|
if data.internalVersion < 4 then
|
|
migrateSchemaV4(data)
|
|
data.internalVersion = 4
|
|
end
|
|
|
|
if data.internalVersion < 5 then
|
|
migrateSchemaV5(data)
|
|
data.internalVersion = 5
|
|
end
|
|
|
|
if data.internalVersion < 6 then
|
|
migrateSchemaV6(data, hadCropX, hadCropY)
|
|
data.internalVersion = 6
|
|
end
|
|
|
|
if data.internalVersion < 7 then
|
|
migrateSchemaV7(data)
|
|
data.internalVersion = 7
|
|
end
|
|
|
|
if data.internalVersion < 8 then
|
|
migrateSchemaV8(data)
|
|
data.internalVersion = 8
|
|
end
|
|
end
|
|
|
|
-- Re-ensures WeakestAurasDB.order is a table on every touch rather than trusting the
|
|
-- one-time `or {}` at file load up top: a SavedVariables table restored after that line
|
|
-- ran (e.g. one saved before this field existed) clobbers the whole WeakestAurasDB
|
|
-- global, wiping out that early init same as MergeDefaults re-ensures data.trigger
|
|
-- exists every call instead of assuming NewAura's initial `{}` stuck.
|
|
local function ensureOrder()
|
|
WeakestAurasDB.order = WeakestAurasDB.order or {}
|
|
return WeakestAurasDB.order
|
|
end
|
|
|
|
-- Addon-wide settings that belong to no single display (the mover's lock and
|
|
-- magnetism toggles). Re-ensured on every touch for the same reason
|
|
-- ensureOrder is -- a SavedVariables table restored from before this field
|
|
-- existed replaces the whole WeakestAurasDB global.
|
|
function WA.Options()
|
|
WeakestAurasDB.options = WeakestAurasDB.options or {}
|
|
return WeakestAurasDB.options
|
|
end
|
|
|
|
-- The ordered child-id array `id` currently sits in: its parent group's
|
|
-- controlledChildren if it has one, WeakestAurasDB.order otherwise. A dangling
|
|
-- .parent (the parent got deleted without going through WA.DeleteAura) falls
|
|
-- back to the top-level order, same as a plain unparented id -- repairOrder
|
|
-- below cleans up the inconsistency on the next read either way.
|
|
local function currentList(id)
|
|
local data = WeakestAurasDB.displays[id]
|
|
if data and data.parent then
|
|
local parent = WeakestAurasDB.displays[data.parent]
|
|
if parent then
|
|
parent.controlledChildren = parent.controlledChildren or {}
|
|
return parent.controlledChildren
|
|
end
|
|
end
|
|
return ensureOrder()
|
|
end
|
|
|
|
local function indexOf(list, id)
|
|
for i = 1, #list do
|
|
if list[i] == id then return i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function removeFromList(list, id)
|
|
local i = indexOf(list, id)
|
|
if i then table.remove(list, i) end
|
|
return i
|
|
end
|
|
|
|
-- Detaches id from wherever it currently sits (its parent's controlledChildren,
|
|
-- or the top-level order) without deciding where it goes next -- the shared
|
|
-- first step of AddChildToGroup, RemoveChildFromGroup, and (for a cross-list
|
|
-- move) ReorderAura.
|
|
local function detach(id)
|
|
local data = WeakestAurasDB.displays[id]
|
|
removeFromList(currentList(id), id)
|
|
if data then data.parent = nil end
|
|
end
|
|
|
|
-- Drops stale/misplaced ids from a group's controlledChildren (a child that
|
|
-- was deleted, or reparented elsewhere since this was last read), and appends
|
|
-- any child whose .parent points here but isn't listed yet. Same self-healing
|
|
-- spirit as repairOrder below, just scoped to one group's own list.
|
|
local function repairGroupChildren(groupId)
|
|
local group = WeakestAurasDB.displays[groupId]
|
|
if not group then return end
|
|
group.controlledChildren = group.controlledChildren or {}
|
|
local list = group.controlledChildren
|
|
local seen = {}
|
|
for i = #list, 1, -1 do
|
|
local id = list[i]
|
|
local child = WeakestAurasDB.displays[id]
|
|
if not child or child.parent ~= groupId or seen[id] then
|
|
table.remove(list, i)
|
|
else
|
|
seen[id] = true
|
|
end
|
|
end
|
|
for id, child in pairs(WeakestAurasDB.displays) do
|
|
if child.parent == groupId and not seen[id] then
|
|
table.insert(list, id)
|
|
seen[id] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Keeps WeakestAurasDB.order in sync with what's actually top-level in .displays
|
|
-- (drops ids a delete/rename left stale or that became a group's child since,
|
|
-- appends any top-level display order doesn't know about yet -- a fresh save, or
|
|
-- a DB saved before order existed), and reconciles every group's controlledChildren
|
|
-- the same way. Cheap and idempotent -- WA.GetOrder just calls it on every read
|
|
-- rather than special-casing each caller that mutates .displays or reparents a child.
|
|
local function repairOrder()
|
|
for id, data in pairs(WeakestAurasDB.displays) do
|
|
if WA.IsGroup(data) then repairGroupChildren(id) end
|
|
end
|
|
|
|
local order = ensureOrder()
|
|
local seen = {}
|
|
for i = #order, 1, -1 do
|
|
local id = order[i]
|
|
local data = WeakestAurasDB.displays[id]
|
|
if not data or data.parent ~= nil or seen[id] then
|
|
table.remove(order, i)
|
|
else
|
|
seen[id] = true
|
|
end
|
|
end
|
|
for id, data in pairs(WeakestAurasDB.displays) do
|
|
if data.parent == nil and not seen[id] then
|
|
table.insert(order, id)
|
|
seen[id] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The display order the aura list (OptionsFrame.lua) renders in -- a user-arranged
|
|
-- order via drag-to-reorder, not alphabetical. Top-level only; a group's own
|
|
-- controlledChildren is its children's order.
|
|
function WA.GetOrder()
|
|
repairOrder()
|
|
return WeakestAurasDB.order
|
|
end
|
|
|
|
-- Detaches childId from whatever group currently owns it (a no-op if it's
|
|
-- already top-level) and returns it to the end of the top-level order. Also
|
|
-- what WA.DeleteAura promotes a deleted group's children with.
|
|
function WA.RemoveChildFromGroup(childId)
|
|
local child = WeakestAurasDB.displays[childId]
|
|
if not child or not child.parent then return end
|
|
local oldParent = child.parent
|
|
detach(childId)
|
|
table.insert(ensureOrder(), childId)
|
|
if WA.RefreshMembership then WA.RefreshMembership(childId, oldParent, nil) end
|
|
end
|
|
|
|
-- True if candidateId is groupId itself, or nested anywhere under it via
|
|
-- controlledChildren -- guards AddChildToGroup against creating a cycle
|
|
-- (dropping a group onto its own descendant in OptionsFrame.lua's tree).
|
|
local function isSelfOrDescendant(groupId, candidateId)
|
|
if groupId == candidateId then return true end
|
|
local data = WeakestAurasDB.displays[groupId]
|
|
if not data or not data.controlledChildren then return false end
|
|
for i = 1, #data.controlledChildren do
|
|
if isSelfOrDescendant(data.controlledChildren[i], candidateId) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Moves childId into parentId's controlledChildren, detaching it from any
|
|
-- previous group (or the top level) first. index positions it within the new
|
|
-- parent's list (1..n+1; omitted or out of range appends). See WA.ReorderAura
|
|
-- for the boundary-move version driven by drag-and-drop.
|
|
function WA.AddChildToGroup(parentId, childId, index)
|
|
local parent = WeakestAurasDB.displays[parentId]
|
|
local child = WeakestAurasDB.displays[childId]
|
|
if not parent or not child or not WA.IsGroup(parent) then return end
|
|
if isSelfOrDescendant(childId, parentId) then return end
|
|
|
|
local oldParent = child.parent
|
|
detach(childId)
|
|
parent.controlledChildren = parent.controlledChildren or {}
|
|
local list = parent.controlledChildren
|
|
local n = #list
|
|
if not index or index < 1 or index > n + 1 then index = n + 1 end
|
|
table.insert(list, index, childId)
|
|
child.parent = parentId
|
|
if WA.RefreshMembership then WA.RefreshMembership(childId, oldParent, parentId) end
|
|
end
|
|
|
|
-- Moves `id` to sit just before whatever currently occupies `before` (1..n+1;
|
|
-- n+1 appends) within newParentId's child list (nil = top level, i.e.
|
|
-- WeakestAurasDB.order). Handles both a same-list reorder -- OptionsFrame.lua's
|
|
-- drag-to-reorder is the only caller today, always with newParentId = nil,
|
|
-- unchanged from the flat-list behavior this replaces -- and a cross-list move
|
|
-- (dragging a child out of a group, into a different one, or between top level
|
|
-- and a group), which the aura list's group drop-targets commit through.
|
|
function WA.ReorderAura(id, newParentId, before)
|
|
if not WeakestAurasDB.displays[id] then return end
|
|
-- A target anywhere inside id's own subtree would detach the branch from
|
|
-- the tree and close it into a cycle; the parent chain up from the target
|
|
-- passing through id itself is the test (covers newParentId == id too).
|
|
local up = newParentId
|
|
while up do
|
|
if up == id then return end
|
|
local upData = WeakestAurasDB.displays[up]
|
|
up = upData and upData.parent
|
|
end
|
|
local oldParent = WeakestAurasDB.displays[id].parent
|
|
|
|
local newList
|
|
if newParentId == nil then
|
|
newList = ensureOrder()
|
|
else
|
|
local parent = WeakestAurasDB.displays[newParentId]
|
|
if not parent or not WA.IsGroup(parent) then return end
|
|
parent.controlledChildren = parent.controlledChildren or {}
|
|
newList = parent.controlledChildren
|
|
end
|
|
|
|
local oldList = currentList(id)
|
|
local sameList = (oldList == newList)
|
|
local from = indexOf(oldList, id)
|
|
if not from then return end
|
|
|
|
local n = #newList
|
|
if before < 1 then before = 1 elseif before > n + 1 then before = n + 1 end
|
|
|
|
if sameList then
|
|
if before == from or before == from + 1 then return end
|
|
table.remove(oldList, from)
|
|
local insertAt = before > from and before - 1 or before
|
|
table.insert(newList, insertAt, id)
|
|
else
|
|
table.remove(oldList, from)
|
|
table.insert(newList, before, id)
|
|
WeakestAurasDB.displays[id].parent = newParentId
|
|
if WA.RefreshMembership then WA.RefreshMembership(id, oldParent, newParentId) end
|
|
end
|
|
end
|
|
|
|
local function newUID()
|
|
local s = ""
|
|
for i = 1, 8 do
|
|
s = s .. string.format("%x", math.random(0, 15))
|
|
end
|
|
return s
|
|
end
|
|
-- Exposed for the importer, which regenerates a stable-across-rename uid for
|
|
-- every imported display (ImportExport.lua).
|
|
WA.NewUID = newUID
|
|
|
|
-- Hook point for the display engine to (re)build this aura's runtime state and
|
|
-- on-screen region. Defined here as a no-op purely so every options callback in
|
|
-- Regions.lua/Triggers.lua has something to call regardless of load order;
|
|
-- StateMachine.lua (loaded later, see WeakestAuras.toc) overrides this with the
|
|
-- real pAdd-lite. `data` is already the live table stored in
|
|
-- WeakestAurasDB.displays, so edits persist even before that loads.
|
|
function WA.Add(data) end
|
|
|
|
-- Mirrors WA.Add: hook point to tear down whatever runtime state/region this
|
|
-- aura was driving, called from WA.DeleteAura right before the display itself
|
|
-- is dropped. Also overridden by StateMachine.lua.
|
|
function WA.Remove(data) end
|
|
|
|
-- Hook point to re-key runtime state (triggerState, regions, scan indexes) from
|
|
-- oldId to newId after WA.RenameAura moves the display's key -- everything in
|
|
-- the engine is keyed by the display id, which is the rename. No-op until
|
|
-- StateMachine.lua overrides it.
|
|
function WA.Rename(oldId, newId) end
|
|
|
|
-- Hook points for the conditions engine (Conditions.lua overrides these):
|
|
-- LoadConditions (re)compiles a display's condition list at WA.Add time,
|
|
-- UnloadConditions tears it down at WA.Remove, RunConditions evaluates it
|
|
-- against the just-applied states, and ReleaseConditionsForClone discards
|
|
-- activation/timer bookkeeping when a pooled clone leaves a display. No-ops
|
|
-- until Conditions.lua loads, so the state machine can call them unconditionally.
|
|
-- RunConditions is declared in StateMachine.lua (its apply path is the primary
|
|
-- caller).
|
|
function WA.LoadConditions(data) end
|
|
function WA.UnloadConditions(data) end
|
|
function WA.ReleaseConditionsForClone(uid, cloneId) end
|
|
|
|
-- Hook points for the load system (Load.lua overrides these): EvalLoad returns
|
|
-- whether a display's data.load conditions currently pass (class/level/zone/
|
|
-- combat/...), EvalLoadStatic the same with the moment-dependent ones skipped
|
|
-- (the Standby label). No-op defaults return true, so before Load.lua loads
|
|
-- (and for a display with no load conditions) "loaded = added".
|
|
function WA.EvalLoad(data) return true end
|
|
function WA.EvalLoadStatic(data) return true end
|
|
|
|
-- Hook point for an option `set` to re-render the current options tab -- used by
|
|
-- toggles that reveal/hide dependent fields (e.g. "Use stacks" showing the
|
|
-- operator+value row), where a plain WA.Add persists the value but the tab needs
|
|
-- rebuilding to show the newly-relevant controls. No-op until OptionsFrame.lua
|
|
-- overrides it with its refreshTabContent; harmless when the window is closed.
|
|
function WA.RefreshOptions() end
|
|
|
|
-- The group that would own an aura created against targetId: the target itself
|
|
-- when it's a group, the target's parent when it's a leaf, nil for top level.
|
|
function WA.PlacementParent(targetId)
|
|
local target = targetId and WeakestAurasDB.displays[targetId]
|
|
if not target then return nil end
|
|
if WA.IsGroup(target) then return targetId end
|
|
return target.parent
|
|
end
|
|
|
|
-- Whether a display of regionType may be placed against targetId. A dynamic
|
|
-- group arranges its children itself and can't own a nested group -- the one
|
|
-- placement refused outright, so a caller offering the type (the aura list's
|
|
-- New picker) asks here before offering it rather than creating and discarding.
|
|
function WA.CanPlaceAura(regionType, targetId)
|
|
local spec = WA.regionTypes[regionType]
|
|
if not spec then return false end
|
|
if spec.isGroup ~= true then return true end
|
|
local parentId = WA.PlacementParent(targetId)
|
|
local parent = parentId and WeakestAurasDB.displays[parentId]
|
|
return not (parent and parent.regionType == "dynamicgroup")
|
|
end
|
|
|
|
-- Moves an already-registered display to where a creation targeted at targetId
|
|
-- belongs: into targetId when it's a group, immediately after it when it's a
|
|
-- leaf, the end of the top level with no target. Shared by WA.NewAura and any
|
|
-- other "put this one beside that one" caller.
|
|
-- `data` must already be in WeakestAurasDB.displays *and* in some sibling list
|
|
-- -- the move runs through WA.ReorderAura, which does nothing for an id it
|
|
-- can't find. WA.NewAura appends top-level before calling this for that reason.
|
|
function WA.PlaceAura(data, targetId)
|
|
if not data or WeakestAurasDB.displays[data.id] ~= data then return nil end
|
|
if targetId == data.id then return data end
|
|
if not WA.CanPlaceAura(data.regionType, targetId) then return nil end
|
|
|
|
local target = targetId and WeakestAurasDB.displays[targetId]
|
|
if not target then
|
|
WA.ReorderAura(data.id, nil, #(ensureOrder()) + 1)
|
|
return data
|
|
end
|
|
|
|
local parentId = WA.PlacementParent(targetId)
|
|
if parentId and isSelfOrDescendant(data.id, parentId) then return nil end
|
|
|
|
if WA.IsGroup(target) then
|
|
WA.AddChildToGroup(targetId, data.id)
|
|
else
|
|
local list = currentList(targetId)
|
|
local at = indexOf(list, targetId)
|
|
WA.ReorderAura(data.id, target.parent, at and at + 1 or #list + 1)
|
|
end
|
|
return data
|
|
end
|
|
|
|
-- The first free id of the form "Base", "Base 2", "Base 3", ... -- the naming
|
|
-- rule new auras, imports and duplicates all name themselves by.
|
|
function WA.UniqueId(base)
|
|
local id, n = base, 1
|
|
while WeakestAurasDB.displays[id] do
|
|
n = n + 1
|
|
id = base .. " " .. n
|
|
end
|
|
return id
|
|
end
|
|
|
|
function WA.NewAura(regionType, targetId)
|
|
regionType = regionType or "icon"
|
|
local spec = WA.regionTypes[regionType]
|
|
if not spec or spec.internal then return nil end
|
|
if not WA.CanPlaceAura(regionType, targetId) then return nil end
|
|
|
|
local id = WA.UniqueId("New Aura")
|
|
|
|
-- No trigger seeded here: MergeDefaults builds the triggers array (or leaves
|
|
-- it nil for a group), so both a fresh aura and older saved data take the
|
|
-- one code path.
|
|
local data = { id = id, uid = newUID(), regionType = regionType }
|
|
WA.MergeDefaults(data)
|
|
WeakestAurasDB.displays[id] = data
|
|
table.insert(ensureOrder(), id)
|
|
WA.PlaceAura(data, targetId)
|
|
return data
|
|
end
|
|
|
|
-- A copy's name: bump a trailing number when the source has one
|
|
-- ("Rejuvenation 2" -> "Rejuvenation 3"), otherwise fall back to the shared
|
|
-- suffixing rule. `^(.-)(%d+)$` needs the non-greedy head, or "Aura 19" would
|
|
-- split as "Aura 1" + "9"; string.find returns the captures after its indices,
|
|
-- there being no string.match on this client.
|
|
local function duplicateId(id)
|
|
local _, _, base, digits = string.find(id, "^(.-)(%d+)$")
|
|
if not base then return WA.UniqueId(id) end
|
|
local n = tonumber(digits)
|
|
local candidate = base .. (n + 1)
|
|
while WeakestAurasDB.displays[candidate] do
|
|
n = n + 1
|
|
candidate = base .. (n + 1)
|
|
end
|
|
return candidate
|
|
end
|
|
|
|
-- One display copied to a fresh id/uid at top level, carrying no children and
|
|
-- no parent -- duplicateTree places it.
|
|
local function copyDisplay(data)
|
|
local copy = WA.DeepCopy(data)
|
|
copy.id = duplicateId(data.id)
|
|
copy.uid = newUID()
|
|
copy.parent = nil
|
|
copy.controlledChildren = WA.IsGroup(data) and {} or nil
|
|
WeakestAurasDB.displays[copy.id] = copy
|
|
table.insert(ensureOrder(), copy.id)
|
|
return copy
|
|
end
|
|
|
|
local function duplicateTree(data)
|
|
local copy = copyDisplay(data)
|
|
local children = data.controlledChildren
|
|
if children then
|
|
for i = 1, #children do
|
|
local child = WeakestAurasDB.displays[children[i]]
|
|
if child then WA.AddChildToGroup(copy.id, duplicateTree(child).id) end
|
|
end
|
|
end
|
|
return copy
|
|
end
|
|
|
|
local function addTree(data)
|
|
WA.Add(data)
|
|
local children = data.controlledChildren
|
|
if children then
|
|
for i = 1, #children do
|
|
local child = WeakestAurasDB.displays[children[i]]
|
|
if child then addTree(child) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Copies `id`, placing the copy directly after the original among its siblings.
|
|
-- Groups copy recursively: upstream leaves that to its display-button layer,
|
|
-- which has no counterpart here, and a group duplicate that came out empty
|
|
-- would read as a bug. The whole tree is built before anything is added, so no
|
|
-- region is created against a half-assembled hierarchy. Returns the new id.
|
|
function WA.DuplicateAura(id)
|
|
local data = WeakestAurasDB.displays[id]
|
|
if not data then return nil end
|
|
|
|
local copy = duplicateTree(data)
|
|
local list = currentList(id)
|
|
local at = indexOf(list, id)
|
|
if data.parent then
|
|
WA.AddChildToGroup(data.parent, copy.id, at and at + 1 or nil)
|
|
elseif at then
|
|
WA.ReorderAura(copy.id, nil, at + 1)
|
|
end
|
|
|
|
addTree(copy)
|
|
return copy.id
|
|
end
|
|
|
|
-- Appends a new trigger (defaulting to an "aura" type) to a leaf display's
|
|
-- triggers array and seeds its defaults via MergeDefaults; returns the new
|
|
-- trigger's index. A group has no triggers, so it's a no-op there. The caller
|
|
-- runs WA.Add (StateMachine recompiles the whole triggers array).
|
|
function WA.AddTrigger(data)
|
|
if WA.IsGroup(data) then return nil end
|
|
data.triggers = data.triggers or {}
|
|
table.insert(data.triggers, { trigger = { type = "aura" } })
|
|
WA.MergeDefaults(data)
|
|
return #data.triggers
|
|
end
|
|
|
|
-- Removes trigger n. Refuses to drop the last one (an aura always needs at
|
|
-- least trigger 1); MergeDefaults re-clamps a now-dangling activeTriggerMode.
|
|
-- The array reindexes implicitly (table.remove), so trigger n+1 becomes n --
|
|
-- the combination core keys everything by position, so nothing else needs
|
|
-- renumbering.
|
|
function WA.DeleteTrigger(data, n)
|
|
if not data.triggers or #data.triggers <= 1 then return end
|
|
table.remove(data.triggers, n)
|
|
WA.MergeDefaults(data)
|
|
end
|
|
|
|
-- Deleting a group ungroups its children (promotes them to top-level) rather
|
|
-- than cascading the delete -- the less-destructive default, matching the
|
|
-- aura list's two-click delete confirm.
|
|
function WA.DeleteAura(id)
|
|
local data = WeakestAurasDB.displays[id]
|
|
if not data then return end
|
|
for _, other in pairs(WeakestAurasDB.displays) do
|
|
if other.anchorFrameType == "SELECTFRAME"
|
|
and other.anchorFrameFrame == "WeakestAuras:" .. id then
|
|
other.anchorFrameFrame = nil
|
|
other.anchorFrameType = "UIPARENT"
|
|
WA.Add(other, true)
|
|
end
|
|
end
|
|
|
|
if WA.IsGroup(data) and data.controlledChildren then
|
|
-- Snapshot first: RemoveChildFromGroup mutates controlledChildren as
|
|
-- it goes, which would skip entries if we iterated the live array.
|
|
local children = {}
|
|
for i = 1, #data.controlledChildren do
|
|
children[i] = data.controlledChildren[i]
|
|
end
|
|
for i = 1, #children do
|
|
WA.RemoveChildFromGroup(children[i])
|
|
end
|
|
end
|
|
|
|
detach(id)
|
|
WA.Remove(data)
|
|
if WA.ClearWarningsFor then WA.ClearWarningsFor(data.uid) end
|
|
WeakestAurasDB.displays[id] = nil
|
|
end
|
|
|
|
-- Deletes a group *with* everything under it, the cascading counterpart to
|
|
-- WA.DeleteAura's promote-the-children default. The descendant set is
|
|
-- snapshotted before any of it is deleted, for the same reason DeleteAura
|
|
-- snapshots controlledChildren: the remove path mutates the lists this would
|
|
-- otherwise be walking. Post-order, so no group is deleted while it still
|
|
-- believes it has children.
|
|
function WA.DeleteAuraTree(id)
|
|
if not WeakestAurasDB.displays[id] then return end
|
|
|
|
local doomed = {}
|
|
local function collect(current)
|
|
local data = WeakestAurasDB.displays[current]
|
|
if not data then return end
|
|
local children = data.controlledChildren
|
|
for i = 1, #(children or {}) do collect(children[i]) end
|
|
table.insert(doomed, current)
|
|
end
|
|
collect(id)
|
|
|
|
for i = 1, #doomed do WA.DeleteAura(doomed[i]) end
|
|
end
|
|
|
|
-- Renames the display, moving it to the new key. Returns false (no-op) if the
|
|
-- name is blank or already taken.
|
|
function WA.RenameAura(id, newId)
|
|
if not newId or newId == "" or newId == id then return false end
|
|
if WeakestAurasDB.displays[newId] then return false end
|
|
local data = WeakestAurasDB.displays[id]
|
|
if not data then return false end
|
|
local dependants = {}
|
|
for _, other in pairs(WeakestAurasDB.displays) do
|
|
if other.anchorFrameType == "SELECTFRAME"
|
|
and other.anchorFrameFrame == "WeakestAuras:" .. id then
|
|
table.insert(dependants, other)
|
|
end
|
|
end
|
|
data.id = newId
|
|
WeakestAurasDB.displays[id] = nil
|
|
WeakestAurasDB.displays[newId] = data
|
|
|
|
-- id doubles as the display's key in .displays, so every other place that
|
|
-- references this one by id string -- its parent's controlledChildren
|
|
-- entry, or (if this is a group) each child's own .parent -- needs
|
|
-- repointing too, not just the top-level order below.
|
|
if data.parent then
|
|
local parent = WeakestAurasDB.displays[data.parent]
|
|
if parent and parent.controlledChildren then
|
|
local i = indexOf(parent.controlledChildren, id)
|
|
if i then parent.controlledChildren[i] = newId end
|
|
end
|
|
end
|
|
if data.controlledChildren then
|
|
for i = 1, #data.controlledChildren do
|
|
local child = WeakestAurasDB.displays[data.controlledChildren[i]]
|
|
if child then child.parent = newId end
|
|
end
|
|
end
|
|
|
|
local order = ensureOrder()
|
|
for i = 1, #order do
|
|
if order[i] == id then
|
|
order[i] = newId
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Re-key the engine's runtime state to match the new id (no-op stub until
|
|
-- StateMachine.lua loads).
|
|
WA.Rename(id, newId)
|
|
for i = 1, #dependants do
|
|
local other = dependants[i]
|
|
other.anchorFrameFrame = "WeakestAuras:" .. newId
|
|
WA.Add(other, true)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Normalizes every saved aura against the currently-registered region/trigger
|
|
-- types. Must run only after all RegisterRegionType/RegisterTriggerType calls
|
|
-- have happened -- see the call site in OptionsFrame.lua (the last file loaded)
|
|
-- for why it doesn't just run here at file load.
|
|
--
|
|
-- safecall-wrapped per aura: this runs unconditionally before
|
|
-- WA.AddAllDisplays (same call site, OptionsFrame.lua's last two lines), so an
|
|
-- unguarded error on one malformed aura previously aborted the whole loop --
|
|
-- silently, since nothing here printed -- and with it every *other* aura's
|
|
-- WA.Add for the rest of the session (AddAllDisplays never got reached).
|
|
function WA.NormalizeAll()
|
|
for id, data in pairs(WeakestAurasDB.displays) do
|
|
WA.safecall(id, WA.MergeDefaults, data)
|
|
end
|
|
end
|