Files
benilla-octo/crates/benilla-ui/src/script/binding_abi.rs
T
SamandClaude Fable 5 80d8bd48db four extra action bars as options, and the bottom band gets one owner
- the four extra action bars become per-character options, off by default, on a new Action Bars options page — the toggles ride the server's own byte (`CMSG_SET_ACTIONBAR_TOGGLES` into `PLAYER_FIELD_BYTES`), so a fresh character's zero is the default for free; turning the bottom-left bar off gives the stance bar its previously unbuilt unraised look, end-caps and 64×64 rings
- bag windows stop drawing straight through a raised bottom bar: the bottom band has ONE owner — the manage pass — the bag stack joins it through a listener seam, and a guard test walks all eight raisable-bar combinations across 24 bar/tenant pairs and refuses overlap
- the item-push card takes the reference's real geometry, closing the gap 0887 measured
- resizable addon windows stop at their bounds instead of pinning and walking across the screen: four per-frame f32s with 0.0 the disable sentinel, the clamp on the interactive drag only with the residual rebated out of the drag delta, `<ResizeBounds>` in XML honoured — and `SetFontString`'s three raises land exactly as the bytes have them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 18:12:19 +07:00

258 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The **registered-binding argument/error ABI** — the marshalling contract every one of build
//! 5875's ~700 registered Lua C-bindings opens with, in one place because getting it wrong is
//! wrong 700 times (wow-re `ui/scratch/binding-arg-error-contract.md`, §5-cross-checked with
//! orchestrator byte-arbitration).
//!
//! ## The three helpers the reference uses, and what they accept
//!
//! | addr | contract |
//! |---|---|
//! | `0x6f34d0` | `is-number(L, idx)` — tag 3, **or** a string coerced through `0x6f7c20` |
//! | `0x6f3510` | `is-number-**or**-string(L, idx)` — strictly wider: tag 3 or tag 4 |
//! | `0x6f3620` → `0x40a2b0` | `tonumber` then `double → int32`, **truncating toward zero** |
//!
//! `0x40a2b0` sets the x87 rounding control to chop (`or ah,0xc`) before `fistp`, so it is a C
//! cast and **not** `floor`: `−2.9 → −2`. [`number_arg`] reproduces that (Rust's `as` on a float
//! truncates toward zero as well), and hands back the low dword the bindings actually consume.
//!
//! ## The headline: the `Usage:` arm RAISES
//!
//! `0x6f4940` is `luaL_error`, and it **does not return**: `luaL_where` + `lua_pushvfstring` +
//! `lua_concat` + `lua_error`, whose own chain (`0x6f4440` → `0x6fc780` → `0x6f5d80`) ends in a
//! CRT `longjmp` or `exit(1)` on both legs. The `xor eax,eax; ret` that MSVC emits after every
//! such call inside a binding is **unreachable boilerplate**, not a "returns zero values" arm — a
//! reading five committed wow-re notes had inverted, corrected at the bytes on 2026-08-11.
//!
//! So a binding's bad-argument path **abandons the caller's statement** and unwinds to the
//! enclosing protected call. It returns neither `nil` nor zero values, and a client that answers
//! `nil` there keeps executing a statement the real client never finishes.
//!
//! Stated as the negative, because it is as load-bearing as the positive: the **zero Lua values**
//! outcome is real only where a binding reaches `xor eax,eax; ret` *without* passing through
//! `0x6f4940` — `GetDefaultLanguage`'s four object/bounds failure edges are this repo's one
//! example ([`super::session`]). Those arms genuinely return nothing.
//!
//! ## The three shapes an "empty answer" can take
//!
//! Settled per binding, never generalised:
//!
//! 1. **push nil**, one Lua value (`UnitAffectingCombat` on a false/unresolved unit,
//! `GetActionText` on a non-macro slot, `UnitInRaid` on a miss).
//! 2. **zero Lua values** — distinct from `nil` for `select('#', …)` and for a multiple
//! assignment, identical to it for a single-value caller (`GetDefaultLanguage`'s failure edges).
//! 3. **raise** — the statement is abandoned.
use mlua::{Lua, Value};
/// `is-number` (`0x6f34d0`) → `tonumber` (`0x6f3620`) → `double → int32` truncating toward zero
/// (`0x40a2b0`), with the binding's own `Usage:` string on the failure edge.
///
/// A **missing** argument fails the same test a wrong-typed one does — `0x6f3410` returns NULL
/// past `L->top`, which `0x6f34d0` reports as "not a number" — so `f()` and `f({})` take the same
/// raise. A numeric *string* passes: Lua 5.1's `lua_isnumber` coerces, and so does
/// [`Lua::coerce_number`].
///
/// The `i32` is deliberate: `0x40a2b0` stores a qword but every binding here consumes only `eax`,
/// the low dword. Rust's `i64 as i32` truncates the same way. (Past ±2^63 the two diverge — the
/// x87 stores the "integer indefinite" pattern where Rust saturates — which no caller can reach
/// and no consumer could tell apart, since both answers are garbage.)
pub(crate) fn number_arg(lua: &Lua, v: Value, usage: &'static str) -> mlua::Result<i32> {
match lua.coerce_number(v)? {
Some(n) => Ok(n as i64 as i32),
None => Err(mlua::Error::RuntimeError(usage.into())),
}
}
/// `is-number-or-string` (`0x6f3510`) → `arg-as-C-string` (`0x6f3690`), with the binding's own
/// `Usage:` string on the failure edge.
///
/// Strictly wider than [`number_arg`]'s guard: a *number* is accepted and stringified, which is
/// why `UnitAffectingCombat(5)` does not raise in the reference — it resolves the token `"5"`,
/// finds nothing, and answers `nil`. Everything else (nil, boolean, table, function) raises.
pub(crate) fn string_arg(lua: &Lua, v: Value, usage: &'static str) -> mlua::Result<String> {
match lua.coerce_string(v)? {
Some(s) => Ok(s.to_str()?.to_owned()),
None => Err(mlua::Error::RuntimeError(usage.into())),
}
}
/// `GetBoolOrDefault` (`0x6f1c10`) — the reference's **boolean argument** coercion, which is *not*
/// Lua truthiness and gets three arms backwards from the obvious reading. Byte-VERIFIED: wow-re
/// `system/ui/scratch/action-bar-toggles.md` §2.1 re-derives the whole jump table at `0x6f1ce8`
/// (`ui.md`, `tooltip-content-law.md` and `object-layer/scratch/helm-cloak-hide.md` cite the same
/// helper).
///
/// `v` is `None` for an **absent** argument (`LUA_TNONE` = −1, unsigned-compares above the table's
/// bound and takes the default arm). mlua collapses "missing" into `Value::Nil` for a plain
/// `Value` parameter, and the two are *different* here, so a binding whose default is `true` must
/// take its argument as a `MultiValue` to tell them apart.
///
/// | Lua type | result |
/// |---|---|
/// | absent (`None`) | `default` |
/// | `nil` | **false** |
/// | boolean | itself |
/// | lightuserdata | `default` |
/// | number | truncate toward zero (`0x40a2b0`), then `!= 0` |
/// | string | the table below |
/// | table / function / userdata / thread | `default` |
///
/// The **string** arm is the one that bites. It does not parse the number; it dispatches on the
/// **first byte** through the 0x4a-entry remap table at `0x6f1d08` (indexed `byte − 0x30`,
/// *signed*, so anything outside `'0'..='y'` — an empty string's terminator included — falls to the
/// keyword arm):
///
/// - first byte `'0' 'F' 'N' 'f' 'n'` → **false**
/// - first byte `'1'..='9' 'T' 'Y' 't' 'y'` → **true**
/// - anything else → whole-string, case-insensitive (`SStrCmpI 0x64a4c0`): `"off"`/`"disabled"` →
/// false, `"on"`/`"enabled"` → true, and **anything unrecognised takes the `default`**
///
/// So `"0"` is FALSE where Lua truthiness says true (in Lua every string is truthy), which is
/// load-bearing rather than pedantic: 1.12's own Interface panel hands its option bindings the
/// *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.
pub(crate) fn bool_or_default(v: Option<&Value>, default: bool) -> bool {
let Some(v) = v else {
return default; // LUA_TNONE
};
match v {
Value::Nil => false,
Value::Boolean(b) => *b,
Value::LightUserData(_) => default,
// `lua_tonumber` then `0x40a2b0`, whose RC = chop makes it a C cast; the caller tests the
// low dword only, which is why the double round-trips through `i64 as i32` (see
// [`number_arg`]).
Value::Integer(i) => (*i as i32) != 0,
Value::Number(n) => (*n as i64 as i32) != 0,
Value::String(s) => {
let bytes = s.as_bytes();
match bytes.first() {
Some(b'0' | b'F' | b'N' | b'f' | b'n') => false,
Some(b'1'..=b'9' | b'T' | b'Y' | b't' | b'y') => true,
// The keyword arm — and the fall-through for every byte the remap table cannot
// index, which is why an empty string lands here too.
_ => {
if bytes.eq_ignore_ascii_case(b"off") || bytes.eq_ignore_ascii_case(b"disabled")
{
false
} else if bytes.eq_ignore_ascii_case(b"on")
|| bytes.eq_ignore_ascii_case(b"enabled")
{
true
} else {
default
}
}
}
}
_ => default,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lua() -> Lua {
Lua::new()
}
/// Every arm of `0x6f1c10`, including the three a plausible implementation gets backwards.
#[test]
fn bool_or_default_is_not_lua_truthiness() {
let lua = lua();
let st = |t: &str| Value::String(lua.create_string(t).unwrap());
// Absent takes the DEFAULT; an explicit nil is FALSE. The two are different arms.
assert!(bool_or_default(None, true));
assert!(!bool_or_default(None, false));
assert!(!bool_or_default(Some(&Value::Nil), true));
// Booleans pass through.
assert!(bool_or_default(Some(&Value::Boolean(true)), false));
assert!(!bool_or_default(Some(&Value::Boolean(false)), true));
// Numbers TRUNCATE toward zero — they do not round. `0.5` is off, not on.
assert!(!bool_or_default(Some(&Value::Number(0.5)), true));
assert!(!bool_or_default(Some(&Value::Number(-0.5)), true));
assert!(!bool_or_default(Some(&Value::Number(-0.999)), true));
assert!(bool_or_default(Some(&Value::Number(1.5)), false));
assert!(bool_or_default(Some(&Value::Number(-1.5)), false));
assert!(!bool_or_default(Some(&Value::Integer(0)), true));
assert!(bool_or_default(Some(&Value::Integer(7)), false));
// THE headline: `"0"` is FALSE, where Lua truthiness would say true.
assert!(!bool_or_default(Some(&st("0")), true));
assert!(bool_or_default(Some(&st("1")), false));
// First byte only — `"0.5"` is false because it starts `'0'`, not because it truncates.
assert!(!bool_or_default(Some(&st("0.5")), true));
assert!(bool_or_default(Some(&st("9lives")), false));
// The letter arms.
for t in ["false", "F", "no", "NIL", "nope"] {
assert!(
!bool_or_default(Some(&st(t)), true),
"{t} starts F/N → false"
);
}
for t in ["true", "T", "yes", "Yup"] {
assert!(
bool_or_default(Some(&st(t)), false),
"{t} starts T/Y → true"
);
}
// The keyword arm, case-insensitive whole-string.
assert!(!bool_or_default(Some(&st("off")), true));
assert!(!bool_or_default(Some(&st("OFF")), true));
assert!(!bool_or_default(Some(&st("Disabled")), true));
assert!(bool_or_default(Some(&st("on")), false));
assert!(bool_or_default(Some(&st("ENABLED")), false));
// Unrecognised strings — and every byte the remap table cannot index — take the DEFAULT.
for t in ["", "-1", "?", "maybe", "@"] {
assert!(bool_or_default(Some(&st(t)), true), "{t:?} → default true");
assert!(
!bool_or_default(Some(&st(t)), false),
"{t:?} → default false"
);
}
// Table/function take the default arm, like an absent argument.
let tbl = Value::Table(lua.create_table().unwrap());
assert!(bool_or_default(Some(&tbl), true));
assert!(!bool_or_default(Some(&tbl), false));
}
#[test]
fn number_arg_truncates_toward_zero_not_floor() {
let lua = lua();
// `0x40a2b0` sets RC = chop before `fistp` — a C cast, not `floor`.
assert_eq!(number_arg(&lua, Value::Number(2.9), "u").unwrap(), 2);
assert_eq!(number_arg(&lua, Value::Number(-2.9), "u").unwrap(), -2);
assert_eq!(number_arg(&lua, Value::Number(-0.5), "u").unwrap(), 0);
}
#[test]
fn number_arg_accepts_a_numeric_string_and_raises_on_everything_else() {
let lua = lua();
let s = lua.create_string("7").unwrap();
assert_eq!(number_arg(&lua, Value::String(s), "u").unwrap(), 7);
// Missing and wrong-typed take the SAME arm: `0x6f3410` hands `0x6f34d0` a NULL for an
// absent argument, which reports "not a number" exactly as a table does.
for v in [Value::Nil, Value::Boolean(true)] {
let err = number_arg(&lua, v, "Usage: Thing(n)").unwrap_err();
assert!(format!("{err}").contains("Usage: Thing(n)"));
}
}
#[test]
fn string_arg_accepts_a_number_and_raises_on_nil() {
let lua = lua();
assert_eq!(string_arg(&lua, Value::Integer(5), "u").unwrap(), "5");
let err = string_arg(&lua, Value::Nil, "Usage: Thing(\"s\")").unwrap_err();
assert!(format!("{err}").contains("Usage: Thing(\"s\")"));
}
}