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.
1063 lines
41 KiB
Lua
1063 lines
41 KiB
Lua
-- WeakestAuras -- dynamic text: the %-placeholder parser and the built-in
|
|
-- symbols it resolves. Mirrors WA2's ReplacePlaceHolders / dynamic_texts (§9),
|
|
-- 5.0-clean (no string.match, no string method syntax -- string.byte/sub state
|
|
-- machine only).
|
|
-- Upstream section refs (§N) point at design/architecture/weakauras2-reference.md
|
|
--
|
|
-- WA.ReplacePlaceHolders(text, region, formatters) walks `text` a byte at a
|
|
-- time, emitting literals verbatim and replacing each %symbol from the region's
|
|
-- active state (region.state) or a specific trigger's state (region.states[N]
|
|
-- via "%N.sym"). `formatters` is the symbol -> formatter map WA.CreateFormatters
|
|
-- builds from the owning sub-region's per-symbol format settings.
|
|
-- Only %p (remaining time) changes between state applies, so
|
|
-- WA.TextNeedsFrameTick reports whether a string needs the per-frame FrameTick
|
|
-- subscription (RegionPrototype.lua) rather than every text polling every frame.
|
|
|
|
if WeakestAuras.disabled then return end
|
|
|
|
local WA = WeakestAuras
|
|
|
|
-- Shared m:ss / decimal-seconds formatter (upstream's default precision-1
|
|
-- behavior, §9): m:ss above a minute, one decimal below, blank at/under 0.
|
|
local function timeFmt(t)
|
|
if type(t) ~= "number" then return "" end
|
|
if t > 60 then
|
|
return string.format("%d:%02d", math.floor(t / 60), math.floor(math.fmod(t, 60)))
|
|
elseif t > 0 then
|
|
return string.format("%.1f", t)
|
|
end
|
|
return ""
|
|
end
|
|
|
|
-- The five classic symbols (§9). get(state) pulls the raw value, func
|
|
-- formats it. A symbol whose name matches a real state field never reaches
|
|
-- here (field-by-name wins first, see resolveInState) -- these are the
|
|
-- fallbacks. `func` is the built-in formatting, which a format chosen in the
|
|
-- options for that symbol replaces (see WA.format_types).
|
|
WA.dynamic_texts = {
|
|
p = {
|
|
get = function(state)
|
|
if state.progressType == "timed" then
|
|
if state.paused then return tonumber(state.remaining) or 0 end
|
|
if not state.expirationTime then return nil end
|
|
local remaining = state.expirationTime - GetTime()
|
|
return remaining >= 0 and remaining or nil
|
|
elseif state.progressType == "static" then
|
|
return state.value
|
|
end
|
|
return nil
|
|
end,
|
|
func = function(v, state)
|
|
if state.progressType ~= "timed" then return v end
|
|
return timeFmt(v)
|
|
end,
|
|
},
|
|
t = {
|
|
get = function(state)
|
|
if state.progressType == "timed" then return state.duration
|
|
elseif state.progressType == "static" then return state.total end
|
|
return nil
|
|
end,
|
|
func = function(v, state)
|
|
if state.progressType ~= "timed" then return v end
|
|
return timeFmt(v)
|
|
end,
|
|
},
|
|
n = {
|
|
-- An *empty* name falls through to the display's own id, not just a nil
|
|
-- one: a trigger system with nothing configured yet reports "" rather
|
|
-- than nil (TriggerAura.GetNameAndIcon), and "" is truthy in Lua, so a
|
|
-- plain `or` chain would render a blank instead of the fallback.
|
|
get = function(state)
|
|
local name = state.name
|
|
if name ~= nil and name ~= "" then return name end
|
|
return state.id or ""
|
|
end,
|
|
func = function(v) return v end,
|
|
},
|
|
-- The state's icon, as an inline texture escape. Empty on a client whose
|
|
-- FontString lays the escape out as literal text, since putting
|
|
-- "Interface\Icons\..." on screen is worse than showing nothing -- and an
|
|
-- imported upstream aura using %i must not spew a texture path.
|
|
--
|
|
-- :0:0 sizes the icon to the line's own font height rather than upstream's
|
|
-- fixed :12:12, so a 24px subtext gets a 24px icon with nothing to plumb.
|
|
-- The 64:64:4:60:4:60 tail is upstream's crop, trimming the standard icon
|
|
-- border.
|
|
i = {
|
|
get = function(state) return state.icon end,
|
|
func = function(v)
|
|
if not v or v == "" or not WA.hasInlineText then return "" end
|
|
return "|T" .. v .. ":0:0:0:0:64:64:4:60:4:60|t"
|
|
end,
|
|
},
|
|
s = {
|
|
get = function(state)
|
|
if not state.stacks or state.stacks == 0 then return "" end
|
|
return state.stacks
|
|
end,
|
|
func = function(v) return v end,
|
|
},
|
|
}
|
|
|
|
-- The state fields this aura's triggers declare, as the %symbols a text can
|
|
-- reference: a sorted name array plus name -> { display, type }. Bools are
|
|
-- dropped, matching upstream's GetAdditionalProperties -- "true"/"false" is not
|
|
-- something anyone writes a placeholder for. Single-trigger here, so every
|
|
-- trigger's variables flatten into one list rather than being scoped per
|
|
-- trigger.
|
|
function WA.TextSymbols(data)
|
|
local names, byName = {}, {}
|
|
local templates = (data and WA.GetConditionTemplates) and WA.GetConditionTemplates(data) or {}
|
|
for _, vars in pairs(templates) do
|
|
for name, spec in pairs(vars) do
|
|
if spec.type ~= "bool" and not byName[name] then
|
|
byName[name] = spec
|
|
table.insert(names, name)
|
|
end
|
|
end
|
|
end
|
|
table.sort(names)
|
|
return names, byName
|
|
end
|
|
|
|
-- The line naming those codes, for a `description` row under a text field.
|
|
-- resolveInState accepts any state field by name, so the placeholder set is
|
|
-- open-ended and nothing else in the options window says which names are live.
|
|
-- nil when the triggers declare none, so the row is absent rather than empty.
|
|
function WA.TextSymbolHint(data)
|
|
local names = WA.TextSymbols(data)
|
|
local n = #names
|
|
if n == 0 then return nil end
|
|
local parts = {}
|
|
for i = 1, n do parts[i] = "%" .. names[i] end
|
|
return "This aura's triggers also supply: " .. table.concat(parts, ", ")
|
|
end
|
|
|
|
-- The bare symbol name behind an optional "N." trigger scope.
|
|
local function bareSymbol(symbol)
|
|
local _, _, _, sym = string.find(symbol, "^(%d+)%.(.+)$")
|
|
return sym or symbol
|
|
end
|
|
|
|
-- The format a symbol takes when nothing has been picked for it (§9
|
|
-- DefaultFormatterFor). Upstream infers from the arg's options *control* type,
|
|
-- which says nothing about a stored state field, so it hand-writes 33
|
|
-- `formatter =` entries to cover them; `conditionType` describes the value
|
|
-- itself, so the same answer falls out of a declaration that is already there.
|
|
--
|
|
-- number and bool are absent deliberately: blanket decimal formatting of every
|
|
-- numeric field is a downgrade, and upstream drops bools from the metadata
|
|
-- entirely.
|
|
local DEFAULT_FORMAT_FOR_TYPE = {
|
|
timer = "timed",
|
|
elapsedTimer = "timed",
|
|
string = "string",
|
|
}
|
|
|
|
-- `byName` is WA.TextSymbols' second return, passed in by callers resolving
|
|
-- several symbols so the trigger walk happens once.
|
|
function WA.DefaultFormatFor(symbol, data, byName)
|
|
local sym = bareSymbol(symbol)
|
|
-- %p and %t are the one place we keep "none" against upstream's hardcoded
|
|
-- `timed`: their built-in rendering already *is* a time format at precision
|
|
-- 1, so adopting it would change the sub-minute look of every deployed
|
|
-- countdown to arrive at the same place.
|
|
if sym == "p" or sym == "t" then return "none" end
|
|
if not byName then
|
|
local _
|
|
_, byName = WA.TextSymbols(data)
|
|
end
|
|
local spec = byName[sym]
|
|
if not spec then return "none" end
|
|
-- An arg may override the mapping by declaring `formatter`; none does yet,
|
|
-- and none should until the mapping is actually wrong for it.
|
|
return spec.formatter or DEFAULT_FORMAT_FOR_TYPE[spec.type] or "none"
|
|
end
|
|
|
|
-- Per-symbol output formatting (§9 format_types). Every symbol renders through
|
|
-- tostring by default, so a float state field like %distance shows its full
|
|
-- precision; picking a format for it in the text sub-region's options routes it
|
|
-- through the formatter built here instead. Settings live on the sub-region
|
|
-- under upstream's key layout, text_text_format_<symbol>_<setting>.
|
|
local PRECISIONS = { 0, 1, 2, 3 }
|
|
local PRECISION_LABELS = { [0] = "12", [1] = "12.3", [2] = "12.34", [3] = "12.345" }
|
|
local ROUND_MODES = { "floor", "ceil", "round" }
|
|
local ROUND_LABELS = { floor = "Floor", ceil = "Ceil", round = "Round" }
|
|
local ROUNDERS = {
|
|
floor = math.floor,
|
|
ceil = math.ceil,
|
|
round = function(v) return math.floor(v + 0.5) end,
|
|
}
|
|
|
|
-- A value that isn't a number (and isn't a string spelling one) passes through
|
|
-- untouched rather than becoming nil -- a format picked for one symbol must not
|
|
-- blank it out when a trigger reports something non-numeric there.
|
|
local function asNumber(v)
|
|
if type(v) == "number" then return v end
|
|
if type(v) == "string" then return tonumber(v) end
|
|
return nil
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Padding, shared by string / BigNumber / Number
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local PAD_MODES = { "left", "right" }
|
|
local PAD_LABELS = { left = "Left", right = "Right" }
|
|
|
|
local function padString(input, mode, length)
|
|
local s = tostring(input)
|
|
local toAdd = length - string.len(s)
|
|
if toAdd <= 0 then return s end
|
|
if mode == "right" then return s .. string.rep(" ", toAdd) end
|
|
return string.rep(" ", toAdd) .. s
|
|
end
|
|
|
|
-- Wraps `fn` (which may be nil, meaning "render as-is") in the symbol's padding
|
|
-- if it asked for any, and returns it unchanged otherwise -- so a format with
|
|
-- nothing switched on still contributes no formatter and the built-in stands.
|
|
local function withPad(symbol, get, fn)
|
|
if not get(symbol .. "_pad", false) then return fn end
|
|
local mode = get(symbol .. "_pad_mode", "left")
|
|
local length = get(symbol .. "_pad_max", 8)
|
|
if fn then
|
|
return function(v, state) return padString(fn(v, state), mode, length) end
|
|
end
|
|
return function(v) return padString(v, mode, length) end
|
|
end
|
|
|
|
-- BuildOptions has no disabled state, so the two dependent rows are omitted
|
|
-- rather than greyed -- the same rule Round Mode follows.
|
|
local function padOptionFields(symbol, get, set, fields)
|
|
table.insert(fields, {
|
|
type = "toggle", name = "Pad", key = "text_text_format_" .. symbol .. "_pad",
|
|
get = function() return get(symbol .. "_pad", false) end,
|
|
set = function(v) set(symbol .. "_pad", v); WA.RefreshOptions() end,
|
|
})
|
|
if not get(symbol .. "_pad", false) then return end
|
|
table.insert(fields, {
|
|
type = "select", name = "Pad Mode", key = "text_text_format_" .. symbol .. "_pad_mode",
|
|
values = PAD_MODES, labels = PAD_LABELS, half = true,
|
|
get = function() return get(symbol .. "_pad_mode", "left") end,
|
|
set = function(v) set(symbol .. "_pad_mode", v) end,
|
|
})
|
|
table.insert(fields, {
|
|
type = "range", name = "Pad To", key = "text_text_format_" .. symbol .. "_pad_max",
|
|
min = 1, max = 20, step = 1, half = true,
|
|
get = function() return get(symbol .. "_pad_max", 8) end,
|
|
set = function(v) set(symbol .. "_pad_max", v) end,
|
|
})
|
|
end
|
|
|
|
local function padSummary(symbol, get)
|
|
if not get(symbol .. "_pad", false) then return nil end
|
|
return "pad " .. (PAD_LABELS[get(symbol .. "_pad_mode", "left")] or "Left")
|
|
.. " " .. tostring(get(symbol .. "_pad_max", 8))
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Time
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Three complete renderings of a number of seconds, not deltas on one another:
|
|
-- a chosen format *replaces* a symbol's built-in formatting, so each has to
|
|
-- stand on its own above and below a minute.
|
|
local TIME_FORMATS = { 0, 1, 2 }
|
|
local TIME_FORMAT_LABELS = {
|
|
[0] = "63:42 | 3:07 | 10",
|
|
[1] = "1h | 3m | 10s",
|
|
[2] = "1h 3m | 3m 7s | 10s",
|
|
}
|
|
local TIME_PRECISIONS = { 1, 2, 3 }
|
|
local TIME_PRECISION_LABELS = { [1] = "12.3", [2] = "12.34", [3] = "12.345" }
|
|
|
|
local TIME_RENDERERS = {
|
|
[0] = function(t)
|
|
if t >= 60 then
|
|
return string.format("%d:%02d", math.floor(t / 60), math.floor(math.fmod(t, 60)))
|
|
end
|
|
return string.format("%d", math.floor(t))
|
|
end,
|
|
[1] = function(t)
|
|
if t >= 3600 then return string.format("%dh", math.floor(t / 3600)) end
|
|
if t >= 60 then return string.format("%dm", math.floor(t / 60)) end
|
|
return string.format("%ds", math.floor(t))
|
|
end,
|
|
[2] = function(t)
|
|
if t >= 3600 then
|
|
return string.format("%dh %dm", math.floor(t / 3600),
|
|
math.floor(math.fmod(t, 3600) / 60))
|
|
end
|
|
if t >= 60 then
|
|
return string.format("%dm %ds", math.floor(t / 60), math.floor(math.fmod(t, 60)))
|
|
end
|
|
return string.format("%ds", math.floor(t))
|
|
end,
|
|
}
|
|
|
|
-- Whether this symbol's state field holds an absolute timestamp rather than a
|
|
-- span. Those are what the `timer`/`elapsedTimer` condition types describe, and
|
|
-- rendering one as a duration means subtracting the clock -- which is also the
|
|
-- one thing here that genuinely has to happen every frame.
|
|
local function isTimePoint(symbol, data)
|
|
if not data then return false end
|
|
local _, byName = WA.TextSymbols(data)
|
|
local spec = byName[bareSymbol(symbol)]
|
|
return (spec and (spec.type == "timer" or spec.type == "elapsedTimer")) and true or false
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Big numbers
|
|
--
|
|
-- Hand-rolled: neither AbbreviateNumbers nor BreakUpLargeNumbers exists on 1.12.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local BIG_NUMBER_MODES = { "abbreviate", "separate" }
|
|
local BIG_NUMBER_LABELS = { abbreviate = "1.2m", separate = "1,234,567" }
|
|
|
|
local function abbreviateNumber(n)
|
|
local mag = n < 0 and -n or n
|
|
if mag >= 1000000 then return string.format("%.1fm", n / 1000000) end
|
|
if mag >= 1000 then return string.format("%.1fk", n / 1000) end
|
|
return string.format("%d", n)
|
|
end
|
|
|
|
local function separateNumber(n)
|
|
local neg = n < 0
|
|
local out = string.format("%d", math.floor(neg and -n or n))
|
|
while true do
|
|
local grouped, count = string.gsub(out, "^(%d+)(%d%d%d)", "%1,%2")
|
|
out = grouped
|
|
if count == 0 then break end
|
|
end
|
|
if neg then return "-" .. out end
|
|
return out
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Money
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Coin art, with g/s/c letters standing in on a client that renders no inline
|
|
-- escape.
|
|
--
|
|
-- NOT upstream's UI-GoldIcon/UI-SilverIcon/UI-CopperIcon: those are a
|
|
-- later-expansion split and no such file exists here. Vanilla ships one 64x16
|
|
-- sheet of four 16px cells, which the escape addresses as a 4x1 grid by cell
|
|
-- index -- byte-identical to what ClassicAPI's own GetCoinTextureString emits.
|
|
--
|
|
-- Two details that look like omissions. There is no pen offset, because the
|
|
-- renderer already pads around an icon symmetrically and a nudge here would
|
|
-- double the gap. And for the same reason the markup form joins its
|
|
-- denominations with nothing while the letter form needs a space.
|
|
local COIN_SHEET = "Interface\\MoneyFrame\\UI-MoneyIcons"
|
|
local COIN_LETTERS = { gold = "g", silver = "s", copper = "c" }
|
|
local COIN_CELLS = { gold = "0:1", silver = "1:2", copper = "2:3" }
|
|
|
|
-- Resolved per call rather than at load: the flag is drivable from a test.
|
|
local function coin(which)
|
|
if not WA.hasInlineText then return COIN_LETTERS[which] end
|
|
return "|T" .. COIN_SHEET .. ":0:0:0:0:4:1:" .. COIN_CELLS[which] .. ":0:1|t"
|
|
end
|
|
local function coinGap()
|
|
if WA.hasInlineText then return "" end
|
|
return " "
|
|
end
|
|
|
|
local COIN_PRECISIONS = { 1, 2, 3 }
|
|
local COIN_PRECISION_LABELS = { [1] = "Gold", [2] = "Gold, Silver", [3] = "Gold, Silver, Copper" }
|
|
|
|
WA.format_types = {
|
|
none = {
|
|
display = "None",
|
|
CreateFormatter = function() return nil end,
|
|
},
|
|
string = {
|
|
display = "String",
|
|
CreateFormatter = function(symbol, get)
|
|
local base
|
|
if get(symbol .. "_abbreviate", false) then
|
|
local max = get(symbol .. "_abbreviate_max", 8)
|
|
base = function(v) return WA.Utf8Sub(tostring(v), max) end
|
|
end
|
|
return withPad(symbol, get, base)
|
|
end,
|
|
summary = function(symbol, get)
|
|
local parts = {}
|
|
if get(symbol .. "_abbreviate", false) then
|
|
table.insert(parts, "to " .. tostring(get(symbol .. "_abbreviate_max", 8)))
|
|
end
|
|
local pad = padSummary(symbol, get)
|
|
if pad then table.insert(parts, pad) end
|
|
if #parts == 0 then return "String" end
|
|
return "String " .. table.concat(parts, ", ")
|
|
end,
|
|
options = function(symbol, get, set)
|
|
local fields = { {
|
|
type = "toggle", name = "Abbreviate",
|
|
key = "text_text_format_" .. symbol .. "_abbreviate",
|
|
get = function() return get(symbol .. "_abbreviate", false) end,
|
|
set = function(v) set(symbol .. "_abbreviate", v); WA.RefreshOptions() end,
|
|
} }
|
|
if get(symbol .. "_abbreviate", false) then
|
|
table.insert(fields, {
|
|
type = "range", name = "Max Characters",
|
|
key = "text_text_format_" .. symbol .. "_abbreviate_max",
|
|
min = 1, max = 40, step = 1,
|
|
get = function() return get(symbol .. "_abbreviate_max", 8) end,
|
|
set = function(v) set(symbol .. "_abbreviate_max", v) end,
|
|
})
|
|
end
|
|
padOptionFields(symbol, get, set, fields)
|
|
return fields
|
|
end,
|
|
},
|
|
timed = {
|
|
display = "Time",
|
|
CreateFormatter = function(symbol, get, data)
|
|
local renderer = TIME_RENDERERS[get(symbol .. "_time_format", 0)] or TIME_RENDERERS[0]
|
|
local threshold = get(symbol .. "_time_dynamic_threshold", 60)
|
|
local decimals = "%." .. get(symbol .. "_time_precision", 1) .. "f"
|
|
local timePoint = isTimePoint(symbol, data)
|
|
local sym = bareSymbol(symbol)
|
|
|
|
local render = function(v, state)
|
|
local t = asNumber(v)
|
|
if t == nil then return v end
|
|
if timePoint then
|
|
t = GetTime() - t
|
|
if t < 0 then t = -t end
|
|
end
|
|
if t <= 0 then return "" end
|
|
if threshold > 0 and t < threshold then return string.format(decimals, t) end
|
|
return renderer(t)
|
|
end
|
|
|
|
-- %p and %t carry a progress type, and their built-in formatting has
|
|
-- always been "seconds only when the progress is timed" -- a static
|
|
-- progress is a count, not a clock. Upstream special-cases the same two.
|
|
if sym == "p" or sym == "t" then
|
|
return function(v, state)
|
|
if not state or state.progressType ~= "timed" then return v end
|
|
return render(v, state)
|
|
end
|
|
end
|
|
-- Only a timestamp has to be recomputed between state updates; a span
|
|
-- renders the same until the state behind it changes, threshold or not.
|
|
return render, timePoint
|
|
end,
|
|
summary = function(symbol, get)
|
|
local s = "Time " .. (TIME_FORMAT_LABELS[get(symbol .. "_time_format", 0)] or "")
|
|
local threshold = get(symbol .. "_time_dynamic_threshold", 60)
|
|
if threshold > 0 then
|
|
s = s .. ", " .. (TIME_PRECISION_LABELS[get(symbol .. "_time_precision", 1)] or "")
|
|
.. " under " .. tostring(threshold) .. "s"
|
|
end
|
|
return s
|
|
end,
|
|
options = function(symbol, get, set)
|
|
local fields = { {
|
|
type = "select", name = "Time Format",
|
|
key = "text_text_format_" .. symbol .. "_time_format",
|
|
values = TIME_FORMATS, labels = TIME_FORMAT_LABELS,
|
|
get = function() return get(symbol .. "_time_format", 0) end,
|
|
set = function(v) set(symbol .. "_time_format", v) end,
|
|
}, {
|
|
type = "range", name = "Decimals Below",
|
|
key = "text_text_format_" .. symbol .. "_time_dynamic_threshold",
|
|
min = 0, max = 60, step = 1, half = true,
|
|
get = function() return get(symbol .. "_time_dynamic_threshold", 60) end,
|
|
set = function(v) set(symbol .. "_time_dynamic_threshold", v); WA.RefreshOptions() end,
|
|
} }
|
|
-- Precision decides nothing with the threshold at zero, where the
|
|
-- decimal branch is never taken.
|
|
if get(symbol .. "_time_dynamic_threshold", 60) > 0 then
|
|
table.insert(fields, {
|
|
type = "select", name = "Precision",
|
|
key = "text_text_format_" .. symbol .. "_time_precision",
|
|
values = TIME_PRECISIONS, labels = TIME_PRECISION_LABELS, half = true,
|
|
get = function() return get(symbol .. "_time_precision", 1) end,
|
|
set = function(v) set(symbol .. "_time_precision", v) end,
|
|
})
|
|
end
|
|
return fields
|
|
end,
|
|
},
|
|
BigNumber = {
|
|
display = "Big Number",
|
|
CreateFormatter = function(symbol, get)
|
|
local mode = get(symbol .. "_big_number_format", "abbreviate")
|
|
local render = (mode == "separate") and separateNumber or abbreviateNumber
|
|
return withPad(symbol, get, function(v)
|
|
local n = asNumber(v)
|
|
if n == nil then return v end
|
|
return render(n)
|
|
end)
|
|
end,
|
|
summary = function(symbol, get)
|
|
local s = "Big Number "
|
|
.. (BIG_NUMBER_LABELS[get(symbol .. "_big_number_format", "abbreviate")] or "")
|
|
local pad = padSummary(symbol, get)
|
|
if pad then s = s .. ", " .. pad end
|
|
return s
|
|
end,
|
|
options = function(symbol, get, set)
|
|
local fields = { {
|
|
type = "select", name = "Number Format",
|
|
key = "text_text_format_" .. symbol .. "_big_number_format",
|
|
values = BIG_NUMBER_MODES, labels = BIG_NUMBER_LABELS,
|
|
get = function() return get(symbol .. "_big_number_format", "abbreviate") end,
|
|
set = function(v) set(symbol .. "_big_number_format", v) end,
|
|
} }
|
|
padOptionFields(symbol, get, set, fields)
|
|
return fields
|
|
end,
|
|
},
|
|
Money = {
|
|
display = "Money",
|
|
CreateFormatter = function(symbol, get)
|
|
local precision = get(symbol .. "_money_precision", 3)
|
|
return function(v)
|
|
local n = asNumber(v)
|
|
if n == nil then return v end
|
|
n = math.floor(n)
|
|
local gold = math.floor(n / 10000)
|
|
local silver = math.floor(math.fmod(math.floor(n / 100), 100))
|
|
local copper = math.floor(math.fmod(n, 100))
|
|
if precision == 1 then
|
|
return separateNumber(gold) .. coin("gold")
|
|
elseif precision == 2 then
|
|
return separateNumber(gold) .. coin("gold") .. coinGap()
|
|
.. silver .. coin("silver")
|
|
end
|
|
return separateNumber(gold) .. coin("gold") .. coinGap()
|
|
.. silver .. coin("silver") .. coinGap() .. copper .. coin("copper")
|
|
end
|
|
end,
|
|
summary = function(symbol, get)
|
|
return "Money " .. (COIN_PRECISION_LABELS[get(symbol .. "_money_precision", 3)] or "")
|
|
end,
|
|
options = function(symbol, get, set)
|
|
return { {
|
|
type = "select", name = "Coin Precision",
|
|
key = "text_text_format_" .. symbol .. "_money_precision",
|
|
values = COIN_PRECISIONS, labels = COIN_PRECISION_LABELS,
|
|
get = function() return get(symbol .. "_money_precision", 3) end,
|
|
set = function(v) set(symbol .. "_money_precision", v) end,
|
|
} }
|
|
end,
|
|
},
|
|
Number = {
|
|
display = "Number",
|
|
CreateFormatter = function(symbol, get)
|
|
local precision = get(symbol .. "_decimal_precision", 1)
|
|
local render
|
|
if precision == 0 then
|
|
local round = ROUNDERS[get(symbol .. "_round_type", "floor")] or math.floor
|
|
render = function(v)
|
|
local n = asNumber(v)
|
|
if n == nil then return v end
|
|
return round(n)
|
|
end
|
|
else
|
|
local fmt = "%." .. precision .. "f"
|
|
render = function(v)
|
|
local n = asNumber(v)
|
|
if n == nil then return v end
|
|
return string.format(fmt, n)
|
|
end
|
|
end
|
|
return withPad(symbol, get, render)
|
|
end,
|
|
summary = function(symbol, get)
|
|
local precision = get(symbol .. "_decimal_precision", 1)
|
|
local s = "Number " .. (PRECISION_LABELS[precision] or tostring(precision))
|
|
if precision == 0 then
|
|
s = s .. " " .. (ROUND_LABELS[get(symbol .. "_round_type", "floor")] or "Floor")
|
|
end
|
|
local pad = padSummary(symbol, get)
|
|
if pad then s = s .. ", " .. pad end
|
|
return s
|
|
end,
|
|
options = function(symbol, get, set)
|
|
local fields = {
|
|
{
|
|
type = "select", name = "Precision",
|
|
key = "text_text_format_" .. symbol .. "_decimal_precision",
|
|
values = PRECISIONS, labels = PRECISION_LABELS, half = true,
|
|
get = function() return get(symbol .. "_decimal_precision", 1) end,
|
|
set = function(v) set(symbol .. "_decimal_precision", v); WA.RefreshOptions() end,
|
|
},
|
|
}
|
|
-- Round Mode decides nothing above precision 0, where string.format
|
|
-- does the rounding. Upstream greys it out; BuildOptions has no
|
|
-- disabled state, so the generator drops the row instead.
|
|
if get(symbol .. "_decimal_precision", 1) == 0 then
|
|
table.insert(fields, {
|
|
type = "select", name = "Round Mode",
|
|
key = "text_text_format_" .. symbol .. "_round_type",
|
|
values = ROUND_MODES, labels = ROUND_LABELS, half = true,
|
|
get = function() return get(symbol .. "_round_type", "floor") end,
|
|
set = function(v) set(symbol .. "_round_type", v) end,
|
|
})
|
|
end
|
|
padOptionFields(symbol, get, set, fields)
|
|
return fields
|
|
end,
|
|
},
|
|
}
|
|
|
|
-- Derived, so a new format type is one entry in the table above: "none" leads
|
|
-- the Format select (it's the default), the rest follow alphabetically.
|
|
local FORMAT_ORDER, FORMAT_LABELS = {}, {}
|
|
for name, spec in pairs(WA.format_types) do
|
|
FORMAT_LABELS[name] = spec.display
|
|
if name ~= "none" then table.insert(FORMAT_ORDER, name) end
|
|
end
|
|
table.sort(FORMAT_ORDER)
|
|
table.insert(FORMAT_ORDER, 1, "none")
|
|
|
|
-- Resolution within one state (§9): a real state field of this name wins,
|
|
-- then a dynamic_texts symbol, else empty. So %name/%stacks/%unitName read the
|
|
-- field directly while %p/%s/%n/%i/%t fall through to the formatters above.
|
|
local function resolveInState(sym, state, formatter)
|
|
if not state then return "" end
|
|
if state[sym] ~= nil then
|
|
local v = state[sym]
|
|
if formatter then v = formatter(v, state) end
|
|
return v ~= nil and tostring(v) or ""
|
|
end
|
|
local dt = WA.dynamic_texts[sym]
|
|
if dt then
|
|
local v = dt.get(state)
|
|
if v == nil then return "" end
|
|
-- A chosen format replaces the symbol's built-in one rather than
|
|
-- stacking on it (§9 ReplaceValuePlaceHolders), so %p as a Number shows
|
|
-- raw seconds instead of m:ss.
|
|
if formatter then
|
|
local r = formatter(v, state)
|
|
return r ~= nil and tostring(r) or ""
|
|
end
|
|
if dt.func then
|
|
local r = dt.func(v, state)
|
|
return r ~= nil and tostring(r) or ""
|
|
end
|
|
return tostring(v)
|
|
end
|
|
return ""
|
|
end
|
|
|
|
-- "%c" and "%c1".."%cN" name the Nth return of the region's custom text function
|
|
-- (§9), which the region ran and left in region.customValues. Bare "%c" is "%c1".
|
|
-- Not a state field: a real state field called `c` would still win, which is
|
|
-- resolveInState's rule and is why this is asked *after* it fails.
|
|
local function customIndex(symbol)
|
|
local _, _, digits = string.find(symbol, "^c(%d*)$")
|
|
if not digits then return nil end
|
|
return tonumber(digits) or 1
|
|
end
|
|
WA.CustomTextIndex = customIndex
|
|
|
|
-- "%N.sym" scopes to trigger N's state (region.states[N]); "%sym" uses the
|
|
-- active state (region.state). A formatter is keyed by the whole symbol, "2.p"
|
|
-- included, since each occurrence is configured separately.
|
|
local function valueForSymbol(symbol, region, formatters, customValues)
|
|
local formatter = formatters and formatters[symbol]
|
|
local _, _, trigStr, sym = string.find(symbol, "^(%d+)%.(.+)$")
|
|
if trigStr and sym then
|
|
local states = region.states or {}
|
|
return resolveInState(sym, states[tonumber(trigStr)], formatter)
|
|
end
|
|
local index = customIndex(symbol)
|
|
if index and not (region.state and region.state[symbol] ~= nil) then
|
|
local values = customValues or region.customValues
|
|
local v = values and values[index]
|
|
if v == nil then return "" end
|
|
if formatter then v = formatter(v, region.state) end
|
|
return v ~= nil and tostring(v) or ""
|
|
end
|
|
return resolveInState(symbol, region.state, formatter)
|
|
end
|
|
|
|
-- The state machine's next-state table (§9 nextState). 37=%, 123={, 125=},
|
|
-- 46=. ; 48-57/65-90/97-122 = alnum.
|
|
local function isWordByte(char)
|
|
return (char >= 48 and char <= 57) or (char >= 65 and char <= 90)
|
|
or (char >= 97 and char <= 122) or char == 46
|
|
end
|
|
|
|
local function nextState(char, state)
|
|
if state == 0 then
|
|
if char == 37 then return 1 end
|
|
return 0
|
|
elseif state == 1 then
|
|
if char == 37 then return 0
|
|
elseif char == 123 then return 3
|
|
elseif isWordByte(char) then return 2 end
|
|
return 0
|
|
elseif state == 2 then
|
|
if isWordByte(char) then return 2 end
|
|
if char == 37 then return 1 end
|
|
return 0
|
|
elseif state == 3 then
|
|
if char == 125 then return 0 end
|
|
return 3
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- Runs the parser over `text`, calling onLiteral(str) for verbatim runs and
|
|
-- onSymbol(name) for each %symbol / %{braced} / %N.sym occurrence. Shared by
|
|
-- ReplacePlaceHolders and the FrameTick probe so the two never drift.
|
|
local function walk(text, onLiteral, onSymbol)
|
|
local endPos = string.len(text)
|
|
local currentPos = 1
|
|
local state = 0
|
|
local start = 1
|
|
while currentPos <= endPos do
|
|
local char = string.byte(text, currentPos)
|
|
if state == 0 then
|
|
if char == 37 and currentPos > start then
|
|
onLiteral(string.sub(text, start, currentPos - 1))
|
|
end
|
|
elseif state == 1 then
|
|
-- After '%': a '{' means the symbol body starts past the brace,
|
|
-- anything else (incl. a second '%', which lands `start` on it so it
|
|
-- prints as a literal) starts the body here.
|
|
if char == 123 then start = currentPos + 1 else start = currentPos end
|
|
elseif state == 2 then
|
|
if not isWordByte(char) then
|
|
onSymbol(string.sub(text, start, currentPos - 1))
|
|
if char ~= 37 then start = currentPos end
|
|
end
|
|
elseif state == 3 then
|
|
if char == 125 then
|
|
onSymbol(string.sub(text, start, currentPos - 1))
|
|
start = currentPos + 1
|
|
end
|
|
end
|
|
state = nextState(char, state)
|
|
currentPos = currentPos + 1
|
|
end
|
|
if state == 0 and currentPos > start then
|
|
onLiteral(string.sub(text, start, currentPos - 1))
|
|
elseif state == 2 and currentPos > start then
|
|
onSymbol(string.sub(text, start, currentPos - 1))
|
|
elseif state == 1 then
|
|
onLiteral("%")
|
|
end
|
|
end
|
|
|
|
-- Raid target markers, {rt1}..{rt8}. One direction only: the reverse, a
|
|
-- localized marker name back to {rtN}, needs ICON_TAG_LIST, which is nil here.
|
|
--
|
|
-- Upstream names UI-RaidTargetingIcon_1.._8. Those per-index files are a
|
|
-- later-expansion split and none of them exists on this client -- only the
|
|
-- 256x256 sheet they were cut from, a 4x2 grid of 64px cells in marker order.
|
|
-- So the escape addresses a cell, exactly as the coin art does.
|
|
local RAID_MARKER_SHEET = "Interface\\TargetingFrame\\UI-RaidTargetingIcons"
|
|
local RAID_SHEET_SIZE = 256
|
|
local RAID_CELL = 64
|
|
|
|
-- The crop is expressed in PIXELS against the sheet's real dimensions, not as a
|
|
-- grid of cell indices the way the coin art is. The escape normalises
|
|
-- left/texW, so both forms are arithmetically equivalent only when the cells
|
|
-- tile the whole sheet -- and here they do not: the eight markers are 64px
|
|
-- cells in a 256x256 sheet, so they fill four columns but only the TOP HALF of
|
|
-- the rows. A "4x2 grid" reads each row as 128px, which stacks two markers into
|
|
-- every icon and samples empty sheet for 5..8.
|
|
local function raidMarkerArt(n)
|
|
local left = math.fmod(n - 1, 4) * RAID_CELL
|
|
local top = math.floor((n - 1) / 4) * RAID_CELL
|
|
return "|T" .. RAID_MARKER_SHEET .. ":0:0:0:0:"
|
|
.. RAID_SHEET_SIZE .. ":" .. RAID_SHEET_SIZE .. ":"
|
|
.. left .. ":" .. (left + RAID_CELL) .. ":"
|
|
.. top .. ":" .. (top + RAID_CELL) .. "|t"
|
|
end
|
|
|
|
-- Applied to the ASSEMBLED string, not to the literal runs. Several upstream
|
|
-- prototypes resolve a unit's mark to the *string* "{rtN}", so a substitution
|
|
-- that only saw what the user typed would leave every imported aura's mark
|
|
-- showing as text. The find() is the fast path: almost no text carries one.
|
|
local function replaceRaidMarkers(text)
|
|
if not WA.hasInlineText then return text end
|
|
if not string.find(text, "{rt", 1, true) then return text end
|
|
for n = 1, 8 do
|
|
text = string.gsub(text, "{rt" .. n .. "}", raidMarkerArt(n))
|
|
end
|
|
return text
|
|
end
|
|
|
|
-- Persistent scratch for the assembly below, fed through upvalues rather than
|
|
-- fresh closures per call: this runs per frame for every %p text, and a table
|
|
-- plus two closures per call was measurable churn. Safe because nothing walk
|
|
-- reaches re-enters ReplacePlaceHolders -- formatters and dynamic_texts are
|
|
-- all internal, and %c values are read precomputed off the region.
|
|
local repOut, repN = {}, 0
|
|
local repRegion, repFormatters, repCustom
|
|
local function repLiteral(lit)
|
|
repN = repN + 1
|
|
repOut[repN] = lit
|
|
end
|
|
local function repSymbol(sym)
|
|
repN = repN + 1
|
|
repOut[repN] = valueForSymbol(sym, repRegion, repFormatters, repCustom)
|
|
end
|
|
|
|
function WA.ReplacePlaceHolders(text, region, formatters, customValues)
|
|
if not text or text == "" then return "" end
|
|
-- A lone two-byte symbol -- "%p", "%s", "%n" -- is the overwhelmingly
|
|
-- common text and runs per frame, so it resolves directly with no walk and
|
|
-- no assembly (upstream's own two-byte fast path). Anything else after the
|
|
-- "%" falls through to the walker, which knows what to do with it.
|
|
if string.len(text) == 2 and string.byte(text, 1) == 37 then
|
|
local sym = string.sub(text, 2)
|
|
if sym == "%" then return "%" end
|
|
if isWordByte(string.byte(text, 2)) then
|
|
local s = replaceRaidMarkers(valueForSymbol(sym, region, formatters, customValues))
|
|
if string.find(s, "\\n", 1, true) then
|
|
s = string.gsub(s, "\\n", "\n")
|
|
end
|
|
return s
|
|
end
|
|
end
|
|
repRegion, repFormatters, repCustom = region, formatters, customValues
|
|
repN = 0
|
|
walk(text, repLiteral, repSymbol)
|
|
repRegion, repFormatters, repCustom = nil, nil, nil
|
|
local s = replaceRaidMarkers(table.concat(repOut, "", 1, repN))
|
|
if string.find(s, "\\n", 1, true) then
|
|
s = string.gsub(s, "\\n", "\n")
|
|
end
|
|
return s
|
|
end
|
|
|
|
-- Walks the distinct formattable symbols of `text`, which may be one string or
|
|
-- an array of them -- a region whose text a condition can replace has to build
|
|
-- formatters for every string it might end up showing, not just the typed one.
|
|
-- The `seen` set spans the whole array, so a symbol shared by two of them is
|
|
-- visited once.
|
|
--
|
|
-- %i is skipped deliberately, and the skip is a GUARD rather than an oversight:
|
|
-- it resolves to a |T...|t escape, and every format would corrupt it. A
|
|
-- `string` format's abbreviate cuts through WA.Utf8Sub and would land inside
|
|
-- the markup, leaving a truncated escape that renders as raw text; pad counts
|
|
-- bytes, so it would treat a 40-byte escape as 40 glyphs and pad to nothing.
|
|
-- Never build a format for it. Upstream skips it in the same two places.
|
|
local function eachFormattableSymbol(text, fn)
|
|
if not text or text == "" then return end
|
|
local seen = {}
|
|
local function scan(str)
|
|
if type(str) ~= "string" or str == "" then return end
|
|
walk(str, function() end, function(symbol)
|
|
if not seen[symbol] and bareSymbol(symbol) ~= "i" then
|
|
seen[symbol] = true
|
|
fn(symbol)
|
|
end
|
|
end)
|
|
end
|
|
if type(text) == "table" then
|
|
for i = 1, #text do scan(text[i]) end
|
|
else
|
|
scan(text)
|
|
end
|
|
end
|
|
|
|
-- symbol -> formatter for every symbol in `text` (§9 CreateFormatters).
|
|
-- get(key, default) reads the owning sub-region's text_text_format_* settings.
|
|
-- A symbol left on "none" gets no entry, so its built-in formatting stands.
|
|
--
|
|
-- The second return is the subset whose formatter declared itself every-frame:
|
|
-- a format whose *output* changes with the clock even though the state behind it
|
|
-- did not. WA.TextNeedsFrameTick takes it, since the symbol alone can no longer
|
|
-- decide that.
|
|
-- The format actually in force for a symbol: the pick if there is one, the
|
|
-- conditionType default otherwise. Every layer that has to agree about a
|
|
-- symbol's format -- the formatter build, the options dropdown, the fold's
|
|
-- summary -- goes through this one function, which is what makes them agree.
|
|
local function formatFor(symbol, get, data, byName)
|
|
local chosen = get(symbol .. "_format")
|
|
if chosen ~= nil then return chosen end
|
|
return WA.DefaultFormatFor(symbol, data, byName)
|
|
end
|
|
WA.FormatFor = formatFor
|
|
|
|
-- `text` is one string or an array of them (see eachFormattableSymbol). `data` is
|
|
-- the owning aura: the time format needs it to tell a timestamp state field from
|
|
-- a span, and the defaults above are read off it. A caller with none gets
|
|
-- duration formatting and no defaults.
|
|
function WA.CreateFormatters(text, get, data)
|
|
local formatters, everyFrame = {}, {}
|
|
local _, byName = WA.TextSymbols(data)
|
|
eachFormattableSymbol(text, function(symbol)
|
|
local spec = WA.format_types[formatFor(symbol, get, data, byName)]
|
|
if spec then
|
|
local fmt, isEveryFrame = spec.CreateFormatter(symbol, get, data)
|
|
formatters[symbol] = fmt
|
|
if isEveryFrame then everyFrame[symbol] = true end
|
|
end
|
|
end)
|
|
return formatters, everyFrame
|
|
end
|
|
|
|
-- Materialises those defaults onto the sub-region (§9 SetDefaultFormatters), so
|
|
-- a symbol whose format was chosen *for* the user is a visible, editable pick
|
|
-- rather than something applied invisibly at render time. Called from a text
|
|
-- field's own `set`.
|
|
--
|
|
-- A format already chosen is never overwritten -- that includes an explicit
|
|
-- "none", which is how a user turns a default back off. Nothing is written where
|
|
-- the default is "none" anyway: the key would only pin an answer the resolution
|
|
-- above already gives, and would then survive a trigger retype that changed it.
|
|
function WA.SetDefaultFormatters(text, get, set, data)
|
|
local _, byName = WA.TextSymbols(data)
|
|
eachFormattableSymbol(text, function(symbol)
|
|
if get(symbol .. "_format") == nil then
|
|
local default = WA.DefaultFormatFor(symbol, data, byName)
|
|
if default ~= "none" then set(symbol .. "_format", default) end
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Option field descriptors for `text`: a Format select per symbol, followed by
|
|
-- whatever the picked format adds (§9 AddTextFormatOption). `set(key, value)`
|
|
-- writes the setting back; picking a format re-renders the tab, since that is
|
|
-- what brings the format's own fields in.
|
|
--
|
|
-- `keyPrefix` is only the descriptor's identifying key, not storage -- `set`
|
|
-- owns that. It defaults to the sub-region's layout, the text region passing its
|
|
-- own so a row's key names the field it actually writes.
|
|
function WA.FormatOptionFields(text, get, set, data, keyPrefix)
|
|
local fields = {}
|
|
local _, byName = WA.TextSymbols(data)
|
|
keyPrefix = keyPrefix or "text_text_format_"
|
|
eachFormattableSymbol(text, function(symbol)
|
|
local fmtKey = symbol .. "_format"
|
|
local current = formatFor(symbol, get, data, byName)
|
|
table.insert(fields, {
|
|
type = "select", name = "Format %" .. symbol,
|
|
key = keyPrefix .. fmtKey,
|
|
values = FORMAT_ORDER, labels = FORMAT_LABELS, half = true,
|
|
get = function() return current end,
|
|
set = function(v) set(fmtKey, v); WA.RefreshOptions() end,
|
|
})
|
|
local spec = WA.format_types[current]
|
|
if spec and spec.options then
|
|
local extra = spec.options(symbol, get, set)
|
|
for i = 1, #extra do table.insert(fields, extra[i]) end
|
|
end
|
|
end)
|
|
return fields
|
|
end
|
|
|
|
-- One line naming the formats picked for `text`'s symbols, for the collapsed
|
|
-- Format Options disclosure -- so folding the rows away doesn't hide what is set.
|
|
-- nil when every symbol is left on "none", which is the common case and wants no
|
|
-- decoration at all. A format type without its own `summary` reports its display
|
|
-- name, so a new one lands here without a second registration.
|
|
function WA.FormatSummary(text, get, data)
|
|
local parts = {}
|
|
local _, byName = WA.TextSymbols(data)
|
|
eachFormattableSymbol(text, function(symbol)
|
|
local name = formatFor(symbol, get, data, byName)
|
|
local spec = WA.format_types[name]
|
|
if spec and name ~= "none" then
|
|
local desc = spec.summary and spec.summary(symbol, get) or spec.display
|
|
table.insert(parts, "%" .. symbol .. " " .. desc)
|
|
end
|
|
end)
|
|
if #parts == 0 then return nil end
|
|
return table.concat(parts, ", ")
|
|
end
|
|
|
|
-- A text field's label, listing the symbols it accepts. %i is offered only on a
|
|
-- client that renders inline textures: advertising a code that resolves to
|
|
-- nothing is worse than not advertising it.
|
|
function WA.TextSymbolLabel(name)
|
|
if WA.hasInlineText then return name .. " (%p %t %n %i %s %c)" end
|
|
return name .. " (%p %t %n %s %c)"
|
|
end
|
|
|
|
-- Whether `text` references the custom text function at all (§9
|
|
-- ContainsCustomPlaceHolder). One string or an array of them, so a caller can ask
|
|
-- about a whole aura's worth at once.
|
|
function WA.ContainsCustomPlaceHolder(text)
|
|
if not text then return false end
|
|
local found = false
|
|
local function scan(str)
|
|
if type(str) ~= "string" or str == "" then return end
|
|
walk(str, function() end, function(symbol)
|
|
if customIndex(symbol) then found = true end
|
|
end)
|
|
end
|
|
if type(text) == "table" then
|
|
for i = 1, #text do scan(text[i]) end
|
|
else
|
|
scan(text)
|
|
end
|
|
return found
|
|
end
|
|
|
|
-- Runs a region's custom text function and returns its results as an array, so
|
|
-- %c1..%cN can index it. Upstream's argument list, unchanged: the raw
|
|
-- expiration/duration behind the state, then the five classic symbols already
|
|
-- rendered -- a custom function should not have to re-derive what %p says.
|
|
--
|
|
-- Run through WA.RunAuraFuncPacked rather than called: this is user-authored
|
|
-- code reached from a repaint, so it needs `aura_env` pointed at this aura
|
|
-- (state.icon is the only way one of these functions can see an icon, the
|
|
-- argument being blank here), and an error in it must name the aura and leave
|
|
-- the rest of the text rendering rather than take the paint down. It is not
|
|
-- sandboxed beyond that (drift §D2).
|
|
function WA.RunCustomTextFunc(region, fn)
|
|
if not fn then return nil end
|
|
local state = region.state
|
|
local expirationTime, duration
|
|
if state then
|
|
if state.progressType == "timed" then
|
|
expirationTime, duration = state.expirationTime, state.duration
|
|
else
|
|
expirationTime, duration = state.total, state.value
|
|
end
|
|
end
|
|
-- `func` runs even where `get` came back nil, which is upstream's shape and
|
|
-- load-bearing: on a timed aura that has just expired, p.get yields nil and
|
|
-- p.func turns that into "". Short-circuiting instead hands the user function
|
|
-- a nil `progress`, so `progress .. "s"` errors in code that works in
|
|
-- WeakAuras. Only an absent state short-circuits -- both halves read fields
|
|
-- off it. `icon` is therefore consistently "": i.func blanks it, because this
|
|
-- client's FontString renders a |T...|t escape as its own path, and a
|
|
-- function wanting the real one reads aura_env.state.icon.
|
|
local dt = WA.dynamic_texts
|
|
local function classic(sym)
|
|
if not state then return nil end
|
|
return dt[sym].func(dt[sym].get(state), state)
|
|
end
|
|
local ok, values = WA.RunAuraFuncPacked(region, (region.id or "?") .. ": custom text", fn,
|
|
expirationTime or math.huge, duration or 0,
|
|
classic("p"), classic("t"), classic("n"), classic("i"), classic("s"))
|
|
if not ok then return nil end
|
|
return values
|
|
end
|
|
|
|
-- Whether a string has to repaint every frame. Drives SubText's FrameTick
|
|
-- subscription (§8/§9). Two independent reasons: it references %p, whose *value*
|
|
-- is the clock; or one of its symbols carries an every-frame formatter, whose
|
|
-- *rendering* is. `everyFrameFormatters` is WA.CreateFormatters' second return
|
|
-- and is optional -- omitting it asks the %p question alone.
|
|
local function symbolIsP(symbol)
|
|
return bareSymbol(symbol) == "p"
|
|
end
|
|
|
|
function WA.TextNeedsFrameTick(text, everyFrameFormatters)
|
|
if not text or text == "" then return false end
|
|
local found = false
|
|
walk(text, function() end, function(sym)
|
|
if symbolIsP(sym) or (everyFrameFormatters and everyFrameFormatters[sym]) then
|
|
found = true
|
|
end
|
|
end)
|
|
return found
|
|
end
|