Files
benilla-octo/crates/benilla-ui/src/script/tests/widget_surface.rs
T
gromandClaude Opus 5 bb2e40d1dd ui: GetDebugName, the name an unnamed frame still answers
`Cinematic/Frames.lua:3734` is `entry.name or entry.frame:GetDebugName()` — an addon that keeps a
list of frames reaches for this precisely because some of them have no name, which is the one case
`GetName` cannot serve. With the method absent, `/cine debug frame <name>` raised on the first
unnamed row in the list instead of printing the row the player asked for.

On all three method tables, since the era puts it on ScriptObject. A named frame answers its name,
so a feature test on it learns nothing a 1.12 client would answer differently. The unnamed leg
deviates in its fallback only: it walks the parent's own fields for the key the child is stored
under (frames here are plain tables, so `parent.icon = tex` is visible) and answers
`Parent.<unnamed>` when there is none, where the era answers an index among the parent's children —
an index here would be the order of a list this client does not keep, and a wrong number reads
worse than an honest absence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 14:17:44 -07:00

839 lines
43 KiB
Rust

//! The widget-method surface gate, the widget sibling of [`super::reference_surface`]. The
//! reference's 23 widget registrar tables are disjoint, so a class's surface is its own table plus
//! those it inherits ([`WIDGET_CHAINS`]): `0x87c9b8` is the base `Region` map (the 19
//! `REGION_MAP_METHODS`), and `LootButton`'s own `0x847ce4` holds only `SetSlot`.
use std::collections::{BTreeMap, BTreeSet};
use crate::script::{widget_method_census, UiScript};
/// `(class, its own registrar table, the tables it inherits)`. `WorldFrame` is a `Frame` to Lua
/// and `TitleRegion` a bare `Region`; like `TaxiRouteFrame`, neither has a table of its own.
const WIDGET_CHAINS: &[(&str, &str, &[&str])] = &[
("Frame", FRAME, &[REGION]),
("WorldFrame", FRAME, &[REGION]),
("Button", BUTTON, FRAME_CHAIN),
("LootButton", "0x847ce4", &[BUTTON, FRAME, REGION]),
("CheckButton", "0x87bf74", &[BUTTON, FRAME, REGION]),
("EditBox", "0x87bb68", FRAME_CHAIN),
("StatusBar", "0x87b010", FRAME_CHAIN),
("Slider", "0x87b260", FRAME_CHAIN),
("ScrollFrame", "0x87b3c0", FRAME_CHAIN),
("Model", MODEL, FRAME_CHAIN),
("PlayerModel", PLAYER_MODEL, &[MODEL, FRAME, REGION]),
(
"DressUpModel",
"0x84f190",
&[PLAYER_MODEL, MODEL, FRAME, REGION],
),
(
"TabardModel",
"0x84ee40",
&[PLAYER_MODEL, MODEL, FRAME, REGION],
),
("MessageFrame", "0x87b960", FRAME_CHAIN),
("ScrollingMessageFrame", "0x87b5c0", FRAME_CHAIN),
("ColorSelect", "0x87abb0", FRAME_CHAIN),
("SimpleHTML", "0x87ba80", FRAME_CHAIN),
("MovieFrame", "0x87ab4c", FRAME_CHAIN),
("GameTooltip", "0x854198", FRAME_CHAIN),
("Minimap", "0x84c538", FRAME_CHAIN),
("Texture", "0x87c128", &[REGION]),
("FontString", "0x87c1d8", &[REGION]),
// The font object (`CreateFont`) inherits nothing.
("Font", "0x87c7c8", &[]),
("TitleRegion", REGION, &[]),
];
const REGION: &str = "0x87c9b8";
const FRAME: &str = "0x878ec0";
const BUTTON: &str = "0x879d00";
const MODEL: &str = "0x878948";
const PLAYER_MODEL: &str = "0x84f1fc";
const FRAME_CHAIN: &[&str] = &[FRAME, REGION];
/// Reference methods this client does not answer yet: one row per name, its classes and why. The
/// list only shrinks; a name with live callers belongs implemented.
const NOT_YET_ANSWERED: &[(&str, &str, &str)] = &[
// ── The MovieFrame trio ──
// The stock glue `MovieFrame.lua` calls all three; benilla plays no movie yet.
(
"StartMovie",
"MovieFrame",
"the intro-movie screen's own verb; benilla plays no movie yet",
),
("StopMovie", "MovieFrame", "the same, on the skip path"),
(
"EnableSubtitles",
"MovieFrame",
"the same — `MovieFrame.lua:35` passes `GetMovieSubtitles()`",
),
// ── The Minimap's four content setters ──
// The app draws the minimap's blips and arrow itself, never through Lua.
(
"SetArrowModel",
"Minimap",
"app-side art; no caller in the stock UI or either corpus",
),
("SetBlipTexture", "Minimap", "the same"),
("SetIconTexture", "Minimap", "the same"),
("SetPlayerModel", "Minimap", "the same"),
// ── The Button font-object and text-colour family ──
// `button.rs` keeps this font state but publishes none of it; nothing calls these.
(
"GetDisabledFontObject",
"Button CheckButton LootButton",
"the button font-state family; no caller anywhere measured",
),
(
"GetDisabledTextColor",
"Button CheckButton LootButton",
"the same family",
),
(
"GetHighlightFontObject",
"Button CheckButton LootButton",
"the same family",
),
(
"GetHighlightTextColor",
"Button CheckButton LootButton",
"the same family",
),
(
"GetTextFontObject",
"Button CheckButton LootButton",
"the same family",
),
(
"GetPushedTextOffset",
"Button CheckButton LootButton",
"the same family",
),
(
"SetPushedTextOffset",
"Button CheckButton LootButton",
"the same family",
),
// ── Line spacing, on the five classes that carry text ──
(
"GetSpacing",
"EditBox Font MessageFrame ScrollingMessageFrame",
"line spacing is not modelled by `ui_text`'s layout. FontString LEFT THIS LIST: a caller \
was measured (DirectedByRobertBWeide `:101`), and the reason given here — that the \
setter is a layout change rather than a stored value — was the wrong trade. It is both, \
and the half this client can honour is the storing half: the pair round-trips, the \
layout still ignores it, and the addon stops aborting mid-initialisation. The four \
classes left are unmeasured, so they stay",
),
(
"SetSpacing",
"EditBox Font MessageFrame ScrollingMessageFrame",
"the same — see `GetSpacing` above for why FontString is no longer here",
),
// ── EditBox mode read-backs ──
(
"IsAutoFocus",
"EditBox",
"the mode is set and not readable back; no caller anywhere measured",
),
("IsMultiLine", "EditBox", "the same"),
("IsNumeric", "EditBox", "the same"),
("IsPassword", "EditBox", "the same"),
// ── ScrollingMessageFrame's line-position surface ──
(
"GetCurrentLine",
"ScrollingMessageFrame",
"the buffer's position is internal; no caller measured",
),
("GetCurrentScroll", "ScrollingMessageFrame", "the same"),
("GetNumLinesDisplayed", "ScrollingMessageFrame", "the same"),
("SetScrollFromBottom", "ScrollingMessageFrame", "the same"),
// ── The colour picker's HSV pair ──
(
"GetColorHSV",
"ColorSelect",
"the RGB pair is published; nothing measured asks for HSV",
),
("SetColorHSV", "ColorSelect", "the same"),
// ── One texture read-back ──
(
"IsDesaturated",
"Texture",
"`SetDesaturated` is here and not readable back; no caller measured",
),
];
/// Methods answered here that 1.12 does not register on that class, each with why it stays.
const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[
(
"GetItem",
"GameTooltip",
"ClassicAPI: what item this tooltip is showing, off TooltipState::subject",
),
(
"GetSpell",
"GameTooltip",
"ClassicAPI: what spell this tooltip is showing, off TooltipState::subject",
),
(
"HasItem",
"GameTooltip",
"ClassicAPI: whether the subject is an item",
),
(
"HasSpell",
"GameTooltip",
"ClassicAPI: whether the subject is a spell",
),
(
"GetUnit",
"GameTooltip",
"ClassicAPI: the display name and token SetUnit was given",
),
(
"HasUnit",
"GameTooltip",
"ClassicAPI: whether SetUnit left a token",
),
(
"GetUnitGUID",
"GameTooltip",
"ClassicAPI: the name and guid of the unit SetUnit saw, off the new TooltipState::unit_guid — a set-time fact, never a re-resolution of the token, which is what makes it answerable at all (classic_api/tooltip.rs's header). pfUI's modules/tooltip.lua:81 reads the second value and feeds UnitTokenFromGUID",
),
(
"GetGameObject",
"GameTooltip",
"ClassicAPI: the name and OBJECT_FIELD_ENTRY of the world object the plate was built for, off TooltipSubject::GameObject — a set-time fact recorded by world_tooltip_gameobject, the same mechanism GetUnitGUID uses and for the same reason. pfQuest's map.lua:296 reads the second value and keys its object node index by it; its absence was 236 raises in one session, because its reader hangs off a frame parented to GameTooltip and so fires on every tooltip",
),
(
"HasGameObject",
"GameTooltip",
"ClassicAPI: whether the plate is showing a world object, the GetGameObject sibling Has* every other subject already has",
),
(
"SetSpellByID",
"GameTooltip",
"ClassicAPI: fill the plate from a spell id alone. The one Set*ByID the family's \"needs an argument-shape check first\" note was lifted for, and the check is why it is allowed rather than removed: tooltip_spell::set_spell_by_id is the body all four of 1.12's own spell verbs funnel through and it already takes a bare id, so this adds a door onto existing behaviour and no behaviour of its own. SuperCleveRoidMacros/Core.lua:4217 reaches it from pfUI's ButtonEnter for a spell:<id> macro naming a spell in neither spellbook; its absence was 7 raises in one session and 7 action buttons with no tooltip at all",
),
(
"GetOwner",
"GameTooltip",
"ClassicAPI: the frame SetOwner was given, which the stock tooltip already keeps",
),
(
"IsEquippedItem",
"GameTooltip",
"ClassicAPI: whether the shown item is worn, over inventory_slots",
),
(
"SetUnitAura",
"GameTooltip",
"ClassicAPI: the modern spelling of 1.12's own SetUnitBuff/SetUnitDebuff pair, picked by the filter argument instead of by the verb name, and forwarding to them through the same method table so they stay the single implementation. HARMFUL anywhere in the filter selects debuffs, everything else (and an absent filter) helpful, which is modern's own default and makes the composite spellings like \"HELPFUL|PLAYER\" land correctly. pfUI's raid and group frames tooltip every buff icon through it (api/unitframes.lua:67), so its absence raised on every buff hover",
),
(
"SetHyperlinkCompareItem",
"GameTooltip",
"ClassicAPI: fill this shopping tooltip with what is WORN in the slot the link's item would take. Not a new mechanism — 1.12's own SetMerchantCompareItem and SetAuctionCompareItem already drive this exact chain, over the same index-into-candidate-slots grammar and the same CURRENTLY_EQUIPPED latch; this is that body reached from a link instead of a list row (tooltip_item/mod.rs's compare_link_against_worn). pfUI's modules/eqcompare.lua:21,24,72 hooks every Set*Item verb on GameTooltip, so its absence was 12 raises in one session, one per item hover",
),
(
"ClearAttribute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: removes one key from the same per-frame store",
),
(
"GetAttribute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: reads the same per-frame store",
),
(
"GetEffectiveAlpha",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: the arena's propagated effective_alpha, where the stock GetAlpha is the frame's own factor",
),
(
"GetRect",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI: left, bottom, width, height from the four stock getters",
),
(
"GetSize",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI (classic_api/frame.rs): the width/height pair, composed from the stock GetWidth/GetHeight",
),
(
"HookScript",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: chains a handler after whatever GetScript answers, through the stock SetScript",
),
(
"IsEventRegistered",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: reads the frame_events set RegisterEvent already fills; all-events mode answers for anything",
),
(
"SetAttribute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: a per-frame key/value store, which is all this is stripped of secure semantics; the secure drivers are NOT built (see classic_api/frame.rs)",
),
(
"SetAttributeNoHandler",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: identical to SetAttribute here — it differs only in skipping the secure OnAttributeChanged handler, which does not exist",
),
(
"SetFormattedText",
"FontString",
"ClassicAPI: SetText(format(...)), composed from the stock SetText",
),
(
"GetStringHeight",
"FontString",
"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",
),
(
"SetNormalFontObject",
"Button CheckButton LootButton",
"ClassicAPI: the later-era spelling of 1.12's SetTextFontObject (0x780400) — the one name \
this family left behind, since SetHighlightFontObject (0x780700) and \
SetDisabledFontObject (0x780580) are 1.12's own. Hands out the same function value, so \
the two spellings cannot drift. Ace3 calls it unguarded on every button it builds \
(AceConfigDialog-3.0.lua:586)",
),
(
"IsMouseOver",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame FontString Texture",
"2.0 moved FrameXML's own `MouseIsOver(region, ...)` onto the widget. 1.12 has the \
computation and not the method, so this is a method spelling of a function the player's \
chain already defines — written out rather than forwarded because the argument order \
differs and because a frame may be asked before UIParent.lua has run. On all three \
tables because 2.0 put it on Region, and the body reads nothing a region lacks",
),
(
"CreateLine",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"A `Line` region: two endpoints and a thickness, drawn as the quad between them. 1.12 has \
no line primitive and nothing stands in for one — a line between two MOVING points is a \
rotated quad whose angle and length both change every frame, which no rect-anchored \
texture expresses. Called unguarded at both sites that need it: a flight route \
(Map/Stitch.lua:717) and the map's drawing tool (Map/Sketch.lua:377). Built over the \
stock CreateTexture on the same table, so a line is an ordinary Texture in every respect \
but its geometry, which is what the callers rely on — they paint it with \
SetColorTexture and SetVertexColor like any texture",
),
(
"SetStartPoint",
"Texture",
"A Line's first end. NOT SetPoint: the point names a place on the TARGET and nothing on \
the line, because a line has ends rather than edges. On the Texture table because a Line \
is a Texture here (RegionData::line), so a plain Texture answers it inertly — the same \
sharing the title region already has with the texture methods",
),
(
"SetEndPoint",
"Texture",
"The Line's other end, see SetStartPoint",
),
(
"SetThickness",
"Texture",
"The Line's width in px; floored at 1, the reference's hairline, because a non-finite or \
negative width makes the quad's corners meaningless",
),
(
"GetThickness",
"Texture",
"The read half, so the pair round-trips; 1 by default",
),
(
"AddMaskTexture",
"Texture",
"The map canvas: 1.12 has no texture masking at all, and nothing can stand in for it \
because the point is that a RECTANGULAR texture draws as a non-rectangular shape. A \
seamless world map is that operation and nothing else — a zone's parchment cut to the \
zone's coastline — so without it every zone draws as a paper rectangle over its \
neighbours' oceans and labels. Called unguarded where it is used: Map/Close.lua:162",
),
(
"RemoveMaskTexture",
"Texture",
"The other half of AddMaskTexture, so a caller that swaps a mask can take the old one \
off; without it a re-masked tile keeps the first cut forever",
),
(
"CreateMaskTexture",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"The region AddMaskTexture takes: a Texture that shapes others and never draws itself. \
Built over the stock CreateTexture on the same table, so a mask is an ordinary Texture \
in every other respect. Map/Close.lua:159 builds one before any texture can ask for it",
),
(
"SetRotation",
"Texture",
"1.12 registers SetRotation on PlayerModel alone (0x84f1fc), so a Texture there cannot \
turn. The renderer has carried the angle since the minimap arrow, which the host draws; \
this is the Lua setter for it. The map's own player arrow and its compass rail need it",
),
(
"GetRotation",
"Texture",
"The read half of SetRotation, so the pair round-trips; 0 by default",
),
(
"SetDesaturation",
"Texture",
"The later-era FRACTIONAL form of 1.12's own SetDesaturated. This renderer greys \
all-or-nothing (one material flag, not a blend weight), so a fraction is taken as on \
past the midpoint — an approximation, and the honest one available, since the only use \
of it in the corpus is dimming art the caller has already decided to dim \
(Map/Close.lua desaturates minimap tiles by a fixed amount so the painted map reads \
over them)",
),
(
"GetDesaturation",
"Texture",
"The read half, answering the caller's own fraction rather than the rounded flag",
),
(
"SetHorizTile",
"Texture",
"Repeat the art across the region at its own texel size. 1.12 reaches the same end with \
SetTexCoord(0, n, 0, 1) — UVs past the texture, which is the idiom this renderer keys \
its wrap mode on — but has no switch meaning 'however many times it takes', because \
that count is rect/texels and 1.12's Lua is never told the texel size. Applied: the \
host computes the count once the art has loaded, and an explicit SetTexCoord wins",
),
(
"SetVertTile",
"Texture",
"As SetHorizTile, for the other axis",
),
(
"GetHorizTile",
"Texture",
"The read half of SetHorizTile; false by default",
),
(
"GetVertTile",
"Texture",
"The read half of SetVertTile; false by default",
),
(
"HasFocus",
"EditBox",
"1.12's EditBox table has SetFocus and ClearFocus and no getter at all, so an addon of \
that era keeps its own flag. The fact is already tracked here (Model::focused_editbox, \
the client's own DAT_00cf4dc8) and was simply never published. Called unguarded inside \
an OnEditFocusLost callback (Map/Nav.lua:374), so a nil method raised every time a \
search box lost focus",
),
(
"SetCursorPosition",
"EditBox",
"1.12 has no cursor setter at all — the caret moves only by typing, clicking or \
HighlightText. Lands through HighlightText's own clamp and UTF-8 snap with the selection \
collapsed, so there is one definition of where a caret may sit. Map/News.lua:282 calls \
it to scroll a long read-only link box back to its start",
),
(
"SetPropagateKeyboardInput",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"4.x's switch for whether a frame with an OnKeyDown handler lets the key reach the next \
frame and the binding system. STORED, NOT APPLIED, and the reason is that 1.12's answer \
is always-propagate: its key routing hands the key to the binding system regardless of \
what a handler did, so there is no swallow mode for false to select. Refusing it costs a \
caller its whole key path — Map/Sketch.lua:579 arms a catcher frame and flips the flag \
inside its own handler, so a nil method raises once per keypress",
),
(
"GetPropagateKeyboardInput",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"The read half, so the pair round-trips; true by default, which is what this client does \
unconditionally",
),
(
"CanWordWrap",
"FontString",
"ClassicAPI: the read half of SetWordWrap, so the pair round-trips. True by default, \
which is what this client's layout actually does",
),
(
"SetFixedFrameLevel",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: 9.0 told the frame manager to stop re-deciding a frame's level. There is no \
such manager here — level is whatever SetFrameLevel last wrote and nothing rewrites it — \
so the guarantee already holds and the no-op is the honest answer. Ace3 calls it \
unguarded at AceConfigDialog-3.0.lua:576",
),
(
"SetFixedFrameStrata",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: as SetFixedFrameLevel, for strata. Ace3, AceConfigDialog-3.0.lua:575",
),
(
"SetWordWrap",
"FontString",
"ClassicAPI: 3.x's whether-to-wrap switch, which 1.12 has no spelling for (its \
SetNonSpaceWrap decides WHERE a wrap may fall, not whether one happens). Stored, not \
applied, like SetSpacing and for the same reason — the layout wraps to the declared \
width unconditionally — and kept because the missing setter cost Cinematic its whole \
options panel at Options.lua:4130",
),
(
"RegisterUnitEvent",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: the stock registration narrowed to named units — same list, same order, and \
tick.rs's walk skips a frame whose row for the event does not name arg1. Addons written \
for it do not guard it: Cinematic/Frames.lua:2611 registers UNIT_AURA for \"player\" at \
file scope, so the nil method stopped the file dead. On every frame kind rather than \
Frame alone because event registration lives on the one shared frame method table, which \
is where 1.12 keeps RegisterEvent too — the two are registered alike or neither is",
),
(
"SetColorTexture",
"Texture",
"ClassicAPI: the modern spelling of SetTexture's colour arm, which 1.12 already has \
(0x770360's 8x8 solid surface; region/paint.rs defaults the alpha to 1 as it does). \
Retail split the two names when SetTexture stopped taking a colour, so every Era-era \
addon writes this one — Cinematic/Options.lua:701 paints its list border with it and \
died at session start without it, and OpenMapClassic its map chrome",
),
(
"GetDebugName",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip \
LootButton MessageFrame Minimap Model MovieFrame ScrollFrame ScrollingMessageFrame \
SimpleHTML Slider StatusBar TabardModel Texture WorldFrame PlayerModel",
"ClassicAPI: the era's \"what is this frame called\" for a log line or an error message, \
and the one that answers for an UNNAMED frame, which GetName cannot. An addon keeping a \
list of frames reaches for it precisely because some of them have no name — \
Cinematic/Frames.lua:3734 is `entry.name or entry.frame:GetDebugName()`, so /cine debug \
frame <name> raised on the first unnamed row instead of printing the one asked for. \
Named frames answer their name, so a feature test on it learns nothing a 1.12 client \
would answer differently; the unnamed leg deviates in its fallback only (Parent.key by \
walking the parent's fields, else Parent.<unnamed>, never the era's child index — a \
wrong number reads worse than an honest absence)",
),
(
"SetEnabled",
"Button CheckButton EditBox LootButton",
"ClassicAPI: retail replaced 1.12's Enable/Disable pair (0x780160, 0x7801b0) with the one \
setter, and later-era addons write it unguarded on their OWN controls — \
Cinematic/Options.lua:4176 is a tick's own Refresh, and the raise there unwound \
RefreshOptions -> SettingsChanged -> SetProfile, so picking a profile left the options \
panel half-refreshed with a traceback in chat. Composed from the stock pair on Button's \
table, which CheckButton and LootButton inherit. EditBox has no pair to compose (the \
reference gives a box no enabled state at all), so there it writes the mouse and \
keyboard and drops focus — input refused, text NOT greyed, which 1.12 has no slot for. \
A Slider still answers neither this nor the trio: addons duck-type on it to tell a \
control from a bar (tests/slider.rs)",
),
(
"SetShown",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI: the Show/Hide pair as one call; frame_state.rs notes 1.12 registers no SetShown. On the two region leaves it is NOT that composition — they have no Show/Hide to compose from (the reference's Region map is geometry and identity only), so classic_api/frame.rs writes the same hidden flag region.rs's own Show/Hide do. pfUI calls it on a FontString: f.group:SetShown(...), api/unitframes.lua:582",
),
(
"SetSize",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI: the width/height pair, composed from the stock SetWidth/SetHeight",
),
];
/// The reference's widget rows, `table_va -> {name}`.
fn reference_tables() -> BTreeMap<String, BTreeSet<String>> {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-shapes.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv");
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for line in text.lines() {
if line.starts_with('#') || line.starts_with("name\t") {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 5 || f[4] != "widget" {
continue;
}
out.entry(f[3].to_string())
.or_default()
.insert(f[0].to_string());
}
out
}
/// `class -> the effective reference surface` (its own table and everything it inherits).
fn reference_surface_by_class() -> BTreeMap<&'static str, BTreeSet<String>> {
let tables = reference_tables();
assert!(
tables.len() == 23,
"the shapes table gave {} widget registrar tables, not the reference's 23 — it changed \
shape, and every count below is against the wrong denominator",
tables.len()
);
let mut out = BTreeMap::new();
for (class, own, bases) in WIDGET_CHAINS {
let mut set = tables.get(*own).cloned().unwrap_or_default();
for base in *bases {
set.extend(tables.get(*base).cloned().unwrap_or_default());
}
assert!(!set.is_empty(), "{class}: empty reference surface");
out.insert(*class, set);
}
out
}
/// `class -> what this VM answers`, asked of a live instance of each.
fn ours_by_class() -> BTreeMap<String, BTreeSet<String>> {
let script = UiScript::new().expect("VM");
let rows = widget_method_census(&script).expect("census");
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (class, method) in rows {
assert_ne!(
method, "!NOT-INSTANTIABLE",
"{class} could not be instantiated — the census cannot speak for a class it never made"
);
out.entry(class).or_default().insert(method);
}
out
}
/// A declared list, `name -> (classes, reason)`.
fn declared(list: &[(&str, &str, &str)]) -> BTreeMap<String, BTreeSet<String>> {
list.iter()
.map(|(name, classes, _)| {
(
(*name).to_string(),
classes.split_whitespace().map(str::to_string).collect(),
)
})
.collect()
}
/// Render a `class -> names` map as declared-list rows, ready to paste in.
fn as_rows(diff: &BTreeMap<String, BTreeSet<String>>) -> String {
let mut by_name: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
for (class, names) in diff {
for n in names {
by_name.entry(n).or_default().push(class);
}
}
by_name
.iter()
.map(|(name, classes)| {
format!(
" (\"{name}\", \"{}\", \"WHY\"),",
classes
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(" ")
)
})
.collect::<Vec<_>>()
.join("\n")
}
/// Every widget method the 1.12 client registers on a class is answered by ours, or listed.
#[test]
fn every_1_12_widget_method_is_answered() {
let reference = reference_surface_by_class();
let ours = ours_by_class();
let listed = declared(NOT_YET_ANSWERED);
let mut missing: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut total = 0usize;
for (class, want) in &reference {
let have = ours.get(*class).cloned().unwrap_or_default();
total += want.len();
let gap: BTreeSet<String> = want
.iter()
.filter(|n| !have.contains(*n))
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(*class)))
.cloned()
.collect();
if !gap.is_empty() {
missing.insert((*class).to_string(), gap);
}
}
assert!(
total > 2_000,
"only {total} class/method pairs on the reference side — the chain stopped resolving"
);
assert!(
missing.is_empty(),
"the 1.12 client registers these on a class benilla does not answer them on, and they are \
not listed:\n{}\n\nImplement it, or add the row to NOT_YET_ANSWERED with the reason it \
has not been built.",
as_rows(&missing)
);
// A listed name that is answered now fails, so the list cannot outlive its fix.
let mut healed: Vec<String> = Vec::new();
for (name, classes, _) in NOT_YET_ANSWERED {
for class in classes.split_whitespace() {
if ours.get(class).is_some_and(|h| h.contains(*name)) {
healed.push(format!("{class}:{name}"));
}
}
}
assert!(
healed.is_empty(),
"these are answered now — take them out of NOT_YET_ANSWERED so the list keeps meaning what \
it says: {healed:?}"
);
}
/// Every widget method benilla answers is one 1.12 registers on that class, or a listed exception.
#[test]
fn our_widget_methods_stay_inside_the_1_12_surface() {
let reference = reference_surface_by_class();
let ours = ours_by_class();
let listed = declared(ALLOWED_BEYOND);
let mut beyond: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (class, have) in &ours {
let Some(want) = reference.get(class.as_str()) else {
panic!("{class} is instantiable here but has no row in WIDGET_CHAINS");
};
let extra: BTreeSet<String> = have
.iter()
.filter(|n| !want.contains(*n))
// Our host bridge and VM internals are marked by prefix, as `reference_surface` does.
.filter(|n| !n.starts_with("Benilla") && !n.starts_with("__benilla_"))
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(class)))
.cloned()
.collect();
if !extra.is_empty() {
beyond.insert(class.clone(), extra);
}
}
assert!(
beyond.is_empty(),
"benilla answers these on a class the 1.12 client does not register them on:\n{}\n\n\
1.12 is the target. Either remove it, give it its 1.12 spelling, or add \
the row to ALLOWED_BEYOND WITH the reason it has to stay — an addon that \
feature-detects an unexplained superset takes a path we cannot \
honour.",
as_rows(&beyond)
);
let mut gone: Vec<String> = Vec::new();
for (name, classes, _) in ALLOWED_BEYOND {
for class in classes.split_whitespace() {
if !ours.get(class).is_some_and(|h| h.contains(*name)) {
gone.push(format!("{class}:{name}"));
}
}
}
assert!(
gone.is_empty(),
"these are no longer exposed — take them out of ALLOWED_BEYOND: {gone:?}"
);
}