the mailbox takes typing again, and events carry their real arguments

- the mail window's send tab takes typing again: the subject, body and money boxes were taking no input while the recipient box swallowed every key, and a sent letter clears its subject and item icon instead of putting them straight back (2145)
- seven events fire the way the reference does: UNIT_DYNAMIC_FLAGS fires at all, swapping two identical stacks between bank slots fires PLAYERBANKSLOTS_CHANGED twice, the pet sheet's training points repaint, and ITEM_LOCK_CHANGED loses its invented arguments (2140)
- nine widget methods 1.12 addons probe for are in: ShaguTweaks stops recolouring textures the reference leaves alone, pfUI and _Nameplates stop raising on a tooltip's GetAnchorType, and 63 AceEvent addons get RegisterAllEvents (2142)
- six post-1.12 methods leave the widget surface (SetSize, SetShown, HasFocus, SetFormattedText and two tooltip verbs), so an addon reaching for a later client's API finds what a real 1.12 client gives it (2142, 2146)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 20:52:50 +07:00
co-authored by Claude Fable 5.1
parent 3f3cf21afa
commit fed2c96ec8
100 changed files with 3837 additions and 869 deletions
@@ -47,6 +47,37 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
Ok(())
})?,
)?;
// `RegisterAllEvents()` — this frame's `OnEvent` receives EVERY event that is dispatched
// (`0x774c20`, table `0x878ec0`, argc 1, arity 0). Cleared by `UnregisterAllEvents` below, and
// by nothing else: there is no `UnregisterAllEvents`-less way back out, which is exactly how
// the reference's pair reads.
//
// **Latent, not live, and the widest-carried gap the corpus census found.** Nothing in either
// addon corpus calls it *today*, but 63 vanilla addons and 6 of the top 20 ship
// **AceEvent-2.0**, whose `AceEvent:RegisterAllEvents(handler)` runs
// `AceEvent.frame:RegisterAllEvents()` the first time any addon asks for all events — so the
// first one that does raised here. AceEvent is also why the *clearing* half is load-bearing
// rather than tidy: while all-events is on it deliberately stops calling
// `frame:UnregisterEvent(event)` at all, and `AceEvent:UnregisterAllEvents()` gets back to a
// per-event registration by calling `frame:UnregisterAllEvents()` and then re-registering each
// event it still wants. A `RegisterAllEvents` that outlived that call would leave every Ace2
// addon on the whole event stream forever.
//
// Registered as a FLAG (the frame joins `Model::all_event_frames`), never as an expansion into
// a name list — see that field for why, and `tick::fire_event_into` for the dispatch.
m.set(
"RegisterAllEvents",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
// Registration order, and re-registering keeps the original position — the same law
// `RegisterEvent` above holds, for the same reason.
if !model.all_event_frames.contains(&h) {
model.all_event_frames.push(h);
}
Ok(())
})?,
)?;
// `UnregisterAllEvents()` — drop every registration this frame holds, in one call.
//
// 10 corpus addons stop on it (decision 1195), and the idiom is why: an addon's "disable me"
@@ -67,6 +98,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
list.retain(|x| x != &h);
}
}
// …and the all-events registration, which is a registration like any other. See
// `RegisterAllEvents` above: AceEvent-2.0's own re-registration path depends on this.
model.all_event_frames.retain(|x| x != &h);
Ok(())
})?,
)?;
@@ -320,12 +354,15 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
/// * **`OnAttributeChanged`** (1 site, `Roid-Macros`) — **raising, permanently.** It is 2.0's secure
/// frame/attribute system; there is no such slot in any 1.12 resolver. That addon is asking for a
/// later client and should hear so.
/// * **`OnHorizontalScroll` · `OnHyperlinkEnter` · `OnHyperlinkLeave` · `OnMessageScrollChanged` ·
/// * **`OnHyperlinkEnter` · `OnHyperlinkLeave` · `OnMessageScrollChanged` ·
/// `OnMovieFinished`/`ShowSubtitle`/`HideSubtitle` · `OnInputLanguageChanged`** — **raising.**
/// Real 1.12 slots that we do not fire, and measured at **zero** call sites across the
/// 218-addon corpus, so there is nothing to weigh against the trap: they land when their
/// mechanism does (horizontal scroll isn't modeled at all — see
/// [`crate::script::scrollframe`]'s module doc).
/// mechanism does.
/// * **`OnHorizontalScroll`** — **accepted, because `SetHorizontalScroll` fires it.** It was on
/// the line above while "horizontal scroll isn't modeled at all", which stopped being true when
/// the ScrollFrame's horizontal offset pair landed beside the vertical one
/// ([`crate::script::scrollframe`]); `aux-addon` reads and writes both axes.
/// * **`OnUpdateModel` · `OnAnimFinished`** — **accepted since decision 2007**, because the tick
/// fires them: the model pane's scene clock runs in this engine now (`tick.rs`), `OnUpdateModel`
/// at the top of every paint of a visible pane with a file (`0x76d1a0`) and `OnAnimFinished`
@@ -14,7 +14,10 @@ use super::{decode_id, draw_layer_from_str, frame_handle_of, frame_wrapper, stra
/// Populate `m`'s visibility/hierarchy/strata/backdrop/mouse methods (see the module doc).
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
// Show / Hide / visibility
// Show / Hide / visibility. **`SetShown` is not here and must not come back**: the branchless
// setter belongs to a later expansion — no 1.12 method table registers it, and neither the
// stock chain nor either addon corpus calls it (decision 2142's census). A frame is shown or
// hidden by the two verbs the era has.
m.set(
"Show",
lua.create_function(|lua, this: Table| set_shown(lua, &this, true))?,
@@ -23,21 +26,12 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
"Hide",
lua.create_function(|lua, this: Table| set_shown(lua, &this, false))?,
)?;
// SetShown(bool) — the live API's branchless Show/Hide (a consensus call across the 0068
// target addons; Lua truthiness, so SetShown(nil) hides).
m.set(
"SetShown",
lua.create_function(|lua, (this, shown): (Table, Value)| {
let show = !matches!(shown, Value::Nil | Value::Boolean(false));
set_shown(lua, &this, show)
})?,
)?;
m.set(
"IsShown",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model.arena.frame(h).map(|f| f.shown).unwrap_or(false),
))
})?,
@@ -47,7 +41,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model
.arena
.frame(h)
@@ -759,7 +753,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model.arena.is_mouse_enabled(h),
))
})?,
@@ -795,7 +789,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model.arena.is_keyboard_enabled(h),
))
})?,
@@ -863,7 +857,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model.arena.is_mouse_wheel_enabled(h),
))
})?,
@@ -889,7 +883,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model.arena.is_clamped_to_screen(h),
))
})?,
@@ -1,6 +1,6 @@
//! Frame method-table cluster: anchoring and size — `SetPoint`/`ClearAllPoints`/`GetPoint`/
//! `SetAllPoints`/`SetWidth`/`SetHeight`/`SetSize`/`GetWidth`/`GetHeight` and the resolved-edge
//! readers (`GetLeft`/`GetRight`/`GetTop`/`GetBottom`). Split out of [`super`] purely for size — see
//! `SetAllPoints`/`SetWidth`/`SetHeight`/`GetWidth`/`GetHeight` and the resolved-edge readers
//! (`GetLeft`/`GetRight`/`GetTop`/`GetBottom`). Split out of [`super`] purely for size — see
//! its module doc for the shared id/handle plumbing and method-table wiring.
use mlua::{Lua, Table, Value};
@@ -41,7 +41,7 @@ fn prefetch_relative_to(
/// Populate `m`'s layout (anchor/size) methods (see the module doc).
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
// Layout: SetPoint / ClearAllPoints / SetSize / SetWidth / SetHeight / GetWidth / GetHeight
// Layout: SetPoint / ClearAllPoints / SetWidth / SetHeight / GetWidth / GetHeight
m.set(
"SetPoint",
lua.create_function(
@@ -209,23 +209,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
Ok(())
})?,
)?;
m.set(
"SetSize",
lua.create_function(|lua, (this, w, ht): (Table, f32, f32)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
let input = model.layout_inputs.entry(h).or_default();
let changed =
input.width.to_bits() != w.to_bits() || input.height.to_bits() != ht.to_bits();
input.width = w;
input.height = ht;
if changed {
model.touch_layout_frame(h);
}
model.note_authored_size(h);
Ok(())
})?,
)?;
// **No `SetSize`.** It is an Era geometry verb, in neither the Frame nor the Region method
// table of 1.12 — and neither the stock chain nor either addon corpus writes it (decision
// 2142's census). The two setters above are the era's whole size surface.
m.set(
"GetWidth",
lua.create_function(|lua, this: Table| {
@@ -370,7 +356,7 @@ pub(crate) fn eff_scale(model: &Model, h: FrameHandle) -> f32 {
/// `GetWidth`/`GetHeight`: the resolved rect's span in LOCAL units (screen ÷ effective scale — the
/// client returns the value as authored, and `SetWidth(w)` on a scaled frame resolves to `w·scale`
/// screen px) if `resolve` has produced one, else the explicit size the frame was given
/// (`SetWidth`/`SetHeight`/`SetSize`, already local) — matching the client's "0 = derive".
/// (`SetWidth`/`SetHeight`, already local) — matching the client's "0 = derive".
fn size_read(model: &Model, h: FrameHandle, width: bool) -> f32 {
if let Some(r) = model.resolved.get(&h) {
let span = if width { r.width() } else { r.height() };
@@ -405,11 +405,11 @@ fn set_flag(lua: &Lua, this: &Table, which: Flag, value: bool) -> mlua::Result<(
}
/// `IsMovable`/`IsResizable`/`IsUserPlaced` — the NUMBER 1 or nil, never a boolean (1830,
/// [`crate::script::binding_abi::predicate`]).
/// [`crate::script::binding_abi::flag`]).
fn get_flag(lua: &Lua, this: &Table, which: Flag) -> mlua::Result<Value> {
let h = frame_handle_of(lua, this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
Ok(crate::script::binding_abi::flag(
model.arena.frame(h).is_some_and(|f| match which {
Flag::Movable => f.movable,
Flag::Resizable => f.resizable,
@@ -132,9 +132,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(crate::script::binding_abi::predicate(
model.arena.is_toplevel(h),
))
Ok(crate::script::binding_abi::flag(model.arena.is_toplevel(h)))
})?,
)?;
// Raise() — `0x775a50` → `0x76a5b0` → `0x7650f0(this, force = 1)`. Note it is legal on any