Files
benilla-octo/crates/benilla-ui/src/script/compat/classic_api/mod.rs
T
gromandClaude Opus 5 af823dd708 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
2026-11-01 17:12:13 -07:00

205 lines
8.6 KiB
Rust

//! ClassicAPI (<https://github.com/brues-code/ClassicAPI>) compatibility: 550+ functions and 50+
//! events across ~60 namespaces backported from modern WoW onto 1.12.1, plus a Lua 5.1 syntax
//! backport. That repo's own `docs/API.md` is the authoritative per-function spec (a point-in-time
//! names-only snapshot lives at `docs/classic-api-functions.md`, re-fetch upstream before
//! resuming); `docs/ADDON_COMPAT.md` tracks namespace-by-namespace status. One file per namespace
//! here, each a `pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()>` (a bare-global
//! namespace) or returning an `mlua::Table` (a `C_*` namespace), called from this file's own
//! `install`.
//!
//! **Both halves of the 5.1 backport are here, and both were maintainer calls:**
//!
//! * the *stdlib* half (`select`, `string.match`/`gmatch`/`reverse`, `math.fmod`/`modf`) in
//! `stdlib_backport.rs`, which hands back the originals `script/lua50.rs` stashed before
//! nil-ing them;
//! * the *syntax* half (`#`, `%`, `...` as an expression, `0x` hex, `[=[ ]=]`) in the Lua fork
//! itself, `third_party/lua-src/BENILLA.md`. It is **not** free, which this comment used to
//! claim: the fork had deliberately deleted the first three from 5.1's parser and 5.0-ified
//! `read_numeral` to match stock 1.12, and the realm's addons are written against a client
//! where ClassicAPI has put them back. SoftSpoken's nine load failures were all of them.
//!
//! Both are always-on, like everything in `compat/`, and both are the same trade: an addon that
//! feature-tests for a modern client now takes the modern branch. On this server, with ClassicAPI
//! loaded, that *is* the correct answer. `lua51_probe_fallout.rs` carries the two addons for
//! which it is not.
//!
//! Detection: [`CLASSIC_API_VERSION`] is set, and deliberately so — see its own comment for what
//! the number claims and why it is the number it is. Most functions here need no flag at all (an
//! addon checks `if FnName then` directly), but pfUI gates its *entire* load on this global, so
//! withholding it is not neutral: it disables the addon outright.
use mlua::Lua;
mod action;
mod addons;
mod animation;
mod callback_registry;
mod chat_bubbles;
mod chat_info;
mod class_color;
mod color_mixin;
mod color_util;
mod container;
mod creature_info;
mod cvar;
mod date_and_time;
mod encoding_util;
mod enums;
mod equipment_set;
mod event_util;
mod expansion;
mod faction;
mod focus;
pub(in crate::script) mod frame;
mod friend_list;
mod gossip;
mod hooks;
mod input;
mod item;
mod item_location;
mod item_mixin;
mod loot_history;
mod loss_of_control;
mod lua51_probe_fallout;
mod lua_ext;
mod macro_ext;
mod macro_icons;
mod map;
mod math_ext;
mod merchant_frame;
mod misc;
// `pub(in …compat)` for its `plate_frame`, which SuperWoW's `UnitNameplate` shares.
pub(in crate::script::compat) mod nameplate;
mod new_items;
mod pixel_util;
mod player_cache;
mod player_info;
mod player_util;
mod pools;
mod quest_log;
mod secure;
mod spell;
mod spellbook;
mod state;
mod stdlib_backport;
mod string_methods;
mod table_util;
mod taxi_map;
mod timer;
mod tooltip;
mod totem;
mod unit;
mod unit_auras;
mod unit_extra;
mod unit_menu;
/// `CLASSIC_API_VERSION`, packed `X*10000 + Y*100 + Z` as upstream packs it.
///
/// **What this number claims, and why it is this number.** It is not a version of benilla's code:
/// it is a claim about which ClassicAPI *API surface* is present, and real addons gate on it. pfUI
/// 9.0.25 refuses to load at all below `11304` (its `API_Check.lua`'s `PFUI_CLASSIC_API_MIN`), so
/// a lower number is not a humbler claim — it is "pfUI is disabled", which is strictly worse for a
/// player than a few missing functions that error where they are actually used.
///
/// So this says v1.13.4: the surface pfUI asks for. We do **not** implement every function of that
/// release (`docs/ADDON_COMPAT.md` has the per-namespace truth, and several namespaces are partial
/// or absent). An addon reaching for something unbuilt gets a clean `nil`-call error naming the
/// function, which is how the rest of this compat layer already behaves and is a far better
/// failure than a silent wrong answer. Raise this only alongside real coverage, and never to
/// quiet a specific addon without checking what else keys on it.
const CLASSIC_API_VERSION: i64 = 11304;
/// `INTERFACE_VERSION`: the client's own interface number, 1.12.1's `11200` — the `## Interface`
/// value a 1.12 `.toc` carries. Unrelated to [`CLASSIC_API_VERSION`] despite living beside it.
const INTERFACE_VERSION: i64 = 11200;
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
unit::install(lua)?;
unit_menu::install(lua)?;
unit_extra::install(lua)?;
focus::install(lua)?;
let g = lua.globals();
g.set("CLASSIC_API_VERSION", CLASSIC_API_VERSION)?;
g.set("INTERFACE_VERSION", INTERFACE_VERSION)?;
g.set("Enum", enums::install(lua)?)?;
g.set("C_Timer", timer::install(lua)?)?;
g.set("C_DateAndTime", date_and_time::install(lua)?)?;
g.set("C_ChatBubbles", chat_bubbles::install(lua)?)?;
g.set("C_ChatInfo", chat_info::install(lua)?)?;
g.set("C_EncodingUtil", encoding_util::install(lua)?)?;
g.set("C_EquipmentSet", equipment_set::install(lua)?)?;
g.set("C_LootHistory", loot_history::install(lua)?)?;
g.set("C_LossOfControl", loss_of_control::install(lua)?)?;
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)?)?;
g.set("C_Item", item::install(lua)?)?;
g.set("C_AddOns", addons::install(lua)?)?;
// `C_Spell`'s bare half hands out the very same function values the table holds, so the
// namespaced and bare spellings cannot drift — hence the table before the globals.
let c_spell = spell::install(lua)?;
spell::install_globals(lua, &c_spell)?;
g.set("C_Spell", c_spell)?;
g.set("C_SpellBook", spellbook::install(lua)?)?;
g.set("C_UnitAuras", unit_auras::install(lua)?)?;
g.set("C_ColorUtil", color_util::install(lua)?)?;
g.set("C_ClassColor", class_color::install(lua)?)?;
g.set("C_QuestLog", quest_log::install(lua)?)?;
g.set("C_Reputation", faction::install(lua)?)?;
g.set("C_FriendList", friend_list::install(lua)?)?;
g.set("C_GossipInfo", gossip::install(lua)?)?;
g.set("C_MerchantFrame", merchant_frame::install(lua)?)?;
g.set("C_Map", map::install(lua)?)?;
taxi_map::install(lua)?;
g.set("C_PlayerCache", player_cache::install(lua)?)?;
g.set("C_PlayerInfo", player_info::install(lua)?)?;
g.set("PlayerUtil", player_util::install(lua)?)?;
item::install_globals(lua)?;
macro_ext::install_globals(lua)?;
action::install(lua)?;
state::install(lua)?;
event_util::install(lua)?;
expansion::install(lua)?;
input::install(lua)?;
lua_ext::install(lua)?;
// After the frame and addon surfaces it probes (`CreateFrame`, `IsAddOnLoaded`).
lua51_probe_fallout::install(lua)?;
math_ext::install(lua)?;
// After `math_ext`: its `Round` is the rounding `PixelUtil.GetNearestPixelSize` does.
pixel_util::install(lua)?;
pools::install(lua)?;
totem::install(lua)?;
timer::install_globals(lua)?;
callback_registry::install(lua)?;
color_mixin::install(lua)?;
misc::install(lua)?;
faction::install_globals(lua)?;
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)?;
table_util::install(lua)?;
// After `item::install`: the mixin's `IsValid` calls `C_Item.DoesItemExist`. Only at *call*
// time, as `color_mixin.rs` explains for `CreateColor`, so the order is for the reader rather
// than for correctness.
item_location::install(lua)?;
item_mixin::install(lua)?;
// After `lua_ext::install`: the mixin chunk is a `CreateFromMixins` consumer and the picker
// constructs it through `CreateAndInitFromMixin`, both of which that file defines. Unlike the
// two above, this order is load-time, not call-time — `CreateAndInitFromMixin` is itself a
// chunk, so it must already be a global.
macro_icons::install(lua)?;
Ok(())
}