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.
383 lines
13 KiB
Lua
383 lines
13 KiB
Lua
-- WeakestAuras -- transport for WeakAuras2 export strings: `!WA:2!<body>` down to
|
|
-- a plain Lua table. Reverses WA2's Transmission.lua and LibSerialize's wire
|
|
-- format, and knows nothing about auras. Import only; nothing here emits the
|
|
-- format, and nothing it decodes is ever executed.
|
|
|
|
if WeakestAuras.disabled then return end
|
|
|
|
local WA = WeakestAuras
|
|
|
|
-- WeakAuras2's B64tobyte, which is the alphabet LibDeflate's EncodeForPrint
|
|
-- writes: a-z, A-Z, 0-9, then "(" and ")".
|
|
local ALPHABET = "abcdefghijklmnopqrstuvwxyz"
|
|
.. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
.. "0123456789()"
|
|
|
|
local SIXBIT = {}
|
|
for i = 1, string.len(ALPHABET) do
|
|
SIXBIT[string.sub(ALPHABET, i, i)] = i - 1
|
|
end
|
|
|
|
local POW2 = { [0] = 1 }
|
|
for i = 1, 13 do POW2[i] = POW2[i - 1] * 2 end
|
|
|
|
-- LibDeflate emits *raw* deflate -- no zlib header, no checksum. That has no
|
|
-- magic bytes, so DecompressString cannot auto-detect it and the method must be
|
|
-- passed explicitly; omitting it (as ImportExport.lua does, where the data
|
|
-- really is zlib) refuses every WeakAuras string. Confirmed on this client by
|
|
-- /wa wa2probe, whose negative test is the live counterpart of this constant.
|
|
local RAW_DEFLATE = (Enum and Enum.CompressionMethod and Enum.CompressionMethod.Deflate) or 0
|
|
|
|
-- A wago aura is a few tens of KB. Anything past this is refused rather than
|
|
-- handed to a deserializer that would grind over it.
|
|
local MAX_PAYLOAD = 512 * 1024
|
|
|
|
-- 6 bits per character, little-endian: each character adds its 6 bits above
|
|
-- whatever is already buffered, and whole bytes are emitted from the bottom.
|
|
-- Lua 5.0 has no bit library, so this is multiply and divide by powers of two;
|
|
-- the bitfield never exceeds 12 bits, so it stays exactly representable.
|
|
--
|
|
-- The loop runs one iteration past the end of the string on purpose. An
|
|
-- iteration emits the byte completed by the *previous* character, so stopping on
|
|
-- the last character would drop the final byte. That trailing out-of-range read
|
|
-- is the only place `or 0` is reachable: callers reject a body carrying a
|
|
-- character outside the alphabet before getting here.
|
|
local function decode6(str)
|
|
local out, n = {}, 0
|
|
local bitfield, bits = 0, 0
|
|
local len = string.len(str)
|
|
local i = 1
|
|
while true do
|
|
if bits >= 8 then
|
|
n = n + 1
|
|
out[n] = string.char(math.fmod(bitfield, 256))
|
|
bitfield = math.floor(bitfield / 256)
|
|
bits = bits - 8
|
|
end
|
|
local ch = SIXBIT[string.sub(str, i, i)]
|
|
bitfield = bitfield + (ch or 0) * POW2[bits]
|
|
bits = bits + 6
|
|
if i > len then break end
|
|
i = i + 1
|
|
end
|
|
return table.concat(out, "", 1, n)
|
|
end
|
|
|
|
-- Offset and character of the first byte the alphabet does not cover, or nil.
|
|
-- WA2's own decoder reads an unknown character as zero, which turns a mangled
|
|
-- paste into plausible garbage that only surfaces as a deserializer bug.
|
|
local function badCharacter(body)
|
|
for i = 1, string.len(body) do
|
|
local ch = string.sub(body, i, i)
|
|
if not SIXBIT[ch] then return i, ch end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- `!WA:2!<body>` -> the LibSerialize bytes underneath, or nil and a reason
|
|
-- phrased for whoever pasted the string.
|
|
function WA.WA2Decode(text)
|
|
if type(text) ~= "string" then return nil, "not a string" end
|
|
|
|
local stripped = string.gsub(text, "%s", "")
|
|
local _, stop, version = string.find(stripped, "^!WA:(%d+)!")
|
|
if not stop then return nil, "not a WeakAuras export string" end
|
|
if version ~= "2" then
|
|
return nil, "WeakAuras transmission version " .. version .. " is not supported"
|
|
end
|
|
|
|
local body = string.sub(stripped, stop + 1)
|
|
if body == "" then return nil, "the string carries no payload" end
|
|
local at, ch = badCharacter(body)
|
|
if at then
|
|
return nil, "character \"" .. ch .. "\" at position " .. at .. " is not part of the format"
|
|
end
|
|
|
|
local ok, bytes = pcall(decode6, body)
|
|
if not ok or type(bytes) ~= "string" or bytes == "" then
|
|
return nil, "the body did not decode"
|
|
end
|
|
|
|
local E = C_EncodingUtil
|
|
if not E or not E.DecompressString then
|
|
return nil, "this client has no decompression API"
|
|
end
|
|
local inflated
|
|
ok, inflated = pcall(E.DecompressString, bytes, RAW_DEFLATE)
|
|
if not ok or type(inflated) ~= "string" or inflated == "" then
|
|
return nil, "the payload did not decompress"
|
|
end
|
|
if string.len(inflated) > MAX_PAYLOAD then
|
|
return nil, "the payload is larger than " .. MAX_PAYLOAD .. " bytes"
|
|
end
|
|
return inflated
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- LibSerialize.
|
|
--
|
|
-- Type indices and the tag layout are transcribed from the library's own
|
|
-- _ReaderIndex, _EmbeddedIndex and "Encoding format" block:
|
|
--
|
|
-- NNNN NNN1 a 7-bit non-negative int, whole value in the tag
|
|
-- CCCC TT10 a 2-bit type index and a 4-bit count
|
|
-- NNNN S100 the low 4 bits of a 12-bit int plus its sign; one byte follows
|
|
-- TTTT T000 a 5-bit type index
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local SERIALIZATION_VERSION_MAX = 2
|
|
local MAX_DEPTH = 64
|
|
|
|
local T_NIL = 0
|
|
local T_NUM_FLOAT = 9
|
|
local T_NUM_FLOATSTR, T_NUM_FLOATSTR_NEG = 10, 11
|
|
local T_BOOL_T, T_BOOL_F = 12, 13
|
|
-- Each of these families runs 8, 16, 24 in consecutive indices, so the count
|
|
-- width is the offset from the first plus one.
|
|
local T_STR_8 = 14
|
|
local T_TABLE_8 = 17
|
|
local T_ARRAY_8 = 20
|
|
local T_MIXED_8 = 23
|
|
local T_STRINGREF_8 = 26
|
|
local T_TABLEREF_8 = 29
|
|
|
|
local EMB_STRING, EMB_TABLE, EMB_ARRAY, EMB_MIXED = 0, 1, 2, 3
|
|
|
|
-- Byte counts for the fixed-width integer types, indices 1-8. NUM_64 is seven
|
|
-- bytes on the wire despite its name -- 56 bits, which covers a double's exact
|
|
-- integer range with room to spare.
|
|
local INT_WIDTH = { 2, 2, 3, 3, 4, 4, 7, 7 }
|
|
local INT_NEGATIVE = { [2] = true, [4] = true, [6] = true, [8] = true }
|
|
|
|
-- Past 2^53 a Lua number stops representing consecutive integers, so a value
|
|
-- above it would come back quietly wrong rather than merely large.
|
|
local MAX_EXACT_INT = 9007199254740992
|
|
|
|
local function readByte(r)
|
|
if r.pos > r.len then error("the payload ends mid-value", 0) end
|
|
local b = string.byte(r.data, r.pos)
|
|
r.pos = r.pos + 1
|
|
return b
|
|
end
|
|
|
|
-- Lua 5.0's string.byte takes one index; the (s, i, j) range form is 5.1. Every
|
|
-- byte is therefore its own call, and a run of bytes is a string.sub instead.
|
|
local function readRaw(r, n)
|
|
if n < 0 or r.pos + n - 1 > r.len then error("the payload ends mid-value", 0) end
|
|
local s = string.sub(r.data, r.pos, r.pos + n - 1)
|
|
r.pos = r.pos + n
|
|
return s
|
|
end
|
|
|
|
-- Multi-byte integers are big-endian, most significant byte first. That is the
|
|
-- opposite order from the 6-bit print decode above. They are separate layers of
|
|
-- the format and sharing one convention between them decodes numbers that are
|
|
-- merely wrong rather than numbers that fail.
|
|
local function readUInt(r, width)
|
|
local value = 0
|
|
for i = 1, width do value = value * 256 + readByte(r) end
|
|
return value
|
|
end
|
|
|
|
local function addStringReference(r, value)
|
|
-- Only strings longer than two bytes enter the pool. Their order must match
|
|
-- the writer's order or every later STRINGREF points at the wrong value.
|
|
if string.len(value) > 2 then
|
|
r.stringCount = r.stringCount + 1
|
|
r.strings[r.stringCount] = value
|
|
end
|
|
end
|
|
|
|
local function newTableReference(r)
|
|
-- Register before reading contents: _ReadMixed fills both halves into the
|
|
-- same table, and a table can refer to itself or an ancestor.
|
|
r.tableCount = r.tableCount + 1
|
|
local value = {}
|
|
r.tables[r.tableCount] = value
|
|
return value
|
|
end
|
|
|
|
-- Sign / 11-bit exponent / 52-bit mantissa reassembled by arithmetic; Lua 5.0
|
|
-- has no string.unpack or bit library. Denormals and inf/nan are decoded for
|
|
-- completeness, but no aura carries one. Powers of two replace the absent
|
|
-- math.ldexp.
|
|
local function readFloat(r)
|
|
local b1 = readByte(r)
|
|
local b2 = readByte(r)
|
|
local b3 = readByte(r)
|
|
local b4 = readByte(r)
|
|
local b5 = readByte(r)
|
|
local b6 = readByte(r)
|
|
local b7 = readByte(r)
|
|
local b8 = readByte(r)
|
|
local negative = b1 >= 128
|
|
local exponent = math.fmod(b1, 128) * 16 + math.floor(b2 / 16)
|
|
local mantissa = math.fmod(b2, 16) * 281474976710656
|
|
+ b3 * 1099511627776
|
|
+ b4 * 4294967296
|
|
+ b5 * 16777216
|
|
+ b6 * 65536
|
|
+ b7 * 256
|
|
+ b8
|
|
local sign = negative and -1 or 1
|
|
if exponent == 0 then
|
|
if mantissa == 0 then return sign * 0.0 end
|
|
return sign * (mantissa / 4503599627370496) * (2 ^ -1022)
|
|
end
|
|
if exponent == 2047 then
|
|
if mantissa == 0 then return sign * math.huge end
|
|
return 0 / 0
|
|
end
|
|
return sign * (1 + mantissa / 4503599627370496) * (2 ^ (exponent - 1023))
|
|
end
|
|
|
|
local readValue
|
|
|
|
local function fillArray(r, t, count, depth)
|
|
for i = 1, count do t[i] = readValue(r, depth + 1) end
|
|
return t
|
|
end
|
|
|
|
local function fillMap(r, t, count, depth)
|
|
for i = 1, count do
|
|
local key = readValue(r, depth + 1)
|
|
if key == nil then error("a table key decoded as nil", 0) end
|
|
t[key] = readValue(r, depth + 1)
|
|
end
|
|
return t
|
|
end
|
|
|
|
local function readExtended(r, idx, depth)
|
|
if idx == T_NIL then return nil end
|
|
if idx == T_BOOL_T then return true end
|
|
if idx == T_BOOL_F then return false end
|
|
|
|
local width = INT_WIDTH[idx]
|
|
if width then
|
|
local value = readUInt(r, width)
|
|
if value > MAX_EXACT_INT then
|
|
error("an integer past 2^53 cannot be represented here", 0)
|
|
end
|
|
if INT_NEGATIVE[idx] then return -value end
|
|
return value
|
|
end
|
|
|
|
if idx == T_NUM_FLOAT or idx == T_NUM_FLOATSTR or idx == T_NUM_FLOATSTR_NEG then
|
|
if idx == T_NUM_FLOAT then return readFloat(r) end
|
|
local text = readRaw(r, readByte(r))
|
|
local value = tonumber(text)
|
|
if value == nil then error("malformed floating point string", 0) end
|
|
if idx == T_NUM_FLOATSTR_NEG then return -value end
|
|
return value
|
|
end
|
|
|
|
if idx >= T_STR_8 and idx <= T_STR_8 + 2 then
|
|
local value = readRaw(r, readUInt(r, idx - T_STR_8 + 1))
|
|
addStringReference(r, value)
|
|
return value
|
|
end
|
|
if idx >= T_TABLE_8 and idx <= T_TABLE_8 + 2 then
|
|
return fillMap(r, newTableReference(r), readUInt(r, idx - T_TABLE_8 + 1), depth)
|
|
end
|
|
if idx >= T_ARRAY_8 and idx <= T_ARRAY_8 + 2 then
|
|
return fillArray(r, newTableReference(r), readUInt(r, idx - T_ARRAY_8 + 1), depth)
|
|
end
|
|
if idx >= T_MIXED_8 and idx <= T_MIXED_8 + 2 then
|
|
local countWidth = idx - T_MIXED_8 + 1
|
|
local arrayCount = readUInt(r, countWidth)
|
|
local mapCount = readUInt(r, countWidth)
|
|
local value = newTableReference(r)
|
|
fillArray(r, value, arrayCount, depth)
|
|
return fillMap(r, value, mapCount, depth)
|
|
end
|
|
|
|
if idx >= T_STRINGREF_8 and idx <= T_STRINGREF_8 + 2 then
|
|
local index = readUInt(r, idx - T_STRINGREF_8 + 1)
|
|
if index < 1 or index > r.stringCount then
|
|
error("string reference " .. index .. " outside 1.." .. r.stringCount, 0)
|
|
end
|
|
return r.strings[index]
|
|
end
|
|
if idx >= T_TABLEREF_8 and idx <= T_TABLEREF_8 + 2 then
|
|
local index = readUInt(r, idx - T_TABLEREF_8 + 1)
|
|
if index < 1 or index > r.tableCount then
|
|
error("table reference " .. index .. " outside 1.." .. r.tableCount, 0)
|
|
end
|
|
return r.tables[index]
|
|
end
|
|
|
|
error("unknown type index " .. idx, 0)
|
|
end
|
|
|
|
function readValue(r, depth)
|
|
if depth > MAX_DEPTH then
|
|
error("the payload nests deeper than " .. MAX_DEPTH .. " levels", 0)
|
|
end
|
|
local tag = readByte(r)
|
|
|
|
if math.fmod(tag, 2) == 1 then
|
|
return math.floor(tag / 2)
|
|
end
|
|
|
|
if math.fmod(tag, 4) == 2 then
|
|
local kind = math.fmod(math.floor(tag / 4), 4)
|
|
local count = math.floor(tag / 16)
|
|
if kind == EMB_STRING then
|
|
local value = readRaw(r, count)
|
|
addStringReference(r, value)
|
|
return value
|
|
end
|
|
if kind == EMB_TABLE then return fillMap(r, newTableReference(r), count, depth) end
|
|
if kind == EMB_ARRAY then return fillArray(r, newTableReference(r), count, depth) end
|
|
-- An embedded MIXED packs both counts into the one nibble -- the array
|
|
-- part in its low two bits, the map part in its high two -- and stores
|
|
-- each **one less** than its true value. A table with an empty half is
|
|
-- written as ARRAY or TABLE instead, so the embedded form never has to
|
|
-- represent zero and spends the bit on a larger range. Dropping either
|
|
-- +1 still decodes simple payloads for a while before drifting, which is
|
|
-- what makes it worth stating rather than deriving.
|
|
local arrayCount = math.fmod(count, 4) + 1
|
|
local mapCount = math.floor(count / 4) + 1
|
|
local value = newTableReference(r)
|
|
fillArray(r, value, arrayCount, depth)
|
|
return fillMap(r, value, mapCount, depth)
|
|
end
|
|
|
|
if math.fmod(tag, 8) == 4 then
|
|
local value = math.floor(tag / 16) + readByte(r) * 16
|
|
if math.fmod(math.floor(tag / 8), 2) == 1 then return -value end
|
|
return value
|
|
end
|
|
|
|
return readExtended(r, math.floor(tag / 8), depth)
|
|
end
|
|
|
|
-- LibSerialize bytes -> a Lua table, or nil and a reason. Every read is bounds
|
|
-- checked, so a truncated or hostile payload is refused rather than thrown.
|
|
function WA.WA2Deserialize(bytes)
|
|
if type(bytes) ~= "string" or bytes == "" then return nil, "empty payload" end
|
|
|
|
local r = {
|
|
data = bytes,
|
|
pos = 1,
|
|
len = string.len(bytes),
|
|
strings = {},
|
|
stringCount = 0,
|
|
tables = {},
|
|
tableCount = 0,
|
|
}
|
|
local ok, result = pcall(function()
|
|
local version = readByte(r)
|
|
if version > SERIALIZATION_VERSION_MAX then
|
|
error("serialization version " .. version .. " is newer than this reader", 0)
|
|
end
|
|
return readValue(r, 0)
|
|
end)
|
|
|
|
if not ok then return nil, tostring(result) end
|
|
-- A WeakAuras string always carries a table, so a top-level nil is a payload
|
|
-- that decoded successfully into nothing worth returning.
|
|
if result == nil then return nil, "the payload decoded to nil" end
|
|
return result
|
|
end
|