the flight map goes stock, and vendors refuse in 1.12's own words

- the pet stable and the flight map are the reference's own files: `TaxiRouteFrame` maps to a plain Frame (factory `0x495ba0` allocates the base size, the ctor adds no field, no new virtual, no method table — and `GetObjectType` is not overridden, so its own kind would have been the divergence), and the swap found three silent behaviours — the close button did nothing (an unknown `inherits=` is a WARNING, so it loaded as a bare button), the "no paths" line drew empty, and the title is READ off `UnitName("npc")`, not received; the WMO skybox rides the interior crossfade — one weight (`[0xce9bdc]`, the same number the MFOG lerp rides), a 4 s alpha fade, so a skybox the flood publishes through a doorway draws at weight 0 instead of popping in whole
- a widget predicate answers the NUMBER 1 or nil, never a boolean: eleven of them (`IsShown`, `IsVisible`, `IsMovable`, `GetChecked`…) move to the 1.12 shape — the reference proves it in its own source (`tostring(this:GetChecked())` saved and read back `== "1"`; `tostring(true)` would never match and buff timers would never appear), 21 corpus sites compare directly (`IsVisible() == nil`, `GetChecked() == 1`), and one of our own tests had the guild rank editor sending `true` where a real client sends 1
- the vendor's refusals are the reference's table: `SMSG_BUY_FAILED`/`SMSG_SELL_ITEM` route through `0x5dcd60`/`0x5dd1f0`'s switches, six lines were wrong (code 3 printed "That vendor can't be found." — vmangos's identifier — where 1.12 prints "That merchant doesn't like you."), silence is now expressible, and keying them gives the vendor a voice (`ERR_NOT_ENOUGH_MONEY` carries error-speech line 0x28); the bank's three ids were right and its "purchase failed (n)" an invention; the logout refusal is one key, `ERR_LOGOUT_FAILED`, for every non-zero reason; the two mail codes we said had no string have one (`ERR_PLAYER_WRONG_FACTION`, `ERR_RESTRICTED_ACCOUNT`) and a sent mail says `ERR_MAIL_SENT` in yellow; a test runs the chain's own `GlobalStrings.lua` and requires non-empty text for all 193 `ERR_*` the workspace names
- a clicked quest timer no longer raises — stock `QuestTimerFrame.lua` calls `QuestLog_SetSelection`/`QuestLog_Update`, which existed nowhere in this tree; our quest log owns the reference's two names now, which also shrinks the delta when that window itself migrates

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:05:15 +07:00
co-authored by Claude Fable 5.1
parent b14dd42cf1
commit b8e0025bc9
38 changed files with 905 additions and 1564 deletions
@@ -163,6 +163,36 @@ pub(crate) fn optional_string(lua: &Lua, v: &Value) -> Option<String> {
/// *strings* `"0"`/`"1"`, and a re-implementation wired to `lua_toboolean` inverts every
/// string-valued option in the game. Equally, `"0.5"` and `"-1"` are decided by their first byte
/// alone — false and `default` — never by their numeric value.
/// A widget **predicate's return**: the NUMBER `1` for true, `nil` for false — never a Lua boolean.
///
/// The return-side counterpart to [`bool_or_default`], and the same class of fact: 1.12 widget
/// bindings do not push booleans. `lua_pushboolean 0x6f39f0` has seven call sites in the whole
/// image and not one of them is inside a widget registrar body — every predicate goes through
/// `lua_pushnumber`/`lua_pushnil`. Decision 1830.
///
/// **Truthiness hides the difference and direct comparison inverts it**, which is why this survived
/// so long. `if frame:IsVisible()` reads the same either way; `if frame:IsVisible() == nil` does
/// not — under a boolean a hidden frame answers `false`, and `false == nil` is FALSE, so the caller
/// concludes the frame is visible exactly when it is not. The 1.12 addon corpus has 21 such direct
/// comparisons (`IsVisible() == nil` ×9, `GetChecked() == 1` ×6, `IsVisible() ~= nil` ×4, and one
/// each of `~= 1` / `== 1`) across Questie, AtlasQuest, CT_BagMod, MikScrollingBattleText's options
/// and `_dl`.
///
/// The reference proves its own shape without needing the bytes: stock `UIOptionsFrame.xml:310`
/// saves a checkbox as `SHOW_BUFF_DURATIONS = tostring(this:GetChecked())` and stock
/// `BuffFrame.lua:71` reads it back as `== "1"`. That round-trip only closes if `GetChecked`
/// returns the number 1 — `tostring(true)` is `"true"`, and buff timers would never appear.
///
/// Adopting it is strictly safer than what it replaces: every `if x` and `not x` site reads
/// identically, and only the direct comparisons change — from wrong to right.
pub(crate) fn predicate(b: bool) -> Value {
if b {
Value::Integer(1)
} else {
Value::Nil
}
}
pub(crate) fn bool_or_default(v: Option<&Value>, default: bool) -> bool {
let Some(v) = v else {
return default; // LUA_TNONE
+11 -1
View File
@@ -816,8 +816,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
c.set(
// GetChecked() -> the NUMBER 1 or nil, never a boolean (1830). Its neighbour
// `IsEnabled` two entries up was corrected for the same reason under 1719, whose comment
// records that a boolean here killed pfUI's widget module outright; this is that call
// finished. The reference proves the shape in its own source rather than only at the
// bytes: stock `UIOptionsFrame.xml:310` saves a checkbox as
// `tostring(this:GetChecked())` and stock `BuffFrame.lua:71` reads it back as `== "1"`,
// a round-trip that only closes on the number.
"GetChecked",
lua.create_function(|lua, this: Table| with_button(lua, &this, |bs| bs.checked))?,
lua.create_function(|lua, this: Table| {
let checked = with_button(lua, &this, |bs| bs.checked)?;
Ok(crate::script::binding_abi::predicate(checked))
})?,
)?;
lua.set_named_registry_value(REG_CHECKBUTTON_METHODS, c)?;
+18
View File
@@ -203,6 +203,24 @@ pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
fn frame_kind_from_str(s: &str) -> Option<FrameKind> {
Some(match enum_token(s).as_str() {
"FRAME" => FrameKind::Frame,
// `TaxiRouteFrame` — a registered `CreateFrame` type that is a `CSimpleFrame` and NOTHING
// else, so it maps to `Frame` rather than earning a kind (decision 1828; wow-re
// `ui/scratch/taxiroute-widget-type.md`). Factory `0x495ba0` allocates `0x314`, the same
// size the plain-`<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.
"TAXIROUTEFRAME" => FrameKind::Frame,
"BUTTON" => FrameKind::Button,
"CHECKBUTTON" => FrameKind::CheckButton,
// A real registered type (`0x4959a6`), not an alias for Button — decision 1799. Registering
@@ -37,7 +37,9 @@ 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(model.arena.frame(h).map(|f| f.shown).unwrap_or(false))
Ok(crate::script::binding_abi::predicate(
model.arena.frame(h).map(|f| f.shown).unwrap_or(false),
))
})?,
)?;
m.set(
@@ -45,11 +47,13 @@ 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(model
.arena
.frame(h)
.map(|f| f.effective_visible)
.unwrap_or(false))
Ok(crate::script::binding_abi::predicate(
model
.arena
.frame(h)
.map(|f| f.effective_visible)
.unwrap_or(false),
))
})?,
)?;
// Identity / hierarchy
@@ -669,7 +673,9 @@ 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(model.arena.is_mouse_enabled(h))
Ok(crate::script::binding_abi::predicate(
model.arena.is_mouse_enabled(h),
))
})?,
)?;
// EnableKeyboard(flag) / IsKeyboardEnabled() — `0x776f90` / `0x776ff0`, real Frame entries in
@@ -703,7 +709,9 @@ 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(model.arena.is_keyboard_enabled(h))
Ok(crate::script::binding_abi::predicate(
model.arena.is_keyboard_enabled(h),
))
})?,
)?;
// `EnableMouseWheel(flag)` / `IsMouseWheelEnabled()` — the wheel's own gate, a separate flag
@@ -769,7 +777,9 @@ 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(model.arena.is_mouse_wheel_enabled(h))
Ok(crate::script::binding_abi::predicate(
model.arena.is_mouse_wheel_enabled(h),
))
})?,
)?;
// Clamp-to-screen (`0x776c00`/`0x776cb0`, geometry flags bit4 — layout.md): the layout resolve
@@ -793,7 +803,9 @@ 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(model.arena.is_clamped_to_screen(h))
Ok(crate::script::binding_abi::predicate(
model.arena.is_clamped_to_screen(h),
))
})?,
)?;
// Hit-rect insets — the MOUSE rect only (widget::Frame::hit_rect_insets): the hit test shrinks
+10 -6
View File
@@ -404,14 +404,18 @@ fn set_flag(lua: &Lua, this: &Table, which: Flag, value: bool) -> mlua::Result<(
Ok(())
}
fn get_flag(lua: &Lua, this: &Table, which: Flag) -> mlua::Result<bool> {
/// `IsMovable`/`IsResizable`/`IsUserPlaced` — the NUMBER 1 or nil, never a boolean (1830,
/// [`crate::script::binding_abi::predicate`]).
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(model.arena.frame(h).is_some_and(|f| match which {
Flag::Movable => f.movable,
Flag::Resizable => f.resizable,
Flag::UserPlaced => f.user_placed,
}))
Ok(crate::script::binding_abi::predicate(
model.arena.frame(h).is_some_and(|f| match which {
Flag::Movable => f.movable,
Flag::Resizable => f.resizable,
Flag::UserPlaced => f.user_placed,
}),
))
}
/// The family's refusal, named frame and all — the reference's own shape (it formats the frame's
@@ -125,7 +125,9 @@ 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(model.arena.is_toplevel(h))
Ok(crate::script::binding_abi::predicate(
model.arena.is_toplevel(h),
))
})?,
)?;
// Raise() — `0x775a50` → `0x76a5b0` → `0x7650f0(this, force = 1)`. Note it is legal on any
+2 -1
View File
@@ -220,7 +220,8 @@ fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() {
s.mouse_button(50.0, 50.0, "LeftButton", true);
s.mouse_button(50.0, 50.0, "LeftButton", false);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
assert!(s.eval::<bool>("return seen_checked == true").unwrap());
// `seen_checked` is whatever `GetChecked()` handed the handler — the NUMBER 1 (1830).
assert!(s.eval::<bool>("return seen_checked == 1").unwrap());
assert!(s.eval::<bool>("return Toggler:GetChecked()").unwrap());
// Programmatic Click() rides the same path: toggles back off.
+3 -1
View File
@@ -17,7 +17,9 @@ fn enable_mouse_gates_hit_testing() {
local b = CreateFrame("Frame", "B")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(false)
b:SetScript("OnEnter", function(self) who = self:GetName() end)
assert(a:IsMouseEnabled() == true and b:IsMouseEnabled() == false)
-- 1/nil, not a boolean (1830): this is the exact comparison shape that inverts, so the
-- test asserts it rather than leaning on truthiness.
assert(a:IsMouseEnabled() == 1 and b:IsMouseEnabled() == nil)
"#,
)
.unwrap();