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.
441 lines
17 KiB
Lua
441 lines
17 KiB
Lua
-- WeakestAuras -- copy-paste transport and reviewed display installation.
|
|
-- C_EncodingUtil handles the local format; WA2 uses the transport helpers below.
|
|
if WeakestAuras.disabled then return end
|
|
|
|
local WA = WeakestAuras
|
|
|
|
local MAGIC = "!WA1!"
|
|
|
|
-- Present only if the whole CBOR chain this file needs is available.
|
|
local E = C_EncodingUtil
|
|
WA.hasImportExport =
|
|
E ~= nil and E.SerializeCBOR ~= nil and E.DeserializeCBOR ~= nil
|
|
and E.CompressString ~= nil and E.DecompressString ~= nil
|
|
and E.EncodeBase64 ~= nil and E.DecodeBase64 ~= nil
|
|
|
|
-- EditBoxes don't soft-wrap one unbroken line; chunk the blob (share.lua:36).
|
|
local function wrap(str, width)
|
|
local out = {}
|
|
for i = 1, string.len(str), width do
|
|
table.insert(out, string.sub(str, i, i + width - 1))
|
|
end
|
|
return table.concat(out, "\n")
|
|
end
|
|
|
|
local function encodeBody(payload)
|
|
return E.EncodeBase64(E.CompressString(E.SerializeCBOR(payload)))
|
|
end
|
|
|
|
-- Which addon's export format a pasted string is, by prefix alone: "wa1"
|
|
-- (ours), "wa2" (WeakAuras 2, !WA:2!), "wa2legacy" (WeakAuras' pre-2020 format,
|
|
-- a bare leading !), or nil for anything else.
|
|
--
|
|
-- Order is load-bearing: !WA1! also begins with the bare ! the legacy format is
|
|
-- recognised by, so ours has to be tested first or every WeakestAuras string
|
|
-- classifies as a decade-old WeakAuras one.
|
|
function WA.ClassifyImport(text)
|
|
if type(text) ~= "string" then return nil end
|
|
text = string.gsub(text, "%s", "")
|
|
if string.sub(text, 1, string.len(MAGIC)) == MAGIC then return "wa1" end
|
|
if string.find(text, "^!WA:%d+!") then return "wa2" end
|
|
if string.sub(text, 1, 1) == "!" then return "wa2legacy" end
|
|
return nil
|
|
end
|
|
|
|
-- Why a paste this file cannot read is unreadable, phrased for someone holding
|
|
-- a string they got from a website. "Not a WeakestAuras export string" is true
|
|
-- of a wago.io string and reads as "yours is corrupt", which sends people to
|
|
-- re-copy a string that was never going to work.
|
|
-- Kept short enough to wrap to two lines in the import dialog's status area
|
|
-- rather than three; naming wago.io is what tells someone holding one of its
|
|
-- strings why theirs is not corrupt.
|
|
local CLASSIFY_REFUSAL = {
|
|
wa2 = "that is a WeakAuras string (wago.io). WeakestAuras cannot import those.",
|
|
wa2legacy = "that is a very old WeakAuras string. Re-export it from a current WeakAuras.",
|
|
}
|
|
|
|
-- Reverses encode; nil on a foreign/garbage/truncated string. The !WA1! prefix
|
|
-- both identifies our format and lets this reject pfUI/WA2 strings cleanly.
|
|
local function decode(text)
|
|
if not text then return nil end
|
|
text = string.gsub(text, "%s", "")
|
|
if string.sub(text, 1, string.len(MAGIC)) ~= MAGIC then return nil end
|
|
local ok, result = pcall(function()
|
|
return E.DeserializeCBOR(E.DecompressString(E.DecodeBase64(string.sub(text, string.len(MAGIC) + 1))))
|
|
end)
|
|
if not ok or type(result) ~= "table" then return nil end
|
|
return result
|
|
end
|
|
|
|
-- A deep copy with the non-transmissible fields stripped (WA2's
|
|
-- stripNonTransmissableFields): parent (re-derived on import),
|
|
-- controlledChildren (children travel in payload.c and get fresh ids). `id`
|
|
-- stays (the suggested name); internalVersion stays (the importer's
|
|
-- MergeDefaults migration reads it). `uid` travels too, as it does upstream:
|
|
-- it is the only thing that lets a later import be recognised as the same aura
|
|
-- rather than an anonymous duplicate.
|
|
local function cleanForExport(data)
|
|
local c = WA.DeepCopy(data)
|
|
c.parent = nil
|
|
c.controlledChildren = nil
|
|
return c
|
|
end
|
|
|
|
-- A display's data as one unwrapped transport string, or nil + reason. Groups
|
|
-- bring their direct children in payload.c. Nested-group export is deliberately
|
|
-- shallow: a child that is itself a group loses its own children
|
|
-- (controlledChildren stripped), the flat single-level group being the common
|
|
-- case -- revisit with recursion if nested groups become one.
|
|
function WA.ExportRaw(id)
|
|
if not WA.hasImportExport then return nil, "C_EncodingUtil is not available on this client" end
|
|
local data = WeakestAurasDB.displays[id]
|
|
if not data then return nil, "no such display: " .. tostring(id) end
|
|
|
|
-- v and mods say what built the string -- the importer's preview names them,
|
|
-- and a bug report over a pasted string gets its environment for free.
|
|
local payload = { m = "WeakestAuras", v = WA.version, d = cleanForExport(data) }
|
|
payload.mods = {
|
|
classicapi = WA.ClassicAPIVersionString(),
|
|
nampower = WA.NampowerVersionString(),
|
|
}
|
|
if WA.IsGroup(data) and data.controlledChildren then
|
|
payload.c = {}
|
|
for i = 1, #data.controlledChildren do
|
|
local child = WeakestAurasDB.displays[data.controlledChildren[i]]
|
|
if child then table.insert(payload.c, cleanForExport(child)) end
|
|
end
|
|
end
|
|
|
|
local ok, body = pcall(encodeBody, payload)
|
|
if not ok then return nil, "serialization failed: " .. tostring(body) end
|
|
return MAGIC .. body
|
|
end
|
|
|
|
-- The same string wrapped for an EditBox. The wrap belongs to the display of an
|
|
-- export and not to the format -- newlines through the chat system are wasteful
|
|
-- at best -- so anything transporting one uses WA.ExportRaw and this stays the
|
|
-- only place that knows the column.
|
|
function WA.Export(id)
|
|
local raw, err = WA.ExportRaw(id)
|
|
if not raw then return nil, err end
|
|
return MAGIC .. wrap(string.sub(raw, string.len(MAGIC) + 1), 92)
|
|
end
|
|
|
|
-- The id of the display holding this uid, if any. uids are unique inside the
|
|
-- database because installDisplay regenerates any incoming one already taken.
|
|
function WA.FindByUID(uid)
|
|
if not uid then return nil end
|
|
for id, d in pairs(WeakestAurasDB.displays) do
|
|
if d.uid == uid then return id end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local UPDATE_PRESERVED = {
|
|
id = true, uid = true, parent = true, controlledChildren = true,
|
|
anchorFrameType = true, anchorFrameFrame = true, anchorFrameParent = true,
|
|
selfPoint = true, anchorPoint = true,
|
|
xOffset = true, yOffset = true, frameStrata = true,
|
|
scale = true, width = true, height = true,
|
|
}
|
|
|
|
local function importReport(reason)
|
|
return { created = {}, dropped = {}, code = {}, refused = reason }
|
|
end
|
|
|
|
local function localPending(payload)
|
|
if type(payload) ~= "table" or payload.m ~= "WeakestAuras"
|
|
or type(payload.d) ~= "table" then
|
|
return nil, "unrecognized or wrong-format string"
|
|
end
|
|
local root = WA.DeepCopy(payload.d)
|
|
local children = {}
|
|
root.parent = nil
|
|
root.controlledChildren = nil
|
|
if payload.c ~= nil then
|
|
if type(payload.c) ~= "table" or not WA.IsGroup(root) then
|
|
return nil, "child displays require a group root"
|
|
end
|
|
for i = 1, #payload.c do
|
|
if type(payload.c[i]) ~= "table" then
|
|
return nil, "child display " .. i .. " is not a table"
|
|
end
|
|
local child = WA.DeepCopy(payload.c[i])
|
|
child.parent = nil
|
|
child.controlledChildren = nil
|
|
table.insert(children, child)
|
|
end
|
|
end
|
|
return { root = root, children = children }
|
|
end
|
|
|
|
local function decodeWA2(text)
|
|
local bytes, err = WA.WA2Decode(text)
|
|
if not bytes then return nil, err end
|
|
local payload, why = WA.WA2Deserialize(bytes)
|
|
if not payload then return nil, why end
|
|
return payload
|
|
end
|
|
|
|
function WA.ImportPreview(str)
|
|
local class = WA.ClassifyImport(str)
|
|
local refusal = class == "wa2legacy" and CLASSIFY_REFUSAL[class] or nil
|
|
if refusal then return nil, importReport(refusal) end
|
|
if class == "wa1" then
|
|
local payload = decode(str)
|
|
if not payload then return nil, importReport("not a WeakestAuras export string") end
|
|
local pending, why = localPending(payload)
|
|
if not pending then return nil, importReport(why) end
|
|
-- A payload whose schema outruns this install imports anyway, warned
|
|
-- about in the summary rather than refused. The migrations here have
|
|
-- never seen that shape, so a field the newer schema renamed or added
|
|
-- reads as absent and MergeDefaults fills it with a default: settings
|
|
-- are lost, nothing is corrupted. Refusing instead strands everyone
|
|
-- whose aura came from someone running ahead of them, which is the
|
|
-- normal case for a string passed between two players. The stamp
|
|
-- travels untouched, so upgrading later does not re-run migrations the
|
|
-- author's build already applied.
|
|
local schema = pending.root.internalVersion or 0
|
|
for i = 1, #pending.children do
|
|
local v = pending.children[i].internalVersion or 0
|
|
if v > schema then schema = v end
|
|
end
|
|
local report = importReport(nil)
|
|
if schema > WA.SCHEMA_VERSION then report.schemaAhead = schema end
|
|
WA.CollectImportCode(pending.root, report, tostring(pending.root.id or "?") .. " - ")
|
|
for i = 1, #pending.children do
|
|
local child = pending.children[i]
|
|
WA.CollectImportCode(child, report, tostring(child.id or "?") .. " - ")
|
|
end
|
|
report.format = class
|
|
report.sourceAddon = "WeakestAuras"
|
|
if type(payload.v) == "string" then report.sourceVersion = payload.v end
|
|
if type(payload.mods) == "table" then report.sourceMods = payload.mods end
|
|
return pending, report
|
|
end
|
|
if class == "wa2" then
|
|
local payload, why = decodeWA2(str)
|
|
if not payload then
|
|
return nil, importReport("WeakAuras string could not be imported: " .. tostring(why))
|
|
end
|
|
local pending, report = WA.WA2Translate(payload)
|
|
report.format = class
|
|
report.sourceAddon = "WeakAuras"
|
|
if type(payload.s) == "string" then report.sourceVersion = payload.s end
|
|
return pending, report
|
|
end
|
|
return nil, importReport("not a WeakestAuras export string")
|
|
end
|
|
|
|
local function canUpdatePending(pending)
|
|
return pending and pending.root and not WA.IsGroup(pending.root)
|
|
and #(pending.children or {}) == 0
|
|
end
|
|
|
|
local function removeId(list, id)
|
|
for i = #(list or {}), 1, -1 do
|
|
if list[i] == id then table.remove(list, i) end
|
|
end
|
|
end
|
|
|
|
local function installOne(data, parentId)
|
|
local base = data.id
|
|
if not base or base == "" then base = "Imported" end
|
|
local newId = WA.UniqueId(base)
|
|
data.id = newId
|
|
if not data.uid or WA.FindByUID(data.uid) then data.uid = WA.NewUID() end
|
|
data.parent = parentId
|
|
WeakestAurasDB.displays[newId] = data
|
|
local ok, err = pcall(WA.MergeDefaults, data)
|
|
if not ok then
|
|
WeakestAurasDB.displays[newId] = nil
|
|
error(err, 0)
|
|
end
|
|
return newId
|
|
end
|
|
|
|
local function rollbackInstalled(installed)
|
|
for i = #installed, 1, -1 do
|
|
local data = installed[i]
|
|
if data.parent then
|
|
local parent = WeakestAurasDB.displays[data.parent]
|
|
if parent then removeId(parent.controlledChildren, data.id) end
|
|
else
|
|
removeId(WeakestAurasDB.order, data.id)
|
|
end
|
|
WeakestAurasDB.displays[data.id] = nil
|
|
end
|
|
end
|
|
|
|
-- A dynamic group's sortHybridTable names its children by id, and every id in an
|
|
-- import is reassigned by WA.UniqueId. Keys naming a display that travelled in
|
|
-- this same import move with it; anything else is a stale key the author's own
|
|
-- profile carried and is left exactly as it arrived.
|
|
--
|
|
-- An anchor reference (`WeakestAuras:<id>`) is the same problem seen from the
|
|
-- other side, and it has to be repaired here rather than at translation time:
|
|
-- a display can be anchored to one installed *after* it -- the corpus's
|
|
-- ComboFill1 anchors to a Cback1 that arrives twenty displays later -- so the
|
|
-- map is only complete once the whole pack is in.
|
|
-- A glow aimed at another display names it the same way an anchor does, so it
|
|
-- needs the same repair -- in both places one can be written: the show/hide
|
|
-- action blocks, and any `glowexternal` condition change.
|
|
local function remapGlowFrame(block, byOldId)
|
|
if type(block) ~= "table" or block.glow_frame_type ~= "FRAMESELECTOR" then return end
|
|
if type(block.glow_frame) ~= "string" then return end
|
|
local prefix = WA.ANCHOR_AURA_PREFIX
|
|
local _, _, oldId = string.find(block.glow_frame, "^" .. prefix .. "(.+)$")
|
|
if oldId and byOldId[oldId] then
|
|
block.glow_frame = prefix .. byOldId[oldId]
|
|
end
|
|
end
|
|
|
|
local function remapGlowFrames(data, byOldId)
|
|
local actions = data.actions
|
|
if type(actions) == "table" then
|
|
for _, phase in ipairs({ "init", "start", "finish" }) do
|
|
remapGlowFrame(actions[phase], byOldId)
|
|
end
|
|
end
|
|
local conditions = data.conditions
|
|
for n = 1, #(conditions or {}) do
|
|
local changes = conditions[n] and conditions[n].changes or {}
|
|
for c = 1, #changes do
|
|
if changes[c] and changes[c].property == "glowexternal" then
|
|
remapGlowFrame(changes[c].value, byOldId)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function remapChildIdKeys(installed, byOldId)
|
|
local prefix = WA.ANCHOR_AURA_PREFIX
|
|
for i = 1, #installed do
|
|
local data = installed[i]
|
|
if type(data.sortHybridTable) == "table" then
|
|
local remapped = {}
|
|
for oldId, value in pairs(data.sortHybridTable) do
|
|
remapped[byOldId[oldId] or oldId] = value
|
|
end
|
|
data.sortHybridTable = remapped
|
|
end
|
|
if data.anchorFrameType == "SELECTFRAME" and type(data.anchorFrameFrame) == "string" then
|
|
local _, _, oldId = string.find(data.anchorFrameFrame, "^" .. prefix .. "(.+)$")
|
|
-- Only an id that travelled in this import is rewritten. One naming a
|
|
-- display already here is the user's own aura and is left alone; one
|
|
-- naming neither is a stale reference from the sender's profile, and
|
|
-- inventing a target for it would anchor the display to a stranger.
|
|
if oldId and byOldId[oldId] then
|
|
data.anchorFrameFrame = prefix .. byOldId[oldId]
|
|
end
|
|
end
|
|
remapGlowFrames(data, byOldId)
|
|
end
|
|
end
|
|
|
|
function WA.InstallPendingImport(pending)
|
|
if type(pending) ~= "table" or type(pending.root) ~= "table" then
|
|
return nil, "no pending import"
|
|
end
|
|
if pending.confirmed then return nil, "import already confirmed" end
|
|
local root, children = pending.root, pending.children or {}
|
|
if #children > 0 and not WA.IsGroup(root) then
|
|
return nil, "child displays require a group root"
|
|
end
|
|
local installed = {}
|
|
local ok, rootId = pcall(function()
|
|
WA.GetOrder()
|
|
-- A child's `parent` still names its group by the id it had in the export;
|
|
-- the map is what turns that into the id it was just given here. The list
|
|
-- is ordered parents-first, so a parent is always already in the map.
|
|
local byOldId = {}
|
|
local rootOldId = root.id
|
|
local id = installOne(root, nil)
|
|
table.insert(installed, root)
|
|
table.insert(WeakestAurasDB.order, id)
|
|
byOldId[rootOldId] = id
|
|
for i = 1, #children do
|
|
local child = children[i]
|
|
local oldId, parentId = child.id, byOldId[child.parent] or id
|
|
child.parent = nil
|
|
local childId = installOne(child, nil)
|
|
table.insert(installed, child)
|
|
table.insert(WeakestAurasDB.order, childId)
|
|
byOldId[oldId] = childId
|
|
-- Reparenting through the primitive rather than writing the arrays:
|
|
-- it owns detach, the cycle guard and RefreshMembership. It answers
|
|
-- nothing, so the placement is read back rather than assumed.
|
|
WA.AddChildToGroup(parentId, childId)
|
|
if WeakestAurasDB.displays[childId].parent ~= parentId then
|
|
error("\"" .. childId .. "\" could not be placed in \"" .. parentId .. "\"", 0)
|
|
end
|
|
end
|
|
remapChildIdKeys(installed, byOldId)
|
|
WA.GetOrder()
|
|
WA.AddMany(installed)
|
|
return id
|
|
end)
|
|
if not ok then
|
|
rollbackInstalled(installed)
|
|
return nil, tostring(rootId)
|
|
end
|
|
pending.confirmed = rootId
|
|
return rootId
|
|
end
|
|
|
|
function WA.UpdatePendingImport(pending, targetId)
|
|
if not canUpdatePending(pending) then return nil, "groups cannot be updated in place" end
|
|
local target = WeakestAurasDB.displays[targetId]
|
|
if not target then return nil, "no aura here matches that one" end
|
|
local incoming = pending.root
|
|
local original = WA.DeepCopy(target)
|
|
for key in pairs(target) do
|
|
if not UPDATE_PRESERVED[key] then target[key] = nil end
|
|
end
|
|
for key, value in pairs(incoming) do
|
|
if not UPDATE_PRESERVED[key] then target[key] = WA.DeepCopy(value) end
|
|
end
|
|
target.id, target.uid, target.parent = targetId, original.uid, original.parent
|
|
local ok, err = pcall(WA.MergeDefaults, target)
|
|
if not ok then
|
|
for key in pairs(target) do target[key] = nil end
|
|
for key, value in pairs(original) do target[key] = value end
|
|
return nil, tostring(err)
|
|
end
|
|
WA.Add(target)
|
|
pending.confirmed = targetId
|
|
return targetId
|
|
end
|
|
|
|
function WA.ConfirmImport(pending, mode, targetId)
|
|
if mode == "update" then return WA.UpdatePendingImport(pending, targetId) end
|
|
return WA.InstallPendingImport(pending)
|
|
end
|
|
|
|
function WA.ImportInfo(str)
|
|
if not WA.hasImportExport then return nil end
|
|
local pending, report = WA.ImportPreview(str)
|
|
if not pending or report.refused then return nil end
|
|
local id = WA.FindByUID(pending.root.uid)
|
|
if not id then return nil end
|
|
return id, canUpdatePending(pending)
|
|
end
|
|
|
|
function WA.ImportOverwrite(str)
|
|
if not WA.hasImportExport then return nil, "C_EncodingUtil is not available on this client" end
|
|
local class = WA.ClassifyImport(str)
|
|
if class == "wa2legacy" then return nil, CLASSIFY_REFUSAL[class] end
|
|
local pending, report = WA.ImportPreview(str)
|
|
if not pending or report.refused then return nil, report.refused or "not a WeakestAuras export string" end
|
|
local targetId = WA.FindByUID(pending.root.uid)
|
|
if not targetId then return nil, "no aura here matches that one" end
|
|
return WA.UpdatePendingImport(pending, targetId)
|
|
end
|
|
|
|
function WA.Import(str)
|
|
if not WA.hasImportExport then return nil, "C_EncodingUtil is not available on this client" end
|
|
local pending, report = WA.ImportPreview(str)
|
|
if not pending or report.refused then return nil, report.refused or "not a WeakestAuras export string" end
|
|
return WA.InstallPendingImport(pending)
|
|
end
|