the inspect and talent windows are the reference's own

- the inspect and talent windows are the reference's own: `Blizzard_InspectUI` and `Blizzard_TalentUI` are LoadOnDemand addons whose real `.xml`/`.lua` live inside `patch.MPQ` — the loose `Interface\AddOns` folder holds only a `.pub` signature, which three notes in this tree had read as "packed, no source to migrate from", one wrong inference that kept five windows on hand-written transcriptions; 2300 lines of ours out, no engine work needed, and three things our talent transcription had quietly invented fell out (there was no level-10 gate, every tree scrolled where a shallow one does not, and a nil `TalentMicroButton` threw out of OnShow before the window painted)
- a nil unit token RAISES in 53 of 83 bindings: wow-re censused the registrar table at `0x850438` — `UnitName(nil)` raises and `UnitIsVisible(nil)` is quiet, one line apart in stock files, so there is no rule to derive, the table is the rule; ours answered a default and our own test had pinned it; both token positions of the seven two-token predicates are gated, `GetRaidTargetIndex` too, and `UnitIsAFK`/`UnitIsDND` are confirmed not to exist in 5875 at all (zero occurrences under a positive control) — we invented them, and they stay deliberately ungated
- `EditBox:SetNumber` did not exist (the only chain file calling it is `MoneyInputFrame.lua`, on the chain since the money kit): it is byte-identical to `SetText`, formats through `%.14g` in MSVC's CRT (`1e+020`, `1.#INF`), and `OnTextChanged` never fires from an edit — it fires from the dirty-word drain inside the box's own OnUpdate, coalesced to one per box per frame, where ours fired synchronously and `MoneyInputFrame_SetCopper` raised on it; `PanelTemplates_TabResize`'s `tab` argument is optional as the reference's is (stock macro tabs call it with `0`)
- the skull or star on your CURRENT target never appeared or cleared until you re-targeted — `RAID_TARGET_UPDATE` was never fired, and stock `TargetFrame.lua` hangs its repaint off it; a raid-to-party conversion now empties the mark board as `0x4ba550` does; and the group-loot roll popups were already on the chain inside stock `LootFrame.xml` — our 546-line `GroupLootFrame.xml` had been shadowing all four of them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:06:18 +07:00
co-authored by Claude Fable 5.1
parent 7308046d60
commit 2bebb94471
41 changed files with 1400 additions and 3134 deletions
@@ -18,7 +18,7 @@
use mlua::Lua;
use super::{fire_script, frame_id_of, set_focus_handle, sync_text_region, with_eb};
use super::{set_focus_handle, sync_text_region, with_eb};
use crate::script::Model;
use crate::widget::{FrameHandle, FrameKind, KindState};
@@ -108,7 +108,7 @@ pub(in crate::script) fn cut_selection(lua: &Lua) -> Option<String> {
let h = focused(lua)?;
let copied = with_eb(lua, h, |eb| eb.cut_selection()).flatten()?;
sync_text_region(lua, h);
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
super::mark_text_changed(lua, h);
Some(copied)
}
@@ -45,6 +45,38 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
"GetText",
lua.create_function(|lua, this: Table| with_editbox(lua, &this, |eb| eb.text.clone()))?,
)?;
// SetNumber(v) — **the same function as SetText**. `0x798690` and `0x7984c0` are byte-identical
// (245 bytes each, zero differences after masking rel32 and absolute-VA operands; only the
// usage string differs), so this does no numeric work of its own: it hands the argument to the
// shared `lua_tostring` marshalling and sets the result as text. Decision 1831.
//
// The GATE is `lua_isstring 0x6f3510`, a pure type test over {number, string} — NOT
// `lua_isnumber` and NOT `luaL_checknumber`. So a STRING is accepted and passed through
// VERBATIM, never parsed: `SetNumber("abc")` sets the text "abc" and does not raise. Anything
// else — nil, boolean, table, or an ABSENT argument — raises the usage string and abandons the
// caller's statement.
//
// The live consumer is `MoneyInputFrame.lua:47/52/57`, on the chain since 1751, whose three
// boxes are `numeric="true"`. Our numeric filter already models the reference's wholesale
// abort, which matters here: on such a box `SetNumber(-5)` or `SetNumber(0.8)` leaves the box
// EMPTY rather than partially filled, because the sign or the point fails the digit test after
// the clear-all has already run. The money path only ever passes non-negative integers
// (`floor`/`mod` results), so it never takes that branch.
m.set(
"SetNumber",
lua.create_function(|lua, (this, v): (Table, Value)| {
let text = match &v {
Value::Integer(i) => crate::script::binding_abi::lua_number_text(*i as f64),
Value::Number(n) => crate::script::binding_abi::lua_number_text(*n),
// A string is not parsed — it is the text.
Value::String(s) => s.to_str()?.to_string(),
_ => return Err(mlua::Error::runtime("Usage: EditBox:SetNumber(number)")),
};
let h = frame_handle_of(lua, &this)?;
set_text(lua, h, &text);
Ok(())
})?,
)?;
// GetNumber: atof of the real text (0 on failure), matching `0x798790`.
m.set(
"GetNumber",
+112 -11
View File
@@ -220,14 +220,19 @@ fn click_focuses_regardless_of_autofocus_and_transition_order_is_lost_then_gaine
// ── §3 text buffer + editing + OnTextChanged/OnTextSet ───────────────────────────────────────
/// **`OnTextChanged` is deferred and coalesced** (decision 1831). An edit only raises the
/// `textChanged` dirty bit; the fire belongs to the drain (`0x77d3e0`) that the box's own OnUpdate
/// runs. So three typed characters are three marks on ONE box and produce exactly ONE fire — this
/// test asserted three before the law was checked.
#[test]
fn typing_builds_text_and_fires_ontextchanged_per_mutation() {
fn typing_coalesces_into_one_deferred_ontextchanged() {
let mut s = script();
s.run(
r#"
changed = 0
E = CreateFrame("EditBox", "E")
E:SetScript("OnTextChanged", function() changed = changed + 1 end)
seen = ""
E:SetScript("OnTextChanged", function() changed = changed + 1 seen = E:GetText() end)
E:SetFocus()
"#,
)
@@ -236,26 +241,50 @@ fn typing_builds_text_and_fires_ontextchanged_per_mutation() {
assert!(s.char_input("b"));
assert!(s.char_input("c"));
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "abc");
assert_eq!(s.eval::<i64>("return changed").unwrap(), 3);
assert_eq!(
s.eval::<i64>("return changed").unwrap(),
0,
"the text is already there, but nothing has drained yet"
);
s.tick(0.0);
assert_eq!(
s.eval::<i64>("return changed").unwrap(),
1,
"three edits, one fire, carrying the final text"
);
assert_eq!(s.eval::<String>("return seen").unwrap(), "abc");
// Nothing is pending now, so a second drain fires nothing.
s.tick(0.0);
assert_eq!(s.eval::<i64>("return changed").unwrap(), 1);
}
/// The two fires part company (decision 1831): **`OnTextSet` is synchronous, inside `SetText`
/// itself** (`0x77be6b`), while `OnTextChanged` waits for the drain. The equality short-circuit
/// still suppresses both. So writing "hi", "hi", "bye" before any drain logs two `set`s and then a
/// SINGLE `changed` carrying only the last value — `A → B` coalesces.
#[test]
fn set_text_short_circuits_on_same_value_else_fires_textset_then_textchanged() {
let s = script();
fn set_text_fires_ontextset_at_once_and_ontextchanged_at_the_drain() {
let mut s = script();
s.run(
r#"
log = {}
E = CreateFrame("EditBox", "E")
E:SetScript("OnTextSet", function() table.insert(log, "set") end)
E:SetScript("OnTextChanged", function() table.insert(log, "changed") end)
E:SetText("hi") -- fires set then changed
E:SetText("hi") -- unchanged → short-circuit, no events
E:SetText("bye") -- fires set then changed
E:SetText("hi") -- fires set NOW, marks changed for the drain
E:SetText("hi") -- unchanged → short-circuit, no events at all
E:SetText("bye") -- fires set NOW; the pending change simply becomes "bye"
"#,
)
.unwrap();
let log: Vec<String> = s.eval("return log").unwrap();
assert_eq!(log, vec!["set", "changed", "set", "changed"]);
assert_eq!(log, vec!["set", "set"], "no drain has run yet");
s.tick(0.0);
let log: Vec<String> = s.eval("return log").unwrap();
assert_eq!(log, vec!["set", "set", "changed"]);
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "bye");
}
@@ -328,8 +357,10 @@ fn paste_inserts_at_the_cursor_and_replaces_the_selection() {
s.run("E:HighlightText(0, -1)").unwrap();
assert!(s.paste("xyz"));
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "xyz");
// Each paste that changed the text fired OnTextChanged once.
assert_eq!(s.eval::<i64>("return changed").unwrap(), 2);
// Both pastes marked the same box, so the drain owes exactly one fire — not one per paste.
assert_eq!(s.eval::<i64>("return changed").unwrap(), 0);
s.tick(0.0);
assert_eq!(s.eval::<i64>("return changed").unwrap(), 1);
}
#[test]
@@ -1450,3 +1481,73 @@ fn an_editbox_is_born_with_the_ctors_five_regions_ahead_of_its_authored_ones() {
"the selection trio and the caret are blank quads"
);
}
/// `SetNumber` is `SetText` with Lua's own `%.14g` in front of it — the two bindings are
/// byte-identical in the reference (`0x798690` / `0x7984c0`), so the numeric verb does no numeric
/// work of its own. Decision 1831; the expected strings are the reference's executed output.
#[test]
fn set_number_formats_like_the_references_printf() {
let s = UiScript::new().unwrap();
s.run(r#"box = CreateFrame("EditBox", "NumBox", UIParent)"#)
.unwrap();
for (input, want) in [
("3", "3"),
("0.8", "0.8"),
("-0.5", "-0.5"),
("0.1 + 0.2", "0.3"),
("1/3", "0.33333333333333"),
("1e13", "10000000000000"),
// The three-digit exponent, which is MSVC's `%g` and not C99's. `1e+020`, never `1e+20`.
("1e14", "1e+014"),
("1e20", "1e+020"),
("1e-5", "1e-005"),
// Just inside the fixed-notation window.
("1e-4", "0.0001"),
// `%g` drops the sign on negative zero.
("-0.0", "0"),
] {
s.run(&format!("box:SetNumber({input})")).unwrap();
assert_eq!(
s.eval::<String>("return box:GetText()").unwrap(),
want,
"SetNumber({input})"
);
}
// A STRING is accepted and passed through verbatim — the gate is `lua_isstring`, so the
// argument is never parsed as a number.
s.run(r#"box:SetNumber("abc")"#).unwrap();
assert_eq!(s.eval::<String>("return box:GetText()").unwrap(), "abc");
// Everything else raises, including an ABSENT argument. `luaL_error` does not return.
for bad in [
"box:SetNumber()",
"box:SetNumber(nil)",
"box:SetNumber(true)",
] {
assert!(s.run(bad).is_err(), "{bad} must raise");
}
}
/// A `numeric` box takes the reference's wholesale abort: the clear-all has already run when the
/// insert fails the digit test, so the box is left EMPTY rather than partly filled. The money
/// boxes (`MoneyInputFrame.xml`) are numeric, which is why this matters — though the money path
/// itself only ever passes non-negative integers.
#[test]
fn set_number_on_a_numeric_box_empties_it_when_the_text_is_not_all_digits() {
let s = UiScript::new().unwrap();
s.run(r#"box = CreateFrame("EditBox", "NumOnly", UIParent) box:SetNumeric(true)"#)
.unwrap();
s.run("box:SetNumber(1234)").unwrap();
assert_eq!(s.eval::<String>("return box:GetText()").unwrap(), "1234");
// The minus sign fails the digit test, and the abort is wholesale.
s.run("box:SetNumber(-5)").unwrap();
assert_eq!(
s.eval::<String>("return box:GetText()").unwrap(),
"",
"a sign empties a numeric box rather than partly filling it"
);
}