Comments in benilla-ui: one line of fact each, no history, no record numbers (#143)
Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one. - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`. - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale. - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent. - A StatusBar fills from its internal normalised fraction, not from `GetValue()`. - The addresses of `StartDuel` and `StartDuelUnit` were swapped. - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default. - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference. Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8505187668
commit
fd386e75ae
@@ -1,19 +1,10 @@
|
||||
//! The FrameScript object model, frame side — `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.
|
||||
//! The FrameScript object model, frame side: `CreateFrame`, the frame wrappers and metatables, and
|
||||
//! the shared frame methods; the region side is [`super::region`].
|
||||
//!
|
||||
//! A frame's Lua value is a table `T` with `T[0] = lightuserdata(id)` (the id encoding stands in for
|
||||
//! the client's `CScriptObject*`; `0x701bd0` writes `T[0] = lightuserdata(this)` — 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 (`0x701bd0`). 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.
|
||||
//! A frame's Lua value is a table `T` with `T[0] = lightuserdata(id)` (`0x701bd0`), cached by id
|
||||
//! and published to `_G` by name without overwriting; its metatable is its kind's
|
||||
//! ([`frame_meta_for`]). Every table lives in the Lua registry under a named key, and Rust holds
|
||||
//! none across calls (the MAXCSTACK discipline).
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
@@ -25,8 +16,7 @@ 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`.
|
||||
// The frame method clusters, split out for size.
|
||||
pub(crate) mod anchor_args;
|
||||
mod events_regions;
|
||||
mod frame_state;
|
||||
@@ -67,12 +57,11 @@ pub(super) fn frame_handle_of(lua: &Lua, this: &Table) -> mlua::Result<FrameHand
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Wrapper cache (`0x701bd0`: lazy bind + named _G publish, non-overwriting)
|
||||
// Wrapper cache (`0x701bd0`)
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 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).
|
||||
/// The wrapper table for a frame id, created and cached on first use and published to `_G` under
|
||||
/// the frame's name if that global is free. Call it with no model borrow alive.
|
||||
pub(super) fn frame_wrapper(lua: &Lua, id: u32) -> mlua::Result<Table> {
|
||||
let wrappers: Table = lua.named_registry_value(REG_WRAPPERS)?;
|
||||
if let Value::Table(t) = wrappers.get::<Value>(id)? {
|
||||
@@ -80,12 +69,8 @@ pub(super) fn frame_wrapper(lua: &Lua, id: u32) -> mlua::Result<Table> {
|
||||
}
|
||||
let t = lua.create_table()?;
|
||||
t.raw_set(0, Value::LightUserData(id_to_lud(id)))?;
|
||||
// The frame's kind and name in one borrow. **The KIND picks the metatable, here rather than
|
||||
// per lookup** — the region side's shape since its leaves split, and decision 2310's whole
|
||||
// change on this side: a wrapper is built once per frame, so resolving the class chain at
|
||||
// construction costs nothing, while resolving it inside `__index` put a Rust call, a model
|
||||
// borrow and a registry lookup in front of EVERY widget method access in the client. A frame's
|
||||
// kind never changes and `id_to_frame` never loses an entry, so the choice cannot go stale.
|
||||
// The kind picks the metatable once, here: a frame's kind never changes and `id_to_frame`
|
||||
// never loses an entry, so the choice cannot go stale.
|
||||
let (kind, name): (Option<FrameKind>, Option<String>) = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let frame = model
|
||||
@@ -111,44 +96,21 @@ pub(super) fn publish_global(lua: &Lua, name: &str, wrapper: &Table) -> mlua::Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A widget **name string** argument, already looked up in `_G` — every Lua binding that takes
|
||||
/// "a frame or its name" resolves one of these ([`prefetch_named_target`] then
|
||||
/// [`resolve_named_target`]).
|
||||
///
|
||||
/// **The client has ONE widget namespace and it is the Lua globals table.** The wrapper binder
|
||||
/// `0x701bd0` publishes `_G[name] = T` for a named widget (non-overwriting, via `lua_settable`),
|
||||
/// and every by-name resolver reads that table back — `0x76c760` (globals index → Lua type 5 →
|
||||
/// **raw** `t[0]` → `lua_touserdata` → `IsA`; `SetParent`'s string arm `0x7a1550`, `SetScrollChild`
|
||||
/// `0x790fa0`, the XML `parent=` chain) and the layout-vtable `+0x28` entry `0x76c700`, which
|
||||
/// expands a leading `$parent` (`0x76c5b0`) and then calls `0x76c760`. There is no engine-side
|
||||
/// name→frame map behind either: for frames and regions a name's only effects are the copy into
|
||||
/// `[widget+0x98]` and that `_G` publish. (`CSimpleFont` names are the one exception — they key a
|
||||
/// Storm hash at `0xcf4e78` — and fonts are not anchor targets.) Full addresses:
|
||||
/// `0x76c5b0`/`0x76c700`/`0x76c760`/`0x7a2540`.
|
||||
///
|
||||
/// So the name is not a *frame name* — it is a **global**, and a frame's published name is only the
|
||||
/// commonest way one comes to exist. Resolving against a private registry instead makes an alias
|
||||
/// invisible: `Bar8Button1 = CharacterBag3Slot` at an addon's file scope is a global that is no
|
||||
/// frame's name, and Bartender2 anchors every bar's buttons through exactly those (decision 2105).
|
||||
///
|
||||
/// ## Why the lookup is its own phase
|
||||
///
|
||||
/// The reference's read is **`lua_gettable 0x6f3a40` → `luaV_index 0x6f7cf0`**, not `lua_rawget`
|
||||
/// (the binary holds both, 0xc0 apart; only the former walks `__index`, MAXTAGLOOP 100). So a
|
||||
/// metatable on `_G` is honoured — and a `__index` can be a *Lua* function that calls straight back
|
||||
/// into a widget binding. Running that under a live `Model` guard would take a second borrow and
|
||||
/// panic, so the `_G` read is done with **no guard alive** and the decode happens after.
|
||||
/// A widget name argument already looked up in `_G` ([`prefetch_named_target`], then
|
||||
/// [`resolve_named_target`]). The globals table is the client's only widget namespace (`0x701bd0`
|
||||
/// publishes there; `0x76c760` and the anchor resolver `0x76c700` read back), so a global alias
|
||||
/// of a frame (`Bar8Button1 = CharacterBag3Slot`) resolves like its name. The read is
|
||||
/// `lua_gettable` (`0x6f3a40` → `0x6f7cf0`), which honours an `__index` on `_G` that may call back
|
||||
/// into a widget binding, so it runs with no `Model` borrow alive and the decode comes after.
|
||||
pub(crate) struct NamedTarget {
|
||||
/// The name as the client looked it up — after `$parent` expansion where the caller allows it.
|
||||
/// This is the spelling a diagnostic must quote.
|
||||
/// The name as looked up, after any `$parent` expansion: the spelling a diagnostic quotes.
|
||||
pub(crate) name: String,
|
||||
/// `_G[name]`, kept only when it is a table (the Lua-type-5 gate). Identity is checked later.
|
||||
/// `_G[name]` if it is a table (Lua type 5); its identity is decoded later.
|
||||
wrapper: Option<Table>,
|
||||
}
|
||||
|
||||
impl NamedTarget {
|
||||
/// A name argument we could not even read (a non-UTF-8 Lua string) — it names nothing, and the
|
||||
/// caller's miss path quotes this placeholder the way it always has.
|
||||
/// A name that is not UTF-8: it names nothing, and the miss path quotes `<non-utf8>`.
|
||||
pub(crate) fn unreadable() -> Self {
|
||||
Self {
|
||||
name: "<non-utf8>".into(),
|
||||
@@ -157,14 +119,11 @@ impl NamedTarget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `_G[name]` for a name-string argument. **Call with no `Model` borrow alive** — see
|
||||
/// [`NamedTarget`].
|
||||
///
|
||||
/// `parent_base` is the name a leading `$parent` expands to, or `None` for the bindings that do not
|
||||
/// expand it. `0x76c5b0` has exactly two call sites — `SetName 0x76c691` and `0x76c71c` inside the
|
||||
/// layout resolver `0x76c700` — so `SetPoint`/`SetAllPoints`' `relativeTo` **is** expanded at
|
||||
/// runtime, and `SetParent`, `SetScrollChild` and the XML `parent=` chain (which call `0x76c760`
|
||||
/// directly) are **not**.
|
||||
/// Read `_G[name]` for a name argument, with no `Model` borrow alive. `parent_base` is what a
|
||||
/// leading `$parent` expands to, `None` where the binding does not expand: `0x76c5b0` is called
|
||||
/// only from `SetName` (`0x76c691`) and the anchor resolver (`0x76c71c` in `0x76c700`), so
|
||||
/// `SetPoint`/`SetAllPoints`' `relativeTo` expands, and `SetParent`, `SetScrollChild` and XML
|
||||
/// `parent=`, which call `0x76c760` directly, do not.
|
||||
pub(crate) fn prefetch_named_target(
|
||||
lua: &Lua,
|
||||
name: &str,
|
||||
@@ -181,23 +140,17 @@ pub(crate) fn prefetch_named_target(
|
||||
NamedTarget { name, wrapper }
|
||||
}
|
||||
|
||||
/// Decode a [`NamedTarget`] to the stable id of a live frame **or** region (the namespace is one —
|
||||
/// a Texture or FontString publishes into `_G` on the same rule a Frame does).
|
||||
///
|
||||
/// `SetPoint` type-checks against the **root** `CScriptRegion` id `[0xcf0c3c]`, which every widget
|
||||
/// accepts; `SetParent`/`SetScrollChild` use the narrower Frame id `[0xcf0c10]`, so those callers
|
||||
/// narrow the result to `id_to_frame` themselves.
|
||||
/// Decode a [`NamedTarget`] to a live frame's or region's id: `SetPoint` accepts any widget (the
|
||||
/// `CScriptRegion` type id `[0xcf0c3c]`), while `SetParent` and `SetScrollChild` check the Frame
|
||||
/// id `[0xcf0c10]` and narrow the result themselves.
|
||||
pub(crate) fn resolve_named_target(model: &Model, target: &NamedTarget) -> Option<u32> {
|
||||
let id = decode_id(target.wrapper.as_ref()?).ok()?;
|
||||
(model.id_to_frame.contains_key(&id) || model.id_to_region.contains_key(&id)).then_some(id)
|
||||
}
|
||||
|
||||
/// The name a leading `$parent` expands to: the first widget at or above `start` with a non-empty
|
||||
/// name (`0x76c5b0`'s `+0x9c` walk, skipping an empty `GetName`), and
|
||||
/// [`crate::framexml::DEFAULT_PARENT_NAME`] (`"Top"`) when there is none (`0x76c5dd`).
|
||||
///
|
||||
/// `start` is the anchoring widget's **parent**: a frame's own anchors say `$parent` of its
|
||||
/// enclosing frame, and a region's say `$parent` of its owner — which is that region's `+0x9c`.
|
||||
/// What a leading `$parent` expands to: the first non-empty name at or above `start` (`0x76c5b0`'s
|
||||
/// `+0x9c` walk), else [`crate::framexml::DEFAULT_PARENT_NAME`] (`0x76c5dd`). `start` is the
|
||||
/// anchoring widget's parent: a frame's enclosing frame, a region's owner.
|
||||
pub(crate) fn parent_token_base(model: &Model, start: Option<FrameHandle>) -> String {
|
||||
let mut cur = start;
|
||||
while let Some(p) = cur {
|
||||
@@ -210,13 +163,13 @@ pub(crate) fn parent_token_base(model: &Model, start: Option<FrameHandle>) -> St
|
||||
crate::framexml::DEFAULT_PARENT_NAME.to_string()
|
||||
}
|
||||
|
||||
/// [`parent_token_base`] for a frame's own anchors — the walk starts at its parent.
|
||||
/// [`parent_token_base`] for a frame's own anchors: the walk starts at its parent.
|
||||
pub(crate) fn frame_parent_token_base(model: &Model, h: FrameHandle) -> String {
|
||||
parent_token_base(model, model.arena.frame(h).and_then(|f| f.parent))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// String → enum parsing (the client's tables, transcribed in order.rs / layout.rs)
|
||||
// String → enum parsing
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn point_from_str(s: &str) -> Option<Point> {
|
||||
@@ -234,10 +187,7 @@ pub(super) fn point_from_str(s: &str) -> Option<Point> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
/// The inverse of [`point_from_str`]: the names of the client's point table `0x811a38`.
|
||||
pub(super) fn point_name(p: Point) -> &'static str {
|
||||
match p {
|
||||
Point::TopLeft => "TOPLEFT",
|
||||
@@ -252,21 +202,16 @@ pub(super) fn point_name(p: Point) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 `<Frames>` 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.
|
||||
/// An enum token, trimmed and uppercased; trimmed here rather than at the attribute read, since a
|
||||
/// `text=` value's spaces count. The reference's strata lookup compares the whole token,
|
||||
/// case-insensitively (`0x6f17ee`), so a padded `"BACKGROUND "` misses there.
|
||||
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, `0x481aff`).
|
||||
/// A strata name. The reference's table (`0x8119f8`) holds the eight `BACKGROUND`..`TOOLTIP`;
|
||||
/// `BLIZZARD` is a later client's, not 1.12's. Stratum 0, `WORLD`, has no name: only the
|
||||
/// WorldFrame's constructor sets it (`0x481aff`).
|
||||
pub(crate) fn strata_from_str(s: &str) -> Option<Strata> {
|
||||
Some(match enum_token(s).as_str() {
|
||||
"BACKGROUND" => Strata::Background,
|
||||
@@ -293,8 +238,7 @@ pub(super) fn draw_layer_from_str(s: &str) -> Option<DrawLayer> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The inverse of [`draw_layer_from_str`] — the token `GetDrawLayer` answers, in the client's own
|
||||
/// spelling (uppercase, the five `<Layer level=>` names).
|
||||
/// The inverse of [`draw_layer_from_str`]: the uppercase token `GetDrawLayer` answers.
|
||||
pub(super) fn draw_layer_name(l: DrawLayer) -> &'static str {
|
||||
match l {
|
||||
DrawLayer::Background => "BACKGROUND",
|
||||
@@ -305,25 +249,16 @@ pub(super) fn draw_layer_name(l: DrawLayer) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// A widget tag or `CreateFrame` type → its [`FrameKind`] ([`enum_token`]). Public so the app asks
|
||||
/// this one mapping rather than keeping a copy.
|
||||
pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
|
||||
frame_kind_from_str(s)
|
||||
}
|
||||
|
||||
/// **The type registry lookup both doors share** (`0x6ee280`'s table `[0xcee9d8]`, its only
|
||||
/// reader) — the tag's [`FrameKind`] if a factory is registered under it right now, else `None`.
|
||||
///
|
||||
/// "Right now" is the WorldFrame's one-shot: the reference unlinks and releases that record the
|
||||
/// moment the first `<WorldFrame>` is instantiated (`0x6ee439`), so a second one — from any XML, or
|
||||
/// `CreateFrame("WorldFrame")` — takes the lookup's miss leg (decision 1984).
|
||||
///
|
||||
/// One function because the two doors disagree only in what a MISS does, never in what a miss is
|
||||
/// (decision 2191): the Lua binding raises ([`create_frame`]), the XML loader logs
|
||||
/// `"Unknown frame type: %s"` and skips the node (`crate::loader`) — both legs off the same
|
||||
/// `0x6ee280`.
|
||||
/// The type registry lookup `CreateFrame` and the XML loader share (`0x6ee280`, table
|
||||
/// `[0xcee9d8]`): the tag's [`FrameKind`] if a factory is registered for it now. The WorldFrame's
|
||||
/// record is released once the first one is made (`0x6ee439`), so a second one misses. On a miss
|
||||
/// Lua raises and XML logs `Unknown frame type: %s` and skips the node.
|
||||
pub(crate) fn registered_frame_kind(lua: &Lua, kind: &str) -> Option<FrameKind> {
|
||||
let frame_kind = frame_kind_from_str(kind)?;
|
||||
let one_shot_spent = frame_kind == FrameKind::WorldFrame
|
||||
@@ -337,33 +272,17 @@ pub(crate) fn registered_frame_kind(lua: &Lua, kind: &str) -> Option<FrameKind>
|
||||
fn frame_kind_from_str(s: &str) -> Option<FrameKind> {
|
||||
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).
|
||||
// Registered at `0x495948`, 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). Factory
|
||||
// `0x495ba0` allocates `0x314`, the same
|
||||
// size the plain-`<Frame>` 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 `<Texture>` children the FrameXML creates and rotates
|
||||
// with the 8-argument `SetTexCoord` we already support; `TaxiRouteFrame.cpp` is a vestigial
|
||||
// C++ renderer that draws nothing.
|
||||
// A plain `CSimpleFrame`: factory `0x495ba0` allocates `0x314` like `<Frame>`'s `0x6eec10`,
|
||||
// the ctor `0x506950` adds no field, and its paint `0x506a90` does nothing, so
|
||||
// `GetObjectType()` is "Frame" and `IsObjectType("TaxiRouteFrame")` false (`0x484800` is
|
||||
// not overridden). The flight lines are the FrameXML's own rotated textures.
|
||||
"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`.
|
||||
// Its own registered type (`0x4959a6`), not a Button: 1.12 has no Lua verb to take loot
|
||||
// slot N (`LootSlot` only continues a bind confirm), so a Button row could loot nothing.
|
||||
"LOOTBUTTON" => FrameKind::LootButton,
|
||||
"EDITBOX" => FrameKind::EditBox,
|
||||
"STATUSBAR" => FrameKind::StatusBar,
|
||||
@@ -393,9 +312,8 @@ pub(super) fn as_f32(v: &Value) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`as_f32`]'s double-width sibling, for the shape-C positions whose store is `f64` — today only
|
||||
/// `ColorSelect:SetColorRGB`, whose channels go through a quantizer where a detour via `f32` could
|
||||
/// move a value across a rounding boundary (`colorselect`'s module doc).
|
||||
/// [`as_f32`] in `f64`, for `ColorSelect:SetColorRGB`, whose quantizer an `f32` detour could push
|
||||
/// across a rounding boundary.
|
||||
pub(super) fn as_f64(v: &Value) -> f64 {
|
||||
match v {
|
||||
Value::Number(n) => *n,
|
||||
@@ -405,42 +323,37 @@ pub(super) fn as_f64(v: &Value) -> f64 {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// install — build the method tables, metatables, CreateFrame, and registry roots
|
||||
// install
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// The wrapper cache + the per-frame script tables (Lua-side roots — the MAXCSTACK discipline).
|
||||
// The wrapper cache and the per-frame script tables, as registry roots.
|
||||
lua.set_named_registry_value(REG_WRAPPERS, lua.create_table()?)?;
|
||||
lua.set_named_registry_value(REG_SCRIPTS, lua.create_table()?)?;
|
||||
|
||||
// The Region-map arm collection, opened before either method table is built and closed by
|
||||
// `region_map::install` below (its [`super::region_map::Arms`] doc says why it is app_data).
|
||||
// Opened before any method table is built; `region_map::install` below closes it.
|
||||
super::region_map::open_arms(lua);
|
||||
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).
|
||||
// After every table above exists: each of the 19 Region-map names gets one shared
|
||||
// implementation, so a method taken off a frame works on a texture.
|
||||
super::region_map::install(lua)?;
|
||||
|
||||
// The per-kind metatable cache ([`frame_meta_for`]) and the base metatable in it: the one a
|
||||
// plain `Frame` wears, whose `__index` is the shared frame method table itself.
|
||||
// The per-kind metatable cache, holding the plain `Frame`'s, whose `__index` is the frame
|
||||
// method table.
|
||||
lua.set_named_registry_value(REG_KIND_METAS, lua.create_table()?)?;
|
||||
let frame_meta = frame_meta_for(lua, None)?;
|
||||
lua.set_named_registry_value(REG_FRAME_META, frame_meta.clone())?;
|
||||
// FrameScript init (`0x7039f7`) publishes the shared metatable as _G["__framescript_meta"].
|
||||
// FrameScript init publishes it as `_G["__framescript_meta"]` (`0x7039f7`).
|
||||
lua.globals().set("__framescript_meta", frame_meta)?;
|
||||
|
||||
// CreateFrame(kind, name?, parent?, inherits?) — a global (the loader's factory is `0x6ee280`;
|
||||
// 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).
|
||||
// `GetScreenWidth`/`GetScreenHeight`: the screen root's size in UI units.
|
||||
lua.globals().set(
|
||||
"GetScreenWidth",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -456,14 +369,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetupFullscreenScale(frame) — the registered binding `0x48c270` the stock WorldMapFrame.xml,
|
||||
// CinematicFrame.xml and UIOptionsFrame.xml call from OnShow (decision 1980): 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.
|
||||
// `SetupFullscreenScale(frame)` (`0x48c270`) sets only the frame's scale, through `SetScale`:
|
||||
// `min(0.75 · a, 1.0)` for the configured aspect `a`, the `gxResolution` width over height,
|
||||
// which is the window's here (the reference's `widescreen` CVar at 0 would make it 4:3).
|
||||
// `UIParent`'s scale does not enter.
|
||||
lua.globals().set(
|
||||
"SetupFullscreenScale",
|
||||
lua.create_function(|lua, frame: Value| {
|
||||
@@ -488,12 +397,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// 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.
|
||||
// `GetCursorPosition()`: the last cursor position the host fed, in screen UI units, y up, as
|
||||
// the reference returns it; a caller in a scaled frame divides by its `GetEffectiveScale()`.
|
||||
lua.globals().set(
|
||||
"GetCursorPosition",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -502,15 +407,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// 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).
|
||||
//
|
||||
// **1/nil, not a Lua boolean.** These three shipped Era booleans on 0068's reasoning that
|
||||
// "every transcribed `if IsShiftKeyDown()` reads both identically" — true while we wrote every
|
||||
// caller, and false the day real 1.12 addons load (1188/1751). `ColorPickerPlus.lua:121` is
|
||||
// `if IsShiftKeyDown() == 1 then`, and a `true` reads there as "not held" (decision 2118).
|
||||
// The modifier keys the app feeds each frame before any mouse event
|
||||
// ([`UiScript::set_modifiers`]), answered 1 or nil as in 1.12, never a boolean: an addon
|
||||
// testing `IsShiftKeyDown() == 1` (`ColorPickerPlus.lua:121`) reads `true` as not held.
|
||||
for (name, pick) in [
|
||||
("IsShiftKeyDown", 0usize),
|
||||
("IsControlKeyDown", 1),
|
||||
@@ -529,36 +428,15 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The registry table of per-kind frame metatables, keyed by the head of the kind's method chain
|
||||
/// (`""` for a kind with none). Lua-side, like every other root here — the MAXCSTACK discipline.
|
||||
/// Registry key of the per-kind frame metatables, each keyed by its kind's first method-table key,
|
||||
/// `""` for a kind with none.
|
||||
const REG_KIND_METAS: &str = "__benilla_frame_meta_by_kind";
|
||||
|
||||
/// The metatable a frame of `kind` wears — built once per kind, cached in [`REG_KIND_METAS`].
|
||||
///
|
||||
/// ## `__index` is a TABLE, and that is the whole point (decision 2310)
|
||||
///
|
||||
/// This used to be one shared metatable whose `__index` was a **Rust function** that resolved the
|
||||
/// receiver's kind and walked its registries per lookup. Every `frame:SetPoint(...)` — every plain
|
||||
/// `frame.Foo` read, including the `if frame.SetValue then` duck-type probe addons open with —
|
||||
/// therefore crossed into Rust, took a model borrow, resolved a handle and did one to three
|
||||
/// named-registry string lookups. Measured in a release build: **217 ns for a plain Frame, 320 ns
|
||||
/// for a Button, against 8.7 ns for a plain Lua table `__index`.** A live sample of a quest-accept
|
||||
/// spike with the director's addon set put ~20 % of the whole UI tick inside that metamethod
|
||||
/// before any method body ran — Questie redraws its map notes with ~19 widget calls per cluster
|
||||
/// over hundreds of clusters, and pays the tax on every one.
|
||||
///
|
||||
/// A table costs one `luaH_get` inside `luaV_gettable`, in C, with no call at all.
|
||||
///
|
||||
/// ## The chain is the reference's own, expressed as metatables
|
||||
///
|
||||
/// 1.12's widget classes are not a Lua metatable chain — each class owns a flat method map and its
|
||||
/// lookup tail-calls exactly one base's lookup ([`super::region_map`]'s header has the tables). But
|
||||
/// `luaV_gettable` follows a table `__index` iteratively, so a chain of metatables IS that probe
|
||||
/// sequence: `CheckButton`'s map, miss, `Button`'s map, miss, `Frame`'s map. So rather than merging
|
||||
/// each kind's methods into one flat copy, this links the existing tables — no copies to go stale
|
||||
/// when a method table is written after the fact, and the per-kind surface stays exactly what
|
||||
/// [`kind_method_registries`] declares. Duck typing is preserved by construction: a plain Frame's
|
||||
/// chain never reaches `StatusBar`'s map, so `frame.SetValue` is still nil.
|
||||
/// The metatable a frame of `kind` wears, built once per kind and cached in [`REG_KIND_METAS`].
|
||||
/// Its `__index` is a table, so a method lookup never leaves `luaV_gettable` for Rust. The kind's
|
||||
/// method tables are linked, not merged, in the reference's probe order, each class's lookup
|
||||
/// tail-calling its base's (`CheckButton`, `Button`, `Frame`): a later write to a table shows
|
||||
/// through, and a plain Frame never reaches `StatusBar`'s methods, so `frame.SetValue` stays nil.
|
||||
fn frame_meta_for(lua: &Lua, kind: Option<FrameKind>) -> mlua::Result<Table> {
|
||||
let chain = kind_method_registries(kind);
|
||||
let key = chain.first().copied().unwrap_or("");
|
||||
@@ -566,8 +444,7 @@ fn frame_meta_for(lua: &Lua, kind: Option<FrameKind>) -> mlua::Result<Table> {
|
||||
if let Value::Table(meta) = metas.raw_get::<Value>(key)? {
|
||||
return Ok(meta);
|
||||
}
|
||||
// Link `own -> base -> ... -> Frame`, idempotently: a base table shared by several kinds (every
|
||||
// button kind reaches `Button`'s) is simply re-pointed at the same place.
|
||||
// Link `own -> base -> ... -> Frame`; relinking a base several kinds share is idempotent.
|
||||
let frame_methods: Table = lua.named_registry_value(REG_FRAME_METHODS)?;
|
||||
let mut below = frame_methods.clone();
|
||||
for reg in chain.iter().rev() {
|
||||
@@ -583,15 +460,9 @@ fn frame_meta_for(lua: &Lua, kind: Option<FrameKind>) -> mlua::Result<Table> {
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// The registry keys of a frame's kind-specific method tables, **in resolution order** — the
|
||||
/// client's own class chain (CheckButton's lookup `0x79a5d0` tail-calls Button's). Empty for a
|
||||
/// plain kind, and for a wrapper whose frame is not live.
|
||||
///
|
||||
/// Every chain here is *its own table followed by its base's whole chain*, which is what lets
|
||||
/// [`frame_meta_for`] realize it as a metatable chain instead of a search: `CheckButton`'s table
|
||||
/// gets `Button`'s as its `__index`, `Button`'s gets `Frame`'s, and one `luaV_gettable` walks the
|
||||
/// lot in C. Keep that property when adding a kind — a chain that is not a suffix of its base's
|
||||
/// cannot be expressed this way.
|
||||
/// The registry keys of a kind's own method tables in the client's lookup order (`CheckButton`'s
|
||||
/// `0x79a5d0` tail-calls `Button`'s); empty for a plain kind or no live frame. Each chain must be
|
||||
/// its own table followed by its base's whole chain, or [`frame_meta_for`] cannot link it.
|
||||
fn kind_method_registries(kind: Option<FrameKind>) -> &'static [&'static str] {
|
||||
match kind {
|
||||
Some(FrameKind::StatusBar) => &[super::statusbar::REG_STATUSBAR_METHODS],
|
||||
@@ -599,18 +470,14 @@ fn kind_method_registries(kind: Option<FrameKind>) -> &'static [&'static str] {
|
||||
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 `<MessageFrame>` 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.
|
||||
// A sibling of `ScrollingMessageFrame` with its own table, not a fallthrough to it.
|
||||
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`.
|
||||
// Its one method, then `Button`'s: `0x4c1be0` probes its map `0xb71b64`, then `0x782c90`.
|
||||
Some(FrameKind::LootButton) => &[
|
||||
super::loot::REG_LOOTBUTTON_METHODS,
|
||||
super::button::REG_BUTTON_METHODS,
|
||||
@@ -619,27 +486,20 @@ fn kind_method_registries(kind: Option<FrameKind>) -> &'static [&'static str] {
|
||||
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.
|
||||
// Its 3 methods, then `Model`'s 23: `0x506260` misses into `0x76f870`.
|
||||
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).
|
||||
// 3 of its own (`0x84f190`), then `PlayerModel`'s 3 (`0x506260`), then `Model`'s 23
|
||||
// (`0x76f870`).
|
||||
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).
|
||||
// 10 of its own (`0x84ee40`), then `PlayerModel`'s 3, then `Model`'s 23.
|
||||
Some(FrameKind::TabardModel) => &[
|
||||
super::tabard::REG_TABARDMODEL_METHODS,
|
||||
super::modelframe::REG_PLAYERMODEL_METHODS,
|
||||
@@ -651,57 +511,30 @@ fn kind_method_registries(kind: Option<FrameKind>) -> &'static [&'static str] {
|
||||
}
|
||||
}
|
||||
|
||||
/// `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.
|
||||
/// `CreateFrame(kind, name?, parent?, inherits?)` (`0x7060b0`), the runtime frame factory.
|
||||
/// `inherits` names one template, applied after the frame is made
|
||||
/// ([`crate::loader::apply_template`]) so its `OnLoad` sees the frame's real name and its `$parent`
|
||||
/// resolves against the caller's name. An unknown template raises `Couldn't find inherited node`
|
||||
/// and creates nothing (`0x7061dd`); a known but unusable one (not virtual, or the wrong shape)
|
||||
/// only warns, since the lookup itself succeeded.
|
||||
pub(super) fn create_frame(
|
||||
lua: &Lua,
|
||||
(kind, name, parent, inherits): (String, Option<Value>, Option<Value>, Option<Value>),
|
||||
) -> mlua::Result<Table> {
|
||||
// The Lua door's miss RAISES: `0x7060b0` reaches `"CreateFrame: Unknown frame type '%s'"`
|
||||
// (`0x872fa8`) through `luaL_error 0x6f4940`, which never returns. The XML door's does not —
|
||||
// see [`registered_frame_kind`].
|
||||
// An unknown type raises. The reference's message (`0x872fa8`) capitalises `Unknown`, and it
|
||||
// tests the template first (`0x7061dd`), raising for the type only at `0x7062a0`.
|
||||
let frame_kind = registered_frame_kind(lua, &kind)
|
||||
.ok_or_else(|| 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.
|
||||
//
|
||||
// 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.
|
||||
// `name` and `inherits` go through `lua_tostring` (`0x6f3690`) unguarded, so a number is a
|
||||
// string: `CreateFrame("Frame", 5)` names the frame "5". For `inherits` the conversion runs
|
||||
// before the string-type gate and retags the slot (`0x70613f`, `0x6f7cb1`), so a number there
|
||||
// raises as a missing template, where `CreateTexture` ignores one.
|
||||
let name: Option<String> = name.as_ref().and_then(|v| optional_string(lua, v));
|
||||
let template: Option<String> = 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.
|
||||
//
|
||||
// It is **parked, not pruned**, on the same footing as `TEXTURE_ONLY_METHODS`' tail three: it
|
||||
// is known to raise, but the exact message was never quoted, 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.
|
||||
// A table is the parent and a name string is looked up; anything else is no parent. The
|
||||
// reference raises `Usage: CreateFrame(...)` (`0x872f60`) for anything but a table or nil
|
||||
// (`0x706112`).
|
||||
let parent_handle: Option<FrameHandle> = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
match &parent {
|
||||
@@ -713,10 +546,8 @@ pub(super) fn create_frame(
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
// The reference silently ignores a fourth argument that is neither string nor number
|
||||
// (`0x7061cb`); the warning is ours and changes nothing.
|
||||
if template.is_none() {
|
||||
if let Some(v) = inherits.as_ref().filter(|v| !v.is_nil()) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -729,30 +560,15 @@ pub(super) fn create_frame(
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 pinned 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.
|
||||
// The template lookup comes before anything is built: a miss (`0x7061dd` → `0x6ee6f0`) raises
|
||||
// at `0x7061ed`, before the node build (`0x706208`) and the name store (`0x70622d`), so it
|
||||
// leaves no partial widget and no global. The raise never returns and `pcall` catches it, as
|
||||
// the widget dispatcher (`0x704f10`) does for every handler.
|
||||
if let Some(t) = template.as_deref().filter(|t| !t.is_empty()) {
|
||||
let known = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let templates = model.framexml_templates.borrow();
|
||||
// One name, verbatim, folded — the law `framexml::expand` resolves under.
|
||||
// One name, verbatim, case-folded, as `framexml::expand` resolves it.
|
||||
templates.contains_key(t) || templates.keys().any(|k| k.eq_ignore_ascii_case(t))
|
||||
};
|
||||
if !known {
|
||||
@@ -762,19 +578,9 @@ pub(super) fn create_frame(
|
||||
}
|
||||
}
|
||||
|
||||
// **A leading `$parent` in the NAME is expanded, here as in XML.** `CreateFrame 0x7060b0`
|
||||
// does not store the name itself: it builds a synthetic node, sets `name=` on it
|
||||
// (`0x706225 push 0x838090 "name"` → `0x70622d` SetAttribute) alongside `parent=` and
|
||||
// `inherits=`, and hands it to the XML frame builder `0x6ee280` with the parent object in
|
||||
// `edx` — so the name reaches `CScriptRegion::SetName 0x76c650` by exactly the route an XML
|
||||
// `name=` does, and `0x76c691` is one of the expander's two call sites.
|
||||
//
|
||||
// The base is the frame's **parent's** first named ancestor — the same walk `$parent` in a
|
||||
// `relativeTo` takes, one link higher than the anchoring frame's own.
|
||||
//
|
||||
// Not academic: `FonzAppraiser` names every widget it builds this way (`"$parentDropdown"..n`,
|
||||
// `"$parentCloseButton"`, ~30 sites), and until decision 2176 made an unresolvable anchor
|
||||
// raise, the unexpanded name only showed up as a warning nobody read.
|
||||
// A leading `$parent` in the name expands as in XML: `CreateFrame` sets `name=` on a synthetic
|
||||
// node (`0x70622d`) for the XML builder `0x6ee280`, so the name reaches `SetName` (`0x76c650`),
|
||||
// whose `0x76c691` call expands it against the parent's first named ancestor.
|
||||
let name = name.map(|n| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
crate::framexml::resolve_name(&n, &parent_token_base(&model, parent_handle))
|
||||
@@ -787,12 +593,8 @@ pub(super) fn create_frame(
|
||||
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).
|
||||
// A child enters its parent's stratum at the parent's level + 1; an explicit
|
||||
// `frameStrata` or `frameLevel` still overrides it afterwards.
|
||||
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);
|
||||
@@ -804,10 +606,8 @@ pub(super) fn create_frame(
|
||||
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.
|
||||
// The wrapper exists, published under its name, before the template's `OnLoad` runs inside
|
||||
// this call, so the handler can reach the frame by name.
|
||||
let wrapper = frame_wrapper(lua, id)?;
|
||||
if let Some(template) = template {
|
||||
let messages = crate::loader::apply_template(lua, &wrapper, &kind, &template);
|
||||
@@ -822,11 +622,10 @@ pub(super) fn create_frame(
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Frame method surface — the table itself is built by the three clusters above
|
||||
// Frame method surface
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build the shared frame method table from the five clusters ([`frame_state`], [`layout_methods`],
|
||||
/// [`events_regions`], [`movable`], [`toplevel`]) and publish it to the registry.
|
||||
/// Build the shared frame method table from its five clusters and publish it to the registry.
|
||||
fn install_frame_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
let m = lua.create_table()?;
|
||||
frame_state::install(lua, &m)?;
|
||||
@@ -838,8 +637,8 @@ fn install_frame_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
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).
|
||||
/// `0x48c270`'s scale: `0.75 · aspect` below 1.0, else 1.0; NaN takes the 1.0 leg, as the
|
||||
/// reference's `fcomp` fails every ordered test.
|
||||
pub(crate) fn fullscreen_scale(aspect: f32) -> f32 {
|
||||
let g = 0.75 * aspect;
|
||||
if g < 1.0 {
|
||||
|
||||
Reference in New Issue
Block a user