addons come alive: Bagnon draws, hovers and clicks
- Lua 5.0's generic-for and nested long strings are restored in a forked VM — 60 corpus addons stopped dying on one deleted opcode - a day of demand-driven carving takes the 218-addon corpus from 24 addons surviving a session start to 114, with 144 loading without a single error - Bagnon draws a real bag and answers real gestures — hover tooltip, pickup, use, drag — because our frames wear the reference's names, and the reference's own ContainerFrame.lua runs off the player's install instead of being transcribed - the physics broad phase leaves: nothing in the client reads a contact, and it was measured at up to 41% of main-thread samples in real sessions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,36 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"GetScript",
|
||||
lua.create_function(|lua, (this, name): (Table, String)| get_script(lua, &this, &name))?,
|
||||
)?;
|
||||
// HasScript(name) — "can this widget carry that script kind at all", NOT "does it have one
|
||||
// set". The distinction is the whole point of the verb: every caller in the corpus uses it to
|
||||
// decide whether it is safe to hook, then reads the current handler separately.
|
||||
//
|
||||
// if parent:HasScript("OnMouseDown") then -- Tablet-2.0.lua:2409
|
||||
// local script = parent:GetScript("OnMouseDown") -- FuBar-compat:2422
|
||||
// parent:SetScript("OnMouseDown", function() … end)
|
||||
// end
|
||||
//
|
||||
// It was the top session-start blocker: 32 of the 39 `attempt to call method` failures were
|
||||
// this one name, across the Tablet/FuBar/oRA2 stack.
|
||||
//
|
||||
// ANSWERED FROM THE FLAT `SCRIPT_KINDS`, and that is a deliberate, stated approximation rather
|
||||
// than the full answer. The reference's table is PER WIDGET TYPE — a plain Frame really does
|
||||
// answer false for `OnClick` — so ours is over-permissive for the type-specific kinds. It is
|
||||
// exactly right for the base kinds, which is what every corpus caller asks about
|
||||
// (`OnMouseDown` here; a Frame genuinely has it). The cost of the over-permission is small and
|
||||
// worth naming: an addon that asks about a type-specific kind installs a handler that never
|
||||
// fires, where the reference would have had it skip. That is the same latitude `SetScript`
|
||||
// already takes — this verb does not widen it, it just stops lying about its existence.
|
||||
//
|
||||
// Making it exact means per-type SCRIPT_KINDS, which newly RAISES on 30 sites that work today
|
||||
// (9 in our own FrameXML). That is behaviour removal and belongs in its own change, with its
|
||||
// own measurement; it is not a tail on this one.
|
||||
m.set(
|
||||
"HasScript",
|
||||
lua.create_function(|_, (_this, name): (Table, String)| {
|
||||
Ok(SCRIPT_KINDS.iter().any(|k| k.eq_ignore_ascii_case(&name)))
|
||||
})?,
|
||||
)?;
|
||||
// RegisterForDrag(...varargs of button names) — the drag-gesture twin of `RegisterForClicks`
|
||||
// (decision 0216 §3), but on the SHARED table: any Frame can be a drag source, not just a
|
||||
// Button. Replace-the-set semantics (empty varargs clears); `crate::script::cursor`'s
|
||||
@@ -140,16 +170,35 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
/// `system/ui/ui.md` l.544-556 summarises it), so what is missing here is never a mystery — it is a
|
||||
/// deliberate not-yet. What the corpus actually asks for, and why each answer is what it is:
|
||||
///
|
||||
/// * **`OnKeyDown` / `OnKeyUp` / `OnChar`** (14 + 1 + 4 corpus sites over 13 addons) — **raising.**
|
||||
/// They are in the base map, so *every* frame type has them, and the delivery mechanism is
|
||||
/// `EnableKeyboard` + the hit-test root's **kind-0/kind-1 index** (wow-re
|
||||
/// `scratch/scripts-auto-enable.md` §1-2: `0x76af00(kind, -1)`, `OnChar` = kind 0,
|
||||
/// `OnKeyDown`/`OnKeyUp` = kind 1) with the frame-script pre-gate `0x76bba0` consuming the key
|
||||
/// ahead of the whole binding dispatch (`scratch/keybinding-dispatch-law.md` §1). benilla has
|
||||
/// none of that: no `EnableKeyboard`, no keyboard index, and its keys go straight to the focused
|
||||
/// EditBox (whose C++ override replaces these slots anyway — RF-0082 §2). Accepting the names
|
||||
/// before the index exists is the exact silent-no-op trap above; the ordering/consumption law is
|
||||
/// dispatched to wow-re.
|
||||
/// * **`OnKeyDown` / `OnKeyUp` / `OnChar`** (14 + 1 + 4 corpus sites over 13 addons) — **raising,
|
||||
/// and now unblocked**: the delivery law was the second §5 this work dispatched, and it landed
|
||||
/// (wow-re `scratch/frame-key-script-delivery.md`). benilla still has none of the machinery it
|
||||
/// describes — no `EnableKeyboard`, no keyboard index, keys routed straight to the focused
|
||||
/// EditBox — so the names stay out until it exists, which is the whole rule above. What has to
|
||||
/// get built, so the next pass does not have to re-derive it:
|
||||
///
|
||||
/// The frame must be in the hit-test root's **kind-0 / kind-1 bucket**
|
||||
/// (`scratch/scripts-auto-enable.md` §1-2: `0x76af00(kind, …)`, `OnChar` = kind 0,
|
||||
/// `OnKeyDown`/`OnKeyUp` = kind 1; XML `enableKeyboard` enables both, a `<Scripts>` block
|
||||
/// auto-enables per handler, and Lua `SetScript` auto-enables **nothing** — so a Lua-only frame
|
||||
/// is not in the bucket at all and its bound handler can never fire). The dispatcher then walks
|
||||
/// **strata 8 → 0, level high → low, ties oldest-registration-first**, calling each frame's base
|
||||
/// input virtual (`0x765f10` key-down → vtable `+0x60`; `0x765df0` char → `+0x5c`) and
|
||||
/// **stopping at the first nonzero return**. `arg1` is a **key-name string** with no modifier
|
||||
/// prefix, decoded from the *same* table the keybinding chord names use — verified byte-identical
|
||||
/// over 273 codes, which is the opposite of the mouse case's two-table shape. `OnChar` gets the
|
||||
/// literal character, UTF-8.
|
||||
///
|
||||
/// Two findings that will bite whoever implements it. The consumption gate is **existence, not
|
||||
/// handling** — a 1.12 handler cannot signal "handled" (the fire's return is discarded at all
|
||||
/// three sites), so merely having a key script bound suppresses the key's binding; and
|
||||
/// asymmetrically, a frame with **only** an `OnKeyUp` script consumes every key-down and runs
|
||||
/// nothing. And `CGWorldFrame` sits at **strata 0 / level 0** — last in the walk — which is
|
||||
/// precisely why any keyboard-enabled frame pre-empts the entire binding system.
|
||||
///
|
||||
/// (The old gloss here called `0x76bba0` a "frame-script pre-gate" walking the index, after
|
||||
/// `scratch/keybinding-dispatch-law.md` §1. That is refuted: `0x76bba0` is `CSimpleFrame::OnKeyUp`,
|
||||
/// one frame's base virtual. The walk is one level up, in the dispatcher.)
|
||||
/// * **`OnCursorChanged`** (4 sites over 3 addons — all of them the Era `ScrollingEdit_OnCursorChanged`
|
||||
/// auto-scroll idiom) — **raising.** It is the EditBox's own slot (RF-28 `+0x428`), fired by the
|
||||
/// caret flush `0x77da80` with **four float caret-POSITION args**, and caret geometry is the one
|
||||
|
||||
@@ -255,6 +255,82 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// `GetBackdrop()` (`0x777370`; wow-re `ui/scratch/widget-api-batch-benilla.md` Q5) — five things
|
||||
// a plausible implementation gets wrong, so each is spelled out with its reason:
|
||||
//
|
||||
// 1. It is a **reconstruction from the struct**, never the caller's table. The reference stores
|
||||
// no Lua reference anywhere: `SetBackdrop` reads six keys into a fresh 0x68-byte struct at
|
||||
// `frame+0x1ac` and drops the table, so `0x777426`–`0x7776b9` re-push every key from those
|
||||
// fields. Handing back a stored clone would leak keys the reader never accepted and would
|
||||
// keep stale values a later `SetBackdropColor` changed.
|
||||
// 2. **No backdrop ⇒ ZERO Lua values, not `nil`** — the early bail is `xor eax,eax; ret`, which
|
||||
// for a *return* path really is "no values" (contrast `binding_abi`'s note: the same two
|
||||
// bytes after a `luaL_error` are unreachable boilerplate). Observable through `select('#')`,
|
||||
// and it is the shape our `GetTitleRegion` will *not* have when it lands — that one pushes
|
||||
// nil, i.e. one value. The client cannot distinguish "never set" from `SetBackdrop(nil)`.
|
||||
// 3. **A partial `SetBackdrop` omits nothing on the way out.** Every `SetBackdrop` allocates a
|
||||
// fresh struct (`0x777801`, ctor `0x77e5f0`), so a key the caller left out is a *ctor
|
||||
// default* here, not an absent key and not the previous backdrop's value: `bgFile`/`edgeFile`
|
||||
// `""`, `tileSize` 0, `edgeSize` **32**. Our `backdrop_from_table` already builds on
|
||||
// `Backdrop::default()`, so this falls out — but only because `None` maps to `""` below
|
||||
// rather than to nil.
|
||||
// 4. **`tile` is the NUMBER `1`, or the key is ABSENT — never `true`/`false`.** The push is
|
||||
// `0x3ff00000` (the double 1.0) on true and `lua_pushnil` on false, and `lua_settable` with a
|
||||
// nil value creates no key (and *erases* one from a recycled table, which is why the nil is
|
||||
// written rather than skipped). An addon reading `if backdrop.tile then` sees the same truth
|
||||
// either way; one that round-trips the table into `SetBackdrop` is why the number matters,
|
||||
// since `tile` there goes through a coercer that takes numbers.
|
||||
// 5. The undocumented **in-place form** (`0x77740e`): `lua_type(L,2) == LUA_TTABLE` skips
|
||||
// `lua_newtable` and fills arg 2, reusing an existing `insets` subtable rather than replacing
|
||||
// it. Implemented — it is four lines, and an addon caching one table across frames would
|
||||
// otherwise silently get a new one each call. A non-table arg 2 is ignored, not an error.
|
||||
//
|
||||
// No `bgColor`/`edgeColor`/`alpha` key exists: the two colors live in the struct but the reader
|
||||
// never pushes them (`GetBackdropColor`/`GetBackdropBorderColor` are their only accessors).
|
||||
m.set(
|
||||
"GetBackdrop",
|
||||
lua.create_function(|lua, (this, target): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
// Copied out before a single Lua write: filling a *caller-supplied* table can run a
|
||||
// `__newindex` metamethod, which can re-enter us and would panic on the app-data borrow.
|
||||
let bd = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
model.backdrops.get(&h).cloned()
|
||||
};
|
||||
let Some(bd) = bd else {
|
||||
return Ok(mlua::MultiValue::new()); // trap 2 — zero values, not nil
|
||||
};
|
||||
let t = match target {
|
||||
Value::Table(t) => t,
|
||||
_ => lua.create_table()?,
|
||||
};
|
||||
t.set("bgFile", bd.bg_file.as_deref().unwrap_or(""))?;
|
||||
t.set("edgeFile", bd.edge_file.as_deref().unwrap_or(""))?;
|
||||
t.set(
|
||||
"tile",
|
||||
if bd.tile {
|
||||
Value::Number(1.0) // trap 4
|
||||
} else {
|
||||
Value::Nil // trap 4 — erases the key from a recycled table
|
||||
},
|
||||
)?;
|
||||
t.set("tileSize", f64::from(bd.tile_size))?;
|
||||
t.set("edgeSize", f64::from(bd.edge_size))?;
|
||||
let insets = match t.get::<Value>("insets") {
|
||||
Ok(Value::Table(existing)) => existing, // trap 5 — reuse, don't replace
|
||||
_ => {
|
||||
let fresh = lua.create_table()?;
|
||||
t.set("insets", &fresh)?;
|
||||
fresh
|
||||
}
|
||||
};
|
||||
insets.set("left", f64::from(bd.insets.left))?;
|
||||
insets.set("right", f64::from(bd.insets.right))?;
|
||||
insets.set("top", f64::from(bd.insets.top))?;
|
||||
insets.set("bottom", f64::from(bd.insets.bottom))?;
|
||||
Ok(mlua::MultiValue::from_vec(vec![Value::Table(t)]))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetBackdropColor",
|
||||
lua.create_function(
|
||||
|
||||
@@ -176,6 +176,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"GetWidth",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
settle(lua);
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(size_read(&model, h, true))
|
||||
})?,
|
||||
@@ -184,6 +185,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"GetHeight",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
settle(lua);
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(size_read(&model, h, false))
|
||||
})?,
|
||||
@@ -198,6 +200,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"GetCenter",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
settle(lua);
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let inv = 1.0 / eff_scale(&model, h);
|
||||
Ok(match model.resolved.get(&h) {
|
||||
@@ -238,6 +241,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
name,
|
||||
lua.create_function(move |lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
settle(lua);
|
||||
let model = lua.app_data_mut::<Model>().expect("model");
|
||||
let inv = 1.0 / eff_scale(&model, h);
|
||||
Ok(model.resolved.get(&h).map(|r| {
|
||||
@@ -255,6 +259,33 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
}
|
||||
|
||||
/// The frame's effective scale, ε-guarded (a zero would poison the local-unit division).
|
||||
/// **Resolve the layout graph NOW if anything has moved since the last pass.**
|
||||
///
|
||||
/// The client answers a geometry query against current layout; ours cached rects in a per-frame
|
||||
/// `resolve()` pass, so a query made EARLIER IN THE SAME CALL STACK than that pass read nil. That
|
||||
/// is not an edge case — it is what every menu does. `Dewdrop-2.0.lua` (embedded in ~65 corpus
|
||||
/// addons) creates its menu frame, anchors it, shows it and then measures it inside one `OnClick`:
|
||||
///
|
||||
/// ```lua
|
||||
/// local left = frame:GetLeft() -- l.1942, nil for us
|
||||
/// frame:SetPoint(point, parent, relativePoint, curX - left - width / 2, 0) -- l.1960, dies
|
||||
/// ```
|
||||
///
|
||||
/// **97 of the 108 addons that drew and then raised on being touched died on that one line.** No
|
||||
/// missing verb: `GetLeft` was always there, and always answered nil.
|
||||
///
|
||||
/// Cheap: `resolve_layout`'s tier-1 gate is one epoch comparison that returns immediately when
|
||||
/// nothing has been touched, so a settled tree pays a compare per getter.
|
||||
///
|
||||
/// **It deliberately does NOT fire `OnSizeChanged`.** That drain runs Lua handlers, and re-entering
|
||||
/// Lua from inside a binding is how a borrow panic or an unbounded recursion happens. The
|
||||
/// size-change queue is drained by the next real `UiScript::resolve`, one tick later than the
|
||||
/// reference's per-rect-application fire (`ApplyRect 0x76b580`). Stated rather than hidden.
|
||||
fn settle(lua: &Lua) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
crate::script::UiScript::resolve_layout(&mut model);
|
||||
}
|
||||
|
||||
pub(super) fn eff_scale(model: &Model, h: FrameHandle) -> f32 {
|
||||
let s = model
|
||||
.arena
|
||||
|
||||
Reference in New Issue
Block a user