//! The FrameScript object model, frame side (RF-0023) — `CreateFrame`, the frame metatable + //! wrapper cache, and the shared frame method surface (decision 0068 v1). The region side lives in //! [`super::region`]; per-kind frame methods in their own modules ([`super::statusbar`], …), //! dispatched by [`kind_method_registries`] before the shared table. //! //! A frame's Lua value is a table `T` with `T[0] = lightuserdata(id)` (the id encoding stands in for //! RF-0023's `CScriptObject*`; see the module docs on ids) and one **shared** metatable whose //! `__index` is a Rust function that dispatches a method name to its implementation. Wrappers are //! cached in a Lua-side registry table keyed by id, so `GetParent()` (etc.) returns the *same* table //! every time (stable identity). Named frames auto-publish to `_G`, non-overwriting (RF-0023). Region //! leaves (`Texture`/`FontString`) use the same `T[0]` scheme but a *distinct* metatable (the "tag" //! that separates their method surface from frames'). //! //! The whole surface honors the MAXCSTACK discipline: the two metatables, the two method tables, and //! the wrapper cache live in the Lua registry (named keys); Rust holds none of them across calls. use std::ffi::c_void; use mlua::{LightUserData, Lua, ObjectLike, Table, Value}; use super::binding_abi::optional_string; use super::{Model, REG_FRAME_META, REG_FRAME_METHODS, REG_SCRIPTS, REG_WRAPPERS}; use crate::layout::Point; use crate::order::{DrawLayer, Strata}; use crate::widget::{FrameHandle, FrameKind}; // The frame method-table clusters — split out purely for size (each module's own doc says what // lives there); this file keeps the shared id/handle plumbing, `install`, and `CreateFrame`. mod events_regions; mod frame_state; mod layout_methods; pub(crate) use layout_methods::eff_scale; pub(crate) mod movable; pub(crate) mod toplevel; pub(crate) use layout_methods::{anchor_bits_eq, anchor_retarget_is_structural}; pub(crate) use movable::{advance_move, advance_size, FrameMove, FrameSizing}; // ───────────────────────────────────────────────────────────────────────────────────────────── // id ↔ lightuserdata // ───────────────────────────────────────────────────────────────────────────────────────────── pub(super) fn id_to_lud(id: u32) -> LightUserData { LightUserData(id as usize as *mut c_void) } /// Read the id out of a wrapper table's `T[0]` lightuserdata (RF-0023). pub(crate) fn decode_id(this: &Table) -> mlua::Result { match this.raw_get::(0)? { Value::LightUserData(l) => Ok(l.0 as usize as u32), _ => Err(mlua::Error::runtime( "not a benilla frame/region object (missing T[0] identity)", )), } } /// Resolve `self` (a frame wrapper) to its live [`FrameHandle`]. pub(super) fn frame_handle_of(lua: &Lua, this: &Table) -> mlua::Result { let id = decode_id(this)?; let model = lua.app_data_ref::().expect("model app_data"); model .id_to_frame .get(&id) .copied() .ok_or_else(|| mlua::Error::runtime("stale or invalid frame handle")) } // ───────────────────────────────────────────────────────────────────────────────────────────── // Wrapper cache (RF-0023: lazy bind + named _G publish, non-overwriting) // ───────────────────────────────────────────────────────────────────────────────────────────── /// Get-or-create the wrapper table for a frame id, caching it and (if the frame is named and `_G` /// has no such global yet) publishing it to `_G`. Callers must hold **no** model borrow (this takes /// a short one to read the frame's name). pub(super) fn frame_wrapper(lua: &Lua, id: u32) -> mlua::Result { let wrappers: Table = lua.named_registry_value(REG_WRAPPERS)?; if let Value::Table(t) = wrappers.get::(id)? { return Ok(t); } let t = lua.create_table()?; t.raw_set(0, Value::LightUserData(id_to_lud(id)))?; let meta: Table = lua.named_registry_value(REG_FRAME_META)?; t.set_metatable(Some(meta))?; wrappers.set(id, t.clone())?; let name: Option = { let model = lua.app_data_ref::().expect("model app_data"); model .id_to_frame .get(&id) .and_then(|h| model.arena.frame(*h)) .and_then(|f| f.name.clone()) }; if let Some(name) = name { publish_global(lua, &name, &t)?; } Ok(t) } /// Publish a wrapper to `_G[name]`, non-overwriting (RF-0023). pub(super) fn publish_global(lua: &Lua, name: &str, wrapper: &Table) -> mlua::Result<()> { let g = lua.globals(); if g.get::(name)?.is_nil() { g.set(name, wrapper.clone())?; } Ok(()) } // ───────────────────────────────────────────────────────────────────────────────────────────── // String → enum parsing (the client's tables, transcribed in order.rs / layout.rs) // ───────────────────────────────────────────────────────────────────────────────────────────── pub(super) fn point_from_str(s: &str) -> Option { Some(match s.to_ascii_uppercase().as_str() { "TOPLEFT" => Point::TopLeft, "TOP" => Point::Top, "TOPRIGHT" => Point::TopRight, "LEFT" => Point::Left, "CENTER" => Point::Center, "RIGHT" => Point::Right, "BOTTOMLEFT" => Point::BottomLeft, "BOTTOM" => Point::Bottom, "BOTTOMRIGHT" => Point::BottomRight, _ => return None, }) } /// The inverse of [`point_from_str`] (the client's point-id table `0x811a38`, names back out). /// /// `pub(super)` because the REGION cluster reports the same names from the same table — one /// spelling of the point set, not two (`region/layout.rs`'s `GetPoint`). pub(super) fn point_name(p: Point) -> &'static str { match p { Point::TopLeft => "TOPLEFT", Point::Top => "TOP", Point::TopRight => "TOPRIGHT", Point::Left => "LEFT", Point::Center => "CENTER", Point::Right => "RIGHT", Point::BottomLeft => "BOTTOMLEFT", Point::Bottom => "BOTTOM", Point::BottomRight => "BOTTOMRIGHT", } } /// Every closed-vocabulary attribute parser **trims** before it matches (decision 1204). /// /// Not tidiness: `zBar.xml:146` — a shipped, working 1.12 addon — declares /// `frameStrata="BACKGROUND "` with a trailing space, and the real client took it. Ours refused, /// the frame's whole `` subtree went with it, and the addon never loaded. Whitespace is /// never meaningful in an enum token, so trimming is both the lenient answer and the correct one; /// it is applied at the three parsers rather than at the attribute read, because a `text=` /// attribute's leading space IS meaningful and must not be touched. fn enum_token(s: &str) -> String { s.trim().to_ascii_uppercase() } /// The reference's strata NAME table (`0x8119f8`) has eight rows, `BACKGROUND`..`TOOLTIP`; stratum /// 0 (`WORLD`) has no name and no XML or Lua can put a frame there — the WorldFrame's constructor /// is its only writer (decision 1984, wow-re `worldframe-widget.md` §4). fn strata_from_str(s: &str) -> Option { Some(match enum_token(s).as_str() { "BACKGROUND" => Strata::Background, "LOW" => Strata::Low, "MEDIUM" => Strata::Medium, "HIGH" => Strata::High, "DIALOG" => Strata::Dialog, "FULLSCREEN" => Strata::Fullscreen, "FULLSCREEN_DIALOG" => Strata::FullscreenDialog, "TOOLTIP" => Strata::Tooltip, "BLIZZARD" => Strata::Blizzard, _ => return None, }) } pub(super) fn draw_layer_from_str(s: &str) -> Option { Some(match enum_token(s).as_str() { "BACKGROUND" => DrawLayer::Background, "BORDER" => DrawLayer::Border, "ARTWORK" => DrawLayer::Artwork, "OVERLAY" => DrawLayer::Overlay, "HIGHLIGHT" => DrawLayer::Highlight, _ => return None, }) } /// The inverse of [`draw_layer_from_str`] — the token `GetDrawLayer` answers, in the client's own /// spelling (uppercase, the five `` names). pub(super) fn draw_layer_name(l: DrawLayer) -> &'static str { match l { DrawLayer::Background => "BACKGROUND", DrawLayer::Border => "BORDER", DrawLayer::Artwork => "ARTWORK", DrawLayer::Overlay => "OVERLAY", DrawLayer::Highlight => "HIGHLIGHT", } } /// A widget tag or `CreateFrame` type string → its [`FrameKind`], case- and separator-insensitive /// ([`enum_token`]). Public because it is the ONE mapping: `benilla-app`'s `frame_flag_gate` sweep /// needs it to ask the engine whether a tag is mouse-enabled by construction, and a second copy /// there is exactly what drifted. pub fn frame_kind_from_tag(s: &str) -> Option { frame_kind_from_str(s) } fn frame_kind_from_str(s: &str) -> Option { Some(match enum_token(s).as_str() { "FRAME" => FrameKind::Frame, // The world frame's own registered type (decision 1983; `0x495948` in the registration // batch, the one row passing `1` as its third argument). "WORLDFRAME" => FrameKind::WorldFrame, // `TaxiRouteFrame` — a registered `CreateFrame` type that is a `CSimpleFrame` and NOTHING // else, so it maps to `Frame` rather than earning a kind (decision 1828; wow-re // `ui/scratch/taxiroute-widget-type.md`). Factory `0x495ba0` allocates `0x314`, the same // size the plain-`` factory `0x6eec10` allocates for the same base ctor `0x769090`, // and the ctor `0x506950` adds no field. Its vtables are the base's length exactly (36 + 11 // slots), so it declares no new virtual; it overrides four slots, of which two are lifetime // plumbing and the other two — `EmitLayer 0x506a60` and a scale hook `0x506ac0` — are DEAD // in 5875: the paint callback `0x506a90` tail-calls `0x506aa0`, which is a bare `ret`, and // every module global it would fill is written by nothing. // // Mapping it to `Frame` is therefore the faithful answer, not a 1203 shortcut — and it is // faithful in the strong sense that `GetObjectType()` must return **"Frame"** and // `IsObjectType("TaxiRouteFrame")` must be **false**: `0x484800` is not overridden, so the // name lives only in the 22-entry tag→factory registry and never becomes a class identity. // The flight-path lines are plain `` children the FrameXML creates and rotates // with the 8-argument `SetTexCoord` we already support; `TaxiRouteFrame.cpp` is a vestigial // C++ renderer that draws nothing. "TAXIROUTEFRAME" => FrameKind::Frame, "BUTTON" => FrameKind::Button, "CHECKBUTTON" => FrameKind::CheckButton, // A real registered type (`0x4959a6`), not an alias for Button — decision 1799. Registering // it AS a Button would load `LootFrame.xml` and leave every row dead, because 1.12 has no // Lua verb for "take slot N": `LootSlot` is the bind-confirm continuation, hard-wired to // `flag = 1`. "LOOTBUTTON" => FrameKind::LootButton, "EDITBOX" => FrameKind::EditBox, "STATUSBAR" => FrameKind::StatusBar, "SLIDER" => FrameKind::Slider, "SCROLLFRAME" => FrameKind::ScrollFrame, "MODEL" => FrameKind::Model, "PLAYERMODEL" => FrameKind::PlayerModel, "DRESSUPMODEL" => FrameKind::DressUpModel, "TABARDMODEL" => FrameKind::TabardModel, "MESSAGEFRAME" => FrameKind::MessageFrame, "SCROLLINGMESSAGEFRAME" => FrameKind::ScrollingMessageFrame, "COLORSELECT" => FrameKind::ColorSelect, "SIMPLEHTML" => FrameKind::SimpleHtml, "MOVIEFRAME" => FrameKind::MovieFrame, "GAMETOOLTIP" => FrameKind::GameTooltip, "MINIMAP" => FrameKind::Minimap, "COOLDOWN" => FrameKind::Cooldown, _ => return None, }) } /// A Lua number-ish → f32 (nil/other → 0.0), for offset/color args. pub(super) fn as_f32(v: &Value) -> f32 { match v { Value::Number(n) => *n as f32, Value::Integer(i) => *i as f32, _ => 0.0, } } // ───────────────────────────────────────────────────────────────────────────────────────────── // install — build the method tables, metatables, CreateFrame, and registry roots // ───────────────────────────────────────────────────────────────────────────────────────────── pub(super) fn install(lua: &Lua) -> mlua::Result<()> { // The wrapper cache + the per-frame script tables (Lua-side roots — the MAXCSTACK discipline). lua.set_named_registry_value(REG_WRAPPERS, lua.create_table()?)?; lua.set_named_registry_value(REG_SCRIPTS, lua.create_table()?)?; install_frame_methods(lua)?; super::region::install(lua)?; super::statusbar::install(lua)?; super::button::install(lua)?; super::editbox::install(lua)?; // AFTER every table above exists: collapse the 19 Region-map names to one shared // implementation each, so a method pulled off a frame works on a texture (decision 1501). super::region_map::install(lua)?; // Shared frame metatable: __index is a Rust dispatcher over the frame method table (RF-0023), // checking the frame's *kind-specific* method table first. Per-kind resolution matters beyond // correctness: addons duck-type widgets (`if frame.SetValue then …`), so a plain frame must // resolve `SetValue` to nil — one shared table would make every frame quack like every widget. let frame_meta = lua.create_table()?; let frame_index = lua.create_function(|lua, (this, key): (Table, Value)| { for reg in kind_method_registries(lua, &this) { let methods: Table = lua.named_registry_value(reg)?; let v = methods.get::(key.clone())?; if !v.is_nil() { return Ok(v); } } let methods: Table = lua.named_registry_value(REG_FRAME_METHODS)?; methods.get::(key) })?; frame_meta.set("__index", frame_index)?; lua.set_named_registry_value(REG_FRAME_META, frame_meta.clone())?; // RF-0023 publishes the shared metatable as _G["__framescript_meta"]. lua.globals().set("__framescript_meta", frame_meta)?; // CreateFrame(kind, name?, parent?, inherits?) — a global (RF-0024's factory is the loader's; // this is the runtime one). `inherits` (4th) applies a registered template to the frame this // call just made, through `crate::loader::apply_template`. let create_frame = lua.create_function(create_frame)?; lua.globals().set("CreateFrame", create_frame)?; // GetScreenWidth/Height — the screen-root rect in UI units (the ref's tooltip side-pick reads // them: ContainerFrameItemButton_OnEnter anchors LEFT when the slot sits in the right half). lua.globals().set( "GetScreenWidth", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model"); Ok(model.screen.width()) })?, )?; lua.globals().set( "GetScreenHeight", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model"); Ok(model.screen.height()) })?, )?; // SetupFullscreenScale(frame) — the registered binding `0x48c270` the stock WorldMapFrame.xml, // CinematicFrame.xml and UIOptionsFrame.xml call from OnShow (decision 1980; wow-re // `system/ui/scratch/setup-fullscreen-scale.md`, VERIFIED at the bytes): the frame's scale // becomes `min(0.75 · a, 1.0)` for the CONFIGURED aspect `a` — the `gxResolution` width over // height (the `widescreen` CVar's default of 1 selects it; at 0 the aspect is 4:3 and the // scale 1) — and nothing else is written: no anchor, size or position, and `UIParent`'s // scale never enters. It is the same `CSimpleFrame::SetScale` the Lua method calls. The // window's aspect is the configured one here (the UI-unit rect keeps the pixel ratio); NaN // takes the 1.0 leg as the reference's `fcomp` does. The three raises are the binding's own. lua.globals().set( "SetupFullscreenScale", lua.create_function(|lua, frame: Value| { let Value::Table(frame) = frame else { return Err(mlua::Error::runtime("Usage: SetupFullscreenScale(frame)")); }; if decode_id(&frame).is_err() { return Err(mlua::Error::runtime( "SetupFullscreenScale(): Couldn't find 'this' in frame object", )); } if frame_handle_of(lua, &frame).is_err() { return Err(mlua::Error::runtime( "SetupFullscreenScale(): Wrong object type, expected frame", )); } let scale = { let model = lua.app_data_ref::().expect("model"); fullscreen_scale(model.screen.width() / model.screen.height()) }; frame.call_method::<()>("SetScale", scale) })?, )?; // GetCursorPosition() → x, y — the last cursor position the host fed (`mouse_move`/ // `mouse_button`), UI units y-up like every other coordinate read — the SCREEN's units, which // is what the reference hands Lua too; a caller inside a scaled frame divides by its // `GetEffectiveScale()` (the reference's own `MouseIsOver` does, and the stock world map at // a scale under 1 is the case that made the division load-bearing here — decision 1985). The // world map polls this every OnUpdate for hover/click math. lua.globals().set( "GetCursorPosition", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model"); Ok(model.cursor_pos) })?, )?; // The modifier-key mirror ([`UiScript::set_modifiers`], fed by the app's input pass before // any mouse event each frame) — the reference handlers fork on these at click time // (ContainerFrame.lua's shift-split / ctrl-dressup, ActionBarFrame.xml's shift-pickup, // SpellBookFrame.lua's shift-pickup). Era booleans, not 1.12's 1/nil — the house API target // (decision 0068); every transcribed `if IsShiftKeyDown()` reads both identically. for (name, pick) in [ ("IsShiftKeyDown", 0usize), ("IsControlKeyDown", 1), ("IsAltKeyDown", 2), ] { lua.globals().set( name, lua.create_function(move |lua, ()| { let model = lua.app_data_ref::().expect("model"); let m = [model.modifiers.0, model.modifiers.1, model.modifiers.2]; Ok(m[pick]) })?, )?; } Ok(()) } /// The registry keys of a frame's kind-specific method tables, in resolution order — the `__index` /// dispatcher walks them before the shared table, mirroring the client's class chain (CheckButton /// resolves through Button's map, RF-28). Empty for plain kinds (and for anything that isn't a live /// frame, e.g. a region wrapper passing through). fn kind_method_registries(lua: &Lua, this: &Table) -> &'static [&'static str] { let Ok(id) = decode_id(this) else { return &[] }; let model = lua.app_data_ref::().expect("model app_data"); let Some(h) = model.id_to_frame.get(&id) else { return &[]; }; match model.arena.frame(*h).map(|f| f.kind) { Some(FrameKind::StatusBar) => &[super::statusbar::REG_STATUSBAR_METHODS], Some(FrameKind::EditBox) => &[super::editbox::REG_EDITBOX_METHODS], Some(FrameKind::ScrollingMessageFrame) => { &[super::messageframe::REG_SCROLLINGMESSAGEFRAME_METHODS] } // Its SIBLING, with its own table — not a fallthrough to the scrolling one. Until this arm // existed a `` resolved no kind-specific method at all, which is what blocked // `EasyCopy`, `QuestHistory` and `QuestItem` on `UIErrorsFrame:AddMessage` (three of the // corpus's six method-class blockers) and made `SetInsertMode` unreachable. Some(FrameKind::MessageFrame) => &[super::messageframe::REG_MESSAGEFRAME_METHODS], Some(FrameKind::ScrollFrame) => &[super::scrollframe::REG_SCROLLFRAME_METHODS], Some(FrameKind::SimpleHtml) => &[super::simplehtml::REG_SIMPLEHTML_METHODS], Some(FrameKind::Slider) => &[super::slider::REG_SLIDER_METHODS], Some(FrameKind::ColorSelect) => &[super::colorselect::REG_COLORSELECT_METHODS], Some(FrameKind::Button) => &[super::button::REG_BUTTON_METHODS], // One method of its own, then all of Button's — the miss order of `0x4c1be0`, which probes // its own 1-entry map `0xb71b64` and tail-calls `Button`'s `0x782c90`. Some(FrameKind::LootButton) => &[ super::loot::REG_LOOTBUTTON_METHODS, super::button::REG_BUTTON_METHODS, ], Some(FrameKind::CheckButton) => &[ super::button::REG_CHECKBUTTON_METHODS, super::button::REG_BUTTON_METHODS, ], // The Model pane's own surface. Its absence is what stopped pfUI — and, through pfUI's // embedded copies, pfQuest, pfQuest-turtle and ShaguDPS — on `SetModel` after the whole // rest of their UI had already built (`super::modelframe`'s header). Some(FrameKind::Model) => &[super::modelframe::REG_MODEL_METHODS], // The chain, not a repetition: `CGCharacterModelBase`'s lookup `0x506260` probes its own // 3-entry map and on a miss tail-calls `CSimpleModel`'s `0x76f870`. Order is the miss // order — three names of its own, then all 23 of `Model`'s. Some(FrameKind::PlayerModel) => &[ super::modelframe::REG_PLAYERMODEL_METHODS, super::modelframe::REG_MODEL_METHODS, ], // Three of its own, then PlayerModel's three, then Model's 23: `CGDressUpModelFrame`'s // lookup probes `0x84f190` and misses into `CGCharacterModelBase`'s `0x506260`, which // misses into `CSimpleModel`'s `0x76f870` (1969). Some(FrameKind::DressUpModel) => &[ super::dressup::REG_DRESSUPMODEL_METHODS, super::modelframe::REG_PLAYERMODEL_METHODS, super::modelframe::REG_MODEL_METHODS, ], // Ten of its own (`0x84ee40`), then PlayerModel's three, then Model's 23 — the same // derived → base probe as its sibling (1977). Some(FrameKind::TabardModel) => &[ super::tabard::REG_TABARDMODEL_METHODS, super::modelframe::REG_PLAYERMODEL_METHODS, super::modelframe::REG_MODEL_METHODS, ], Some(FrameKind::Minimap) => &[super::minimap::REG_MINIMAP_METHODS], Some(FrameKind::Cooldown) => &[super::cooldown::REG_COOLDOWN_METHODS], Some(FrameKind::GameTooltip) => &[super::tooltip::REG_TOOLTIP_METHODS], _ => &[], } } /// `CreateFrame(kind, name?, parent?, inherits?)` — the runtime frame factory. /// /// The fourth argument is a **template name list**, and honouring it is what makes /// `CreateFrame("Button", "MyButton", UIParent, "UIPanelButtonTemplate")` — the corpus's most /// common single line — produce a button with art, regions, scripts and a fired `OnLoad` rather /// than an empty frame. The frame is created first and decorated second /// ([`crate::loader::apply_template`]), in that order and never the other way: the template's own /// `OnLoad` must be able to see the frame's real name, and `$parent` inside the template must /// resolve against **the caller's** name, not the template's. /// /// An **unresolvable** template name raises `CreateFrame(): Couldn't find inherited node "%s"` and /// creates nothing — the reference's own behaviour, byte-verified (`0x7061dd` → `luaL_error`, which /// never returns). A template that resolves but is *unusable in some other way* — declared without /// `virtual="true"`, or of a shape that does not fit the kind asked for — is still a warning plus a /// working frame, because the registry lookup itself succeeded and that is the only thing the miss /// branch tests. pub(super) fn create_frame( lua: &Lua, (kind, name, parent, inherits): (String, Option, Option, Option), ) -> mlua::Result
{ let frame_kind = frame_kind_from_str(&kind) .ok_or_else(|| mlua::Error::runtime(format!("CreateFrame: unknown frame type '{kind}'")))?; // The WorldFrame's registry record is a ONE-SHOT: the reference unlinks and releases it the // moment the first `` is instantiated (`0x6ee439`), so a second one — from any // XML, or `CreateFrame("WorldFrame")` — takes the lookup's miss leg, `Unknown frame type` // (decision 1984). The loader reaches this through the same global, so it covers both. if frame_kind == FrameKind::WorldFrame && lua .app_data_ref::() .expect("model app_data") .world_frame_made { return Err(mlua::Error::runtime(format!( "CreateFrame: unknown frame type '{kind}'" ))); } // **`name` and `inherits` are `lua_tostring` positions, and a NUMBER is a string to it.** // `0x7060b0` reads both through `0x6f3690` with no type guard at all, so `CreateFrame("Frame", // 5)` names the frame `"5"` — a `Value::String`-only match drops it (wow-re // `ui/scratch/xml-template-name-lookup.md` §5.2). // // The fourth argument's quirk, which is real and reads like a bug in the client: `lua_tostring` // runs at `0x70613f` **before** the `cmp 4` gate, and `0x6f7cb1 mov dword ptr [esi],0x4` // retags the number's stack slot **in place** — so by the time the tag is tested it IS a // string. `CreateFrame("Frame", nil, nil, 5)` therefore raises `Couldn't find inherited node // "5"`, while `f:CreateTexture(nil, nil, 5)` silently ignores the 5, because the region // constructors ask `lua_type` first and never coerce. Same value, opposite outcome, decided by // argument order alone — which is why this is spelled out rather than shared with them. let name: Option = name.as_ref().and_then(|v| optional_string(lua, v)); let template: Option = inherits.as_ref().and_then(|v| optional_string(lua, v)); // Resolve the parent handle (a wrapper table, or a name string) under a short read borrow. // // **The name-string arm is OURS, and the reference RAISES on it** — `0x7060b0` asks // `lua_type` three times and `cmp eax,5`: a table is the parent, nil/absent means no parent, // and a string takes the error leg (`xml-template-name-lookup.md` §5.2's per-position table). // // It is **parked, not pruned**, on the same footing as `TEXTURE_ONLY_METHODS`' tail three: the // §5 established *that* it raises but did not quote the message, and inventing the text of a // client error is exactly what decision 1721 was written about — a plausible string in the // voice of a finding. Measured meanwhile: **zero** call sites pass a string parent, in the // 109-folder corpus or in our own FrameXML, so the superset is unreachable in practice and // removing it blind would buy nothing. Pruning it needs the message, not more confidence. let parent_handle: Option = { let model = lua.app_data_ref::().expect("model app_data"); match &parent { Some(Value::Table(t)) => decode_id(t) .ok() .and_then(|id| model.id_to_frame.get(&id).copied()), Some(Value::String(s)) => s.to_str().ok().and_then(|n| model.arena.lookup(n.as_ref())), _ => None, } }; // A 4th argument that is present but neither a string nor a number is **silently ignored by // the reference** — `lua_tostring` returns NULL for it and the `cmp 4` gate then falls into // the ordinary construction path. So this is a benilla-only diagnostic and stays a WARNING, // never a raise: it tells a reader something was dropped without changing what runs. if template.is_none() { if let Some(v) = inherits.as_ref().filter(|v| !v.is_nil()) { let mut model = lua.app_data_mut::().expect("model app_data"); model.warnings.push(format!( "CreateFrame: the 4th argument (inherits) must be a template-name string, got {}; \ ignored for '{}'", v.type_name(), name.as_deref().unwrap_or("") )); } } // ── THE TEMPLATE LOOKUP HAPPENS HERE — before anything is constructed or published ────────── // // `0x7061dd` calls the registry lookup `0x6ee6f0` and, on NULL, falls into // `luaL_error(L, "CreateFrame(): Couldn't find inherited node \"%s\"")` at `0x7061ed`. That // never returns — `luaG_errormsg`/`luaD_throw` contain no `ret` at all and end in `longjmp`, so // the `xor eax,eax; ret` after the call is dead code. The frame is **not created**, no global // is published, and the calling statement is abandoned; the error is ordinary Lua propagation // and `pcall`-catchable, which is how the widget dispatcher (`0x704f10`, every handler under // `lua_pcall`) turns it into one dead handler rather than a dead client. // // We used to warn and hand back a usable bare frame, and the doc above said the reference // "creates the frame anyway". That claim is refuted at the bytes. It is the same class as 1249: // a failure the client reports and we swallowed — and swallowing is the worse half, because the // addon then runs on a frame with none of the art, regions or scripts it asked for and fails // later, somewhere unrelated. // // Ordering is the carved part, not a detail: the miss branch precedes the synthetic-node build // (`0x706208`) and the `name` store (`0x70622d`), so a miss leaves no partial widget and no // orphan global behind. Doing this after construction would leave both. if let Some(t) = template.as_deref().filter(|t| !t.is_empty()) { let known = { let model = lua.app_data_ref::().expect("model app_data"); let templates = model.framexml_templates.borrow(); // One name, verbatim, folded — the law `framexml::expand` resolves under. templates.contains_key(t) || templates.keys().any(|k| k.eq_ignore_ascii_case(t)) }; if !known { return Err(mlua::Error::runtime(format!( "CreateFrame(): Couldn't find inherited node \"{t}\"" ))); } } // Create in the arena, mint the id, seed a default layout input. All under one write borrow. let id = { let mut model = lua.app_data_mut::().expect("model app_data"); let h = model.arena.create(frame_kind, name, parent_handle); if frame_kind == FrameKind::WorldFrame { model.world_frame_made = true; } // The client's CreateFrame inheritance (the ctor doc's "loader/CreateFrame concern" — // widget/mod.rs `create`): a child enters its PARENT's stratum at the parent's level + 1. // The ctor's bare MEDIUM/0 left a DIALOG-strata popup drawing its own translucent // backdrop OVER its buttons (the children stayed in MEDIUM — the abandon dialog's dark // buttons, pixel-diffed against the raw art). An explicit frameStrata/frameLevel attr // still overrides right after (apply_attrs). if let Some(p) = parent_handle { if let Some((pstrata, plevel)) = model.arena.frame(p).map(|f| (f.strata, f.level)) { model.arena.set_frame_strata(h, pstrata); model.arena.set_frame_level(h, plevel + 1, false); } } let id = model.frame_id(h); model.layout_inputs.entry(h).or_default(); id }; // The wrapper exists (and, if named, is published to `_G`) BEFORE the template runs — a // template's `OnLoad` fires inside this call, and the reference's addons rely on both facts: // `local b = CreateFrame(...)` hands back a frame whose OnLoad has already run, and that // handler can reach itself by name. let wrapper = frame_wrapper(lua, id)?; if let Some(template) = template { let messages = crate::loader::apply_template(lua, &wrapper, &kind, &template); if !messages.is_empty() { let mut model = lua.app_data_mut::().expect("model app_data"); model.warnings.extend(messages); } } Ok(wrapper) } // ───────────────────────────────────────────────────────────────────────────────────────────── // Frame method surface — the table itself is built by the three clusters above // ───────────────────────────────────────────────────────────────────────────────────────────── /// Build the shared frame method table from the five clusters ([`frame_state`], [`layout_methods`], /// [`events_regions`], [`movable`], [`toplevel`]) and publish it to the registry. fn install_frame_methods(lua: &Lua) -> mlua::Result<()> { let m = lua.create_table()?; frame_state::install(lua, &m)?; layout_methods::install(lua, &m)?; events_regions::install(lua, &m)?; movable::install(lua, &m)?; toplevel::install(lua, &m)?; lua.set_named_registry_value(REG_FRAME_METHODS, m)?; Ok(()) } /// `0x48c270`'s law: `g = 0.75 · aspect`, and the scale is `g` below 1.0, else 1.0 — NaN lands on /// the 1.0 leg (the `fcomp` compare fails every ordered test). pub(crate) fn fullscreen_scale(aspect: f32) -> f32 { let g = 0.75 * aspect; if g < 1.0 { g } else { 1.0 } } #[cfg(test)] mod fullscreen_scale_tests { use super::fullscreen_scale; use crate::script::UiScript; #[test] fn the_scale_is_three_quarters_of_the_aspect_capped_at_one() { assert_eq!(fullscreen_scale(4.0 / 3.0), 1.0); assert_eq!(fullscreen_scale(16.0 / 9.0), 1.0); assert!((fullscreen_scale(5.0 / 4.0) - 0.9375).abs() < 1e-6); assert_eq!(fullscreen_scale(f32::NAN), 1.0); } #[test] fn the_verb_scales_the_frame_and_raises_its_three_strings() { let mut s = UiScript::new().unwrap(); s.set_screen_size(1280.0, 1024.0); s.run(r#"f = CreateFrame("Frame", "FS") SetupFullscreenScale(f)"#) .unwrap(); assert!((s.eval::("return f:GetScale()").unwrap() - 0.9375).abs() < 1e-6); s.set_screen_size(1600.0, 900.0); s.run("SetupFullscreenScale(f)").unwrap(); assert_eq!(s.eval::("return f:GetScale()").unwrap(), 1.0); for (call, needle) in [ ( "SetupFullscreenScale()", "Usage: SetupFullscreenScale(frame)", ), ( "SetupFullscreenScale(7)", "Usage: SetupFullscreenScale(frame)", ), ( "SetupFullscreenScale({})", "Couldn't find 'this' in frame object", ), ( "SetupFullscreenScale(f:CreateTexture())", "Wrong object type, expected frame", ), ] { let err = s.run(call).unwrap_err().to_string(); assert!(err.contains(needle), "{call}: {err}"); } } }