Lua bindings coerce numbers, flags and colours as 1.12.1 does (#991)
Lua bindings now read their arguments the way the 1.12.1 client does:
- Numeric strings parse as Storm's `SStrToInt` (`0x64ac60`) and `SStrToFloat` (`0x64aaa0`). That covers channel keys, `GetNumber` and `item:` fields.
- Flag arguments go through `GetBoolOrDefault` (`0x6f1c10`) with each site's default. That is about 40 bindings, `SetChecked()` and `ShowHelm()` among them.
- Colour channels take numeric strings (`lua_tonumber`), and alphas are `isnumber`-gated: no raise, 1.0 by default. A three-argument `SetVertexColor` keeps the current alpha (`0x79ae0a`). So action-bar rings dimmed by a drag stay dimmed, as in 1.12.1.
- `SetTexture("1")` takes the colour form. MessageFrame channels follow `0x795590`.
- `LoggingChat`/`LoggingCombat` return the flag.
Binding-level tests use the reference's values.
Fixes #135
Fixes #144
Fixes #160
Fixes #161
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b2926edf53
commit
1efd9a7163
@@ -840,9 +840,16 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
let occupied = ring_alphas(&s, quickslot2);
|
||||
assert_eq!(occupied, vec![0.5], "the occupied ring ghosts too");
|
||||
|
||||
// Filling a ghosted well runs `ActionButton_UpdateUsable` (ActionButton.lua:269-283), whose
|
||||
// three-argument `SetVertexColor(1, 1, 1)` sets alpha 1.0 in this engine; the reference keeps
|
||||
// the region's alpha (`0x79abd0`), which would leave this ring dim.
|
||||
// Filling a ghosted well runs `ActionButton_Update` (ActionButton.lua:162, 170). Its
|
||||
// `SetNormalTexture(path)` (`0x781960` -> `0x781970`) reuses the XML-defined region in
|
||||
// `[button + 4*state + 0x4b8]` (`0x778f10`) and sets only its file through `0x770200`, which
|
||||
// writes `[tex+0xcc]` and `[tex+0x128]` and redraws (`0x77fdf0`); the colour array
|
||||
// (`+0xa4/+0xa8/+0xb4/+0xb8`) is written by `0x77f750` alone, whose callers are the Lua
|
||||
// colour setters and the XML loader, none a button state change. Then
|
||||
// `ActionButton_UpdateUsable` (:274-275, :281) calls the three-argument
|
||||
// `SetVertexColor(1, 1, 1)`, which keeps the region's alpha (`0x79abd0` re-tests the alpha
|
||||
// gate at `0x79adfe` and copies the old byte back at `0x79ae0a`). So the filled well's ring
|
||||
// stays at the grid's 0.5 like the occupied one.
|
||||
s.set_action(
|
||||
2,
|
||||
Some(ActionSlot {
|
||||
@@ -857,9 +864,8 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
ring_alphas(&s, quickslot2),
|
||||
vec![0.5, 1.0],
|
||||
"the well that got an action is opaque again (its Update ran UpdateUsable); the other \
|
||||
occupied ring keeps its ghost, payload still held"
|
||||
vec![0.5, 0.5],
|
||||
"the filled well's UpdateUsable keeps the ghost alpha, as the occupied ring's does"
|
||||
);
|
||||
|
||||
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
|
||||
@@ -868,12 +874,14 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
let after = ring_alphas(&s, quickslot2);
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![0.5, 1.0],
|
||||
"HideGrid touches no colour: a ring the grid ghosted stays dim past the payload — the \
|
||||
reference's own rule (ours used to clear it here)"
|
||||
vec![0.5, 0.5],
|
||||
"HideGrid touches no colour (ActionButton.lua:251-258): every ring the grid ghosted stays \
|
||||
dim past the payload"
|
||||
);
|
||||
// The ghosted ring's next `ActionButton_UpdateUsable` is not asserted: this engine resets its
|
||||
// alpha to 1.0, the reference keeps 0.5 (`0x79abd0`).
|
||||
// A later `ActionButton_UpdateUsable` (a target change fires it) keeps the dim alpha too.
|
||||
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
||||
s.resolve();
|
||||
assert_eq!(ring_alphas(&s, quickslot2), vec![0.5, 0.5]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
|
||||
@@ -346,7 +346,10 @@ fn set_checked_uses_blizzard_bool_coercion() {
|
||||
("\"false\"", false),
|
||||
("\"1\"", true),
|
||||
("\"0\"", false),
|
||||
("\"junk\"", false),
|
||||
// An unknown first byte takes the default, 1 (`0x6f1c81`-`0x6f1cdc`).
|
||||
("\"junk\"", true),
|
||||
("\"off\"", false),
|
||||
("", true),
|
||||
] {
|
||||
let got = s
|
||||
.eval::<bool>(&format!(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
use super::binding_abi::flag;
|
||||
use super::binding_abi::{bool_or_default, flag};
|
||||
use super::Model;
|
||||
|
||||
/// The kind byte, bits 24-31 of the packed slot word (vmangos `Player.h:133`). Must match
|
||||
@@ -137,17 +137,6 @@ impl super::UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// `UseAction`'s flag test: `nil`, `false` and `0` are false, unlike Lua, where `0` is true.
|
||||
pub(super) fn truthy_nonzero(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Nil => false,
|
||||
Value::Boolean(b) => *b,
|
||||
Value::Integer(i) => *i != 0,
|
||||
Value::Number(n) => *n != 0.0,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the action globals.
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
@@ -253,9 +242,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"UseAction",
|
||||
lua.create_function(|lua, (action, rest): (u32, MultiValue)| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let mut rest = rest.iter();
|
||||
let check_cursor = rest.next().is_some_and(truthy_nonzero);
|
||||
let on_self = rest.next().is_some_and(truthy_nonzero);
|
||||
// Both flags go through `GetBoolOrDefault` (`0x4e717e`, `0x4e716f`), default 0.
|
||||
let check_cursor = bool_or_default(rest.front(), false);
|
||||
let on_self = bool_or_default(rest.get(1), false);
|
||||
if check_cursor && model.cursor.is_some() {
|
||||
super::cursor::place_action(&mut model, action);
|
||||
} else {
|
||||
|
||||
@@ -155,6 +155,118 @@ fn trim_g(s: &str) -> &str {
|
||||
s.trim_end_matches('0').trim_end_matches('.')
|
||||
}
|
||||
|
||||
/// Storm's `SStrToInt` (`0x64ac60`): an optional leading `-` (and no `+`), then the decimal digits
|
||||
/// up to the first other byte, with no whitespace skip; none is 0. The accumulator is a 32-bit
|
||||
/// register that wraps (`0x64ac98`-`0x64aca5`) and `neg` wraps too, so `"4294967297"` is 1.
|
||||
/// `RemoveChatWindowChannel` and `LeaveChannelByName` key on it (`0x49e880`), and the `item:` link
|
||||
/// fields and `SStrToFloat`'s exponent read it.
|
||||
pub(crate) fn s_str_to_int(s: &str) -> i64 {
|
||||
let b = s.as_bytes();
|
||||
let (neg, mut i) = match b.first() {
|
||||
Some(b'-') => (true, 1),
|
||||
_ => (false, 0),
|
||||
};
|
||||
let mut n: i32 = 0;
|
||||
while let Some(&c) = b.get(i) {
|
||||
if !c.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
n = n.wrapping_mul(10).wrapping_add(i32::from(c - b'0'));
|
||||
i += 1;
|
||||
}
|
||||
i64::from(if neg { n.wrapping_neg() } else { n })
|
||||
}
|
||||
|
||||
/// Storm's `SStrToFloat` (`0x64aaa0`), which `EditBox:GetNumber` (`0x798790`) pushes: a prefix
|
||||
/// parse that stops at the first byte outside its grammar and skips no whitespace, evaluated with
|
||||
/// the x87 at 53-bit precision (`_PC_53`), so every operation below rounds as an `f64` does.
|
||||
///
|
||||
/// - an optional `-` (no `+`)
|
||||
/// - the integer digits, which may be none (`".5"`, `"-"`), summed in chunks: a 32-bit register
|
||||
/// takes digits until it reaches `0x19999999` (`0x64ab19`), then `value = value * 10^n + chunk`
|
||||
/// (`0x64ab21`-`0x64ab48`) and a new chunk starts
|
||||
/// - a `.` is consumed whether or not a digit follows it (`0x64ab9f`: `esi` is advanced before the
|
||||
/// test), so `"5.e3"` is 5000; each fraction digit `d` at place `k` (0 for the first) then adds
|
||||
/// the table term `d * 10^-(k+1)` (`0x64abe4`, table `0xc52560` built at `0x64a680`; past place
|
||||
/// 19 the same product is formed on the spot, `0x64abbb`-`0x64abda`), so `"0.12"` is
|
||||
/// `0.1 + 0.02` = 0.12000000000000001
|
||||
/// - `e` or `E`, an optional `+`, then [`s_str_to_int`] of what follows scales the value by
|
||||
/// `pow(10, exp)` (`0x64ac13`-`0x64ac40`): `"1e"` is 1, `"1e+-2"` is 0.01, and the wrapped
|
||||
/// exponent means `"1e4294967297"` is 10
|
||||
///
|
||||
/// Deviation: `pow(10, n)` is the platform's, where the reference's (`0x73f940`, `fyl2x`) rounds
|
||||
/// its extended-precision steps at 53 bits, so a power or a fraction term can differ in the last
|
||||
/// place. The reference also keeps the product `value * 10^exp` in the x87's wider exponent range
|
||||
/// until it is stored, so an exponent past +-308 against a value that pulls it back in range
|
||||
/// (`"0.000...1e320"`) is reached here only by scaling in steps of 300 digits, which rounds more
|
||||
/// often; `"0e999"` is 0 as there, and only an exponent past +-4932 makes 0 * inf, NaN.
|
||||
pub(crate) fn s_str_to_float(s: &str) -> f64 {
|
||||
let b = s.as_bytes();
|
||||
let at = |i: usize| b.get(i).copied().unwrap_or(0);
|
||||
let neg = at(0) == b'-';
|
||||
let mut i = usize::from(neg);
|
||||
let mut value = 0.0f64;
|
||||
let (mut chunk, mut count) = (0u32, 0i32);
|
||||
while at(i).is_ascii_digit() {
|
||||
chunk = chunk * 10 + u32::from(at(i) - b'0');
|
||||
count += 1;
|
||||
i += 1;
|
||||
if chunk >= 0x1999_9999 {
|
||||
value = value * 10f64.powf(f64::from(count)) + f64::from(chunk);
|
||||
(chunk, count) = (0, 0);
|
||||
}
|
||||
}
|
||||
value = value * 10f64.powf(f64::from(count)) + f64::from(chunk);
|
||||
if at(i) == b'.' {
|
||||
i += 1;
|
||||
let mut place = 1.0;
|
||||
while at(i).is_ascii_digit() {
|
||||
value += f64::from(at(i) - b'0') * 10f64.powf(-place);
|
||||
place += 1.0;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if matches!(at(i), b'e' | b'E') {
|
||||
i += 1;
|
||||
if at(i) == b'+' {
|
||||
i += 1;
|
||||
}
|
||||
value = scale_by_pow10(value, s_str_to_int(&s[i..]) as i32);
|
||||
}
|
||||
if neg {
|
||||
-value
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/// `value * pow(10, exp)` as the x87 forms it: the power keeps the extended exponent range until
|
||||
/// the product is stored as a double (see [`s_str_to_float`]).
|
||||
fn scale_by_pow10(value: f64, exp: i32) -> f64 {
|
||||
let p = 10f64.powf(f64::from(exp));
|
||||
if p.is_finite() && p != 0.0 {
|
||||
return value * p;
|
||||
}
|
||||
if value == 0.0 || !value.is_finite() {
|
||||
// 0 * a finite extended power is 0; the power overflows the extended range only past 4932.
|
||||
return if exp > 4932 || !value.is_finite() {
|
||||
value * p
|
||||
} else {
|
||||
value * 0.0
|
||||
};
|
||||
}
|
||||
let mut v = value;
|
||||
let mut left = exp;
|
||||
while left.abs() > 300 {
|
||||
v *= 10f64.powf(f64::from(300 * left.signum()));
|
||||
left -= 300 * left.signum();
|
||||
if v == 0.0 || v.is_infinite() {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
v * 10f64.powf(f64::from(left))
|
||||
}
|
||||
|
||||
/// `GetBoolOrDefault` (`0x6f1c10`, jump table `0x6f1ce8`), the reference's boolean argument
|
||||
/// coercion, which is not Lua truthiness:
|
||||
///
|
||||
@@ -176,9 +288,10 @@ pub(crate) fn bool_or_default(v: Option<&Value>, default: bool) -> bool {
|
||||
Value::Nil => false,
|
||||
Value::Boolean(b) => *b,
|
||||
Value::LightUserData(_) => default,
|
||||
// Truncated like `0x40a2b0`, testing only the low dword.
|
||||
// Truncated like `0x40a2b0` ([`ftol`]), which keeps only the low dword: 2^32 reads 0, and
|
||||
// an infinity or NaN too.
|
||||
Value::Integer(i) => (*i as i32) != 0,
|
||||
Value::Number(n) => (*n as i64 as i32) != 0,
|
||||
Value::Number(n) => ftol(*n) != 0,
|
||||
Value::String(s) => {
|
||||
let bytes = s.as_bytes();
|
||||
match bytes.first() {
|
||||
@@ -227,6 +340,75 @@ mod tests {
|
||||
Lua::new()
|
||||
}
|
||||
|
||||
/// `SStrToInt` (`0x64ac60`): an optional `-`, digits, nothing skipped, a 32-bit wrap.
|
||||
#[test]
|
||||
fn s_str_to_int_is_the_references_atoi() {
|
||||
assert_eq!(s_str_to_int("2589"), 2589);
|
||||
assert_eq!(s_str_to_int("-5"), -5);
|
||||
assert_eq!(s_str_to_int(""), 0);
|
||||
assert_eq!(s_str_to_int("abc"), 0);
|
||||
assert_eq!(s_str_to_int(" 12"), 0, "no leading-whitespace skip");
|
||||
assert_eq!(s_str_to_int("+12"), 0, "no unary plus");
|
||||
assert_eq!(s_str_to_int("12abc"), 12, "stops at the first non-digit");
|
||||
assert_eq!(s_str_to_int("-"), 0);
|
||||
assert_eq!(
|
||||
s_str_to_int("2147483648"),
|
||||
-2_147_483_648,
|
||||
"the register wraps"
|
||||
);
|
||||
assert_eq!(s_str_to_int("4294967297"), 1);
|
||||
assert_eq!(s_str_to_int("-4294967297"), -1);
|
||||
}
|
||||
|
||||
/// `SStrToFloat` (`0x64aaa0`): the grammar's quirks, each read off the bytes.
|
||||
#[test]
|
||||
fn s_str_to_float_is_the_references_prefix_parse() {
|
||||
for (text, want) in [
|
||||
("12", 12.0),
|
||||
("12abc", 12.0),
|
||||
("-12.5x", -12.5),
|
||||
(" 12", 0.0),
|
||||
("+12", 0.0),
|
||||
("inf", 0.0),
|
||||
("1e", 1.0),
|
||||
("1e3", 1000.0),
|
||||
("1E+3", 1000.0),
|
||||
("1e-2", 0.01),
|
||||
("1e+-2", 0.01),
|
||||
("1e3.5", 1000.0),
|
||||
(".5", 0.5),
|
||||
("-.5", -0.5),
|
||||
("5.", 5.0),
|
||||
("5.e2", 500.0),
|
||||
("5.5.5", 5.5),
|
||||
("e5", 0.0),
|
||||
("0x1f", 0.0),
|
||||
("1234567890123", 1_234_567_890_123.0),
|
||||
] {
|
||||
assert_eq!(s_str_to_float(text), want, "{text:?}");
|
||||
}
|
||||
// The fraction is a sum of per-digit table terms, not one correctly rounded parse.
|
||||
assert_eq!(s_str_to_float("0.12"), 0.1 + 0.02);
|
||||
assert_ne!(s_str_to_float("0.12"), 0.12);
|
||||
assert_eq!(s_str_to_float("1.5"), 1.5);
|
||||
// The exponent is `SStrToInt`'s 32-bit wrap.
|
||||
assert_eq!(s_str_to_float("1e4294967297"), 10.0);
|
||||
// The integer part sums in chunks of up to ten digits.
|
||||
assert_eq!(
|
||||
s_str_to_float("12345678901234567890"),
|
||||
(1_234_567_890.0 * 1e10) + 1_234_567_890.0
|
||||
);
|
||||
// The product keeps the x87's wide exponent until it is stored.
|
||||
assert_eq!(s_str_to_float("0e999"), 0.0);
|
||||
assert!(
|
||||
s_str_to_float("0e5000").is_nan(),
|
||||
"0 * inf past the extended range"
|
||||
);
|
||||
assert_eq!(s_str_to_float("1e-999"), 0.0);
|
||||
assert_eq!(s_str_to_float("1e999"), f64::INFINITY);
|
||||
assert!(s_str_to_float("-").is_sign_negative(), "'-' alone is -0");
|
||||
}
|
||||
|
||||
/// `_ftol`'s low dword: chopped toward zero, wrapped to 32 bits, and 0 for whatever the qword
|
||||
/// `fistp` cannot hold.
|
||||
#[test]
|
||||
@@ -260,6 +442,19 @@ mod tests {
|
||||
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));
|
||||
// Only the low dword of the truncation counts (`0x40a2b0`): 2^32 reads 0, and so does an
|
||||
// infinity or NaN, which the `fistp` stores as the integer indefinite.
|
||||
assert!(!bool_or_default(
|
||||
Some(&Value::Number(4_294_967_296.0)),
|
||||
true
|
||||
));
|
||||
assert!(bool_or_default(
|
||||
Some(&Value::Number(4_294_967_297.0)),
|
||||
false
|
||||
));
|
||||
for n in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, 9.3e18] {
|
||||
assert!(!bool_or_default(Some(&Value::Number(n)), true), "{n}");
|
||||
}
|
||||
assert!(!bool_or_default(Some(&Value::Integer(0)), true));
|
||||
assert!(bool_or_default(Some(&Value::Integer(7)), false));
|
||||
|
||||
|
||||
@@ -757,16 +757,15 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetTextColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
// Shape C on the channels (`Button:SetTextColor 0x780ee0`, `2=C 3=C 4=C 5=B`).
|
||||
let (r, g, b) = (
|
||||
super::object::as_f32(&r),
|
||||
super::object::as_f32(&g),
|
||||
super::object::as_f32(&b),
|
||||
super::object::tonumber_f32(lua, &r),
|
||||
super::object::tonumber_f32(lua, &g),
|
||||
super::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
with_button(lua, &this, |bs| {
|
||||
bs.normal_color = Some([r, g, b, a.unwrap_or(1.0)])
|
||||
})
|
||||
with_button(lua, &this, |bs| bs.normal_color = Some([r, g, b, a]))
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
@@ -790,32 +789,30 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetHighlightTextColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
// Shape C on the channels (`Button:SetTextColor 0x780ee0`, `2=C 3=C 4=C 5=B`).
|
||||
let (r, g, b) = (
|
||||
super::object::as_f32(&r),
|
||||
super::object::as_f32(&g),
|
||||
super::object::as_f32(&b),
|
||||
super::object::tonumber_f32(lua, &r),
|
||||
super::object::tonumber_f32(lua, &g),
|
||||
super::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
with_button(lua, &this, |bs| {
|
||||
bs.highlight_color = Some([r, g, b, a.unwrap_or(1.0)])
|
||||
})
|
||||
with_button(lua, &this, |bs| bs.highlight_color = Some([r, g, b, a]))
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
m.set(
|
||||
"SetDisabledTextColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
// Shape C on the channels (`Button:SetTextColor 0x780ee0`, `2=C 3=C 4=C 5=B`).
|
||||
let (r, g, b) = (
|
||||
super::object::as_f32(&r),
|
||||
super::object::as_f32(&g),
|
||||
super::object::as_f32(&b),
|
||||
super::object::tonumber_f32(lua, &r),
|
||||
super::object::tonumber_f32(lua, &g),
|
||||
super::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
with_button(lua, &this, |bs| {
|
||||
bs.disabled_color = Some([r, g, b, a.unwrap_or(1.0)])
|
||||
})
|
||||
with_button(lua, &this, |bs| bs.disabled_color = Some([r, g, b, a]))
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
@@ -928,24 +925,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
texture_pair(lua, &c, "DisabledChecked", Slot::DisabledChecked)?;
|
||||
c.set(
|
||||
"SetChecked",
|
||||
lua.create_function(|lua, (this, v): (Table, Value)| {
|
||||
// `SetChecked` (`0x799bf0`) reads its argument through `0x6f1c10` with default 1
|
||||
// (`0x799c77`), not Lua truthiness: a number truncates toward zero (`0x40a2b0`), so
|
||||
// `SetChecked(0)` unchecks, and the stock UI passes `"true"`/`"false"`
|
||||
// (`SpellBookFrame.lua:296-303`). Here a missing argument unchecks and a string checks
|
||||
// only as `"true"` or a nonzero number, where the reference checks for a missing one
|
||||
// and reads a string by its first byte, so `"yes"` checks there.
|
||||
let checked = match v {
|
||||
Value::Boolean(b) => b,
|
||||
Value::Integer(i) => i != 0,
|
||||
Value::Number(n) => n.trunc() != 0.0,
|
||||
Value::String(s) => s.to_str().ok().is_some_and(|s| {
|
||||
let t = s.trim();
|
||||
t.eq_ignore_ascii_case("true")
|
||||
|| t.parse::<f64>().is_ok_and(|n| n.trunc() != 0.0)
|
||||
}),
|
||||
_ => false,
|
||||
};
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
// `SetChecked` (`0x799bf0`) reads its argument through `GetBoolOrDefault` (`0x6f1c10`)
|
||||
// with default 1 (`0x799c77`), not Lua truthiness: no argument, `""`, a table or
|
||||
// `"yes"` check, `nil`, `0` and `"0"` uncheck. The stock UI passes `"true"`/`"false"`
|
||||
// (`SpellBookFrame.lua:296-303`).
|
||||
let checked = super::binding_abi::bool_or_default(args.front(), true);
|
||||
with_button_shown(lua, &this, |bs| bs.refresh_checked(checked, false))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -398,7 +398,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.to_string(),
|
||||
_ => return Err(mlua::Error::runtime(r#"Usage: LeaveChannelByName("name")"#)),
|
||||
};
|
||||
if super::chat_window::leading_int(&key) != 0 {
|
||||
if super::binding_abi::s_str_to_int(&key) != 0 {
|
||||
push(lua, ChannelCommand::Leave { name: key });
|
||||
return Ok(());
|
||||
}
|
||||
@@ -711,6 +711,22 @@ mod command_tests {
|
||||
"…and strips no window: a slot name never equals a window entry"
|
||||
);
|
||||
|
||||
// `SStrToInt` takes no `+`: "+2" is a channel name, so its window entry is stripped, which
|
||||
// the numeric leg (a slot, never a window entry) does not do.
|
||||
s.run("AddChatWindowChannel(1, '+2')").unwrap();
|
||||
s.take_channel_commands();
|
||||
s.take_chat_window_changes();
|
||||
s.run("LeaveChannelByName('+2')").unwrap();
|
||||
assert!(
|
||||
!s.chat_window_looks()[0]
|
||||
.channels
|
||||
.iter()
|
||||
.any(|(c, _)| c == "+2"),
|
||||
"a '+'-prefixed key is a name"
|
||||
);
|
||||
s.take_channel_commands();
|
||||
s.take_chat_window_changes();
|
||||
|
||||
s.run("LeaveChannelByName('general')").unwrap();
|
||||
assert_eq!(
|
||||
s.take_channel_commands(),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
use super::binding_abi::bool_or_default;
|
||||
use super::Model;
|
||||
|
||||
/// `DoEmote(token [, target])`: the `EmotesText.dbc` name token (`"WAVE"`, not `/wave`) and an
|
||||
@@ -31,17 +32,6 @@ fn to_u32(v: &Value) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn flag_arg(v: Option<Value>) -> Option<bool> {
|
||||
match v {
|
||||
None | Some(Value::Nil) => None,
|
||||
Some(Value::Boolean(b)) => Some(b),
|
||||
Some(Value::Integer(n)) => Some(n != 0),
|
||||
Some(Value::Number(n)) => Some(n != 0.0),
|
||||
Some(Value::String(s)) => Some(s.to_str().is_ok_and(|s| s != "0" && !s.is_empty())),
|
||||
Some(_) => Some(true),
|
||||
}
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
/// `DoEmote` calls since the last drain.
|
||||
pub fn take_emote_requests(&mut self) -> Vec<EmoteRequest> {
|
||||
@@ -167,27 +157,30 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
for (name, chat) in [("LoggingChat", true), ("LoggingCombat", false)] {
|
||||
g.set(
|
||||
name,
|
||||
lua.create_function(move |lua, flag: Option<Value>| {
|
||||
lua.create_function(move |lua, args: MultiValue| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let slot = if chat {
|
||||
&mut model.logging_chat
|
||||
} else {
|
||||
&mut model.logging_combat
|
||||
};
|
||||
match flag_arg(flag) {
|
||||
None => Ok(MultiValue::from_vec(vec![if *slot {
|
||||
Value::Integer(1)
|
||||
// With an argument (`lua_gettop > 0`, `0x49fe57`) the flag is set from
|
||||
// `GetBoolOrDefault` (`0x49fe69`, default 1), so an explicit nil clears it. Both
|
||||
// paths then answer the flag as it stands, 1 or nil, one value (`0x49feb2`-`0x49fee1`):
|
||||
// the reference re-reads it after the log-open attempt, which can clear it
|
||||
// (`0x49feab`); the app owns that attempt here.
|
||||
if let Some(on) = args.front().map(|v| bool_or_default(Some(v), true)) {
|
||||
let slot = if chat {
|
||||
&mut model.logging_chat
|
||||
} else {
|
||||
Value::Nil
|
||||
}])),
|
||||
Some(on) => {
|
||||
if *slot != on {
|
||||
*slot = on;
|
||||
model.logging_changed = true;
|
||||
}
|
||||
Ok(MultiValue::new())
|
||||
&mut model.logging_combat
|
||||
};
|
||||
if *slot != on {
|
||||
*slot = on;
|
||||
model.logging_changed = true;
|
||||
}
|
||||
}
|
||||
let now = if chat {
|
||||
model.logging_chat
|
||||
} else {
|
||||
model.logging_combat
|
||||
};
|
||||
Ok(if now { Value::Integer(1) } else { Value::Nil })
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
@@ -289,8 +282,8 @@ mod tests {
|
||||
assert!(s.eval::<bool>("return LoggingCombat() == nil").unwrap());
|
||||
assert_eq!(
|
||||
s.arity("LoggingChat(true)").unwrap(),
|
||||
0,
|
||||
"the setter returns nothing"
|
||||
1,
|
||||
"the setter answers the flag too (`0x49fed3`)"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return LoggingChat()").unwrap(), 1);
|
||||
assert_eq!(s.logging_flags(), (true, false));
|
||||
@@ -303,5 +296,9 @@ mod tests {
|
||||
!s.take_logging_changes(),
|
||||
"a write that moves nothing cues nothing"
|
||||
);
|
||||
// The setter answers the flag it leaves: 1 or nil, one value either way.
|
||||
assert_eq!(s.eval::<i64>("return LoggingChat(true)").unwrap(), 1);
|
||||
assert_eq!(s.arity("LoggingChat(nil)").unwrap(), 1);
|
||||
assert!(s.eval::<bool>("return LoggingChat(nil) == nil").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,28 +205,6 @@ impl ChatWindowLook {
|
||||
}
|
||||
}
|
||||
|
||||
/// The leading decimal integer after optional whitespace and a sign, 0 when there is none. The
|
||||
/// reference's `SStrToInt` (`0x64ac60`) takes neither the whitespace nor a `+`.
|
||||
pub(super) fn leading_int(s: &str) -> i64 {
|
||||
let s = s.trim_start();
|
||||
let (neg, digits) = match s.strip_prefix('-') {
|
||||
Some(rest) => (true, rest),
|
||||
None => (false, s.strip_prefix('+').unwrap_or(s)),
|
||||
};
|
||||
let n: i64 = digits
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_digit())
|
||||
.fold(0i64, |acc, c| {
|
||||
acc.saturating_mul(10)
|
||||
.saturating_add(i64::from(c as u8 - b'0'))
|
||||
});
|
||||
if neg {
|
||||
-n
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
/// The byte `__ftol(x · 255.0)` stores, clamped (the module's deviation); non-finite stores 0.
|
||||
fn to_byte(x: f64) -> u8 {
|
||||
if !x.is_finite() {
|
||||
@@ -407,11 +385,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// SetChatWindowLocked(id, isLocked) (`0x4a1650`): the record's `+0x8c` (`0x4a16a2`), 1 at
|
||||
// boot (`0x4984e4`). A `bool` holds it, as the cache writer stores it through `setne`
|
||||
// (`0x499e8b`). The argument is Lua truthiness, where the reference coerces through `0x6f1c10`
|
||||
// (default 0): `0` and "off" unlock there and lock here. FrameXML passes only 1 and nil.
|
||||
// (`0x499e8b`). The argument goes through `GetBoolOrDefault` (`0x4a1697`, default 0), so `0`
|
||||
// and "off" unlock and "yes" locks. FrameXML passes only 1 and nil.
|
||||
lua.globals().set(
|
||||
"SetChatWindowLocked",
|
||||
lua.create_function(|lua, (id, locked): (i64, bool)| {
|
||||
lua.create_function(|lua, (id, locked): (i64, Option<Value>)| {
|
||||
let locked = crate::script::binding_abi::bool_or_default(locked.as_ref(), false);
|
||||
let i = window_index(id)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let look = &mut model.chat_window_looks[i];
|
||||
@@ -513,31 +492,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetChatWindowShown(id [, shown]): `+0x94` through the flag coercion `0x6f1c10`, default 1.
|
||||
// No argument stores 1; nil, false, 0, "0", "off" and "disabled" store 0. The reference also
|
||||
// stores 0 for a string opening F or N ("false", "no"), which stores 1 here.
|
||||
// SetChatWindowShown(id [, shown]): `+0x94` (`0x4a1782`) through `GetBoolOrDefault`
|
||||
// (`0x4a1777`) with default 1: no argument stores 1; nil, false, `0`, "0", "off", "disabled"
|
||||
// and anything opening F or N ("false", "no") store 0.
|
||||
lua.globals().set(
|
||||
"SetChatWindowShown",
|
||||
lua.create_function(|lua, (id, rest): (i64, MultiValue)| {
|
||||
let shown = match rest.front() {
|
||||
None => true,
|
||||
Some(Value::Nil) | Some(Value::Boolean(false)) => false,
|
||||
Some(Value::Boolean(true)) => true,
|
||||
Some(Value::Integer(n)) => *n != 0,
|
||||
Some(Value::Number(n)) => n.is_finite() && n.trunc() != 0.0,
|
||||
Some(Value::String(s)) => {
|
||||
let s = s
|
||||
.to_str()
|
||||
.map(|s| s.trim().to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
if s.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
|
||||
leading_int(&s) != 0
|
||||
} else {
|
||||
!(s == "off" || s == "disabled")
|
||||
}
|
||||
}
|
||||
Some(_) => true,
|
||||
};
|
||||
let shown = crate::script::binding_abi::bool_or_default(rest.front(), true);
|
||||
with_look(lua, id, |look| look.shown = shown)
|
||||
})?,
|
||||
)?;
|
||||
@@ -620,7 +581,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// RemoveChatWindowChannel(id, name | n) (`0x4a1260`): a numeric key names the joined channel
|
||||
// in slot n, and an empty slot removes nothing; the key then resolves to its DBC Shortcut as
|
||||
// in slot n, and an empty slot or a negative number removes nothing; the key then resolves to its DBC Shortcut as
|
||||
// above, and the entry of that name is freed.
|
||||
lua.globals().set(
|
||||
"RemoveChatWindowChannel",
|
||||
@@ -630,8 +591,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
};
|
||||
let key = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let n = leading_int(&key);
|
||||
let key = if n > 0 {
|
||||
let n = crate::script::binding_abi::s_str_to_int(&key);
|
||||
// `0x49e880`: a non-zero `SStrToInt` names a joined slot, and `0x49be50` answers
|
||||
// NULL below slot 1, so `"-1"` removes nothing (`0x4a12c9`); a zero keeps the
|
||||
// string raw, untrimmed, for the case-insensitive compares (`0x4a1315`,
|
||||
// `0x4a1359`).
|
||||
let key = if n != 0 {
|
||||
match usize::try_from(n)
|
||||
.ok()
|
||||
.and_then(|n| model.joined_channels.get(n - 1))
|
||||
@@ -641,7 +606,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
None => return Ok(()),
|
||||
}
|
||||
} else {
|
||||
key.trim().to_string()
|
||||
key
|
||||
};
|
||||
match model
|
||||
.zone_channel_catalog
|
||||
@@ -1062,6 +1027,54 @@ mod record_tests {
|
||||
assert_eq!(s.take_chat_window_changes(), vec![0]);
|
||||
}
|
||||
|
||||
/// A channel key is `SStrToInt` (`0x64ac60`, through `0x49e880`): an optional `-` and digits,
|
||||
/// with no whitespace skip and no `+`, so only a plain number names a joined slot.
|
||||
#[test]
|
||||
fn a_channel_key_is_a_slot_only_as_sstrtoint_reads_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_joined_channels(vec![Some("MyChan".into())]);
|
||||
s.run("AddChatWindowChannel(1, 'MyChan')").unwrap();
|
||||
for key in ["+1", " 1", "x1"] {
|
||||
s.run(&format!("RemoveChatWindowChannel(1, {key:?})"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.chat_window_looks()[0].channels,
|
||||
vec![("MyChan".to_string(), 0)],
|
||||
"{key:?} is a name, not slot 1"
|
||||
);
|
||||
}
|
||||
// A negative number is a slot too (non-zero, `0x49e889`) and no slot is below 1.
|
||||
s.run("AddChatWindowChannel(1, '-1')").unwrap();
|
||||
s.run("RemoveChatWindowChannel(1, '-1')").unwrap();
|
||||
assert_eq!(
|
||||
s.chat_window_looks()[0].channels,
|
||||
vec![("MyChan".to_string(), 0), ("-1".to_string(), 0)],
|
||||
"'-1' removes nothing"
|
||||
);
|
||||
// A name is raw: the blanks around it are part of it.
|
||||
s.set_chat_window_looks([(
|
||||
4,
|
||||
ChatWindowLook {
|
||||
channels: vec![(" pad ".into(), 0)],
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
s.run("RemoveChatWindowChannel(5, 'pad')").unwrap();
|
||||
assert_eq!(
|
||||
s.chat_window_looks()[4].channels.len(),
|
||||
1,
|
||||
"'pad' is not ' pad '"
|
||||
);
|
||||
s.run("RemoveChatWindowChannel(5, ' pad ')").unwrap();
|
||||
assert!(s.chat_window_looks()[4].channels.is_empty());
|
||||
s.run("RemoveChatWindowChannel(1, '1abc')").unwrap();
|
||||
assert_eq!(
|
||||
s.chat_window_looks()[0].channels,
|
||||
vec![("-1".to_string(), 0)],
|
||||
"'1abc' reads 1 and frees the channel joined in slot 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_host_loaded_record_is_what_the_verbs_read() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -239,9 +239,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// Shape C on r, g, b (`ColorSelect:SetColorRGB 0x78eae0`, `2=C 3=C 4=C 5=B`).
|
||||
lua.create_function(|lua, (this, r, g, b): (Table, Value, Value, Value)| {
|
||||
let (r, g, b) = (
|
||||
crate::script::object::as_f64(&r),
|
||||
crate::script::object::as_f64(&g),
|
||||
crate::script::object::as_f64(&b),
|
||||
crate::script::object::tonumber_f64(lua, &r),
|
||||
crate::script::object::tonumber_f64(lua, &g),
|
||||
crate::script::object::tonumber_f64(lua, &b),
|
||||
);
|
||||
// Store through the client's quantizer, then fire with what the widget now holds: the
|
||||
// round-tripped values, identical to the next `GetColorRGB`, not the raw arguments.
|
||||
|
||||
@@ -57,13 +57,13 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// GetNumber: the real text as a number, else 0. The reference (`0x798790`) is atof, which also
|
||||
// reads the leading number of a text like "12abc".
|
||||
// GetNumber (`0x798790`): the real text through Storm's `SStrToFloat` (`0x64aaa0`), a prefix
|
||||
// parse, so "12abc" is 12 and " 12", "+12" and "inf" are 0.
|
||||
m.set(
|
||||
"GetNumber",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let text = with_editbox(lua, &this, |eb| eb.text.clone())?;
|
||||
Ok(text.trim().parse::<f64>().unwrap_or(0.0))
|
||||
Ok(crate::script::binding_abi::s_str_to_float(&text))
|
||||
})?,
|
||||
)?;
|
||||
// Insert: a nil is a no-op, not an error, since the reference reads the argument through
|
||||
@@ -293,8 +293,10 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let refresh_text = name == "SetMultiLine";
|
||||
m.set(
|
||||
name,
|
||||
lua.create_function(move |lua, (this, v): (Table, Value)| {
|
||||
let on = !matches!(v, Value::Nil | Value::Boolean(false));
|
||||
lua.create_function(move |lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
// `GetBoolOrDefault` (`0x6f1c10`) with default 1 (`0x797d1e`, `0x797e8e`,
|
||||
// `0x797ffe`, `0x79816e`): a bare call enables, `"0"` and `nil` disable.
|
||||
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
|
||||
with_editbox(lua, &this, |eb| set(eb, on))?;
|
||||
if refresh_text {
|
||||
// A multi-line box seats its text TOP and as tall as the text, a single-line
|
||||
@@ -372,7 +374,7 @@ fn install_font_block(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
/// A `Set<Flag>` method name and the field write it drives.
|
||||
type FlagSetter = (&'static str, fn(&mut EditBoxState, bool));
|
||||
|
||||
/// The four config-flag setters, each taking Lua truthiness.
|
||||
/// The four config-flag setters, each reading its flag with `GetBoolOrDefault`.
|
||||
fn flag_setters() -> [FlagSetter; 4] {
|
||||
[
|
||||
("SetAutoFocus", |eb, on| eb.auto_focus = on),
|
||||
|
||||
@@ -656,6 +656,85 @@ fn get_number_parses_the_text() {
|
||||
assert_eq!(s.eval::<f64>("return E:GetNumber()").unwrap(), 0.0);
|
||||
}
|
||||
|
||||
/// `GetNumber` pushes `SStrToFloat` of the text (`0x798790` → `0x64aaa0`): a prefix parse with no
|
||||
/// whitespace skip and no `+`.
|
||||
#[test]
|
||||
fn get_number_is_the_references_prefix_parse() {
|
||||
let s = script();
|
||||
s.run(r#"E = CreateFrame("EditBox", "E")"#).unwrap();
|
||||
for (text, want) in [
|
||||
("12abc", 12.0),
|
||||
(" 12", 0.0),
|
||||
("+12", 0.0),
|
||||
("inf", 0.0),
|
||||
("nan", 0.0),
|
||||
("-12.5x", -12.5),
|
||||
(".5", 0.5),
|
||||
("5.", 5.0),
|
||||
("1e3", 1000.0),
|
||||
("1E+2", 100.0),
|
||||
("0x10", 0.0),
|
||||
("", 0.0),
|
||||
] {
|
||||
s.run(&format!("E:SetText({text:?})")).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return E:GetNumber()").unwrap(),
|
||||
want,
|
||||
"{text:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The four config flags are `GetBoolOrDefault` reads with default 1 (`0x797d1e`, `0x797e8e`,
|
||||
/// `0x797ffe`, `0x79816e`): a bare call enables, nil, `"0"` and `"no"` disable.
|
||||
#[test]
|
||||
fn editbox_flag_setters_read_get_bool_or_default_with_default_one() {
|
||||
use crate::script::object::frame_handle_of;
|
||||
use crate::widget::EditBoxState;
|
||||
type Pick = fn(&EditBoxState) -> bool;
|
||||
let s = script();
|
||||
s.run(r#"E = CreateFrame("EditBox", "E")"#).unwrap();
|
||||
let state = |pick: Pick| {
|
||||
let t: mlua::Table = s.lua().globals().get("E").unwrap();
|
||||
let h = frame_handle_of(s.lua(), &t).unwrap();
|
||||
super::with_eb(s.lua(), h, |eb| pick(eb)).unwrap()
|
||||
};
|
||||
for (set, pick) in [
|
||||
("SetAutoFocus", (|eb| eb.auto_focus) as Pick),
|
||||
("SetNumeric", |eb| eb.numeric),
|
||||
("SetPassword", |eb| eb.password),
|
||||
("SetMultiLine", |eb| eb.multi_line),
|
||||
] {
|
||||
let put = |arg: &str| s.run(&format!("E:{set}({arg})")).unwrap();
|
||||
put("true");
|
||||
put("false");
|
||||
put("");
|
||||
assert!(state(pick), "{set}: no argument takes the default 1");
|
||||
for (arg, want) in [
|
||||
("nil", false),
|
||||
("true", true),
|
||||
("false", false),
|
||||
("0", false),
|
||||
("0.5", false),
|
||||
("1", true),
|
||||
("2^32", false),
|
||||
(r#""0""#, false),
|
||||
(r#""1""#, true),
|
||||
(r#""no""#, false),
|
||||
(r#""yes""#, true),
|
||||
(r#""off""#, false),
|
||||
(r#""on""#, true),
|
||||
(r#""""#, true),
|
||||
("{}", true),
|
||||
] {
|
||||
put(if want { "false" } else { "true" });
|
||||
assert_eq!(state(pick), !want, "{set}: setup");
|
||||
put(arg);
|
||||
assert_eq!(state(pick), want, "{set}({arg})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── the generic OnChar and OnKeyDown ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -43,14 +43,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"FollowByName",
|
||||
lua.create_function(|lua, (name, exact): (String, Option<mlua::Value>)| {
|
||||
// The reference reads this with `GetBoolOrDefault` (`0x6f1c10`), default false; here
|
||||
// nil, false and 0 are false and any other value is true.
|
||||
let exact = match exact {
|
||||
None | Some(mlua::Value::Nil) | Some(mlua::Value::Boolean(false)) => false,
|
||||
Some(mlua::Value::Integer(n)) => n != 0,
|
||||
Some(mlua::Value::Number(n)) => n != 0.0,
|
||||
Some(_) => true,
|
||||
};
|
||||
// `GetBoolOrDefault` (`0x6f1c10`, `0x489f14`) with default false.
|
||||
let exact = super::binding_abi::bool_or_default(exact.as_ref(), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model
|
||||
.follow_requests
|
||||
|
||||
@@ -285,16 +285,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetTextColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
let (r, g, b) = (
|
||||
super::object::as_f32(&r),
|
||||
super::object::as_f32(&g),
|
||||
super::object::as_f32(&b),
|
||||
super::object::tonumber_f32(lua, &r),
|
||||
super::object::tonumber_f32(lua, &g),
|
||||
super::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
edit(lua, &this, |fo| {
|
||||
let keep = fo.color.map_or(1.0, |c| c[3]);
|
||||
fo.color = Some([r, g, b, a.unwrap_or(keep)]);
|
||||
})
|
||||
// Alpha is staged 1.0 (`0x79f59b`) and never read back, so a bare colour call
|
||||
// resets it.
|
||||
edit(lua, &this, |fo| fo.color = Some([r, g, b, a]))
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
@@ -326,17 +326,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"SetShadowColor",
|
||||
lua.create_function(
|
||||
// Shape C on r, g, b (`Font:SetShadowColor 0x79f730`): never raises.
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
let (r, g, b) = (
|
||||
crate::script::object::as_f32(&r),
|
||||
crate::script::object::as_f32(&g),
|
||||
crate::script::object::as_f32(&b),
|
||||
crate::script::object::tonumber_f32(lua, &r),
|
||||
crate::script::object::tonumber_f32(lua, &g),
|
||||
crate::script::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
edit(lua, &this, |fo| {
|
||||
let offset = fo.shadow.map_or([0.0, 0.0], |s| s.offset);
|
||||
fo.shadow = Some(FontShadow {
|
||||
offset,
|
||||
color: [r, g, b, a.unwrap_or(1.0)],
|
||||
color: [r, g, b, a],
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
@@ -314,16 +314,17 @@ pub(super) fn install(
|
||||
m.set(
|
||||
"SetTextColor",
|
||||
lua.create_function(
|
||||
move |lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
move |lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
let (r, g, b) = (
|
||||
super::object::as_f32(&r),
|
||||
super::object::as_f32(&g),
|
||||
super::object::as_f32(&b),
|
||||
super::object::tonumber_f32(lua, &r),
|
||||
super::object::tonumber_f32(lua, &g),
|
||||
super::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
let rh = resolve(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.vertex_color = Some([r, g, b, a.unwrap_or(1.0)]);
|
||||
d.vertex_color = Some([r, g, b, a]);
|
||||
// Clears the colour inherit bit (`0x79dbd0`): it survives a font-object repaint.
|
||||
d.font_explicit.color = true;
|
||||
Ok(())
|
||||
@@ -353,11 +354,12 @@ pub(super) fn install(
|
||||
"SetShadowColor",
|
||||
lua.create_function(
|
||||
// r, g and b are a bare `lua_tonumber` (`0x79dd40`), as in `SetTextColor`.
|
||||
move |lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
move |lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = super::object::alpha_f32(lua, &a);
|
||||
let (r, g, b) = (
|
||||
super::object::as_f32(&r),
|
||||
super::object::as_f32(&g),
|
||||
super::object::as_f32(&b),
|
||||
super::object::tonumber_f32(lua, &r),
|
||||
super::object::tonumber_f32(lua, &g),
|
||||
super::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
let rh = resolve(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
@@ -365,7 +367,7 @@ pub(super) fn install(
|
||||
let offset = d.font_shadow.map_or([0.0, 0.0], |s| s.offset);
|
||||
d.font_shadow = Some(FontShadow {
|
||||
offset,
|
||||
color: [r, g, b, a.unwrap_or(1.0)],
|
||||
color: [r, g, b, a],
|
||||
});
|
||||
d.font_explicit.shadow = true;
|
||||
Ok(())
|
||||
|
||||
@@ -247,18 +247,6 @@ fn member_at(roster: &[GuildMemberInfo], index: i64) -> Option<&GuildMemberInfo>
|
||||
.and_then(|i| roster.get(i))
|
||||
}
|
||||
|
||||
/// A flag argument: `nil`, `false` and `0` are off, anything else on. The reference reads both
|
||||
/// flags with `GetBoolOrDefault` (`0x6f1c10`, `binding_abi::bool_or_default`), which differs on
|
||||
/// strings and fractions; the FrameXML's checkbox states, `1` or `nil`, read the same in both.
|
||||
fn truthy(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Nil | Value::Boolean(false) => false,
|
||||
Value::Integer(0) => false,
|
||||
Value::Number(n) => *n != 0.0,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the player's own rank holds right `index`, 1-based into [`RANK_RIGHT_BITS`].
|
||||
fn has_right(model: &Model, index: usize) -> bool {
|
||||
model.guild.in_guild && model.guild.rights & RANK_RIGHT_BITS[index - 1] != 0
|
||||
@@ -335,12 +323,17 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// ── The roster ───────────────────────────────────────────────────────────────────────────
|
||||
// GetNumGuildMembers() (`0x4d1190`): the loop bound (`FriendsFrame.lua:446`), which respects
|
||||
// show-offline, not the addressable range (`GuildState::num_members`).
|
||||
// GetNumGuildMembers([includeOffline]) (`0x4d1190`): the loop bound (`FriendsFrame.lua:446`),
|
||||
// which respects show-offline, not the addressable range (`GuildState::num_members`).
|
||||
g.set(
|
||||
"GetNumGuildMembers",
|
||||
lua.create_function(|lua, ()| {
|
||||
lua.create_function(|lua, args: MultiValue| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
// The flag is `GetBoolOrDefault` (`0x4d11a0`, default 0): a set one counts the whole
|
||||
// roster (`[0xb73118]`) whatever show-offline says, else the loop bound.
|
||||
if binding_abi::bool_or_default(args.front(), false) {
|
||||
return Ok(model.guild.roster.len() as i64);
|
||||
}
|
||||
Ok(model.guild.num_members as i64)
|
||||
})?,
|
||||
)?;
|
||||
@@ -438,14 +431,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(binding_abi::flag(model.guild.show_offline))
|
||||
})?,
|
||||
)?;
|
||||
// With no argument it turns show-offline on, unlike an explicit `nil`, hence `MultiValue`.
|
||||
// `GetBoolOrDefault` (`0x4d1e17`) with default 1: no argument turns show-offline on, unlike an
|
||||
// explicit `nil`, hence `MultiValue`.
|
||||
g.set(
|
||||
"SetGuildRosterShowOffline",
|
||||
lua.create_function(|lua, args: MultiValue| {
|
||||
let on = match args.into_iter().next() {
|
||||
None => true,
|
||||
Some(flag) => truthy(&flag),
|
||||
};
|
||||
let on = binding_abi::bool_or_default(args.front(), true);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.guild.show_offline = on;
|
||||
model.guild_requests.push(GuildRequest::SetShowOffline(on));
|
||||
@@ -653,7 +644,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// GuildControlSetRankFlag(index, enabled) (`0x4d2070`): stage one checkbox, 1 to 13.
|
||||
g.set(
|
||||
"GuildControlSetRankFlag",
|
||||
lua.create_function(|lua, (index, enabled): (i64, Value)| {
|
||||
lua.create_function(|lua, (index, enabled): (i64, Option<Value>)| {
|
||||
let Some(slot) = usize::try_from(index)
|
||||
.ok()
|
||||
.and_then(|i| i.checked_sub(1))
|
||||
@@ -662,7 +653,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.guild_control.staged[slot] = truthy(&enabled);
|
||||
// `GetBoolOrDefault` (`0x4d20b7`), default 0.
|
||||
model.guild_control.staged[slot] =
|
||||
binding_abi::bool_or_default(enabled.as_ref(), false);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -315,33 +315,6 @@ fn equip_loc_token(inventory_type: u32) -> &'static str {
|
||||
TOKENS.get(inventory_type as usize).copied().unwrap_or("")
|
||||
}
|
||||
|
||||
/// The reference's `SStrToInt` (`0x64ac60`) over an `item:` field: an optional `-`, then digits up
|
||||
/// to the first non-digit, with no whitespace skip and no `+`. Deviation: an overflow saturates at
|
||||
/// `u32::MAX` where the reference wraps, because only a script can overflow it.
|
||||
fn reference_atoi(s: &str) -> i64 {
|
||||
let b = s.as_bytes();
|
||||
let (neg, mut i) = match b.first() {
|
||||
Some(b'-') => (true, 1),
|
||||
_ => (false, 0),
|
||||
};
|
||||
let mut n: i64 = 0;
|
||||
while let Some(&c) = b.get(i) {
|
||||
if !c.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
n = n
|
||||
.saturating_mul(10)
|
||||
.saturating_add(i64::from(c - b'0'))
|
||||
.min(i64::from(u32::MAX));
|
||||
i += 1;
|
||||
}
|
||||
if neg {
|
||||
-n
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetItemInfo`'s argument as the reference parses it (`0x48e0a3`-`0x48e16d`): a number, or a
|
||||
/// string `lua_isnumber` (`0x6f34d0`) coerces, is the id alone; another string (`0x6f3510`) must
|
||||
/// pass `strnicmp(s, "item:", 5)` (`0x64a4c0`) for its four fields, else it is id 0, so a name or
|
||||
@@ -372,7 +345,9 @@ fn parse_item_arg(v: &Value) -> mlua::Result<(i64, u32, u32, u32)> {
|
||||
else {
|
||||
return Ok((0, 0, 0, 0));
|
||||
};
|
||||
let mut fields = rest.splitn(4, ':').map(reference_atoi);
|
||||
let mut fields = rest
|
||||
.splitn(4, ':')
|
||||
.map(crate::script::binding_abi::s_str_to_int);
|
||||
let id = fields.next().unwrap_or(0);
|
||||
let mut next = || u32::try_from(fields.next().unwrap_or(0)).unwrap_or(0);
|
||||
Ok((id, next(), next(), next()))
|
||||
@@ -577,7 +552,7 @@ mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod get_item_info_tests {
|
||||
use super::{equip_loc_token, reference_atoi, ItemTemplateView};
|
||||
use super::{equip_loc_token, ItemTemplateView};
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// Worn Shortsword, item 25, as vmangos `item_template` holds it (item level 2).
|
||||
@@ -806,17 +781,6 @@ mod get_item_info_tests {
|
||||
assert_eq!(equip_loc_token(29), "", "the array's trailing \"\"");
|
||||
assert_eq!(equip_loc_token(9999), "", "past the array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_field_parser_is_the_references_atoi() {
|
||||
assert_eq!(reference_atoi("2589"), 2589);
|
||||
assert_eq!(reference_atoi("-5"), -5);
|
||||
assert_eq!(reference_atoi(""), 0);
|
||||
assert_eq!(reference_atoi("abc"), 0);
|
||||
assert_eq!(reference_atoi(" 12"), 0, "no leading-whitespace skip");
|
||||
assert_eq!(reference_atoi("+12"), 0, "no unary plus");
|
||||
assert_eq!(reference_atoi("12abc"), 12, "stops at the first non-digit");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -204,16 +204,17 @@ fn create_macro(
|
||||
Some(MacroIndex { per_character, pos }.lua_index())
|
||||
}
|
||||
|
||||
/// `EditMacro(index, name, icon, body, local)`: an omitted argument leaves its field alone, since
|
||||
/// the stock UI calls it with disjoint halves, `(sel, name, icon)` from the rename popup and
|
||||
/// `(sel, nil, nil, text)` from `MacroFrame_SaveMacro`.
|
||||
/// `EditMacro(index, name, icon, body, local)`: an omitted name, icon or body leaves its field
|
||||
/// alone, since the stock UI calls it with disjoint halves, `(sel, name, icon)` from the rename
|
||||
/// popup and `(sel, nil, nil, text)` from `MacroFrame_SaveMacro`. The `local` flag is always
|
||||
/// written (`0x4f12dc`, `[macro+0x20]`), so an omitted one clears it.
|
||||
fn edit_macro(
|
||||
model: &mut Model,
|
||||
index: usize,
|
||||
name: Option<String>,
|
||||
texture: Option<Option<String>>,
|
||||
body: Option<String>,
|
||||
local_only: Option<bool>,
|
||||
local_only: bool,
|
||||
) -> Option<usize> {
|
||||
let at = MacroIndex::split(index)?;
|
||||
let entry = model.macros.list_mut(at.per_character).get_mut(at.pos)?;
|
||||
@@ -230,9 +231,7 @@ fn edit_macro(
|
||||
if let Some(body) = body {
|
||||
entry.body = clamp_chars(&body, MAX_MACRO_BODY);
|
||||
}
|
||||
if let Some(local_only) = local_only {
|
||||
entry.local_only = local_only;
|
||||
}
|
||||
entry.local_only = local_only;
|
||||
model.macros_dirty = true;
|
||||
model.macros_generation += 1;
|
||||
Some(index)
|
||||
@@ -280,11 +279,11 @@ fn opt_string(v: Option<&Value>) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `local` and `perCharacter` flags: `nil`, `false` and `0` are false, as in `UseAction`. The
|
||||
/// reference reads them with `GetBoolOrDefault` (`0x6f1c10`), which also takes `"0"` and any
|
||||
/// number that truncates to 0 as false; the stock popup passes a Lua boolean and never `local`.
|
||||
/// The `local` and `perCharacter` flags: `GetBoolOrDefault` (`0x6f1c10`) with default 0
|
||||
/// (`CreateMacro` `0x4f167b`, `0x4f168a`; `EditMacro` `0x4f19ad`), so `"0"` and `0.5` are false and
|
||||
/// `"yes"` true. The stock popup passes a Lua boolean and never `local`.
|
||||
fn flag(v: Option<&Value>) -> bool {
|
||||
v.is_some_and(super::action::truthy_nonzero)
|
||||
super::binding_abi::bool_or_default(v, false)
|
||||
}
|
||||
|
||||
/// Register the macro globals.
|
||||
@@ -393,10 +392,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
};
|
||||
let name = opt_string(a.get(1));
|
||||
let body = opt_string(a.get(3));
|
||||
let local_only = match a.get(4) {
|
||||
None | Some(Value::Nil) => None,
|
||||
other => Some(flag(other)),
|
||||
};
|
||||
let local_only = flag(a.get(4));
|
||||
Ok(
|
||||
match edit_macro(&mut model, index, name, texture, body, local_only) {
|
||||
Some(i) => Value::Integer(i as i64),
|
||||
|
||||
@@ -43,6 +43,17 @@ pub(super) fn update(model: &mut Model, h: FrameHandle, elapsed: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `lua_isnumber` (`0x6f34d0`) then `lua_tonumber` (`0x6f3620`): a number, or a string Lua reads
|
||||
/// as one. The message-frame verbs gate every colour channel and id on it.
|
||||
pub(super) fn is_number(lua: &mlua::Lua, v: &mlua::Value) -> Option<f64> {
|
||||
lua.coerce_number(v.clone()).ok().flatten()
|
||||
}
|
||||
|
||||
/// A channel the verb has already gated with [`is_number`], as the `f32` it stores.
|
||||
pub(super) fn channel(lua: &mlua::Lua, v: &mlua::Value) -> f32 {
|
||||
is_number(lua, v).map_or(0.0, |n| n as f32)
|
||||
}
|
||||
|
||||
/// `AddMessage`'s text; `None` means the whole call does nothing, no line and no error. Both
|
||||
/// bindings (`0x795590`, `0x792900`) jump to their epilogue (`0x79582b`, `0x792b81`) when
|
||||
/// `lua_isstring` (`0x6f3510`) fails, `lua_tostring` (`0x6f3690`) gives NULL or the string is empty
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
//! there and forces alpha opaque. `SetInsertMode` and `GetInsertMode` (`0x794ed0`, `0x794ff0`)
|
||||
//! exist on this class only.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use super::{channel, is_number};
|
||||
use crate::script::object::frame_handle_of;
|
||||
use crate::script::Model;
|
||||
use crate::widget::{InsertMode, KindState, MessageFrameState};
|
||||
@@ -33,15 +34,6 @@ fn with_mf<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// A colour argument as f32, 0 for anything but a number.
|
||||
fn num_f32(v: &Value) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let m = lua.create_table()?;
|
||||
|
||||
@@ -60,18 +52,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let Some(text) = super::message_text(lua, &text) else {
|
||||
return Ok(());
|
||||
};
|
||||
let has_rgb = !matches!(
|
||||
(&r, &g, &b),
|
||||
(Value::Nil, _, _) | (_, Value::Nil, _) | (_, _, Value::Nil)
|
||||
);
|
||||
// Each of r, g and b is `lua_isnumber`-gated (`0x79565f`-`0x795687`) and the trio is
|
||||
// all or nothing; a failed gate still adds the line, in white (`0x79581c`).
|
||||
let has_rgb = [&r, &g, &b].iter().all(|v| is_number(lua, v).is_some());
|
||||
let (r, g, b) = if has_rgb {
|
||||
(num_f32(&r), num_f32(&g), num_f32(&b))
|
||||
(channel(lua, &r), channel(lua, &g), channel(lua, &b))
|
||||
} else {
|
||||
(1.0, 1.0, 1.0)
|
||||
};
|
||||
// The alpha counts only with the trio, read in the same parse.
|
||||
let a = match (&a, has_rgb) {
|
||||
(Value::Number(_) | Value::Integer(_), true) => num_f32(&a),
|
||||
// The alpha is its own `lua_isnumber` gate (`0x795759`), default 1.0, read with
|
||||
// the trio.
|
||||
let a = match (is_number(lua, &a), has_rgb) {
|
||||
(Some(n), true) => n as f32,
|
||||
_ => 1.0,
|
||||
};
|
||||
with_mf(lua, &this, |mf| mf.add(text, r, g, b, a))
|
||||
@@ -116,8 +108,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// The fade accessors, as on the scrolling class; XML's `displayDuration` is `TimeVisible`.
|
||||
m.set(
|
||||
"SetFading",
|
||||
lua.create_function(|lua, (this, on): (Table, Value)| {
|
||||
let on = !matches!(on, Value::Nil | Value::Boolean(false));
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
// `GetBoolOrDefault` (`0x6f1c10`, `0x7950d0`) with default 1: a bare call enables.
|
||||
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
|
||||
with_mf(lua, &this, |mf| mf.fading_enabled = on)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
//! every scroll call re-arming the displayed lines' fade (`0x788b80` when refused at an end,
|
||||
//! `0x788af0` when it moves), which is what brings a faded chat back.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use super::{channel, is_number};
|
||||
use crate::script::object::frame_handle_of;
|
||||
use crate::script::{Model, UiScript};
|
||||
use crate::widget::{KindState, ScrollingMessageState};
|
||||
@@ -33,24 +34,6 @@ fn with_smf<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// `lua_isnumber`: a number or a numeric string. The rgb presence test is three of these.
|
||||
fn is_number(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Integer(_) | Value::Number(_) => true,
|
||||
Value::String(s) => s.to_str().is_ok_and(|s| s.trim().parse::<f64>().is_ok()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A colour argument as f32, 0 for anything but a number.
|
||||
fn num_f32(v: &Value) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a scroll call with the viewport's row count, which every scroll needs: each re-arms the
|
||||
/// fade of the lines on display ([`ScrollingMessageState::reset_all_fade_times`]).
|
||||
fn scroll(
|
||||
@@ -94,15 +77,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let Some(text) = super::message_text(lua, &text) else {
|
||||
return Ok(());
|
||||
};
|
||||
let has_rgb = is_number(&r) && is_number(&g) && is_number(&b);
|
||||
// The chat-type index (`ChatFrame.lua` passes `info.id`), stack index 6 or 3.
|
||||
let id = match if has_rgb { &id } else { &r } {
|
||||
Value::Integer(i) => u32::try_from(*i).unwrap_or(0),
|
||||
Value::Number(n) if n.is_finite() && *n >= 0.0 => *n as u32,
|
||||
_ => 0,
|
||||
};
|
||||
let has_rgb = [&r, &g, &b].iter().all(|v| is_number(lua, v).is_some());
|
||||
// The chat-type index (`ChatFrame.lua` passes `info.id`), stack index 6 or 3, a
|
||||
// `lua_isnumber`-gated `_ftol` (`0x792b1b`, `0x792b4f`); 0 when the gate fails.
|
||||
let id = is_number(lua, if has_rgb { &id } else { &r })
|
||||
.map_or(0, |n| crate::script::binding_abi::ftol(n) as u32);
|
||||
let (r, g, b) = if has_rgb {
|
||||
(num_f32(&r), num_f32(&g), num_f32(&b))
|
||||
(channel(lua, &r), channel(lua, &g), channel(lua, &b))
|
||||
} else {
|
||||
(1.0, 1.0, 1.0)
|
||||
};
|
||||
@@ -117,12 +98,17 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"UpdateColorByID",
|
||||
lua.create_function(
|
||||
|lua, (this, id, r, g, b): (Table, Value, Value, Value, Value)| {
|
||||
let id = match &id {
|
||||
Value::Integer(i) => u32::try_from(*i).unwrap_or(0),
|
||||
Value::Number(n) if n.is_finite() && *n >= 0.0 => *n as u32,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let (r, g, b) = (num_f32(&r), num_f32(&g), num_f32(&b));
|
||||
// All four arguments are `lua_isnumber`-gated (`0x793345`-`0x793388`); any failure
|
||||
// is a silent no-op, and the id is an `_ftol` (`0x7933a1`).
|
||||
if ![&id, &r, &g, &b]
|
||||
.iter()
|
||||
.all(|v| is_number(lua, v).is_some())
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let id =
|
||||
crate::script::binding_abi::ftol(is_number(lua, &id).unwrap_or(0.0)) as u32;
|
||||
let (r, g, b) = (channel(lua, &r), channel(lua, &g), channel(lua, &b));
|
||||
with_smf(lua, &this, |smf| {
|
||||
smf.update_color_by_id(id, r, g, b);
|
||||
})
|
||||
@@ -195,8 +181,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
m.set(
|
||||
"SetFading",
|
||||
lua.create_function(|lua, (this, on): (Table, Value)| {
|
||||
let on = !matches!(on, Value::Nil | Value::Boolean(false));
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
// `GetBoolOrDefault` (`0x6f1c10`, `0x7939a0`) with default 1: a bare call enables.
|
||||
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
|
||||
with_smf(lua, &this, |smf| smf.fading_enabled = on)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -334,23 +334,26 @@ fn frame_type(s: &str) -> Option<FrameType> {
|
||||
Some(FrameType { kind, factory })
|
||||
}
|
||||
|
||||
/// A Lua number-ish → f32 (nil/other → 0.0), for offset/color args.
|
||||
pub(super) fn as_f32(v: &Value) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 0.0,
|
||||
}
|
||||
/// A colour or coordinate argument by the reference's bare `lua_tonumber` (`0x6f3620`): a number,
|
||||
/// or a string `luaO_str2d` (`0x6f5900`) reads as one, and 0.0 for anything else, with no type gate
|
||||
/// and no raise. [`super::binding_abi::coerced_number`] narrowed to the `f32` the widgets store.
|
||||
pub(super) fn tonumber_f32(lua: &Lua, v: &Value) -> f32 {
|
||||
super::binding_abi::coerced_number(lua, Some(v.clone())) as f32
|
||||
}
|
||||
|
||||
/// [`as_f32`] in `f64`, for `ColorSelect:SetColorRGB`, whose quantizer an `f32` detour could push
|
||||
/// across a rounding boundary.
|
||||
pub(super) fn as_f64(v: &Value) -> f64 {
|
||||
match v {
|
||||
Value::Number(n) => *n,
|
||||
Value::Integer(i) => *i as f64,
|
||||
_ => 0.0,
|
||||
}
|
||||
/// A colour's alpha argument: `lua_isnumber`-gated (`0x781037`, `0x79f5a2`, ...), so a numeric
|
||||
/// string counts and anything else, a table or boolean included, stages 1.0 without raising.
|
||||
pub(super) fn alpha_f32(lua: &Lua, v: &Value) -> f32 {
|
||||
lua.coerce_number(v.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
.map_or(1.0, |n| n as f32)
|
||||
}
|
||||
|
||||
/// [`tonumber_f32`] in `f64`, for `ColorSelect:SetColorRGB`, whose quantizer an `f32` detour could
|
||||
/// push across a rounding boundary.
|
||||
pub(super) fn tonumber_f64(lua: &Lua, v: &Value) -> f64 {
|
||||
super::binding_abi::coerced_number(lua, Some(v.clone()))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Frame methods: visibility, hierarchy, strata, level, scale, alpha, backdrop and input flags.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::script::binding_abi::bool_or_default;
|
||||
|
||||
use crate::script::region_map::{set_shared, Side};
|
||||
|
||||
@@ -578,7 +580,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Mouse interaction: `EnableMouse` gates hit-testing.
|
||||
m.set(
|
||||
"EnableMouse",
|
||||
lua.create_function(|lua, (this, enable): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let enable = bool_or_default(args.front(), true); // `0x7770fe`
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
visibility::set_mouse_enabled(lua, h, enable);
|
||||
Ok(())
|
||||
@@ -598,7 +601,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// (`script::keyboard`), separate from having a handler (`0x76af00`).
|
||||
m.set(
|
||||
"EnableKeyboard",
|
||||
lua.create_function(|lua, (this, enable): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let enable = bool_or_default(args.front(), true); // `0x776f4e`
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
@@ -647,7 +651,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// in the reference, and the one the wheel hit test gates on.
|
||||
m.set(
|
||||
"EnableMouseWheel",
|
||||
lua.create_function(|lua, (this, enable): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let enable = bool_or_default(args.front(), true); // `0x77727e`
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
@@ -670,7 +675,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// frame's rect inside the window, size preserved.
|
||||
m.set(
|
||||
"SetClampedToScreen",
|
||||
lua.create_function(|lua, (this, clamp): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let clamp = bool_or_default(args.front(), true); // `0x776c8e`
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let changed = model.arena.is_clamped_to_screen(h) != clamp;
|
||||
@@ -726,10 +732,9 @@ fn backdrop_from_table(t: &Table) -> mlua::Result<Backdrop> {
|
||||
if let Ok(Value::String(s)) = t.get::<Value>("edgeFile") {
|
||||
bd.edge_file = Some(s.to_str()?.to_string());
|
||||
}
|
||||
bd.tile = !matches!(
|
||||
t.get::<Value>("tile").unwrap_or(Value::Nil),
|
||||
Value::Nil | Value::Boolean(false)
|
||||
);
|
||||
// `GetBoolOrDefault` (`0x7778a3`) with default 0 on the `tile` key's value, so `tile = "0"`
|
||||
// and `tile = 0` are off and `tile = "yes"` is on.
|
||||
bd.tile = bool_or_default(Some(&t.get::<Value>("tile").unwrap_or(Value::Nil)), false);
|
||||
if let Ok(Value::Number(n)) = t.get::<Value>("tileSize") {
|
||||
bd.tile_size = n as f32;
|
||||
} else if let Ok(Value::Integer(n)) = t.get::<Value>("tileSize") {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::layout::{Anchor, Anchors, Point};
|
||||
use crate::script::binding_abi::bool_or_default;
|
||||
use crate::script::{Model, UiScript, SCREEN};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
@@ -37,10 +38,12 @@ pub(crate) struct FrameMove {
|
||||
/// Populate `m`'s movable and resizable methods.
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetMovable / IsMovable (`0x776420`/`0x7764d0`): a pure flag write, so clearing it mid-drag
|
||||
// stops nothing. The argument is Lua truthiness, as the reference's `toboolean`.
|
||||
// stops nothing. The argument goes through `GetBoolOrDefault` (`0x6f1c10`) with default 1
|
||||
// (`0x7764ae`), as every flag setter of the Frame table does.
|
||||
m.set(
|
||||
"SetMovable",
|
||||
lua.create_function(|lua, (this, flag): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let flag = bool_or_default(args.front(), true);
|
||||
set_flag(lua, &this, Flag::Movable, flag)
|
||||
})?,
|
||||
)?;
|
||||
@@ -51,7 +54,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetResizable / IsResizable (`0x776590`/`0x776640`): the same pure flag write.
|
||||
m.set(
|
||||
"SetResizable",
|
||||
lua.create_function(|lua, (this, flag): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let flag = bool_or_default(args.front(), true); // `0x77661e`
|
||||
set_flag(lua, &this, Flag::Resizable, flag)
|
||||
})?,
|
||||
)?;
|
||||
@@ -132,7 +136,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// nor resizable (`0x776adb`).
|
||||
m.set(
|
||||
"SetUserPlaced",
|
||||
lua.create_function(|lua, (this, flag): (Table, bool)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let flag = bool_or_default(args.front(), true); // `0x776b29`
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let ok = model
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! per-tick pass that keeps it (`0x7657d0`). The wired triggers are `Raise` (`0x775b0a`), Show
|
||||
//! (`0x76aeec`), drag start (`0x7652d7`) and mouse-down (`0x766392`).
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
use mlua::{Lua, MultiValue, Table};
|
||||
|
||||
use crate::layout::Rect;
|
||||
use crate::script::{Model, UiScript};
|
||||
@@ -25,14 +25,12 @@ use super::frame_handle_of;
|
||||
|
||||
/// Populate `m`'s toplevel and raise methods.
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetToplevel's argument is optional and defaults to true (`0x775440`), unlike `SetMovable`'s.
|
||||
// SetToplevel's argument goes through `GetBoolOrDefault` (`0x6f1c10`) with default 1
|
||||
// (`0x775440`, `0x7754ce`): a bare call sets it, an explicit nil clears it.
|
||||
m.set(
|
||||
"SetToplevel",
|
||||
lua.create_function(|lua, (this, flag): (Table, Option<Value>)| {
|
||||
let on = match flag {
|
||||
None => true,
|
||||
Some(v) => !matches!(v, Value::Nil | Value::Boolean(false)),
|
||||
};
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
|
||||
@@ -625,12 +625,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// ConfirmReadyCheck(ready) reads its argument as truth: the reference's No button passes no
|
||||
// argument, Yes passes `1`.
|
||||
// ConfirmReadyCheck(ready) (`0x4bc090`) reads its argument through `GetBoolOrDefault`
|
||||
// (`0x4bc0ad`) with default 0: the reference's No button passes no argument, Yes passes `1`.
|
||||
g.set(
|
||||
"ConfirmReadyCheck",
|
||||
lua.create_function(|lua, ready: Value| {
|
||||
let ready = !matches!(ready, Value::Nil | Value::Boolean(false));
|
||||
lua.create_function(|lua, ready: Option<Value>| {
|
||||
let ready = crate::script::binding_abi::bool_or_default(ready.as_ref(), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model
|
||||
.party_requests
|
||||
@@ -1339,8 +1339,8 @@ mod tests {
|
||||
PartyRequest::ReadyCheckAnswer(true),
|
||||
PartyRequest::ReadyCheckAnswer(false),
|
||||
PartyRequest::ReadyCheckAnswer(false),
|
||||
// `0` is true in Lua.
|
||||
PartyRequest::ReadyCheckAnswer(true),
|
||||
// `0` truncates to 0 (`GetBoolOrDefault`, `0x4bc0ad`), where Lua calls it true.
|
||||
PartyRequest::ReadyCheckAnswer(false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::script::object::{as_f32, draw_layer_from_str, draw_layer_name};
|
||||
use crate::script::object::{draw_layer_from_str, draw_layer_name, tonumber_f32};
|
||||
use crate::script::{BlendMode, Model, TexCoords};
|
||||
|
||||
use super::region_handle_of;
|
||||
@@ -36,18 +36,26 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetVertexColor (`0x79abd0`) reads r, g and b with a bare `lua_tonumber`, so nil or a table is
|
||||
// 0 and never raises; stock `QuestLogFrame.lua:337` can pass three nils. A missing alpha is 1
|
||||
// here; the reference keeps the region's current alpha (read back at `0x79ac81`, copied over
|
||||
// at `0x79adfe`/`0x79ae0a`), so there a three-argument call cannot restore opacity.
|
||||
// 0 and never raises; stock `QuestLogFrame.lua:337` can pass three nils. The alpha is staged
|
||||
// 1.0 (`0x79ad43`) but re-tested at the end (`0x79adfe`): when it is not a `lua_isnumber` the
|
||||
// colour read back at the start (`0x79ac81`) lends its alpha byte (`0x79ae0a`), so a
|
||||
// three-argument call keeps the region's alpha. Stock `ActionButton_UpdateUsable`
|
||||
// (`ActionButton.lua:274-281`) leans on it: a ring `ActionButton_ShowGrid` dimmed stays dim.
|
||||
m.set(
|
||||
"SetVertexColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
let (r, g, b) = (as_f32(&r), as_f32(&g), as_f32(&b));
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = lua.coerce_number(a).ok().flatten().map(|n| n as f32);
|
||||
let (r, g, b) = (
|
||||
tonumber_f32(lua, &r),
|
||||
tonumber_f32(lua, &g),
|
||||
tonumber_f32(lua, &b),
|
||||
);
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.vertex_color = Some([r, g, b, a.unwrap_or(1.0)]);
|
||||
let kept = d.vertex_color.map_or(1.0, |c| c[3]);
|
||||
d.vertex_color = Some([r, g, b, a.unwrap_or(kept)]);
|
||||
// The slot `SetTextColor` writes on a FontString, so it overrides the font object.
|
||||
d.font_explicit.color = true;
|
||||
Ok(())
|
||||
@@ -90,8 +98,21 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
};
|
||||
let n = if with_alpha { 8 } else { 6 };
|
||||
let mut c = [0.0f32; 8];
|
||||
for slot in c.iter_mut().take(n) {
|
||||
*slot = it.next().as_ref().map(as_f32).unwrap_or(0.0);
|
||||
for (i, slot) in c.iter_mut().take(n).enumerate() {
|
||||
let v = it.next();
|
||||
// Every channel is a bare `lua_tonumber` (`0x79aef3`-`0x79b083`, `0x79b242`-
|
||||
// `0x79b490`) except the two alphas, which are `lua_isnumber`-gated with
|
||||
// default 1 (`0x79b30d`, `0x79b480`).
|
||||
*slot = match v {
|
||||
Some(v) if with_alpha && matches!(i, 3 | 7) => lua
|
||||
.coerce_number(v)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map_or(1.0, |n| n as f32),
|
||||
Some(v) => tonumber_f32(lua, &v),
|
||||
None if with_alpha && matches!(i, 3 | 7) => 1.0,
|
||||
None => 0.0,
|
||||
};
|
||||
}
|
||||
let (start, end) = if with_alpha {
|
||||
([c[0], c[1], c[2], c[3]], [c[4], c[5], c[6], c[7]])
|
||||
@@ -121,6 +142,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(
|
||||
|lua, (this, arg, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
// `lua_isnumber` (`0x79bbd5`) picks the colour form, a numeric string included.
|
||||
let colour_form = lua.coerce_number(arg.clone()).ok().flatten().is_some();
|
||||
// Scoped so the model borrow ends before the layout touch; the path arm drops it
|
||||
// mid-way to call the host probe.
|
||||
let loaded = {
|
||||
@@ -135,7 +158,6 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// the colour form (`0x770360`) never writes it.
|
||||
let same_path = matches!((&arg, &data.texture),
|
||||
(Value::String(s), Some(cur)) if s.to_str().is_ok_and(|s| *s == **cur));
|
||||
let colour_form = matches!(&arg, Value::Number(_) | Value::Integer(_));
|
||||
if !same_path && !colour_form {
|
||||
data.desaturated = false;
|
||||
}
|
||||
@@ -143,6 +165,24 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// each clears the other; neither touches the vertex colour (`+0xb8`), so a
|
||||
// tint outlives its art.
|
||||
let loaded = match &arg {
|
||||
// The colour form (`lua_isnumber`, `0x79bbd5`, so a numeric string too): g
|
||||
// and b are a bare `lua_tonumber`, and alpha is `lua_isnumber`-gated with
|
||||
// default 1.
|
||||
_ if colour_form => {
|
||||
let alpha = lua
|
||||
.coerce_number(a.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
.map_or(1.0, |n| n as f32);
|
||||
data.fill = Some([
|
||||
tonumber_f32(lua, &arg),
|
||||
tonumber_f32(lua, &g),
|
||||
tonumber_f32(lua, &b),
|
||||
alpha,
|
||||
]);
|
||||
data.texture = None;
|
||||
true
|
||||
}
|
||||
// "" clears, as nil does (stock `QuestLogFrame.lua:166`), and answers nil
|
||||
// like a failed load; the reference's answer for "" is untraced.
|
||||
Value::String(s) if s.to_str()?.is_empty() => {
|
||||
@@ -172,23 +212,6 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
}
|
||||
resolvable && had_probe
|
||||
}
|
||||
// The colour form, the only one to read the trailing three; a non-number
|
||||
// there takes the default a missing one does.
|
||||
Value::Number(_) | Value::Integer(_) => {
|
||||
let chan = |v: &Value, dflt: f32| match v {
|
||||
Value::Number(_) | Value::Integer(_) => as_f32(v),
|
||||
Value::String(s) => s
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<f32>().ok())
|
||||
.unwrap_or(dflt),
|
||||
_ => dflt,
|
||||
};
|
||||
data.fill =
|
||||
Some([as_f32(&arg), chan(&g, 0.0), chan(&b, 0.0), chan(&a, 1.0)]);
|
||||
data.texture = None;
|
||||
true
|
||||
}
|
||||
// nil clears, so the region draws nothing, and answers 1 (`0x79bb40`).
|
||||
Value::Nil => {
|
||||
data.texture = None;
|
||||
@@ -213,10 +236,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
|
||||
// SetDesaturated(flag) (`0x79c1e0`), Texture only, answers shaderSupported, which stock
|
||||
// `ItemButtonTemplate.lua:69` reads to fall back to a grey tint; the renderer greys the texel,
|
||||
// so this answers 1. The flag is `GetBoolOrDefault` (`0x6f1c10`, default 1, jump table
|
||||
// `0x6f1ce8`): no argument is on, hence the `MultiValue`, nil is off, a number truncating to 0
|
||||
// is off (`0x6f3620`, `0x40a2b0`), and a table, function or userdata is on. Every string is on
|
||||
// here; the reference's string arm (`0x6f1c51`, against `0x871460` and `0x853758`) is not.
|
||||
// so this answers 1. The flag is `GetBoolOrDefault` (`0x6f1c10`, default 1, `0x79c27e`): no
|
||||
// argument is on, hence the `MultiValue`, nil is off, and a string goes by its first byte.
|
||||
m.set(
|
||||
"SetDesaturated",
|
||||
lua.create_function(|lua, args: MultiValue| {
|
||||
@@ -225,14 +246,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Some(Value::Table(t)) => t,
|
||||
_ => return Ok(Value::Nil),
|
||||
};
|
||||
let on = match args.next() {
|
||||
None => true,
|
||||
Some(Value::Nil) => false,
|
||||
Some(Value::Boolean(b)) => b,
|
||||
Some(Value::Integer(i)) => i != 0,
|
||||
Some(Value::Number(n)) => n.trunc() != 0.0,
|
||||
Some(_) => true,
|
||||
};
|
||||
let on = crate::script::binding_abi::bool_or_default(args.next().as_ref(), true);
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().desaturated = on;
|
||||
@@ -312,11 +326,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// flag makes `SetTexCoord` re-derive the rect from the UV quad (`0x770462`).
|
||||
m.set(
|
||||
"SetTexCoordModifiesRect",
|
||||
lua.create_function(|lua, (this, arg): (Table, Value)| {
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
// `GetBoolOrDefault` (`0x6f1c10`) with default 1 (`0x79c105`), but a missing argument
|
||||
// arrives here as nil and reads false, where the reference sets the flag.
|
||||
let on = crate::script::binding_abi::bool_or_default(Some(&arg), false);
|
||||
// `GetBoolOrDefault` (`0x6f1c10`) with default 1 (`0x79c10e`): a bare call sets it.
|
||||
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model
|
||||
.region_data
|
||||
@@ -397,7 +410,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Each coordinate is a bare `lua_tonumber` (`0x79beb0`), so nil or a table is 0; only the
|
||||
// count is gated, 4 or 8 (`0x79bf5d`).
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::Variadic<Value>)| {
|
||||
let rest: Vec<f32> = args.iter().map(as_f32).collect();
|
||||
let rest: Vec<f32> = args.iter().map(|v| tonumber_f32(lua, v)).collect();
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let coords = match rest.len() {
|
||||
4 => Some(TexCoords::Rect([rest[0], rest[1], rest[2], rest[3]])),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The FontString-only methods: the string, its measured width, justification, non-space wrap and
|
||||
//! text height. The font block it shares with EditBox comes from [`crate::script::font_block`].
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use crate::justify;
|
||||
use crate::script::Model;
|
||||
@@ -160,15 +160,12 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetNonSpaceWrap/CanNonSpaceWrap (`0x79e9f0`/`0x79ead0`), FontString only: the getter
|
||||
// answers 1 or nil, and a call with no argument enables.
|
||||
// answers 1 or nil. The flag is `GetBoolOrDefault` (`0x6f1c10`, default 1, `0x79ea7e`): a call
|
||||
// with no argument enables, an explicit nil disables.
|
||||
m.set(
|
||||
"SetNonSpaceWrap",
|
||||
lua.create_function(|lua, (this, enable): (Table, Value)| {
|
||||
let on = match &enable {
|
||||
Value::Nil => true,
|
||||
Value::Boolean(b) => *b,
|
||||
_ => true,
|
||||
};
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
let on = crate::script::binding_abi::bool_or_default(args.front(), true);
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().non_space_wrap = Some(on);
|
||||
|
||||
@@ -734,7 +734,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let raw = match &text {
|
||||
// Bytes, lossily; never a raise.
|
||||
Value::String(s) => s.to_string_lossy(),
|
||||
Value::Number(_) | Value::Integer(_) => super::object::as_f32(&text).to_string(),
|
||||
Value::Number(n) => (*n as f32).to_string(),
|
||||
Value::Integer(i) => (*i as f32).to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let h = html_handle(lua, &this)?;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use super::binding_abi::string_arg;
|
||||
use super::binding_abi::{bool_or_default, string_arg};
|
||||
use super::who_sort::WhoSortChain;
|
||||
use super::Model;
|
||||
|
||||
@@ -434,16 +434,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetWhoToUI(flag): the Who frame passes 1 on show and 0 on hide; nil, false and 0 are off.
|
||||
// SetWhoToUI(flag) (`0x5ad870`): the Who frame passes 1 on show and 0 on hide; the flag goes
|
||||
// through `GetBoolOrDefault` (`0x5ad877`) with default 0.
|
||||
g.set(
|
||||
"SetWhoToUI",
|
||||
lua.create_function(|lua, flag: Value| {
|
||||
let on = match flag {
|
||||
Value::Nil | Value::Boolean(false) => false,
|
||||
Value::Integer(0) => false,
|
||||
Value::Number(n) => n != 0.0,
|
||||
_ => true,
|
||||
};
|
||||
lua.create_function(|lua, flag: Option<Value>| {
|
||||
let on = bool_or_default(flag.as_ref(), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.social_requests.push(SocialRequest::SetWhoToUi(on));
|
||||
Ok(())
|
||||
|
||||
@@ -243,11 +243,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetStatusBarColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let a = crate::script::object::alpha_f32(lua, &a);
|
||||
let (r, g, b) = (
|
||||
crate::script::object::as_f32(&r),
|
||||
crate::script::object::as_f32(&g),
|
||||
crate::script::object::as_f32(&b),
|
||||
crate::script::object::tonumber_f32(lua, &r),
|
||||
crate::script::object::tonumber_f32(lua, &g),
|
||||
crate::script::object::tonumber_f32(lua, &b),
|
||||
);
|
||||
// `0x78fc20` hands the colour to the bar (`0x782dd0`), which sets its vertex colour
|
||||
// only if a bar exists and keeps nothing otherwise.
|
||||
@@ -255,8 +256,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.region_data.entry(rh).or_default().vertex_color =
|
||||
Some([r, g, b, a.unwrap_or(1.0)]);
|
||||
model.region_data.entry(rh).or_default().vertex_color = Some([r, g, b, a]);
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
|
||||
@@ -1375,3 +1375,62 @@ fn button_get_text_color_answers_four_values_through_the_state_font() {
|
||||
.unwrap();
|
||||
assert_eq!(s.arity("ChkColorBtn:GetTextColor()").unwrap(), 4);
|
||||
}
|
||||
|
||||
/// `CheckButton:SetChecked` (`0x799bf0`) reads its argument through `GetBoolOrDefault` (`0x6f1c10`)
|
||||
/// with default 1 (`0x799c77`): absent, a table, a function and an unknown string check; nil and
|
||||
/// zero do not; a string goes by its first byte, then by `on`/`off`/`enabled`/`disabled`; a number
|
||||
/// keeps only the low dword of its truncation.
|
||||
#[test]
|
||||
fn set_checked_reads_its_argument_as_get_bool_or_default() {
|
||||
let s = script();
|
||||
s.run(r#"C = CreateFrame("CheckButton", "C")"#).unwrap();
|
||||
for (arg, checked) in [
|
||||
("", true),
|
||||
("nil", false),
|
||||
("true", true),
|
||||
("false", false),
|
||||
("0", false),
|
||||
("0.5", false),
|
||||
("-0.5", false),
|
||||
("1", true),
|
||||
("-1", true),
|
||||
("2^32", false),
|
||||
("2^32 + 1", true),
|
||||
("1/0", false),
|
||||
("{}", true),
|
||||
("function() end", true),
|
||||
(r#""yes""#, true),
|
||||
(r#""Yes""#, true),
|
||||
(r#""y""#, true),
|
||||
(r#""t""#, true),
|
||||
(r#""on""#, true),
|
||||
(r#""enabled""#, true),
|
||||
(r#""1abc""#, true),
|
||||
(r#""01""#, false),
|
||||
(r#"" 0""#, true),
|
||||
("\"\"", true),
|
||||
(r#""abc""#, true),
|
||||
(r#""off""#, false),
|
||||
(r#""Disabled""#, false),
|
||||
(r#""no""#, false),
|
||||
(r#""false""#, false),
|
||||
(r#""true""#, true),
|
||||
(r#""0""#, false),
|
||||
(r#""1""#, true),
|
||||
(r#"" 1""#, true),
|
||||
(r#""-1""#, true),
|
||||
] {
|
||||
// Start from the opposite state so a call that changes nothing reads as a failure.
|
||||
s.run(&format!(
|
||||
"C:SetChecked({})",
|
||||
if checked { "nil" } else { "1" }
|
||||
))
|
||||
.unwrap();
|
||||
s.run(&format!("C:SetChecked({arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>("return C:GetChecked() == 1").unwrap(),
|
||||
checked,
|
||||
"SetChecked({arg})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Colour and coordinate channels are a bare `lua_tonumber` (`0x6f3620`) with no type gate: a
|
||||
//! numeric string is its number (`luaV_tonumber` `0x6f7c20`, `luaO_str2d` `0x6f5900`), anything
|
||||
//! else 0, and the setter never raises. Sites: `Button:SetTextColor` `0x780ee0`,
|
||||
//! `SetHighlightTextColor` `0x7815e0`, `SetDisabledTextColor` `0x781260`, the Font object setters
|
||||
//! `0x79f4d0` and `0x79f730`, `FontString:SetTextColor` `0x79d9c0` and `SetShadowColor` `0x79dd40`,
|
||||
//! `Texture:SetVertexColor` `0x79abd0`, `SetTexCoord` `0x79beb0`, `SetGradient` `0x79ae30`,
|
||||
//! `SetGradientAlpha` `0x79b180`, `StatusBar:SetStatusBarColor` `0x78fc20` and
|
||||
//! `ColorSelect:SetColorRGB` `0x78eae0`.
|
||||
|
||||
use super::common::script;
|
||||
|
||||
/// Numeric strings and a hostile one; each reads as the number on the right.
|
||||
const CHANNELS: &str = r#""1", "0.5", "abc""#;
|
||||
|
||||
/// `set(r, g, b)` with string channels, read back through `get` as three numbers.
|
||||
fn check(setup: &str, call: &str, get: &str) {
|
||||
check_as(setup, call, get, (1.0, 0.5, 0.0));
|
||||
}
|
||||
|
||||
fn check_as(setup: &str, call: &str, get: &str, want: (f64, f64, f64)) {
|
||||
let s = script();
|
||||
s.run(setup).unwrap();
|
||||
s.run(&call.replace("ARGS", CHANNELS)).unwrap();
|
||||
let (r, g, b) = s
|
||||
.eval::<(f64, f64, f64)>(&format!("local r, g, b = {get} return r, g, b"))
|
||||
.unwrap();
|
||||
assert_eq!((r, g, b), want, "{call}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_and_shadow_colour_setters_coerce_numeric_strings() {
|
||||
check(
|
||||
r#"B = CreateFrame("Button", "B")"#,
|
||||
"B:SetTextColor(ARGS)",
|
||||
"B:GetTextColor()",
|
||||
);
|
||||
check(
|
||||
r#"CreateFont("Fnt")"#,
|
||||
"Fnt:SetTextColor(ARGS)",
|
||||
"Fnt:GetTextColor()",
|
||||
);
|
||||
check(
|
||||
r#"CreateFont("Fnt")"#,
|
||||
"Fnt:SetShadowColor(ARGS)",
|
||||
"Fnt:GetShadowColor()",
|
||||
);
|
||||
let setup = r#"F = CreateFrame("Frame", "F") FS = F:CreateFontString("FS")"#;
|
||||
check(setup, "FS:SetTextColor(ARGS)", "FS:GetTextColor()");
|
||||
check(setup, "FS:SetShadowColor(ARGS)", "FS:GetShadowColor()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn button_highlight_and_disabled_colours_coerce_numeric_strings() {
|
||||
let s = script();
|
||||
s.run(r#"B = CreateFrame("Button", "B")"#).unwrap();
|
||||
s.run(&format!(
|
||||
"B:SetHighlightTextColor({CHANNELS}) B:SetDisabledTextColor({CHANNELS})"
|
||||
))
|
||||
.unwrap();
|
||||
let t: mlua::Table = s.lua().globals().get("B").unwrap();
|
||||
let h = crate::script::object::frame_handle_of(s.lua(), &t).unwrap();
|
||||
let model = s.model_ref();
|
||||
let Some(crate::widget::KindState::Button(bs)) = model.arena.frame(h).map(|f| &f.kind_state)
|
||||
else {
|
||||
panic!("not a button");
|
||||
};
|
||||
assert_eq!(bs.highlight_color, Some([1.0, 0.5, 0.0, 1.0]));
|
||||
assert_eq!(bs.disabled_color, Some([1.0, 0.5, 0.0, 1.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertex_bar_and_picker_colours_coerce_numeric_strings() {
|
||||
check(
|
||||
r#"F = CreateFrame("Frame", "F") T = F:CreateTexture("T")"#,
|
||||
"T:SetVertexColor(ARGS)",
|
||||
"T:GetVertexColor()",
|
||||
);
|
||||
check(
|
||||
r#"S = CreateFrame("StatusBar", "S") S:SetStatusBarTexture("Interface\\Bar")"#,
|
||||
"S:SetStatusBarColor(ARGS)",
|
||||
"S:GetStatusBarColor()",
|
||||
);
|
||||
// The widget quantizes each channel to a byte: 0.5 is 128 / 255.
|
||||
check_as(
|
||||
r#"C = CreateFrame("ColorSelect", "C")"#,
|
||||
"C:SetColorRGB(ARGS)",
|
||||
"C:GetColorRGB()",
|
||||
(1.0, 128.0 / 255.0, 0.0),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tex_coord_and_gradients_coerce_numeric_strings() {
|
||||
let s = script();
|
||||
s.run(r#"F = CreateFrame("Frame", "F") T = F:CreateTexture("T")"#)
|
||||
.unwrap();
|
||||
s.run(r#"T:SetTexCoord("0.25", "0.75", "0.5", "1")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<Vec<f64>>("return {T:GetTexCoord()}").unwrap(),
|
||||
vec![0.25, 0.5, 0.25, 1.0, 0.75, 0.5, 0.75, 1.0]
|
||||
);
|
||||
s.run(r#"T:SetTexCoord("0", "1", "abc", "{}")"#).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<Vec<f64>>("return {T:GetTexCoord()}").unwrap(),
|
||||
vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0]
|
||||
);
|
||||
|
||||
let t: mlua::Table = s.lua().globals().get("T").unwrap();
|
||||
let rh = crate::script::region::region_handle_of(s.lua(), &t).unwrap();
|
||||
let gradient = || {
|
||||
let g = s
|
||||
.model_ref()
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.gradient)
|
||||
.expect("a gradient");
|
||||
(g.start, g.end)
|
||||
};
|
||||
s.run(r#"T:SetGradient("VERTICAL", "1", "0.5", "abc", "0.25", "0.75", "1")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
gradient(),
|
||||
([1.0, 0.5, 0.0, 1.0], [0.25, 0.75, 1.0, 1.0]),
|
||||
"SetGradient"
|
||||
);
|
||||
s.run(r#"T:SetGradientAlpha("HORIZONTAL", "1", "0.5", "abc", "0.5", "0.25", "0.75", "1", "0.25")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
gradient(),
|
||||
([1.0, 0.5, 0.0, 0.5], [0.25, 0.75, 1.0, 0.25]),
|
||||
"SetGradientAlpha"
|
||||
);
|
||||
// The alphas are `lua_isnumber`-gated with default 1 (`0x79b30d`), not a bare read.
|
||||
s.run(r#"T:SetGradientAlpha("VERTICAL", 0, 0, 0, nil, 1, 1, 1, "x")"#)
|
||||
.unwrap();
|
||||
assert_eq!(gradient(), ([0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0]));
|
||||
}
|
||||
|
||||
/// `SetTexture`'s colour form (`0x79bb40`): g and b are a bare read, alpha `lua_isnumber`-gated,
|
||||
/// and `luaO_str2d` skips the blanks around a number.
|
||||
#[test]
|
||||
fn texture_colour_form_coerces_numeric_strings() {
|
||||
let s = script();
|
||||
s.run(r#"F = CreateFrame("Frame", "F") T = F:CreateTexture("T")"#)
|
||||
.unwrap();
|
||||
s.run(r#"T:SetTexture(1, " 0.5 ", "abc", "0.25")"#).unwrap();
|
||||
let t: mlua::Table = s.lua().globals().get("T").unwrap();
|
||||
let rh = crate::script::region::region_handle_of(s.lua(), &t).unwrap();
|
||||
assert_eq!(
|
||||
s.model_ref().region_data.get(&rh).and_then(|d| d.fill),
|
||||
Some([1.0, 0.5, 0.0, 0.25])
|
||||
);
|
||||
}
|
||||
|
||||
/// A frame's message lines as `(text, rgb bytes, id, alpha)`.
|
||||
fn lines(s: &crate::script::UiScript, name: &str) -> Vec<(String, [u8; 3], u32, f32)> {
|
||||
let m = s.model_ref();
|
||||
let h = m.arena.lookup(name).expect("named frame");
|
||||
m.arena
|
||||
.frame(h)
|
||||
.and_then(|f| f.kind_state.message_lines())
|
||||
.map(|l| {
|
||||
l.iter()
|
||||
.map(|l| (l.text.clone(), l.color, l.id, l.alpha))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `MessageFrame:AddMessage` (`0x795590`) gates r, g and b with `lua_isnumber` (`0x79565f`-
|
||||
/// `0x795687`) and reads the alpha under its own gate (`0x795759`): "abc" or a table is white and
|
||||
/// opaque, a numeric string is its number.
|
||||
#[test]
|
||||
fn message_frame_add_message_gates_its_channels_with_is_number() {
|
||||
let s = script();
|
||||
s.run(r#"CreateFrame("MessageFrame", "MF")"#).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
MF:AddMessage("a", "abc", 1, 1)
|
||||
MF:AddMessage("b", "0.5", "1", "0", "0.5")
|
||||
MF:AddMessage("c", 1, 1, 1, {})
|
||||
MF:AddMessage("d", {}, 1, 1)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let got = lines(&s, "MF");
|
||||
assert_eq!(got[0].1, [255, 255, 255], "a failed gate is white");
|
||||
assert_eq!(got[1].1, [128, 255, 0]);
|
||||
assert_eq!(
|
||||
got[1].3,
|
||||
128.0 / 255.0,
|
||||
"a numeric alpha string counts, byte-quantized"
|
||||
);
|
||||
assert_eq!(got[2].3, 1.0, "a table alpha stages 1.0");
|
||||
assert_eq!(got[3].1, [255, 255, 255]);
|
||||
}
|
||||
|
||||
/// `ScrollingMessageFrame:AddMessage` (`0x792900`) gates the same three, then reads the id at 6
|
||||
/// (or r's slot at 3 when the gate failed) through `lua_isnumber` and `_ftol`; `UpdateColorByID`
|
||||
/// (`0x7932b0`) gates all four and does nothing if any fails.
|
||||
#[test]
|
||||
fn scrolling_message_frame_gates_colour_and_id_with_is_number() {
|
||||
let s = script();
|
||||
s.run(r#"CreateFrame("ScrollingMessageFrame", "SMF")"#)
|
||||
.unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
SMF:AddMessage("a", "0.5", "1", "0", "7")
|
||||
SMF:AddMessage("b", "abc", 1, 1, 8)
|
||||
SMF:AddMessage("c", "9")
|
||||
SMF:AddMessage("d", 1, 1, 1, 2.9)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let got = lines(&s, "SMF");
|
||||
assert_eq!((got[0].1, got[0].2), ([128, 255, 0], 7), "numeric strings");
|
||||
assert_eq!((got[1].1, got[1].2), ([255, 255, 255], 0), "id is r's slot");
|
||||
assert_eq!((got[2].1, got[2].2), ([255, 255, 255], 9));
|
||||
assert_eq!(got[3].2, 2, "the id is chopped by _ftol");
|
||||
s.run(r#"SMF:UpdateColorByID(7, 0, "abc", 1)"#).unwrap();
|
||||
assert_eq!(
|
||||
lines(&s, "SMF")[0].1,
|
||||
[128, 255, 0],
|
||||
"a failed gate is a no-op"
|
||||
);
|
||||
s.run(r#"SMF:UpdateColorByID("7", "0", "0.5", 0)"#).unwrap();
|
||||
assert_eq!(lines(&s, "SMF")[0].1, [0, 128, 0]);
|
||||
}
|
||||
|
||||
/// The text-colour alpha is `lua_isnumber`-gated with default 1.0 (`0x781037`, `0x79f5a2`,
|
||||
/// `0x78fd77`, `0x79db17`, `0x79de97`, `0x79f802`): a table or a boolean neither raises nor
|
||||
/// sticks, and a numeric string is its number.
|
||||
#[test]
|
||||
fn colour_alphas_are_gated_not_raised() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
B = CreateFrame("Button", "B")
|
||||
F = CreateFrame("Frame", "F")
|
||||
FS = F:CreateFontString("FS")
|
||||
CreateFont("Fnt")
|
||||
S = CreateFrame("StatusBar", "S") S:SetStatusBarTexture("Interface\\Bar")
|
||||
T = F:CreateTexture("T")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for (call, get) in [
|
||||
("B:SetTextColor(1, 1, 1, ARG)", "B:GetTextColor()"),
|
||||
("FS:SetTextColor(1, 1, 1, ARG)", "FS:GetTextColor()"),
|
||||
("FS:SetShadowColor(1, 1, 1, ARG)", "FS:GetShadowColor()"),
|
||||
("Fnt:SetTextColor(1, 1, 1, ARG)", "Fnt:GetTextColor()"),
|
||||
("Fnt:SetShadowColor(1, 1, 1, ARG)", "Fnt:GetShadowColor()"),
|
||||
("S:SetStatusBarColor(1, 1, 1, ARG)", "S:GetStatusBarColor()"),
|
||||
] {
|
||||
for (arg, want) in [
|
||||
(r#""0.5""#, 0.5),
|
||||
("{}", 1.0),
|
||||
("true", 1.0),
|
||||
(r#""abc""#, 1.0),
|
||||
("nil", 1.0),
|
||||
("0.25", 0.25),
|
||||
] {
|
||||
s.run(&call.replace("ARG", arg))
|
||||
.unwrap_or_else(|e| panic!("{call} with {arg} raised: {e}"));
|
||||
let a = s
|
||||
.eval::<f64>(&format!("local _, _, _, a = {get} return a"))
|
||||
.unwrap();
|
||||
assert_eq!(a, want, "{call} with {arg}");
|
||||
}
|
||||
}
|
||||
// A Font object's colour is replaced whole (`0x79f5f5`...`0x79f65f`): a bare call resets the
|
||||
// alpha rather than keeping it.
|
||||
s.run("Fnt:SetTextColor(1, 1, 1, 0.25) Fnt:SetTextColor(1, 1, 1)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("local _, _, _, a = Fnt:GetTextColor() return a")
|
||||
.unwrap(),
|
||||
1.0
|
||||
);
|
||||
}
|
||||
|
||||
/// `SetVertexColor` (`0x79abd0`) stages alpha 1.0 but re-tests the gate at the end (`0x79adfe`):
|
||||
/// anything but a number lends the region's current alpha back (`0x79ae0a`), and never raises.
|
||||
#[test]
|
||||
fn set_vertex_color_keeps_the_alpha_unless_given_a_number() {
|
||||
let s = script();
|
||||
s.run(r#"F = CreateFrame("Frame", "F") T = F:CreateTexture("T")"#)
|
||||
.unwrap();
|
||||
let alpha = || {
|
||||
s.eval::<f64>("local _, _, _, a = T:GetVertexColor() return a")
|
||||
.unwrap()
|
||||
};
|
||||
s.run("T:SetVertexColor(1, 1, 1, 0.5)").unwrap();
|
||||
assert_eq!(alpha(), 0.5);
|
||||
for arg in ["", ", nil", ", {}", ", true", r#", "x""#] {
|
||||
s.run(&format!("T:SetVertexColor(0, 0, 0{arg})"))
|
||||
.unwrap_or_else(|e| panic!("SetVertexColor(0, 0, 0{arg}) raised: {e}"));
|
||||
assert_eq!(alpha(), 0.5, "SetVertexColor(0, 0, 0{arg}) keeps it");
|
||||
}
|
||||
s.run(r#"T:SetVertexColor(0, 0, 0, "0.25")"#).unwrap();
|
||||
assert_eq!(alpha(), 0.25, "a numeric string is a number");
|
||||
}
|
||||
|
||||
/// `lua_isnumber` on the first argument picks `SetTexture`'s colour form (`0x79bbd5`), so a
|
||||
/// numeric string is a fill, not a path.
|
||||
#[test]
|
||||
fn set_texture_takes_a_numeric_string_as_a_colour() {
|
||||
let s = script();
|
||||
s.run(r#"F = CreateFrame("Frame", "F") T = F:CreateTexture("T")"#)
|
||||
.unwrap();
|
||||
s.run(r#"T:SetTexture("1", "0.5", 0)"#).unwrap();
|
||||
let t: mlua::Table = s.lua().globals().get("T").unwrap();
|
||||
let rh = crate::script::region::region_handle_of(s.lua(), &t).unwrap();
|
||||
let m = s.model_ref();
|
||||
let d = m.region_data.get(&rh).unwrap();
|
||||
assert_eq!(d.fill, Some([1.0, 0.5, 0.0, 1.0]));
|
||||
assert_eq!(d.texture, None, "a numeric string is not a path");
|
||||
}
|
||||
|
||||
/// `GameTooltip:SetText`'s alpha (`0x531d23`) is `lua_isnumber`-gated too.
|
||||
#[test]
|
||||
fn tooltip_set_text_alpha_accepts_a_numeric_string() {
|
||||
let s = script();
|
||||
s.run(r#"TT = CreateFrame("GameTooltip", "TT")"#).unwrap();
|
||||
s.run(r#"TT:SetText("x", 1, 1, 1, "0.5")"#).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("local _, _, _, a = TTTextLeft1:GetTextColor() return a")
|
||||
.unwrap(),
|
||||
0.5
|
||||
);
|
||||
s.run(r#"TT:SetText("x", 1, 1, 1, {})"#).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("local _, _, _, a = TTTextLeft1:GetTextColor() return a")
|
||||
.unwrap(),
|
||||
1.0
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
//! The flag arguments the reference reads through `GetBoolOrDefault` (`0x6f1c10`): absent takes the
|
||||
//! binding's default, nil is false, a boolean is itself, a number is its truncation's low dword, a
|
||||
//! string goes by its first byte and then by `on`/`off`/`enabled`/`disabled`, and a table or
|
||||
//! function takes the default. Each binding below is one call site of the helper, with the default
|
||||
//! its site pushes.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::*;
|
||||
|
||||
/// Every argument form with the answer the helper gives it for `default`.
|
||||
fn cases(default: bool) -> Vec<(&'static str, bool)> {
|
||||
vec![
|
||||
("nil", false),
|
||||
("true", true),
|
||||
("false", false),
|
||||
("0", false),
|
||||
("0.5", false),
|
||||
("-0.5", false),
|
||||
("1", true),
|
||||
("-1", true),
|
||||
("2^32", false),
|
||||
("2^32 + 1", true),
|
||||
("1/0", false),
|
||||
(r#""0""#, false),
|
||||
(r#""1""#, true),
|
||||
(r#""0.5""#, false),
|
||||
(r#""9lives""#, true),
|
||||
(r#""f""#, false),
|
||||
(r#""False""#, false),
|
||||
(r#""n""#, false),
|
||||
(r#""no""#, false),
|
||||
(r#""t""#, true),
|
||||
(r#""True""#, true),
|
||||
(r#""y""#, true),
|
||||
(r#""yes""#, true),
|
||||
(r#""off""#, false),
|
||||
(r#""OFF""#, false),
|
||||
(r#""Disabled""#, false),
|
||||
(r#""on""#, true),
|
||||
(r#""Enabled""#, true),
|
||||
(r#""""#, default),
|
||||
(r#""zzz""#, default),
|
||||
(r#"" 0""#, default),
|
||||
(r#""-1""#, default),
|
||||
("{}", default),
|
||||
("function() end", default),
|
||||
]
|
||||
}
|
||||
|
||||
/// Drive one flag binding: `set(arg)` writes it, `get()` reads it back as a Lua boolean. Each case
|
||||
/// starts from the opposite of its answer, so an ignored argument fails.
|
||||
fn check(s: &UiScript, what: &str, set: &str, get: &str, default: bool) {
|
||||
let put = |arg: &str| {
|
||||
let call = set.replace("ARG", arg);
|
||||
s.run(&call)
|
||||
.unwrap_or_else(|e| panic!("{what}: {call}: {e}"));
|
||||
};
|
||||
let read = || s.eval::<bool>(&format!("return ({get}) == true")).unwrap();
|
||||
// The bare call takes the default.
|
||||
put("true");
|
||||
put("false");
|
||||
s.run(&set.replace(", ARG", "").replace("ARG", "")).unwrap();
|
||||
assert_eq!(read(), default, "{what}: no argument");
|
||||
for (arg, want) in cases(default) {
|
||||
put(if want { "false" } else { "true" });
|
||||
assert_eq!(read(), !want, "{what}: setup");
|
||||
put(arg);
|
||||
assert_eq!(read(), want, "{what}({arg})");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Frame methods, all default 1 (`0x775440` ... `0x77727e`).
|
||||
#[test]
|
||||
fn frame_flag_setters_read_get_bool_or_default_with_default_one() {
|
||||
let s = script();
|
||||
s.run(r#"F = CreateFrame("Frame", "F")"#).unwrap();
|
||||
for (set, get) in [
|
||||
("SetToplevel", "IsToplevel"),
|
||||
("SetMovable", "IsMovable"),
|
||||
("SetResizable", "IsResizable"),
|
||||
("SetClampedToScreen", "IsClampedToScreen"),
|
||||
("EnableKeyboard", "IsKeyboardEnabled"),
|
||||
("EnableMouse", "IsMouseEnabled"),
|
||||
("EnableMouseWheel", "IsMouseWheelEnabled"),
|
||||
] {
|
||||
// The predicates answer the number 1 or nil, never a boolean.
|
||||
check(
|
||||
&s,
|
||||
set,
|
||||
&format!("F:{set}(ARG)"),
|
||||
&format!("F:{get}() == 1"),
|
||||
true,
|
||||
);
|
||||
}
|
||||
// `SetUserPlaced` (`0x776a50`) refuses a frame that is neither movable nor resizable.
|
||||
s.run("F:SetMovable(true)").unwrap();
|
||||
check(
|
||||
&s,
|
||||
"SetUserPlaced",
|
||||
"F:SetUserPlaced(ARG)",
|
||||
"F:IsUserPlaced() == 1",
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/// `SetFading` on both message-frame classes (`0x7939a0`, `0x7950d0`), default 1.
|
||||
#[test]
|
||||
fn set_fading_reads_get_bool_or_default_with_default_one() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
S = CreateFrame("ScrollingMessageFrame", "S")
|
||||
M = CreateFrame("MessageFrame", "M")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for f in ["S", "M"] {
|
||||
check(
|
||||
&s,
|
||||
&format!("{f}:SetFading"),
|
||||
&format!("{f}:SetFading(ARG)"),
|
||||
&format!("{f}:GetFading() == 1"),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `SetTexCoordModifiesRect`, `SetDesaturated` (`0x79c10e`, `0x79c27e`) and `SetNonSpaceWrap`
|
||||
/// (`0x79ea7e`), default 1.
|
||||
#[test]
|
||||
fn texture_and_fontstring_flags_read_get_bool_or_default_with_default_one() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
F = CreateFrame("Frame", "F")
|
||||
T = F:CreateTexture("T")
|
||||
FS = F:CreateFontString("FS")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
&s,
|
||||
"SetTexCoordModifiesRect",
|
||||
"T:SetTexCoordModifiesRect(ARG)",
|
||||
"T:GetTexCoordModifiesRect() == 1",
|
||||
true,
|
||||
);
|
||||
check(
|
||||
&s,
|
||||
"SetNonSpaceWrap",
|
||||
"FS:SetNonSpaceWrap(ARG)",
|
||||
"FS:CanNonSpaceWrap() == 1",
|
||||
true,
|
||||
);
|
||||
// Desaturation has no getter; read it off the region.
|
||||
let desat = || {
|
||||
let t: mlua::Table = s.lua().globals().get("T").unwrap();
|
||||
let rh = crate::script::region::region_handle_of(s.lua(), &t).unwrap();
|
||||
s.model_ref()
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.is_some_and(|d| d.desaturated)
|
||||
};
|
||||
s.run("T:SetDesaturated(false) T:SetDesaturated()").unwrap();
|
||||
assert!(desat(), "SetDesaturated: no argument takes the default 1");
|
||||
for (arg, want) in cases(true) {
|
||||
s.run(&format!(
|
||||
"T:SetDesaturated({})",
|
||||
if want { "false" } else { "true" }
|
||||
))
|
||||
.unwrap();
|
||||
s.run(&format!("T:SetDesaturated({arg})")).unwrap();
|
||||
assert_eq!(desat(), want, "SetDesaturated({arg})");
|
||||
}
|
||||
}
|
||||
|
||||
/// `SetBackdrop`'s `tile` key (`0x7778a3`), default 0.
|
||||
#[test]
|
||||
fn backdrop_tile_reads_get_bool_or_default_with_default_zero() {
|
||||
let s = script();
|
||||
s.run(r#"F = CreateFrame("Frame", "F")"#).unwrap();
|
||||
for (arg, want) in cases(false) {
|
||||
if arg == "nil" {
|
||||
continue; // an absent key and a nil value are the same table slot
|
||||
}
|
||||
s.run(&format!("F:SetBackdrop({{ tile = {arg} }})"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>("return F:GetBackdrop().tile == 1").unwrap(),
|
||||
want,
|
||||
"tile = {arg}"
|
||||
);
|
||||
}
|
||||
s.run("F:SetBackdrop({})").unwrap();
|
||||
assert!(!s.eval::<bool>("return F:GetBackdrop().tile == 1").unwrap());
|
||||
}
|
||||
|
||||
/// The chat-window records (`0x4a1777` default 1, `0x4a1697` default 0).
|
||||
#[test]
|
||||
fn chat_window_flags_read_get_bool_or_default() {
|
||||
let s = script();
|
||||
// `GetChatWindowInfo` answers shown 7th and locked 8th, each 1 or nil; Lua 5.0 has no `select`.
|
||||
s.run(
|
||||
"function ChatShown() local _, _, _, _, _, _, v = GetChatWindowInfo(2) return v end \
|
||||
function ChatLocked() local _, _, _, _, _, _, _, v = GetChatWindowInfo(2) return v end",
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
&s,
|
||||
"SetChatWindowShown",
|
||||
"SetChatWindowShown(2, ARG)",
|
||||
"ChatShown() == 1",
|
||||
true,
|
||||
);
|
||||
check(
|
||||
&s,
|
||||
"SetChatWindowLocked",
|
||||
"SetChatWindowLocked(2, ARG)",
|
||||
"ChatLocked() == 1",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// The guild flags: show-offline (`0x4d1e17`, default 1), the rank checkbox (`0x4d20b7`, default 0).
|
||||
#[test]
|
||||
fn guild_flags_read_get_bool_or_default() {
|
||||
let s = script();
|
||||
check(
|
||||
&s,
|
||||
"SetGuildRosterShowOffline",
|
||||
"SetGuildRosterShowOffline(ARG)",
|
||||
"GetGuildRosterShowOffline() == 1",
|
||||
true,
|
||||
);
|
||||
let staged = || s.model_ref().guild_control.staged[0];
|
||||
s.run("GuildControlSetRankFlag(1, true) GuildControlSetRankFlag(1)")
|
||||
.unwrap();
|
||||
assert!(!staged(), "no argument takes the default 0");
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!(
|
||||
"GuildControlSetRankFlag(1, {})",
|
||||
if want { "false" } else { "true" }
|
||||
))
|
||||
.unwrap();
|
||||
s.run(&format!("GuildControlSetRankFlag(1, {arg})"))
|
||||
.unwrap();
|
||||
assert_eq!(staged(), want, "GuildControlSetRankFlag(1, {arg})");
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetNumGuildMembers`' flag (`0x4d11a0`, default 0) counts the whole roster.
|
||||
#[test]
|
||||
fn get_num_guild_members_flag_counts_the_whole_roster() {
|
||||
let mut s = script();
|
||||
let member = |name: &str, online: bool| GuildMemberInfo {
|
||||
name: name.into(),
|
||||
online,
|
||||
..Default::default()
|
||||
};
|
||||
s.set_guild(GuildState {
|
||||
in_guild: true,
|
||||
roster: vec![member("A", true), member("B", false), member("C", false)],
|
||||
num_members: 1,
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(s.eval::<i64>("return GetNumGuildMembers()").unwrap(), 1);
|
||||
assert_eq!(s.eval::<i64>("return GetNumGuildMembers(nil)").unwrap(), 1);
|
||||
assert_eq!(s.eval::<i64>("return GetNumGuildMembers(0)").unwrap(), 1);
|
||||
assert_eq!(s.eval::<i64>("return GetNumGuildMembers('0')").unwrap(), 1);
|
||||
assert_eq!(s.eval::<i64>("return GetNumGuildMembers(1)").unwrap(), 3);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetNumGuildMembers('yes')").unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return GetNumGuildMembers(true)").unwrap(), 3);
|
||||
}
|
||||
|
||||
/// The request-queueing verbs, each reading one flag through the helper.
|
||||
#[test]
|
||||
fn queued_verbs_read_their_flags_through_get_bool_or_default() {
|
||||
let mut s = script();
|
||||
s.set_guild(GuildState::default());
|
||||
|
||||
// SetWhoToUI(flag) 0x5ad877, default 0.
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!("SetWhoToUI({arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_social_requests(),
|
||||
vec![SocialRequest::SetWhoToUi(want)],
|
||||
"SetWhoToUI({arg})"
|
||||
);
|
||||
}
|
||||
s.run("SetWhoToUI()").unwrap();
|
||||
assert_eq!(
|
||||
s.take_social_requests(),
|
||||
vec![SocialRequest::SetWhoToUi(false)]
|
||||
);
|
||||
|
||||
// TargetByName / FollowByName exact-match flag, default 0 (`0x489d8e`, `0x489f14`).
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!("FollowByName('Bob', {arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_follow_requests(),
|
||||
vec![FollowRequest::ByName {
|
||||
name: "Bob".into(),
|
||||
exact: want
|
||||
}],
|
||||
"FollowByName('Bob', {arg})"
|
||||
);
|
||||
s.run(&format!("TargetByName('Bob', {arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_script_calls(),
|
||||
vec![ScriptCall::TargetByName {
|
||||
name: "Bob".into(),
|
||||
exact: want
|
||||
}],
|
||||
"TargetByName('Bob', {arg})"
|
||||
);
|
||||
}
|
||||
|
||||
// ConfirmReadyCheck(ready) 0x4bc0ad, default 0.
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!("ConfirmReadyCheck({arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_party_requests(),
|
||||
vec![PartyRequest::ReadyCheckAnswer(want)],
|
||||
"ConfirmReadyCheck({arg})"
|
||||
);
|
||||
}
|
||||
s.run("ConfirmReadyCheck()").unwrap();
|
||||
assert_eq!(
|
||||
s.take_party_requests(),
|
||||
vec![PartyRequest::ReadyCheckAnswer(false)]
|
||||
);
|
||||
|
||||
// UseAction(action, checkCursor, onSelf) 0x4e717e, 0x4e716f, defaults 0.
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!("UseAction(1, 0, {arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_script_calls(),
|
||||
vec![ScriptCall::UseAction(ActionUse {
|
||||
action: 1,
|
||||
on_self: want
|
||||
})],
|
||||
"UseAction(1, 0, {arg})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `LoggingChat(flag)` (`0x49fe69`, default 1) sets whenever an argument is passed, so an explicit
|
||||
/// nil clears; a bare call is the getter.
|
||||
#[test]
|
||||
fn logging_flags_read_get_bool_or_default_with_default_one() {
|
||||
let s = script();
|
||||
for name in ["LoggingChat", "LoggingCombat"] {
|
||||
for (arg, want) in cases(true) {
|
||||
s.run(&format!("{name}({})", if want { "false" } else { "true" }))
|
||||
.unwrap();
|
||||
s.run(&format!("{name}({arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>(&format!("return {name}() == 1")).unwrap(),
|
||||
want,
|
||||
"{name}({arg})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `CreateMacro`'s `local` and `perCharacter` flags (`0x4f167b`, `0x4f168a`) and `EditMacro`'s
|
||||
/// `local` (`0x4f19ad`), default 0; `EditMacro` always writes it.
|
||||
#[test]
|
||||
fn macro_flags_read_get_bool_or_default_with_default_zero() {
|
||||
let s = script();
|
||||
s.run("function MacroLocal(i) local _, _, _, v = GetMacroInfo(i) return v end")
|
||||
.unwrap();
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!(
|
||||
"i = CreateMacro('m', 1, '', {arg}) _, _, _, loc = GetMacroInfo(i)"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>("return loc == 1").unwrap(),
|
||||
want,
|
||||
"CreateMacro(name, icon, body, {arg})"
|
||||
);
|
||||
s.run("DeleteMacro(i)").unwrap();
|
||||
}
|
||||
s.run("i = CreateMacro('m', 1, '', true) EditMacro(i, nil, nil, 'body')")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return MacroLocal(i) == 1").unwrap(),
|
||||
"an omitted EditMacro `local` is stored as 0"
|
||||
);
|
||||
for (arg, want) in cases(false) {
|
||||
s.run(&format!("EditMacro(i, nil, nil, nil, {arg})"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>("return MacroLocal(i) == 1").unwrap(),
|
||||
want,
|
||||
"EditMacro(i, nil, nil, nil, {arg})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The GameTooltip wrap flag (`0x531811`, `0x531dd3`, `0x531b60`), default 0: a wrapped line pins
|
||||
/// its left cell to the wrap width. A double line wraps only with an empty right side, as the line
|
||||
/// core clears the flag otherwise (`0x530284`).
|
||||
#[test]
|
||||
fn tooltip_wrap_flags_read_get_bool_or_default_with_default_zero() {
|
||||
let s = script();
|
||||
s.run(r#"TT = CreateFrame("GameTooltip", "TT")"#).unwrap();
|
||||
let pinned = |line: u32| {
|
||||
let t: mlua::Table = s
|
||||
.lua()
|
||||
.globals()
|
||||
.get(format!("TTTextLeft{line}"))
|
||||
.expect("left line");
|
||||
let rh = crate::script::region::region_handle_of(s.lua(), &t).unwrap();
|
||||
s.model_ref()
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.is_some_and(|d| d.size.is_some())
|
||||
};
|
||||
for (arg, want) in cases(false) {
|
||||
s.run("TT:ClearLines()").unwrap();
|
||||
s.run(&format!("TT:AddLine('line', 1, 1, 1, {arg})"))
|
||||
.unwrap();
|
||||
assert_eq!(pinned(1), want, "AddLine(text, r, g, b, {arg})");
|
||||
s.run(&format!("TT:SetText('line', 1, 1, 1, 1, {arg})"))
|
||||
.unwrap();
|
||||
assert_eq!(pinned(1), want, "SetText(text, r, g, b, a, {arg})");
|
||||
s.run(&format!(
|
||||
"TT:ClearLines() TT:AddDoubleLine('line', '', 1, 1, 1, 1, 1, 1, {arg})"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
pinned(1),
|
||||
want,
|
||||
"AddDoubleLine(.., {arg}) with an empty right"
|
||||
);
|
||||
s.run(&format!(
|
||||
"TT:ClearLines() TT:AddDoubleLine('line', 'right', 1, 1, 1, 1, 1, 1, {arg})"
|
||||
))
|
||||
.unwrap();
|
||||
assert!(!pinned(1), "a double line with a right side never wraps");
|
||||
}
|
||||
s.run("TT:ClearLines() TT:AddLine('line')").unwrap();
|
||||
assert!(!pinned(1), "no flag takes the default 0");
|
||||
}
|
||||
@@ -12,6 +12,7 @@ mod capture;
|
||||
mod channel;
|
||||
mod chat_bubble;
|
||||
mod clear_points;
|
||||
mod colour_args;
|
||||
mod common;
|
||||
mod cooldown;
|
||||
mod create_frame_template;
|
||||
@@ -23,6 +24,7 @@ mod dying_target;
|
||||
mod end_to_end;
|
||||
mod engine_updates;
|
||||
mod events;
|
||||
mod flag_args;
|
||||
mod font_object;
|
||||
mod frame_api;
|
||||
mod generic_for;
|
||||
|
||||
@@ -567,39 +567,34 @@ pub(in crate::script) fn plural_template(
|
||||
crate::strings::plural(token, Some(n), get)
|
||||
}
|
||||
|
||||
/// A colour component by `lua_tonumber`'s coercion: a numeric string counts, other strings do not.
|
||||
fn color_num(v: Option<&Value>) -> Option<f32> {
|
||||
match v {
|
||||
Some(Value::Number(n)) => Some(*n as f32),
|
||||
Some(Value::Integer(i)) => Some(*i as f32),
|
||||
Some(Value::String(s)) => s.to_str().ok().and_then(|s| s.trim().parse::<f32>().ok()),
|
||||
_ => None,
|
||||
}
|
||||
/// A colour component by `lua_tonumber`'s coercion: a number or a string `luaO_str2d` reads as
|
||||
/// one, else none.
|
||||
fn color_num(lua: &Lua, v: Option<&Value>) -> Option<f32> {
|
||||
lua.coerce_number(v?.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|n| n as f32)
|
||||
}
|
||||
|
||||
/// A wrap flag: nil, false, 0 and the strings "0", "off" and "disabled" are false, anything else
|
||||
/// true. The reference reads it with `GetBoolOrDefault` (`0x6f1c10`,
|
||||
/// `binding_abi::bool_or_default`), where "false", "no" and 0.5 are false too.
|
||||
fn bool_arg(v: Option<&Value>) -> bool {
|
||||
match v {
|
||||
None | Some(Value::Nil) | Some(Value::Boolean(false)) => false,
|
||||
Some(Value::Integer(i)) => *i != 0,
|
||||
Some(Value::Number(n)) => *n != 0.0,
|
||||
Some(Value::String(s)) => !s.to_str().ok().is_some_and(|s| {
|
||||
s == "0" || s.eq_ignore_ascii_case("off") || s.eq_ignore_ascii_case("disabled")
|
||||
}),
|
||||
_ => true,
|
||||
}
|
||||
/// A line's wrap flag, `GetBoolOrDefault` (`0x6f1c10`) with default 0 at `AddLine`'s sixth
|
||||
/// argument (`0x53181a`), `SetText`'s seventh (`0x531ddc`) and `AddDoubleLine`'s tenth (`0x531b69`).
|
||||
fn wrap_arg(v: Option<&Value>) -> bool {
|
||||
crate::script::binding_abi::bool_or_default(v, false)
|
||||
}
|
||||
|
||||
/// An `AddLine`-family colour, gated on the r-slot alone (`lua_isnumber`, `0x6f34d0`), else
|
||||
/// [`DEFAULT_TEXT_GOLD`]; past the gate a non-number g or b reads 0, and alpha is always opaque.
|
||||
fn parse_line_color(r: Option<&Value>, g: Option<&Value>, b: Option<&Value>) -> [f32; 4] {
|
||||
match color_num(r) {
|
||||
fn parse_line_color(
|
||||
lua: &Lua,
|
||||
r: Option<&Value>,
|
||||
g: Option<&Value>,
|
||||
b: Option<&Value>,
|
||||
) -> [f32; 4] {
|
||||
match color_num(lua, r) {
|
||||
Some(r) => [
|
||||
r,
|
||||
color_num(g).unwrap_or(0.0),
|
||||
color_num(b).unwrap_or(0.0),
|
||||
color_num(lua, g).unwrap_or(0.0),
|
||||
color_num(lua, b).unwrap_or(0.0),
|
||||
1.0,
|
||||
],
|
||||
None => DEFAULT_TEXT_GOLD,
|
||||
@@ -608,10 +603,10 @@ fn parse_line_color(r: Option<&Value>, g: Option<&Value>, b: Option<&Value>) ->
|
||||
|
||||
/// An `AddLine` tail, positional `r, g, b, wrap` (`0x531630`); the wrap flag is read whatever the
|
||||
/// colour gate decides.
|
||||
fn parse_line_tail(args: &[Value]) -> ([f32; 4], bool) {
|
||||
fn parse_line_tail(lua: &Lua, args: &[Value]) -> ([f32; 4], bool) {
|
||||
(
|
||||
parse_line_color(args.first(), args.get(1), args.get(2)),
|
||||
bool_arg(args.get(3)),
|
||||
parse_line_color(lua, args.first(), args.get(1), args.get(2)),
|
||||
wrap_arg(args.get(3)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ use crate::script::Model;
|
||||
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
|
||||
|
||||
use super::{
|
||||
append_line, bool_arg, clear, parse_line_color, parse_line_tail, set_owner_core, set_shown,
|
||||
text_of, tip_mut, with_tip, write_cell, REG_TOOLTIP_METHODS,
|
||||
append_line, clear, color_num, parse_line_color, parse_line_tail, set_owner_core, set_shown,
|
||||
text_of, tip_mut, with_tip, wrap_arg, write_cell, REG_TOOLTIP_METHODS,
|
||||
};
|
||||
|
||||
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
@@ -115,13 +115,11 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
));
|
||||
}
|
||||
let text = text_of(args.first());
|
||||
let [r, g, b, _] = parse_line_color(args.get(1), args.get(2), args.get(3));
|
||||
let a = match args.get(4) {
|
||||
Some(Value::Number(n)) => *n as f32,
|
||||
Some(Value::Integer(i)) => *i as f32,
|
||||
_ => 1.0,
|
||||
};
|
||||
let wrap = bool_arg(args.get(5));
|
||||
let [r, g, b, _] = parse_line_color(lua, args.get(1), args.get(2), args.get(3));
|
||||
// The alpha is its own `lua_isnumber` gate (`0x531ddc`'s neighbour at idx 6), so a
|
||||
// numeric string counts.
|
||||
let a = color_num(lua, args.get(4)).unwrap_or(1.0);
|
||||
let wrap = wrap_arg(args.get(5));
|
||||
clear(lua, h);
|
||||
append_line(lua, &this, (text, [r, g, b, a]), None, wrap)?;
|
||||
set_shown(lua, h, true);
|
||||
@@ -161,25 +159,28 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"AddLine",
|
||||
lua.create_function(|lua, (this, args): (Table, Variadic<Value>)| {
|
||||
let text = text_of(args.first());
|
||||
let (color, wrap) = parse_line_tail(&args[1.min(args.len())..]);
|
||||
let (color, wrap) = parse_line_tail(lua, &args[1.min(args.len())..]);
|
||||
append_line(lua, &this, (text, color), None, wrap)
|
||||
})?,
|
||||
)?;
|
||||
// AddDoubleLine(textL, textR, rL, gL, bL, rR, gR, bR [, wrap]) (`0x531840`): each side's
|
||||
// colour gates on its own r-slot. The core forces wrap off when the right text is non-empty;
|
||||
// this never wraps.
|
||||
// colour gates on its own r-slot, and the wrap flag is the tenth argument.
|
||||
m.set(
|
||||
"AddDoubleLine",
|
||||
lua.create_function(
|
||||
|lua, (this, tl, tr, cl): (Table, Value, Value, Variadic<Value>)| {
|
||||
let left_color = parse_line_color(cl.first(), cl.get(1), cl.get(2));
|
||||
let right_color = parse_line_color(cl.get(3), cl.get(4), cl.get(5));
|
||||
let left_color = parse_line_color(lua, cl.first(), cl.get(1), cl.get(2));
|
||||
let right_color = parse_line_color(lua, cl.get(3), cl.get(4), cl.get(5));
|
||||
let right = text_of(Some(&tr));
|
||||
// The line core (`0x530270`) clears the flag when the right text is non-empty
|
||||
// (`0x530284`), so only a double line with an empty right side can wrap.
|
||||
let wrap = right.is_empty() && wrap_arg(cl.get(6));
|
||||
append_line(
|
||||
lua,
|
||||
&this,
|
||||
(text_of(Some(&tl)), left_color),
|
||||
Some((text_of(Some(&tr)), right_color)),
|
||||
false,
|
||||
Some((right, right_color)),
|
||||
wrap,
|
||||
)
|
||||
},
|
||||
)?,
|
||||
|
||||
@@ -1045,12 +1045,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let name =
|
||||
super::super::binding_abi::string_arg(lua, name, "Usage: TargetByName(\"name\")")?;
|
||||
// Argument 2 comes from `0x6f1c10` with default 0: it never raises, and 0 is absent.
|
||||
let exact = match exact {
|
||||
None | Some(Value::Nil) | Some(Value::Boolean(false)) => false,
|
||||
Some(Value::Integer(n)) => n != 0,
|
||||
Some(Value::Number(n)) => n != 0.0,
|
||||
Some(_) => true,
|
||||
};
|
||||
let exact = super::super::binding_abi::bool_or_default(exact.as_ref(), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model
|
||||
.script_calls
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
//! `ToggleFlag`), so a set queues a flip only when it differs from a belief that moves at the call
|
||||
//! and is overwritten when the wire bit moves.
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
use mlua::{Lua, MultiValue};
|
||||
|
||||
use super::binding_abi::bool_or_default;
|
||||
use super::Model;
|
||||
|
||||
/// A queued flip: one `CMSG_TOGGLE_HELM` or `CMSG_TOGGLE_CLOAK` for the app to send.
|
||||
@@ -38,24 +39,6 @@ impl super::UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// Numeric, not Lua-truthy: the stock panel passes the string `"0"` for off
|
||||
/// (`UIOptionsFrame_Save`), which Lua 5.0 holds truthy. Nil, false, 0 and `"0"` hide.
|
||||
fn shown_arg(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Nil => false,
|
||||
Value::Boolean(b) => *b,
|
||||
Value::Integer(i) => *i != 0,
|
||||
Value::Number(n) => *n != 0.0,
|
||||
// A non-numeric string is never handed to this API; it shows rather than hiding gear.
|
||||
Value::String(s) => s
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|t| t.trim().parse::<f64>().ok())
|
||||
.is_none_or(|n| n != 0.0),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask for a state, and queue the flip only if we are not already in it.
|
||||
fn want(model: &mut Model, which: WornDisplay, show: bool) {
|
||||
let held = match which {
|
||||
@@ -78,9 +61,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
] {
|
||||
g.set(
|
||||
name,
|
||||
lua.create_function(move |lua, v: Value| {
|
||||
// `GetBoolOrDefault` (`0x6f1c10`) with default 1 (`0x48de4e`, `0x48de9e`): a bare call
|
||||
// shows, and the stock panel's string `"0"` hides where Lua 5.0 holds it truthy.
|
||||
lua.create_function(move |lua, args: MultiValue| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
want(&mut model, which, shown_arg(&v));
|
||||
want(&mut model, which, bool_or_default(args.front(), true));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -140,6 +125,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The flag is `GetBoolOrDefault` with default 1 (`0x48de4e`): a bare call shows, a table
|
||||
/// takes the default, and a string goes by its first byte.
|
||||
#[test]
|
||||
fn the_setter_takes_the_flag_with_default_one() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
for (arg, shown) in [
|
||||
("", true),
|
||||
("nil", false),
|
||||
("{}", true),
|
||||
(r#""no""#, false),
|
||||
(r#""yes""#, true),
|
||||
(r#""off""#, false),
|
||||
(r#""""#, true),
|
||||
("0.5", false),
|
||||
] {
|
||||
s.run(&format!("ShowHelm({})", if shown { "0" } else { "1" }))
|
||||
.unwrap();
|
||||
s.take_worn_display_toggles();
|
||||
s.run(&format!("ShowHelm({arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<bool>("return ShowingHelm() == 1").unwrap(),
|
||||
shown,
|
||||
"ShowHelm({arg})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Options window's Defaults button writes every row, so a flip on no difference would
|
||||
/// invert a preference already at its default.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user