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:
@@ -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
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user