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:
@@ -163,6 +163,83 @@ 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.
|
||||
/// Lua 5.0's own number→string rule as the 1.12 client compiles it: `sprintf("%.14g")`
|
||||
/// (`luaV_tostring 0x6f7c80`, format string `0x871960`). Decision 1831.
|
||||
///
|
||||
/// This is the whole of `EditBox:SetNumber`'s formatting, because **`SetNumber 0x798690` and
|
||||
/// `SetText 0x7984c0` are byte-identical functions** — 245 bytes each, zero differences after
|
||||
/// masking rel32 and absolute-VA operands, differing only in which usage string they carry. The
|
||||
/// numeric verb does no numeric work of its own; the shared `lua_tostring` marshalling does it all.
|
||||
///
|
||||
/// The rule, and the three places it is not Rust's `{}`:
|
||||
///
|
||||
/// - **14 significant digits**, then trailing zeros and a bare trailing `.` are dropped.
|
||||
/// - **Exponential iff `exp < -4 || exp >= 14`** — the C `%g` switch.
|
||||
/// - **A three-digit exponent, always.** `1e20` prints `1e+020`, not `1e+20`: the exponent is
|
||||
/// copied from the literal template `"e+000"` and digits are written onto it. This is MSVC's
|
||||
/// `%g`, not C99's, and it is the single most surprising thing here.
|
||||
///
|
||||
/// **What this deliberately does NOT reproduce**, stated so it is not mistaken for an oversight:
|
||||
/// the reference's printf is not correctly rounded. It generates 17 digits half-up and then
|
||||
/// re-rounds *that string* to 14, also half-up and with no sticky bit, so it disagrees with a
|
||||
/// correctly-rounded formatter on roughly 0.05% of doubles (`-0.11594045739607` where a correct
|
||||
/// one gives `…606`). Reproducing that needs the two-stage decimal path, not a format string, and
|
||||
/// no consumer we have is sensitive to the fourteenth digit. If one ever is, this is the note that
|
||||
/// says where to look.
|
||||
pub(crate) fn lua_number_text(v: f64) -> String {
|
||||
if v.is_nan() {
|
||||
// MSVC's own spellings, which are not "NaN".
|
||||
return if v.is_sign_negative() {
|
||||
"-1.#IND".into()
|
||||
} else {
|
||||
"1.#QNAN".into()
|
||||
};
|
||||
}
|
||||
if v.is_infinite() {
|
||||
return if v < 0.0 {
|
||||
"-1.#INF".into()
|
||||
} else {
|
||||
"1.#INF".into()
|
||||
};
|
||||
}
|
||||
if v == 0.0 {
|
||||
// `%g` prints negative zero without the sign.
|
||||
return "0".into();
|
||||
}
|
||||
const SIG: i32 = 14;
|
||||
let exp = v.abs().log10().floor() as i32;
|
||||
// Re-derive the exponent from the rounded form: 9.9999e2 rounds to 1e3 and changes style.
|
||||
let exp = {
|
||||
let probe = format!("{:.*e}", (SIG - 1) as usize, v);
|
||||
probe
|
||||
.rsplit('e')
|
||||
.next()
|
||||
.and_then(|e| e.parse::<i32>().ok())
|
||||
.unwrap_or(exp)
|
||||
};
|
||||
// C's `%g` style rule verbatim: exponential when `exp < -4 || exp >= P`, else fixed.
|
||||
if !(-4..SIG).contains(&exp) {
|
||||
let mantissa = format!("{:.*e}", (SIG - 1) as usize, v);
|
||||
let (m, _) = mantissa.split_once('e').unwrap_or((mantissa.as_str(), "0"));
|
||||
let m = trim_g(m);
|
||||
let sign = if exp < 0 { '-' } else { '+' };
|
||||
// Three digits minimum, and MORE if the exponent needs them — the template is padded,
|
||||
// never truncated.
|
||||
format!("{m}e{sign}{:03}", exp.abs())
|
||||
} else {
|
||||
let decimals = (SIG - 1 - exp).max(0) as usize;
|
||||
trim_g(&format!("{v:.decimals$}")).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip `%g`'s trailing zeros, and the decimal point if nothing follows it.
|
||||
fn trim_g(s: &str) -> &str {
|
||||
if !s.contains('.') {
|
||||
return s;
|
||||
}
|
||||
s.trim_end_matches('0').trim_end_matches('.')
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -98,7 +98,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// BenillaDressUpModel_SetFacing(radians) — the pane's bake yaw, the exact twin of
|
||||
// `BenillaPaperDollModel_SetFacing` / `BenillaInspectModel_SetFacing`.
|
||||
// `BenillaPaperDollModel_SetFacing` (the inspect pane's twin went with 1832's migration).
|
||||
g.set(
|
||||
"BenillaDressUpModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(super) fn paste(lua: &Lua, text: &str) -> bool {
|
||||
// this layer's.
|
||||
if with_eb(lua, h, |eb| eb.paste(text)).is_some_and(|o| o.text_changed) {
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -339,7 +339,7 @@ fn insert(lua: &Lua, h: FrameHandle, ins: &str, fire_space: bool) {
|
||||
.errors
|
||||
.push(e.to_string());
|
||||
}
|
||||
fire_script(lua, id, "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
if fire_space {
|
||||
for _ in 0..out.spaces {
|
||||
fire_script(lua, id, "OnSpacePressed");
|
||||
@@ -355,7 +355,7 @@ fn set_text(lua: &Lua, h: FrameHandle, s: &str) {
|
||||
sync_text_region(lua, h);
|
||||
let id = frame_id_of(lua, h);
|
||||
fire_script(lua, id, "OnTextSet");
|
||||
fire_script(lua, id, "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +369,7 @@ fn delete_span(lua: &Lua, h: FrameHandle, target_of: impl FnOnce(&EditBoxState)
|
||||
});
|
||||
if changed == Some(true) {
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ fn delete_dir(lua: &Lua, h: FrameHandle, forward: bool) {
|
||||
let changed = with_eb(lua, h, |eb| eb.delete_dir(forward));
|
||||
if changed == Some(true) {
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,3 +618,55 @@ pub(super) use methods::install;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Raise the `textChanged` dirty bit (`[E+0x31c]` bit 0) rather than firing — **`OnTextChanged` is
|
||||
/// deferred**, and this is the whole of decision 1831's second half.
|
||||
///
|
||||
/// The reference never fires it from an edit. `SetText 0x77be00`, `Insert 0x77bee0` and the deletes
|
||||
/// all only OR the bit; the single fire site is `0x77d498`, inside the dirty-word drain `0x77d3e0`,
|
||||
/// whose three callers are the box's own `OnUpdate` (`0x77a790`), its `OnKeyDown` (`0x77b160`) and
|
||||
/// its `OnMouseDown` (`0x77b800`). So a Lua caller gets control back with the handler still
|
||||
/// unrun, and repeated changes **coalesce**: the box appears once on the pending list however many
|
||||
/// times it is written, and the one fire carries the final text.
|
||||
pub(super) fn mark_text_changed(lua: &Lua, h: FrameHandle) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if !model.dirty_editboxes.contains(&h) {
|
||||
model.dirty_editboxes.push(h);
|
||||
}
|
||||
}
|
||||
|
||||
/// The dirty-word drain (`0x77d3e0`): fire the pending `OnTextChanged`s.
|
||||
///
|
||||
/// Called from the frame tick — the first of the reference's three callers (`0x77a7a1`, inside the
|
||||
/// box's own `OnUpdate` `0x77a790`, which is also where our caret blink lives). Its other two, the
|
||||
/// box's `OnKeyDown` and `OnMouseDown`, are **not** wired: they only make a pending fire arrive
|
||||
/// sooner within a frame, and our key and mouse paths reach the tick anyway. Decision 1831 says so
|
||||
/// out loud rather than leaving it to be discovered.
|
||||
///
|
||||
/// A box that is **not effectively visible stays pending**: `Hide`
|
||||
/// splices it out of the chain the update walk follows, so its fire waits for a `Show` (verified
|
||||
/// chain mechanics; the "hidden ⇒ no OnUpdate" step is the RE's own stated inference).
|
||||
///
|
||||
/// A handler may itself write to a box — including this one — so the list is taken before any Lua
|
||||
/// runs and anything re-marked during the sweep lands on the NEXT drain rather than extending this
|
||||
/// one. That is the reference's shape too: its bit is cleared before its fire.
|
||||
pub(in crate::script) fn drain_text_changed(lua: &Lua) {
|
||||
let pending = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if model.dirty_editboxes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (ready, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut model.dirty_editboxes)
|
||||
.into_iter()
|
||||
.partition(|&h| model.arena.frame(h).is_some_and(|f| f.effective_visible));
|
||||
// A frame that has been destroyed drops out of both halves.
|
||||
model.dirty_editboxes = waiting
|
||||
.into_iter()
|
||||
.filter(|&h| model.arena.frame(h).is_some())
|
||||
.collect();
|
||||
ready
|
||||
};
|
||||
for h in pending {
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,12 +146,6 @@ impl super::UiScript {
|
||||
pub fn take_inspect_clear(&mut self) -> bool {
|
||||
std::mem::take(&mut self.model_mut().inspect_clear)
|
||||
}
|
||||
|
||||
/// The inspect model pane's bake yaw in radians — the twin of
|
||||
/// [`Self::paperdoll_yaw`], read by the app onto the `"inspect"` booth slot each frame.
|
||||
pub fn inspect_yaw(&self) -> f32 {
|
||||
self.model_ref().inspect_yaw
|
||||
}
|
||||
}
|
||||
|
||||
/// What the app resolved for `token`, or `None` when it resolved to no live unit at all — the
|
||||
@@ -271,19 +265,5 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BenillaInspectModel_SetFacing(radians) — the inspect pane's own bake yaw, the exact twin of
|
||||
// `BenillaPaperDollModel_SetFacing` (decision 0208 §5's rotate-adjusts-the-bake). Two scalars
|
||||
// rather than one shared: the two windows can be open at different facings, and the ref's own
|
||||
// `InspectModelFrame` carries its own `rotation` independent of the character pane's.
|
||||
g.set(
|
||||
"BenillaInspectModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.inspect_yaw = radians;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -288,6 +288,18 @@ pub(crate) struct Model {
|
||||
/// resolve at a 3,988-frame roster, which made it the biggest phase left in the preamble
|
||||
/// (decision 1634's `[layout-pre] watched=`).
|
||||
pub(crate) on_size_changed_frames: Vec<FrameHandle>,
|
||||
/// Edit boxes whose text changed and whose `OnTextChanged` has **not fired yet** — the
|
||||
/// reference's `textChanged` dirty bit (`[E+0x31c]` bit 0), which `SetText`/`Insert` raise and
|
||||
/// only the dirty-word drain `0x77d3e0` clears (decision 1831).
|
||||
///
|
||||
/// The fire is deferred, not immediate: `SetText` returns to Lua before any handler runs, and N
|
||||
/// changes to one box between two drains produce exactly ONE fire carrying the final text. The
|
||||
/// reference's own FrameXML depends on both halves — `MoneyInputFrame_SetCopper` sets three
|
||||
/// boxes while counting the fires it expects back, and errors if one arrives mid-loop.
|
||||
///
|
||||
/// A hidden box stays on this list: `Hide` splices it out of the chain the update walk follows,
|
||||
/// so its pending fire waits until it is shown again (or a key/mouse-down reaches it).
|
||||
pub(crate) dirty_editboxes: Vec<FrameHandle>,
|
||||
/// `event name → frames registered for it` (RegisterEvent), in **registration order** — an
|
||||
/// ordered Vec, never a set: the client's `SignalEvent 0x703e50` walks a per-event listener
|
||||
/// LIST, so cross-frame dispatch order is a law, not an accident (the abbey territory-line
|
||||
@@ -1330,8 +1342,6 @@ pub(crate) struct Model {
|
||||
/// The inspect model pane's bake yaw — the last of the benilla-named pane scalars beside
|
||||
/// [`Self::dressup_yaw`]. A migrated window's pane carries its own facing in `ModelState`
|
||||
/// (`UiScript::model_pane_facing`, decision 1751); these two remain because their windows are
|
||||
/// still ours, and each retires with its file.
|
||||
pub(crate) inspect_yaw: f32,
|
||||
/// The dressing room's queued intents (decision 1060) — `BenillaDressUpModel_Dress/TryOn/Close`,
|
||||
/// drained by the app in order (see [`super::dressup`] on why order matters).
|
||||
pub(crate) dressup_intents: Vec<super::dressup::DressUpIntent>,
|
||||
@@ -1574,6 +1584,7 @@ impl Model {
|
||||
scripts: HashMap::new(),
|
||||
on_update_frames: Vec::new(),
|
||||
on_size_changed_frames: Vec::new(),
|
||||
dirty_editboxes: Vec::new(),
|
||||
event_to_frames: HashMap::new(),
|
||||
frame_events: HashMap::new(),
|
||||
focused_editbox: None,
|
||||
@@ -1865,7 +1876,6 @@ impl Model {
|
||||
inspect: None,
|
||||
inspect_notifies: Vec::new(),
|
||||
inspect_clear: false,
|
||||
inspect_yaw: 0.0,
|
||||
dressup_intents: Vec::new(),
|
||||
dressup_yaw: 0.0,
|
||||
unit_reach: HashMap::new(),
|
||||
|
||||
@@ -117,7 +117,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// the bake replaces them. A later `SetTexture`/`SetPortraitToTexture` clears the binding.
|
||||
lua.globals().set(
|
||||
"SetPortraitTexture",
|
||||
lua.create_function(|lua, (region, unit): (Table, String)| {
|
||||
lua.create_function(|lua, (region, unit): (Table, Value)| {
|
||||
// `0x519ef0` gates its unit argument at `0x519fb4` with `lua_isstring` and fails into
|
||||
// `luaL_error` with this exact string (`0x8513b8`); `luaL_error` does not return, so
|
||||
// nil/absent/boolean/table abandon the caller's statement. Stock
|
||||
// `Blizzard_InspectUI.lua:49` reaches it on a re-open, which is how it was found.
|
||||
// A token that RESOLVES to nothing is a different, quiet case — it blanks the portrait
|
||||
// and returns no values. Decision 1834.
|
||||
let unit = crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
unit,
|
||||
r#"Usage: SetPortraitTexture(texture, "unit")"#,
|
||||
)?;
|
||||
let rh = region_handle_of(lua, ®ion)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
|
||||
@@ -107,6 +107,11 @@ impl UiScript {
|
||||
}
|
||||
// The focused edit box's caret blink (`0x77a790` runs on the client's frame tick).
|
||||
editbox::tick_blink(&self.lua, elapsed);
|
||||
// …and, from inside that same `0x77a790` (`0x77a7a1`), the dirty-word drain that fires the
|
||||
// `OnTextChanged`s an edit only marked (decision 1831). It runs BEFORE this tick's
|
||||
// OnUpdate sweep, matching the reference's order: the box's own OnUpdate override drains
|
||||
// before the FrameXML handlers that read the box get their turn.
|
||||
editbox::drain_text_changed(&self.lua);
|
||||
// Events queued by Lua bindings last tick (`Model::pending_events` — e.g. `SetMapZoom` →
|
||||
// `WORLD_MAP_UPDATE`; the cursor arc's `CURSOR_UPDATE`/`ITEM_LOCK_CHANGED`/
|
||||
// `DELETE_ITEM_CONFIRM`, decision 0216) fire first, so handlers see them before this
|
||||
|
||||
@@ -72,8 +72,11 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `lua_isstring` gate admits tag 3 and hands the resolver `"5"`). `check_unit_token` inside
|
||||
// `with_unit` is the second of those; the first is the `?` on the argument type below.
|
||||
// The gate is the BINDING's, not `with_unit`'s: `check_unit_token` lets a nil through by
|
||||
// design, because that is right for `UnitExists`, `UnitIsVisible` and most of this family. It
|
||||
// is wrong for exactly these two, so the shape-A test happens here, before the shared path.
|
||||
// design, because that is right for `UnitExists`, `UnitIsVisible` and eleven others. **"and
|
||||
// most of this family" is what this comment used to say, and it was the wrong way round** —
|
||||
// wow-re has since censused all 83 entries of the table at `0x850438` and 53 of them gate and
|
||||
// raise, with only 13 unit-token bindings quiet (decision 1834). These two were never the
|
||||
// exception; they were an early instance of the rule.
|
||||
for (name, usage, by_player) in [
|
||||
("UnitIsTapped", r#"Usage: UnitIsTapped("unit")"#, false),
|
||||
(
|
||||
@@ -151,7 +154,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
g.set(
|
||||
"UnitName",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitName("unit")"#,
|
||||
)?);
|
||||
let name = with_unit(lua, &token, None, |u| u.name.clone())?;
|
||||
match name {
|
||||
Some(n) => Ok(Value::String(lua.create_string(&n)?)),
|
||||
@@ -162,14 +170,24 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
g.set(
|
||||
"UnitHealth",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitHealth("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.health))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
g.set(
|
||||
"UnitHealthMax",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitHealthMax("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.max_health))
|
||||
})?,
|
||||
)?;
|
||||
@@ -183,7 +201,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `b` INFERRED and 0 in normal play — it can never drive the value ≤ 0 anyway).
|
||||
g.set(
|
||||
"UnitLevel",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitLevel("unit")"#,
|
||||
)?);
|
||||
check_unit_token(&token)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let Some(u) = token.as_ref().and_then(|t| model.unit(t)) else {
|
||||
@@ -223,7 +246,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// (`UnitCanAttack("player", "target")`).
|
||||
g.set(
|
||||
"UnitCanAttack",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitCanAttack("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitCanAttack("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.can_attack)? {
|
||||
Value::Integer(1)
|
||||
@@ -247,7 +282,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
g.set(
|
||||
"UnitIsDead",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsDead("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, false, |u| u.dead)
|
||||
})?,
|
||||
)?;
|
||||
@@ -256,13 +296,23 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// so IsDead is false for it and the popup flow branches on all three.
|
||||
g.set(
|
||||
"UnitIsGhost",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsGhost("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, false, |u| u.ghost)
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"UnitIsDeadOrGhost",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsDeadOrGhost("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, false, |u| u.dead || u.ghost)
|
||||
})?,
|
||||
)?;
|
||||
@@ -274,7 +324,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// frame paints its name plate blue then, exactly like the reference.
|
||||
g.set(
|
||||
"UnitReaction",
|
||||
lua.create_function(|lua, (token, _other): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (token, _other): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitReaction("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let _other = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
_other,
|
||||
r#"Usage: UnitReaction("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(if r == 0 {
|
||||
Value::Nil
|
||||
@@ -294,7 +356,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// reaction (0) → nil for both, the API's "can't tell". `1`/nil returns, era-style.
|
||||
g.set(
|
||||
"UnitIsEnemy",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitIsEnemy("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitIsEnemy("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(if (1..=2).contains(&r) {
|
||||
@@ -306,7 +380,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitIsFriend",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitIsFriend("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitIsFriend("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(if r >= 5 {
|
||||
@@ -335,7 +421,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// target plate disagree about the same mob.
|
||||
g.set(
|
||||
"UnitIsCivilian",
|
||||
lua.create_function(|lua, unit: Option<String>| {
|
||||
lua.create_function(|lua, unit: Value| {
|
||||
let unit = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
unit,
|
||||
r#"Usage: UnitIsCivilian("unit")"#,
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let player_level = model.player_req.level;
|
||||
Ok(match unit.and_then(|u| model.unit(&u)) {
|
||||
@@ -374,7 +465,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// is trivially the same unit; otherwise both snapshots must carry a real (nonzero) guid.
|
||||
g.set(
|
||||
"UnitIsUnit",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitIsUnit("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitIsUnit("unit", "otherUnit")"#,
|
||||
)?);
|
||||
// BOTH arguments go through the resolver, so either being unrecognised raises.
|
||||
check_unit_token(&a)?;
|
||||
check_unit_token(&b)?;
|
||||
@@ -494,7 +597,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// whisper a same-faction player), without the faction machinery the engine doesn't carry.
|
||||
g.set(
|
||||
"UnitCanCooperate",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitCanCooperate("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitCanCooperate("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let ok = with_unit(lua, &token, false, |u| {
|
||||
u.exists && u.is_player && u.reaction >= 5
|
||||
@@ -507,7 +622,14 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// fed per token).
|
||||
g.set(
|
||||
"GetRaidTargetIndex",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
// Gated (`0x4bb4be` → `0x4bb4cd`), and the usage string names the
|
||||
// argument unquoted — the reference's own spelling. Decision 1836.
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
"Usage: GetRaidTargetIndex(unit)",
|
||||
)?);
|
||||
let idx = with_unit(lua, &token, 0u8, |u| u.raid_target)?;
|
||||
Ok(if idx > 0 {
|
||||
Value::Integer(i64::from(idx))
|
||||
@@ -522,7 +644,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// stated v1 shortcut, module doc), these are new and follow the era shape from the start.
|
||||
g.set(
|
||||
"UnitIsConnected",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsConnected("unit")"#,
|
||||
)?);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.is_connected)? {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
@@ -554,7 +681,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// two callers) — see the field doc.
|
||||
g.set(
|
||||
"UnitIsPVP",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsPVP("unit")"#,
|
||||
)?);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.pvp)? {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
@@ -564,7 +696,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitIsPVPFreeForAll",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsPVPFreeForAll("unit")"#,
|
||||
)?);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.is_pvp_ffa)? {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
@@ -588,7 +725,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// files are on our chain.
|
||||
g.set(
|
||||
"UnitFactionGroup",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitFactionGroup("unit")"#,
|
||||
)?);
|
||||
let pair = with_unit(lua, &token, (None, None), |u| {
|
||||
(u.faction_group.clone(), u.faction_group_localized.clone())
|
||||
})?;
|
||||
@@ -615,7 +757,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// reports nil, nil — the live API's shape for an absent unit.
|
||||
g.set(
|
||||
"UnitRace",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitRace("unit")"#,
|
||||
)?);
|
||||
let pair = with_unit(lua, &token, None, |u| {
|
||||
u.race.clone().zip(u.race_file.clone())
|
||||
})?;
|
||||
@@ -630,7 +777,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitClass",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitClass("unit")"#,
|
||||
)?);
|
||||
let pair = with_unit(lua, &token, None, |u| {
|
||||
u.class.clone().zip(u.class_file.clone())
|
||||
})?;
|
||||
@@ -667,7 +819,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// The truthy leg is the NUMBER 1, never a boolean; the false leg is nil, never `false`.
|
||||
g.set(
|
||||
"UnitHasRelicSlot",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitHasRelicSlot("unit")"#,
|
||||
)?);
|
||||
let has = with_unit(lua, &token, false, |u| u.has_relic_slot)?;
|
||||
Ok(if has { Value::Integer(1) } else { Value::Nil })
|
||||
})?,
|
||||
@@ -677,7 +834,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// unit is absent or the sex hasn't streamed (`0`), the API's "can't tell".
|
||||
g.set(
|
||||
"UnitSex",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitSex("unit")"#,
|
||||
)?);
|
||||
let sex = with_unit(lua, &token, 0u8, |u| u.sex)?;
|
||||
Ok(if sex == 0 {
|
||||
Value::Nil
|
||||
@@ -763,7 +925,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// alt-power color components; addons that read those handle nil).
|
||||
g.set(
|
||||
"UnitPowerType",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitPowerType("unit")"#,
|
||||
)?);
|
||||
let ty = with_unit(lua, &token, 0u8, |u| u.power_type)?;
|
||||
Ok((i64::from(ty), power_token(ty).to_string()))
|
||||
})?,
|
||||
@@ -782,13 +949,23 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// have" is spelled `UnitPowerType(unit)` first, which we already provide and which IS 1.12.
|
||||
g.set(
|
||||
"UnitMana",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitMana("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.power))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"UnitManaMax",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitManaMax("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.max_power))
|
||||
})?,
|
||||
)?;
|
||||
@@ -800,7 +977,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let is_player = |token: &Option<String>| token.as_deref() == Some("player");
|
||||
g.set(
|
||||
"UnitXP",
|
||||
lua.create_function(move |lua, token: Option<String>| {
|
||||
lua.create_function(move |lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitXP("unit")"#,
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(if is_player(&token) {
|
||||
i64::from(model.player_xp)
|
||||
@@ -811,7 +993,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitXPMax",
|
||||
lua.create_function(move |lua, token: Option<String>| {
|
||||
lua.create_function(move |lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitXPMax("unit")"#,
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(if is_player(&token) {
|
||||
i64::from(model.player_next_level_xp)
|
||||
|
||||
@@ -644,9 +644,14 @@ pub(crate) fn token_recognised(token: &str) -> bool {
|
||||
/// 1203's shape pointed the other way: a failure the client reports, silently swallowed.
|
||||
///
|
||||
/// The split is THREE-way, not two, and the two quiet legs are as carved:
|
||||
/// * **absent argument** — quiet nil (the per-binding argument gates are NOT uniform; `UnitName`
|
||||
/// has its own `lua_isstring` gate and `UnitExists` has none at all, and only those two poles
|
||||
/// are verified, so this does not invent a gate for the other ~102);
|
||||
/// * **absent argument** — quiet nil *here*, because this helper is only the resolver's half.
|
||||
/// Whether a nil ever reaches it is the BINDING's question, and it is settled per binding:
|
||||
/// wow-re's census of all 83 entries at `0x850438` found 53 that gate the token position with
|
||||
/// `lua_isstring` and raise `Usage:`, against 13 unit-token bindings with no gate at all
|
||||
/// (decision 1834). The gated ones call `binding_abi::string_arg` before they get here, so a
|
||||
/// nil never arrives; the quiet 13 pass it straight through. This comment used to say the
|
||||
/// gates were "NOT uniform … only those two poles are verified" and decline to guess, which
|
||||
/// was the right call at the time — the table now exists, so it is applied rather than feared;
|
||||
/// * **`""`** — quiet nil;
|
||||
/// * a **recognised** token naming nothing (`"party5"` solo, `"playerfoo"`) — quiet nil.
|
||||
pub(crate) fn check_unit_token(token: &Option<String>) -> mlua::Result<()> {
|
||||
|
||||
@@ -984,11 +984,32 @@ fn an_unrecognised_unit_token_raises_and_a_recognised_empty_one_does_not() {
|
||||
implementation that raises on \"did not resolve\" gets wrong."
|
||||
);
|
||||
}
|
||||
// QUIET NIL, leg 3: the empty string and an absent argument. The per-binding argument gates are
|
||||
// NOT uniform in the client and only two poles are verified, so this asserts the resolver's
|
||||
// behaviour and does not invent a gate.
|
||||
// QUIET NIL, leg 3: the empty string — which passes the `lua_isstring` gate (it IS a string)
|
||||
// and dies quietly in the resolver's own NULL/empty guard.
|
||||
assert!(s.run(r#"UnitName("")"#).is_ok());
|
||||
assert!(s.run("UnitName()").is_ok());
|
||||
|
||||
// …but an ABSENT or nil argument is NOT leg 3, and this line used to assert that it was.
|
||||
// `UnitName 0x517020` gates its token position at `0x517048` and raises
|
||||
// `Usage: UnitName("unit")` (`0x850ee0`); `luaL_error` does not return. The comment this
|
||||
// replaces said the per-binding gates were "not uniform … only two poles verified", which was
|
||||
// true when it was written — wow-re has since censused all 83 entries of the table at
|
||||
// `0x850438`: 53 gate and raise, and only 13 unit-token bindings are quiet. Decision 1834.
|
||||
assert!(s.run("UnitName()").is_err(), "absent argument raises");
|
||||
assert!(s.run("UnitName(nil)").is_err(), "nil argument raises");
|
||||
// A NUMBER passes the gate — `lua_isstring` admits tag 3 — and is handed to the resolver as
|
||||
// "5", which then raises the family's OTHER message rather than the `Usage:` one.
|
||||
assert!(
|
||||
s.run("UnitName(5)").is_err(),
|
||||
"a number reaches the resolver"
|
||||
);
|
||||
|
||||
// The thirteen that stay quiet, three of them here: no gate at all, so nil is fine.
|
||||
for quiet in ["UnitExists", "UnitIsVisible", "UnitClassification"] {
|
||||
assert!(
|
||||
s.run(&format!("{quiet}()")).is_ok(),
|
||||
"{quiet} is one of the 13 with no gate — nil must stay quiet"
|
||||
);
|
||||
}
|
||||
|
||||
// A two-unit call gates BOTH arguments.
|
||||
assert!(s.run(r#"UnitIsUnit("player", "bogus")"#).is_err());
|
||||
@@ -1371,3 +1392,58 @@ fn unit_has_relic_slot_answers_one_or_nil() {
|
||||
"function"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The two-token predicates gate BOTH positions** — decision 1836, closing the half 1834 left
|
||||
/// open on purpose.
|
||||
///
|
||||
/// 1834 applied the `lua_isstring` gate only where wow-re's partial list named a single-token
|
||||
/// binding, because a census that finds "a gate somewhere in this body" cannot say *which*
|
||||
/// argument carries it. The complete 83-row table (`nil-unit-token-arg-law.md` §10) resolves it:
|
||||
/// these seven each carry **two** `lua_isstring` sites, so either argument being nil raises.
|
||||
#[test]
|
||||
fn a_two_token_predicate_raises_on_either_nil_argument() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_unit("player", Some(player()));
|
||||
s.set_unit("target", Some(player()));
|
||||
|
||||
for verb in [
|
||||
"UnitIsUnit",
|
||||
"UnitIsEnemy",
|
||||
"UnitIsFriend",
|
||||
"UnitCanCooperate",
|
||||
"UnitCanAttack",
|
||||
"UnitReaction",
|
||||
] {
|
||||
assert!(
|
||||
s.run(&format!(r#"{verb}("player", "target")"#)).is_ok(),
|
||||
"{verb} with both tokens is fine"
|
||||
);
|
||||
assert!(
|
||||
s.run(&format!(r#"{verb}("player")"#)).is_err(),
|
||||
"{verb} with the SECOND argument absent must raise"
|
||||
);
|
||||
assert!(
|
||||
s.run(&format!(r#"{verb}(nil, "target")"#)).is_err(),
|
||||
"{verb} with the FIRST argument nil must raise"
|
||||
);
|
||||
assert!(s.run(&format!("{verb}()")).is_err(), "{verb} with neither");
|
||||
}
|
||||
|
||||
// `GetRaidTargetIndex` is the one of six previously-unclassified names that takes a token, and
|
||||
// it is gated. Its usage string names the argument UNQUOTED — the reference's own spelling.
|
||||
assert!(s.run(r#"GetRaidTargetIndex("player")"#).is_ok());
|
||||
assert!(s.run("GetRaidTargetIndex()").is_err());
|
||||
|
||||
// …while the other five take no argument at all and have no gate. `GetTimeToWellRested` is a
|
||||
// three-instruction stub in the reference that always pushes nil; computing a value there
|
||||
// would be divergence, not a feature.
|
||||
for none in [
|
||||
"GetQuestGreenRange",
|
||||
"GetRestState",
|
||||
"GetXPExhaustion",
|
||||
"GetTimeToWellRested",
|
||||
"GetBillingTimeRested",
|
||||
] {
|
||||
assert!(s.run(&format!("{none}()")).is_ok(), "{none} takes no token");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user