compat: the secure-handler family and the animation surface
Both modules land together because `classic_api/mod.rs` and `widget_surface.rs` carry rows for
each: split, the first commit named a module the second added, and would not have compiled.
## The secure-handler family, because 1.12 has nothing to be secure from
`frame.rs` declined it with a stated reason — "modern restricted execution, which 1.12 has no
equivalent of" — and that is right about the *sandbox* and is the whole of why it is now built.
A secure handler exists so an untainted snippet may call **protected** functions; 1.12 has
neither taint nor protected functions, so the restriction has no counterpart here, while the
plumbing underneath (a named frame store, a snippet on a child's handler) is ordinary
behaviour the snippets need. The body runs as plain Lua with the names the secure environment
would bind.
**The per-header shared table is the part worth getting right.** `MenuWheel.lua:484` writes a
bare `held = false` inside a snippet, and in the real secure environment that lands in a table
per header, not `_G`. `setfenv` onto one table per frame, `__index` to `_G` so the rest of the
API resolves; the test asserts `held` is **not** a global afterwards. My first test asserted
through `_G` and failed — the implementation was right and the test measured the wrong thing.
**`SecureActionButtonTemplate` is not empty**: reading your own attributes on click is plain
dispatch. `type="click"` clicks the frame in `clickbutton` (how the wheels reach the game's
micro buttons), and the lookup follows upstream's order (`type-<button>`, `*type*`, `type`).
**That exposed a real bug of ours**: `SetAttribute` silently dropped a frame.
`attribute_value` kept bool/int/number/string and sent everything else to `ScriptValue::Nil`,
so `SetAttribute("clickbutton", SomeFrame)` stored nothing — and upstream's `clickbutton` is
routinely a frame, which `Cinematic/MenuWheel.lua` relies on. A widget table is now kept as
`ScriptValue::Frame(id)`, the variant `NAME_PLATE_CREATED`'s arg1 already uses, which
round-trips through `object::frame_wrapper`. A non-widget table is still nil, documented.
**Not built, named rather than left to be discovered**: the binding half (`ClearBindings`,
`SetBindingClick`, …) are no-ops — no override-binding layer here to write into — so a wheel
opened with the mouse works and hold-a-key-and-release does not; and the `_onshow`/`_onhide`
attribute snippets are not run, since `SetAttribute` is a plain store with nothing to drive
them.
Templates in `assets/ui/CompatTemplates.xml`, listed on that wall with the reason: no stock
file to migrate from, and XML rather than `compat/` only because `inherits=` is loader-resolved.
A trap caught before shipping: `BackdropTemplateMixin.GetBackdropColor` forwarding to
`self:GetBackdropColor()` calls **itself** once `Mixin()` copies it onto the frame — the stock
getters answer, so there are no getter rows, and a test mixes in and reads back.
## `CreateAnimationGroup`: the surface, with nothing animated
1.12 has no animation system and this UI pass has no interpolation to hang one on. The cost is
the **absent constructor**: `Cinematic/Options.lua:7106` builds a rotating spinner and the nil
method aborted the whole options panel. Measured first — that is the addon's only animation in
55,661 lines, so this is a surface for a decoration.
**A non-looping group finishes the moment it plays**, firing `OnFinished`, because nothing
moves and that is the reading that keeps an addon's flow right; one that chains its next step
off `OnFinished` would otherwise wait forever. A looping group plays until stopped. Stated in
the header, since the name implies motion: a spinner does not spin, a fade appears at its
destination, a sequence lands on its last frame. Setters and getters round-trip.
Corpus: all 100 of grom's addons surveyed before and after — 85 ok / 1 fail, rosters
identical. benilla-ui 2081 passed. Full chain green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -55,7 +55,89 @@
|
||||
</Backdrop>
|
||||
</Frame>
|
||||
|
||||
<!--
|
||||
The secure-handler family. On a modern client these exist so an *untainted* snippet may
|
||||
call protected functions; 1.12 has neither taint nor protected functions, so what the
|
||||
template encodes is a restriction that does not exist here and the frame itself carries no
|
||||
behaviour. Empty, therefore — the header methods the snippets need (`SetFrameRef`,
|
||||
`WrapScript`, `Execute`) are composed in `script/compat/classic_api/secure.rs`, beside the
|
||||
rest of the compat surface.
|
||||
|
||||
Cinematic's two wheels (`MenuWheel.lua:312`, `MarkerWheel.lua:205`) name the first two.
|
||||
-->
|
||||
<Frame name="SecureHandlerBaseTemplate" virtual="true"/>
|
||||
<Frame name="SecureHandlerShowHideTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
<Frame name="SecureHandlerAttributeTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
<Frame name="SecureHandlerClickTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
<Frame name="SecureHandlerStateTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
<Frame name="SecureHandlerEnterLeaveTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
<Frame name="SecureHandlerDragTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
<Frame name="SecureHandlerMouseUpDownTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
|
||||
|
||||
<!--
|
||||
`SecureActionButtonTemplate`, and this one is **not** empty: its whole job is to read its
|
||||
own attributes on click and act on them, and that dispatch is plain behaviour rather than
|
||||
anything protected. `type="click"` clicks the frame named by `clickbutton`, which is how
|
||||
Cinematic's wheels reach the game's own menu buttons (`MenuWheel.lua:357`), and the three
|
||||
cast-ish types forward to the stock verbs. The taint machinery is what 1.12 has no
|
||||
counterpart for, and none of this needs it.
|
||||
|
||||
`RegisterForClicks` is the caller's to set, as upstream: the wheels ask for `"AnyUp"`.
|
||||
-->
|
||||
<Button name="SecureActionButtonTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnClick>BenillaSecureAction_OnClick(this, arg1);</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Script><![CDATA[
|
||||
-- `SecureActionButtonTemplate`'s dispatch. `self:GetAttribute(k)` first tries the
|
||||
-- button-specific key the real template prefers (`<type>-<button>`, then `*<key>*`), then
|
||||
-- the plain one, which is the lookup order upstream documents.
|
||||
local function attr(self, key, button)
|
||||
local v
|
||||
if button then
|
||||
v = self:GetAttribute(key .. "-" .. button)
|
||||
if v ~= nil then return v end
|
||||
v = self:GetAttribute("*" .. key .. "-" .. button)
|
||||
if v ~= nil then return v end
|
||||
end
|
||||
v = self:GetAttribute("*" .. key .. "*")
|
||||
if v ~= nil then return v end
|
||||
return self:GetAttribute(key)
|
||||
end
|
||||
|
||||
function BenillaSecureAction_OnClick(self, button)
|
||||
local kind = attr(self, "type", button)
|
||||
if not kind or kind == "none" then return end
|
||||
if kind == "click" then
|
||||
local target = attr(self, "clickbutton", button)
|
||||
if type(target) == "string" then target = getglobal(target) end
|
||||
if target and target.Click then target:Click(button) end
|
||||
elseif kind == "spell" then
|
||||
local spell = attr(self, "spell", button)
|
||||
if spell then
|
||||
local unit = attr(self, "unit", button)
|
||||
if unit then
|
||||
CastSpellByName(spell, unit)
|
||||
else
|
||||
CastSpellByName(spell)
|
||||
end
|
||||
end
|
||||
elseif kind == "macro" then
|
||||
local text = attr(self, "macrotext", button)
|
||||
if text then
|
||||
RunScript(text)
|
||||
else
|
||||
local name = attr(self, "macro", button)
|
||||
if name then RunMacro(name) end
|
||||
end
|
||||
elseif kind == "item" then
|
||||
local item = attr(self, "item", button)
|
||||
if item then UseItemByName(item, attr(self, "unit", button)) end
|
||||
end
|
||||
end
|
||||
|
||||
-- The mixin 9.0 shipped so addons could call the backdrop methods it had removed. Here
|
||||
-- the stock methods are the real ones, so each row forwards; the table exists for the
|
||||
-- `BackdropTemplateMixin and "BackdropTemplate"` test and for `Mixin()`.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//! `assets/ui/CompatTemplates.xml`: the ClassicAPI-era templates addons inherit by name. The
|
||||
//! methods behind the secure family live in `benilla-ui`'s `compat/classic_api/secure.rs` and are
|
||||
//! tested there; what needs this crate is the templates themselves, since `assets/ui` is ours.
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn harness() -> UiScript {
|
||||
let s = UiScript::new().unwrap();
|
||||
load_xml(&s, "CompatTemplates.xml");
|
||||
s
|
||||
}
|
||||
|
||||
/// Ace3 names `BackdropTemplate` and `DialogBorderOpaqueTemplate` unguarded
|
||||
/// (`AceGUIContainer-Frame.lua:179`, `AceConfigDialog-3.0.lua:573`), so a missing one raises
|
||||
/// inside the library. `BackdropTemplate` is deliberately empty: 1.12's backdrop methods are
|
||||
/// native, so a frame inheriting it already answers all four.
|
||||
#[test]
|
||||
fn the_backdrop_and_dialog_templates_resolve_and_the_mixin_is_announced() {
|
||||
let s = harness();
|
||||
s.run(
|
||||
r#"
|
||||
plain = CreateFrame("Frame", "BdPlain", nil, "BackdropTemplate")
|
||||
bordered = CreateFrame("Frame", "BdBordered", nil, "DialogBorderOpaqueTemplate")
|
||||
opaque = CreateFrame("Frame", "BdSoft", nil, "DialogBorderTemplate")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// The idiom every 9.0-aware addon writes, and the reason the mixin table has to exist.
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return BackdropTemplateMixin and "BackdropTemplate" or "nil""#)
|
||||
.unwrap(),
|
||||
"BackdropTemplate"
|
||||
);
|
||||
for m in ["SetBackdrop", "GetBackdrop", "SetBackdropColor"] {
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return type(BdPlain.{m})"))
|
||||
.unwrap(),
|
||||
"function",
|
||||
"{m} is native on 1.12, so the template needs no row for it"
|
||||
);
|
||||
}
|
||||
// The border carries art; the plain one is empty by design.
|
||||
assert!(
|
||||
s.eval::<bool>("return BdBordered:GetBackdrop() ~= nil")
|
||||
.unwrap(),
|
||||
"DialogBorderOpaqueTemplate draws a backdrop"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return BdPlain:GetBackdrop() == nil")
|
||||
.unwrap(),
|
||||
"BackdropTemplate paints nothing of its own"
|
||||
);
|
||||
// `Mixin()` must not shadow the native methods with recursing forwards — there are no getter
|
||||
// rows for exactly that reason, so a mixed-in frame still answers its own colour. Asserted on
|
||||
// the bordered frame, because a colour on `BackdropTemplate` has no backdrop to tint: 1.12's
|
||||
// setter needs one, so the empty template reads back the default.
|
||||
s.run(
|
||||
r#"BdBordered:SetBackdropColor(0.25, 0.5, 0.75, 1)
|
||||
Mixin(BdBordered, BackdropTemplateMixin)"#,
|
||||
)
|
||||
.unwrap();
|
||||
let (r, g, b) = s
|
||||
.eval::<(f32, f32, f32)>("local r, g, b = BdBordered:GetBackdropColor() return r, g, b")
|
||||
.unwrap();
|
||||
// Within a byte: the colour round-trips through 8 bits per channel, as 1.12 stores a vertex
|
||||
// colour, so 0.25 comes back as 64/255. What is being asserted is that it answers *its own*
|
||||
// colour at all — a forwarding getter row would have recursed here instead.
|
||||
let within_a_byte = |got: f32, want: f32| (got - want).abs() <= 1.0 / 255.0;
|
||||
assert!(
|
||||
within_a_byte(r, 0.25) && within_a_byte(g, 0.5) && within_a_byte(b, 0.75),
|
||||
"got ({r}, {g}, {b})"
|
||||
);
|
||||
}
|
||||
|
||||
/// `SecureActionButtonTemplate`'s one behaviour that is plain dispatch rather than anything
|
||||
/// protected: `type="click"` clicks the frame named by `clickbutton`. That is how Cinematic's
|
||||
/// wheels reach the game's own menu buttons (`MenuWheel.lua:357`).
|
||||
#[test]
|
||||
fn a_secure_action_button_of_type_click_clicks_the_frame_it_names() {
|
||||
let s = harness();
|
||||
s.run(
|
||||
r#"
|
||||
clicked = 0
|
||||
target = CreateFrame("Button", "ClickTarget")
|
||||
ClickTarget:SetScript("OnClick", function() clicked = clicked + 1 end)
|
||||
act = CreateFrame("Button", "ActBtn", nil, "SecureActionButtonTemplate")
|
||||
ActBtn:SetAttribute("type", "click")
|
||||
ActBtn:SetAttribute("clickbutton", ClickTarget)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.run("ActBtn:Click()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return clicked").unwrap(), 1);
|
||||
|
||||
// A name string resolves through `_G`, which the real template accepts too.
|
||||
s.run(r#"ActBtn:SetAttribute("clickbutton", "ClickTarget")"#)
|
||||
.unwrap();
|
||||
s.run("ActBtn:Click()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return clicked").unwrap(), 2);
|
||||
|
||||
// No type at all is a no-op, not a raise: an unconfigured secure button is inert upstream.
|
||||
s.run(r#"ActBtn:SetAttribute("type", nil)"#).unwrap();
|
||||
s.run("ActBtn:Click()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return clicked").unwrap(), 2);
|
||||
|
||||
// The per-button key wins over the plain one, which is the lookup order upstream documents
|
||||
// and how a wheel gives each mouse button its own action.
|
||||
s.run(
|
||||
r#"other = CreateFrame("Button", "OtherTarget")
|
||||
othered = 0
|
||||
OtherTarget:SetScript("OnClick", function() othered = othered + 1 end)
|
||||
ActBtn:SetAttribute("type", "click")
|
||||
ActBtn:SetAttribute("clickbutton", ClickTarget)
|
||||
ActBtn:SetAttribute("clickbutton-RightButton", OtherTarget)"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.run(r#"ActBtn:Click("RightButton")"#).unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
s.eval::<i64>("return clicked").unwrap(),
|
||||
s.eval::<i64>("return othered").unwrap()
|
||||
),
|
||||
(2, 1),
|
||||
"the RightButton key took precedence over the plain clickbutton"
|
||||
);
|
||||
}
|
||||
|
||||
/// The whole secure-handler family resolves, so `CreateFrame` with any of them does not raise.
|
||||
/// The list is what the corpus names plus the siblings of the same family, since an addon reaching
|
||||
/// for one of these reaches for the others.
|
||||
#[test]
|
||||
fn every_secure_handler_template_resolves() {
|
||||
let s = harness();
|
||||
for t in [
|
||||
"SecureHandlerBaseTemplate",
|
||||
"SecureHandlerShowHideTemplate",
|
||||
"SecureHandlerAttributeTemplate",
|
||||
"SecureHandlerClickTemplate",
|
||||
"SecureHandlerStateTemplate",
|
||||
"SecureHandlerEnterLeaveTemplate",
|
||||
"SecureHandlerDragTemplate",
|
||||
"SecureHandlerMouseUpDownTemplate",
|
||||
] {
|
||||
s.run(&format!(r#"h = CreateFrame("Frame", nil, nil, "{t}")"#))
|
||||
.unwrap_or_else(|e| panic!("{t}: {e}"));
|
||||
}
|
||||
}
|
||||
@@ -857,6 +857,10 @@ mod money_frame_tests;
|
||||
#[cfg(test)]
|
||||
mod faux_scroll_tests;
|
||||
|
||||
/// `assets/ui/CompatTemplates.xml`: the ClassicAPI-era templates addons inherit by name.
|
||||
#[cfg(test)]
|
||||
mod compat_templates_tests;
|
||||
|
||||
/// The stock UIPanelTemplates and OptionsFrameTemplates kit, driven the way an addon drives it.
|
||||
#[cfg(test)]
|
||||
mod panel_template_tests;
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! `CreateAnimationGroup` and the animation objects under it: the surface, with **nothing
|
||||
//! animated**.
|
||||
//!
|
||||
//! 1.12 has no animation system at all — it arrived in 3.x — and this client's UI pass has no
|
||||
//! per-frame interpolation to hang one on. What it does have is a lot of addons written for a
|
||||
//! later client that build a group at load, so the *absence of the constructor* is what costs:
|
||||
//! `Cinematic/Options.lua:7106` builds a rotating spinner beside its "Add mode" button, and the
|
||||
//! nil method aborted the options panel there.
|
||||
//!
|
||||
//! So the objects exist, their setters and getters round-trip, and the group's play state is
|
||||
//! tracked honestly. The one thing to understand before trusting it:
|
||||
//!
|
||||
//! **A non-looping group finishes the moment it is played.** Nothing moves, so the animation's end
|
||||
//! state is reached at once, and that is the interpretation that keeps an addon's *flow* correct:
|
||||
//! a group whose `OnFinished` hands over to the next step would otherwise wait forever. A looping
|
||||
//! group stays playing until `Stop`/`Finish`, since that is what a loop means and the common use
|
||||
//! (a spinner while something is on) reads `IsPlaying` rather than waiting for an end.
|
||||
//!
|
||||
//! What that costs, plainly: a spinner does not spin, a fade appears instantly at its destination
|
||||
//! rather than fading, and a staged sequence lands on its last frame immediately. No addon is
|
||||
//! stuck, nothing raises, and nothing pretends to be moving. The step that would make these real
|
||||
//! is a UI-pass animator plus `Texture:SetRotation` — both absent, and both bigger than this file.
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::script::{REG_FONTSTRING_METHODS, REG_FRAME_METHODS, REG_TEXTURE_METHODS};
|
||||
|
||||
use super::frame::delegate;
|
||||
|
||||
/// The constructor and the two object shapes, in Lua: these are plain tables with closures, not
|
||||
/// widgets, so there is nothing for the arena to hold and nothing for the host to tick.
|
||||
const ANIMATION: &str = r#"
|
||||
return function(m)
|
||||
-- One animation under a group. Every setter stores and every getter answers what was stored, so
|
||||
-- an addon that writes a duration and reads it back sees its own number.
|
||||
local function newAnimation(group, kind, name)
|
||||
local a = {
|
||||
__kind = kind,
|
||||
__group = group,
|
||||
__order = 1,
|
||||
__duration = 0,
|
||||
__startDelay = 0,
|
||||
__endDelay = 0,
|
||||
__smoothing = "NONE",
|
||||
__scripts = {},
|
||||
}
|
||||
function a.GetObjectType(self) return "Animation" end
|
||||
function a.IsObjectType(self, t) return t == "Animation" end
|
||||
function a.GetRegionParent(self) return group.__owner end
|
||||
function a.GetAnimationGroup(self) return group end
|
||||
|
||||
function a.SetDuration(self, d) self.__duration = d end
|
||||
function a.GetDuration(self) return self.__duration end
|
||||
function a.SetStartDelay(self, d) self.__startDelay = d end
|
||||
function a.GetStartDelay(self) return self.__startDelay end
|
||||
function a.SetEndDelay(self, d) self.__endDelay = d end
|
||||
function a.GetEndDelay(self) return self.__endDelay end
|
||||
function a.SetOrder(self, o) self.__order = o end
|
||||
function a.GetOrder(self) return self.__order end
|
||||
function a.SetSmoothing(self, s) self.__smoothing = s end
|
||||
function a.GetSmoothing(self) return self.__smoothing end
|
||||
function a.SetParent(self, p) self.__group = p end
|
||||
|
||||
-- Rotation
|
||||
function a.SetDegrees(self, d) self.__degrees = d end
|
||||
function a.GetDegrees(self) return self.__degrees end
|
||||
function a.SetRadians(self, r) self.__radians = r end
|
||||
function a.GetRadians(self) return self.__radians end
|
||||
function a.SetOrigin(self, point, x, y)
|
||||
self.__origin = { point = point, x = x, y = y }
|
||||
end
|
||||
function a.GetOrigin(self)
|
||||
local o = self.__origin
|
||||
if o then return o.point, o.x, o.y end
|
||||
end
|
||||
|
||||
-- Alpha
|
||||
function a.SetFromAlpha(self, v) self.__fromAlpha = v end
|
||||
function a.GetFromAlpha(self) return self.__fromAlpha end
|
||||
function a.SetToAlpha(self, v) self.__toAlpha = v end
|
||||
function a.GetToAlpha(self) return self.__toAlpha end
|
||||
function a.SetChange(self, v) self.__change = v end
|
||||
function a.GetChange(self) return self.__change end
|
||||
|
||||
-- Scale and Translation
|
||||
function a.SetScale(self, x, y) self.__scaleX, self.__scaleY = x, y end
|
||||
function a.GetScale(self) return self.__scaleX, self.__scaleY end
|
||||
function a.SetFromScale(self, x, y) self.__fromScaleX, self.__fromScaleY = x, y end
|
||||
function a.GetFromScale(self) return self.__fromScaleX, self.__fromScaleY end
|
||||
function a.SetToScale(self, x, y) self.__toScaleX, self.__toScaleY = x, y end
|
||||
function a.GetToScale(self) return self.__toScaleX, self.__toScaleY end
|
||||
function a.SetOffset(self, x, y) self.__offsetX, self.__offsetY = x, y end
|
||||
function a.GetOffset(self) return self.__offsetX, self.__offsetY end
|
||||
|
||||
function a.SetScript(self, kind, fn) self.__scripts[kind] = fn end
|
||||
function a.GetScript(self, kind) return self.__scripts[kind] end
|
||||
function a.HookScript(self, kind, fn)
|
||||
local prev = self.__scripts[kind]
|
||||
self.__scripts[kind] = function()
|
||||
if prev then prev(self) end
|
||||
fn(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-animation play state, which an addon reads to decide whether to restart.
|
||||
function a.Play(self) self.__playing = true end
|
||||
function a.Stop(self) self.__playing = false end
|
||||
function a.Pause(self) self.__playing = false end
|
||||
function a.IsPlaying(self) return self.__playing and 1 or nil end
|
||||
function a.IsDelaying(self) return nil end
|
||||
function a.IsDone(self) return not self.__playing and 1 or nil end
|
||||
function a.GetProgress(self) return self.__playing and 0 or 1 end
|
||||
function a.GetElapsed(self) return 0 end
|
||||
|
||||
if name then setglobal(name, a) end
|
||||
table.insert(group.__animations, a)
|
||||
return a
|
||||
end
|
||||
|
||||
local function fire(obj, kind)
|
||||
local fn = obj.__scripts[kind]
|
||||
if fn then
|
||||
local ok, err = pcall(fn, obj)
|
||||
if not ok then
|
||||
local h = geterrorhandler and geterrorhandler()
|
||||
if h then h(err) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function newGroup(owner, name)
|
||||
local g = { __owner = owner, __animations = {}, __scripts = {}, __looping = "NONE" }
|
||||
function g.GetObjectType(self) return "AnimationGroup" end
|
||||
function g.IsObjectType(self, t) return t == "AnimationGroup" end
|
||||
function g.GetParent(self) return self.__owner end
|
||||
|
||||
function g.CreateAnimation(self, kind, animName, template)
|
||||
return newAnimation(self, kind, animName)
|
||||
end
|
||||
function g.GetAnimations(self) return unpack(self.__animations) end
|
||||
function g.HasScript(self, kind)
|
||||
return kind == "OnPlay" or kind == "OnStop" or kind == "OnFinished" or kind == "OnLoop"
|
||||
end
|
||||
function g.SetScript(self, kind, fn) self.__scripts[kind] = fn end
|
||||
function g.GetScript(self, kind) return self.__scripts[kind] end
|
||||
function g.HookScript(self, kind, fn)
|
||||
local prev = self.__scripts[kind]
|
||||
self.__scripts[kind] = function()
|
||||
if prev then prev(self) end
|
||||
fn(self)
|
||||
end
|
||||
end
|
||||
|
||||
function g.SetLooping(self, mode) self.__looping = mode or "NONE" end
|
||||
function g.GetLooping(self) return self.__looping end
|
||||
function g.GetDuration(self)
|
||||
local longest = 0
|
||||
for _, a in ipairs(self.__animations) do
|
||||
local total = (a.__duration or 0) + (a.__startDelay or 0) + (a.__endDelay or 0)
|
||||
if total > longest then longest = total end
|
||||
end
|
||||
return longest
|
||||
end
|
||||
|
||||
function g.Play(self)
|
||||
self.__playing = true
|
||||
for _, a in ipairs(self.__animations) do a:Play() end
|
||||
fire(self, "OnPlay")
|
||||
-- Nothing animates, so a one-shot is over at once; see this module's header for why that
|
||||
-- is the safe reading rather than leaving the group playing forever.
|
||||
if self.__looping == "NONE" then
|
||||
self:Finish()
|
||||
end
|
||||
end
|
||||
function g.Stop(self)
|
||||
self.__playing = false
|
||||
for _, a in ipairs(self.__animations) do a:Stop() end
|
||||
fire(self, "OnStop")
|
||||
end
|
||||
function g.Pause(self)
|
||||
self.__paused = true
|
||||
self.__playing = false
|
||||
end
|
||||
function g.Resume(self)
|
||||
self.__paused = false
|
||||
self:Play()
|
||||
end
|
||||
function g.Finish(self)
|
||||
self.__playing = false
|
||||
for _, a in ipairs(self.__animations) do a:Stop() end
|
||||
fire(self, "OnFinished")
|
||||
end
|
||||
function g.Restart(self)
|
||||
self:Stop()
|
||||
self:Play()
|
||||
end
|
||||
function g.IsPlaying(self) return self.__playing and 1 or nil end
|
||||
function g.IsPaused(self) return self.__paused and 1 or nil end
|
||||
function g.IsDone(self) return not self.__playing and 1 or nil end
|
||||
function g.IsPendingFinish(self) return nil end
|
||||
function g.GetProgress(self) return self.__playing and 0 or 1 end
|
||||
|
||||
if name then setglobal(name, g) end
|
||||
return g
|
||||
end
|
||||
|
||||
function m.CreateAnimationGroup(self, name, template)
|
||||
return newGroup(self, name)
|
||||
end
|
||||
end
|
||||
"#;
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// On the frame table and both region leaves: a Texture owns a group as readily as a Frame
|
||||
// does, and `Cinematic/Options.lua:7106` builds its spinner's group on a Texture.
|
||||
for key in [
|
||||
REG_FRAME_METHODS,
|
||||
REG_TEXTURE_METHODS,
|
||||
REG_FONTSTRING_METHODS,
|
||||
] {
|
||||
delegate(lua, key, ANIMATION)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The shape `Cinematic/Options.lua:7106` builds: a group on a **Texture**, one Rotation
|
||||
/// animation, looping. A looping group keeps playing, which is what its caller reads.
|
||||
#[test]
|
||||
fn a_looping_group_on_a_texture_keeps_playing_until_stopped() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "AnimHost")
|
||||
tex = AnimHost:CreateTexture(nil, "OVERLAY")
|
||||
spin = tex:CreateAnimationGroup()
|
||||
turn = spin:CreateAnimation("Rotation")
|
||||
turn:SetDegrees(-360)
|
||||
turn:SetDuration(1)
|
||||
spin:SetLooping("REPEAT")
|
||||
spin:Play()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s.eval::<u32>("return spin:IsPlaying()").unwrap(), 1);
|
||||
assert_eq!(s.eval::<f64>("return turn:GetDegrees()").unwrap(), -360.0);
|
||||
assert_eq!(s.eval::<f64>("return spin:GetDuration()").unwrap(), 1.0);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return spin:GetLooping()").unwrap(),
|
||||
"REPEAT"
|
||||
);
|
||||
s.run("spin:Stop()").unwrap();
|
||||
assert!(s.eval::<bool>("return spin:IsPlaying() == nil").unwrap());
|
||||
}
|
||||
|
||||
/// A **one-shot** group finishes the moment it plays, and `OnFinished` fires — the property
|
||||
/// the header argues for, because an addon that chains its next step off `OnFinished` would
|
||||
/// otherwise wait forever.
|
||||
#[test]
|
||||
fn a_one_shot_group_finishes_at_once_so_a_chain_keeps_moving() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
log = ""
|
||||
f = CreateFrame("Frame", "FadeHost")
|
||||
fade = FadeHost:CreateAnimationGroup()
|
||||
a = fade:CreateAnimation("Alpha")
|
||||
a:SetFromAlpha(1) a:SetToAlpha(0) a:SetDuration(0.3)
|
||||
fade:SetScript("OnPlay", function() log = log .. "play," end)
|
||||
fade:SetScript("OnFinished", function() log = log .. "finished," end)
|
||||
fade:Play()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s.eval::<String>("return log").unwrap(), "play,finished,");
|
||||
assert!(s.eval::<bool>("return fade:IsPlaying() == nil").unwrap());
|
||||
assert!(s.eval::<bool>("return fade:IsDone() == 1").unwrap());
|
||||
// The setters round-trip, so an addon reading back its own numbers is not lied to.
|
||||
assert_eq!(s.eval::<f64>("return a:GetToAlpha()").unwrap(), 0.0);
|
||||
assert_eq!(s.eval::<f64>("return a:GetDuration()").unwrap(), 0.3);
|
||||
}
|
||||
|
||||
/// `GetAnimations` returns them all, and a named animation publishes its global, as upstream's
|
||||
/// constructors do — AceGUI-style code reaches for both.
|
||||
#[test]
|
||||
fn the_group_hands_back_its_animations_and_a_named_one_is_global() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "MultiHost")
|
||||
g = MultiHost:CreateAnimationGroup("MultiGroup")
|
||||
one = g:CreateAnimation("Alpha", "MultiAlpha")
|
||||
two = g:CreateAnimation("Scale")
|
||||
n = 0
|
||||
for _ in pairs({ g:GetAnimations() }) do n = n + 1 end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s.eval::<i64>("return n").unwrap(), 2);
|
||||
assert!(s
|
||||
.eval::<bool>("return MultiAlpha == one and MultiGroup == g")
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return g:GetObjectType()").unwrap(),
|
||||
"AnimationGroup"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return one:GetObjectType()").unwrap(),
|
||||
"Animation"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,18 @@ end
|
||||
/// An attribute value as [`ScriptValue`], so no Lua handle is held past the call. The four scalar
|
||||
/// kinds round-trip; anything else (a table, a function) stores as nil, since this crate keeps no
|
||||
/// reference to a Lua object and a half-stored table would read back wrong.
|
||||
fn attribute_value(v: &Value) -> ScriptValue {
|
||||
/// One attribute's value as the store keeps it.
|
||||
///
|
||||
/// A **widget** table is kept as its arena id, because the attribute that matters most in this
|
||||
/// family is `SecureActionButtonTemplate`'s `clickbutton`, which upstream is routinely a frame
|
||||
/// rather than a name — `Cinematic/MenuWheel.lua` sets it that way, and a dropped one is a wheel
|
||||
/// slot that clicks nothing at all. `ScriptValue::Frame` round-trips through
|
||||
/// `object::frame_wrapper`, so `GetAttribute` hands back the same wrapper the caller passed.
|
||||
///
|
||||
/// Any other table is still `Nil`: upstream's store holds arbitrary values, this one holds what
|
||||
/// [`ScriptValue`] can carry, and a plain data table has no representation here. Nothing in the
|
||||
/// corpus stores one.
|
||||
fn attribute_value(lua: &Lua, v: &Value) -> ScriptValue {
|
||||
match v {
|
||||
Value::Boolean(b) => ScriptValue::Bool(*b),
|
||||
Value::Integer(i) => ScriptValue::Int(*i),
|
||||
@@ -114,6 +125,13 @@ fn attribute_value(v: &Value) -> ScriptValue {
|
||||
.to_str()
|
||||
.map(|s| ScriptValue::Str(s.to_string()))
|
||||
.unwrap_or(ScriptValue::Nil),
|
||||
Value::Table(t) => crate::script::object::frame_handle_of(lua, t)
|
||||
.ok()
|
||||
.map(|h| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
ScriptValue::Frame(model.frame_id(h))
|
||||
})
|
||||
.unwrap_or(ScriptValue::Nil),
|
||||
_ => ScriptValue::Nil,
|
||||
}
|
||||
}
|
||||
@@ -121,7 +139,7 @@ fn attribute_value(v: &Value) -> ScriptValue {
|
||||
/// Run one delegate chunk against one method table. The chunk returns a function rather than
|
||||
/// reading `...`: this VM is the 1.12 client's Lua 5.0 (`script/lua50.rs`), where `...` is a
|
||||
/// parameter-list token and never an expression.
|
||||
fn delegate(lua: &Lua, key: &str, chunk: &str) -> mlua::Result<()> {
|
||||
pub(super) fn delegate(lua: &Lua, key: &str, chunk: &str) -> mlua::Result<()> {
|
||||
let m: Table = lua.named_registry_value(key)?;
|
||||
let install: mlua::Function = lua.load(chunk).call(())?;
|
||||
install.call::<()>(m)
|
||||
@@ -338,7 +356,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
name,
|
||||
lua.create_function(|lua, (this, key, value): (Table, String, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let value = attribute_value(&value);
|
||||
let value = attribute_value(lua, &value);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model
|
||||
.frame_attributes
|
||||
|
||||
@@ -32,6 +32,7 @@ use mlua::Lua;
|
||||
|
||||
mod action;
|
||||
mod addons;
|
||||
mod animation;
|
||||
mod callback_registry;
|
||||
mod chat_bubbles;
|
||||
mod chat_info;
|
||||
@@ -76,6 +77,7 @@ mod player_info;
|
||||
mod player_util;
|
||||
mod pools;
|
||||
mod quest_log;
|
||||
mod secure;
|
||||
mod spell;
|
||||
mod spellbook;
|
||||
mod state;
|
||||
@@ -131,6 +133,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set("C_Container", container::install(lua)?)?;
|
||||
g.set("C_CreatureInfo", creature_info::install(lua)?)?;
|
||||
g.set("C_CVar", cvar::install(lua)?)?;
|
||||
cvar::install_globals(lua)?;
|
||||
g.set("C_NamePlate", nameplate::install(lua)?)?;
|
||||
g.set("C_NewItems", new_items::install(lua)?)?;
|
||||
g.set("C_Macro", macro_ext::install(lua)?)?;
|
||||
@@ -178,6 +181,11 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
merchant_frame::install_mail_globals(lua)?;
|
||||
hooks::install(lua)?;
|
||||
frame::install(lua)?;
|
||||
// After `frame::install`: the secure-handler methods go onto the same frame method table and
|
||||
// use its `delegate`, and nothing in them is read at load time.
|
||||
secure::install(lua)?;
|
||||
// Likewise: `CreateAnimationGroup` goes onto the frame table and both region leaves.
|
||||
animation::install(lua)?;
|
||||
tooltip::install(lua)?;
|
||||
stdlib_backport::install(lua)?;
|
||||
string_methods::install(lua)?;
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
//! The secure-handler methods the `SecureHandler*Template` frames answer, which
|
||||
//! `assets/ui/CompatTemplates.xml` declares as empty frames.
|
||||
//!
|
||||
//! ## Why these are pass-throughs rather than a sandbox
|
||||
//!
|
||||
//! On a modern client a secure handler exists so a snippet may call *protected* functions from an
|
||||
//! environment the addon cannot taint. 1.12 has neither taint nor protected functions: any code may
|
||||
//! click anything, which is the whole reason `script/compat/classic_api/frame.rs` declined the
|
||||
//! secure-template family as "modern restricted execution, which 1.12 has no equivalent of". The
|
||||
//! conclusion drawn here is the other half of that sentence — the *restriction* has no equivalent,
|
||||
//! so emulating it buys nothing, while the **plumbing** (a named frame store, a snippet attached to
|
||||
//! a child's handler) is ordinary behaviour the snippets genuinely need. So the snippet body runs
|
||||
//! as plain Lua with the names the secure environment would have bound.
|
||||
//!
|
||||
//! What that does and does not get right:
|
||||
//!
|
||||
//! - A snippet reading `self`, `owner`, `button`, `down`, calling methods on either frame, reading
|
||||
//! and writing attributes, and returning `false` to suppress the wrapped handler: correct.
|
||||
//! - The per-header **shared table** a snippet writes a bare global into (`held = false` in
|
||||
//! `Cinematic/MenuWheel.lua:484`): correct, and per header, via `setfenv` onto one table per
|
||||
//! frame whose `__index` is `_G` so the rest of the API still resolves.
|
||||
//! - The **binding** half (`ClearBindings`, `SetBindingClick`, …): **no-ops**, because this client
|
||||
//! has no override-binding layer for them to write into. A wheel opened with the mouse works; the
|
||||
//! hold-a-key-and-release path does not, and that is the one piece of those two files still
|
||||
//! missing. Named here rather than silently absent so the next pass knows where to start.
|
||||
//! - The attribute-driven snippets (`_onshow`, `_onhide`, `_onattributechanged`): **not run**.
|
||||
//! `SetAttribute` is a plain store here, so nothing drives them. The only shipped use in this
|
||||
//! corpus is the `_onhide` above, whose body is the two lines already covered by the bullet
|
||||
//! before this one.
|
||||
//!
|
||||
//! A snippet that raises does so inside a handler, where the error is collected like any other
|
||||
//! rather than taking the frame down.
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::script::REG_FRAME_METHODS;
|
||||
|
||||
use super::frame::delegate;
|
||||
|
||||
/// The header methods, composed over the stock verbs plus one table per frame.
|
||||
///
|
||||
/// `loadstring` wraps the body in a named-parameter function rather than reading `...`: this VM is
|
||||
/// 1.12's Lua 5.0, where `...` is a parameter-list token and never an expression, so a chunk
|
||||
/// cannot read it. The parameters are then copied into the environment table as well, because a
|
||||
/// snippet refers to them as globals (`owner:GetFrameRef("area")`), not as arguments.
|
||||
const SECURE_HANDLER: &str = r#"
|
||||
return function(m)
|
||||
local function state(self)
|
||||
local s = self.__benilla_secure
|
||||
if not s then
|
||||
s = { refs = {}, env = setmetatable({}, { __index = getfenv(0) }) }
|
||||
self.__benilla_secure = s
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- A body compiled once, at WrapScript time, as the secure environment compiles it.
|
||||
local function snippet(body)
|
||||
if type(body) ~= "string" then return nil end
|
||||
local chunk = loadstring("return function() " .. body .. " end")
|
||||
if not chunk then return nil end
|
||||
local ok, fn = pcall(chunk)
|
||||
if ok then return fn end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function run(self, fn, this_frame, button, down)
|
||||
local s = state(self)
|
||||
local env = s.env
|
||||
env.self, env.owner = this_frame, self
|
||||
env.button, env.down = button, down
|
||||
setfenv(fn, env)
|
||||
local ok, kept = pcall(fn)
|
||||
if not ok then
|
||||
-- The snippet's own error, through the VM's handler as a handler's own error goes.
|
||||
local h = geterrorhandler and geterrorhandler()
|
||||
if h then h(kept) end
|
||||
return nil
|
||||
end
|
||||
return kept
|
||||
end
|
||||
|
||||
function m.SetFrameRef(self, name, frame)
|
||||
state(self).refs[name] = frame
|
||||
end
|
||||
|
||||
function m.GetFrameRef(self, name)
|
||||
return state(self).refs[name]
|
||||
end
|
||||
|
||||
function m.Execute(self, body)
|
||||
local fn = snippet(body)
|
||||
if fn then run(self, fn, self) end
|
||||
end
|
||||
|
||||
function m.WrapScript(self, child, handler, pre, post)
|
||||
local preFn, postFn = snippet(pre), snippet(post)
|
||||
local original = child:GetScript(handler)
|
||||
child:SetScript(handler, function()
|
||||
local button, down = arg1, arg2
|
||||
local allow = true
|
||||
if preFn and run(self, preFn, child, button, down) == false then allow = false end
|
||||
if allow and original then original() end
|
||||
if postFn then run(self, postFn, child, button, down) end
|
||||
end)
|
||||
end
|
||||
|
||||
function m.UnwrapScript(self, child, handler)
|
||||
child:SetScript(handler, nil)
|
||||
end
|
||||
|
||||
-- The binding half: no override-binding layer here to write into, so these answer rather than
|
||||
-- raise and the module header says what that costs.
|
||||
function m.ClearBindings(self) end
|
||||
function m.SetBinding(self, key, action) end
|
||||
function m.SetBindingClick(self, key, name, button) end
|
||||
function m.SetBindingSpell(self, key, spell) end
|
||||
function m.SetBindingItem(self, key, item) end
|
||||
function m.SetBindingMacro(self, key, macro) end
|
||||
function m.ClearBinding(self, key) end
|
||||
end
|
||||
"#;
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
delegate(lua, REG_FRAME_METHODS, SECURE_HANDLER)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The shape both of Cinematic's wheels are built on: a header frame, a child button, a
|
||||
/// snippet on the child's click that reaches the header's frame refs and can suppress the
|
||||
/// handler it wrapped.
|
||||
#[test]
|
||||
fn a_wrapped_snippet_sees_owner_and_refs_and_can_suppress_the_original() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
log = ""
|
||||
-- A plain frame: the header methods sit on the frame method table, so the
|
||||
-- `SecureHandler*Template` rows are only how an addon asks for them. The template's
|
||||
-- own test lives beside `assets/ui` in benilla-app, which is what loads it.
|
||||
header = CreateFrame("Frame", "SecHeader")
|
||||
area = CreateFrame("Frame", "SecArea", SecHeader)
|
||||
slot = CreateFrame("Button", "SecSlot", SecHeader)
|
||||
SecHeader:SetFrameRef("area", SecArea)
|
||||
SecSlot:SetScript("OnClick", function() log = log .. "original," end)
|
||||
-- The snippet's own effect has to be a method call: a bare assignment inside it is
|
||||
-- NOT a global — it lands in the header's shared table, asserted below.
|
||||
SecHeader:WrapScript(SecSlot, "OnClick", [[
|
||||
sawRef = owner:GetFrameRef("area") == SecArea
|
||||
sawSelf = self
|
||||
held = "kept"
|
||||
owner:Hide()
|
||||
]])
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.run("SecSlot:Click()").unwrap();
|
||||
// The snippet ran — its one externally visible act is `owner:Hide()` — and the handler it
|
||||
// wrapped still ran after it.
|
||||
assert!(
|
||||
!s.eval::<bool>("return SecHeader:IsShown() and true or false")
|
||||
.unwrap(),
|
||||
"the snippet's owner:Hide() took effect"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return log").unwrap(),
|
||||
"original,",
|
||||
"the wrapped handler still ran"
|
||||
);
|
||||
|
||||
// `self` is the wrapped child and `owner` the header, as the secure environment binds
|
||||
// them, and the ref came back through `owner`.
|
||||
assert!(s
|
||||
.eval::<bool>("return SecHeader.__benilla_secure.env.sawRef")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return SecHeader.__benilla_secure.env.sawSelf == SecSlot")
|
||||
.unwrap());
|
||||
|
||||
// A bare assignment inside a snippet lands in the header's own table, not in `_G`: the
|
||||
// per-header shared environment the secure one gives a snippet, which
|
||||
// `Cinematic/MenuWheel.lua` relies on (`held = false`).
|
||||
assert!(
|
||||
s.eval::<bool>("return held == nil").unwrap(),
|
||||
"not a global"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SecHeader.__benilla_secure.env.held")
|
||||
.unwrap(),
|
||||
"kept"
|
||||
);
|
||||
|
||||
// `false` from the snippet suppresses the handler it wrapped.
|
||||
s.run(
|
||||
r#"log = ""
|
||||
SecSlot:SetScript("OnClick", function() log = log .. "original," end)
|
||||
SecHeader:WrapScript(SecSlot, "OnClick", [[ return false ]])"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.run("SecSlot:Click()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return log").unwrap(),
|
||||
"",
|
||||
"returning false kept the wrapped handler from running"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,112 @@ const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[
|
||||
"ClassicAPI: the laid-out wrapped height, the other half of GetStringWidth's measure — \
|
||||
AceGUI-3.0's Label and CheckBox call it unguarded",
|
||||
),
|
||||
(
|
||||
"Execute",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): runs a snippet against the header's own shared environment. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"GetFrameRef",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): reads the named frame store. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"SetFrameRef",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): writes it; Cinematic's wheels pass their click-catcher this way (MenuWheel.lua:352). The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"WrapScript",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): attaches a snippet to a child's handler, which may suppress it by returning false (MenuWheel.lua:398). The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"UnwrapScript",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): the pair's other half. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"ClearBindings",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): the binding half, a NO-OP: no override-binding layer here to write into. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"ClearBinding",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"SetBinding",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"SetBindingClick",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"SetBindingSpell",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"SetBindingItem",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"SetBindingMacro",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
|
||||
family exists so an untainted snippet may call protected functions, and 1.12 has \
|
||||
neither taint nor protected functions — the restriction has no counterpart here, the \
|
||||
plumbing does",
|
||||
),
|
||||
(
|
||||
"CreateAnimationGroup",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame FontString Texture",
|
||||
"ClassicAPI (compat/classic_api/animation.rs, which states what it does and does not do): \
|
||||
the animation SURFACE with nothing animated — 1.12 has no animation system and this \
|
||||
client's UI pass has no interpolation to hang one on, so a non-looping group finishes \
|
||||
the moment it plays and a looping one keeps playing. What it buys is the constructor: a \
|
||||
nil method aborted Cinematic's whole options panel at Options.lua:7106, over one \
|
||||
spinner. On both region leaves too, because that spinner's group is on a Texture",
|
||||
),
|
||||
(
|
||||
"CanWordWrap",
|
||||
"FontString",
|
||||
|
||||
Reference in New Issue
Block a user