benilla-ui: unused globals and queues removed, one true/nil helper, three tests that test their claim (#305)

Code health in `benilla-ui`, found while its comments were rewritten. No behaviour a player can see changes.

- **Unused globals and queues removed.**
  - The Rust `ChatFrame_OpenChat` and `ChatFrame_SendTell` queued text that nothing drained. The stock `ChatFrame.lua` defines both globals and replaces them when it loads, as the reference's FrameXML does.
  - `BenillaGetItemStats` had no Lua caller. Its test of the item cache's miss, ask and push now runs through `GetItemInfo`, which shares that path.
  - `UiScript::queue_social_request` had no caller.
- **One true/nil helper.** Seven modules each carried their own copy of `binding_abi::flag`, the reference's `1`-or-nil boolean. They call it now.
- **Three tests test their claim.**
  - `click_focuses_regardless_of_autofocus_…` built both boxes with autoFocus on. One is now off.
  - `level_shift_saturates_at_zero` never saturated. Its child now drops past 0.
  - The layout profiler's `plan` bucket was never set, so it always printed 0. It is gone.
- **Smaller fixes.** An empty `if buyback {}` branch is gone, and a doc comment no longer sits on a `use` line instead of on `install_justify`.

Checked with `cargo clippy -p benilla-ui --all-targets -D warnings` and `cargo test -p benilla-ui` (1513 passed) before the land gates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-25 17:26:43 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 036e3d0240
commit 9863554f69
18 changed files with 60 additions and 229 deletions
+1 -36
View File
@@ -1,4 +1,4 @@
//! The chat window settings bindings: `GetChatWindowInfo`, its setters and `ChatFrame_OpenChat`.
//! The chat window settings bindings: `GetChatWindowInfo` and its setters.
//!
//! The reference keeps one record per window (`0xb4fe50`, stride 0x98, 10 slots), loaded from and
//! saved to `chat-cache.txt`: name `+0x00`, message-group flags `+0x20`, channel names and ids
@@ -331,13 +331,6 @@ impl super::UiScript {
}
}
impl super::UiScript {
/// Drains the texts `ChatFrame_OpenChat` queued since the last call.
pub fn take_open_chat_requests(&mut self) -> Vec<String> {
std::mem::take(&mut self.model_mut().open_chat_requests)
}
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// GetChatWindowInfo(id) → name, fontSize, r, g, b, a, shown, locked, docked.
lua.globals().set(
@@ -470,19 +463,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// ChatFrame_OpenChat(text, chatFrame) queues `text` for the host to open the chat box with. The
// frame is inert: in 1.12 every chat frame's `editBox` is the one `ChatFrameEditBox`
// (FloatingChatFrame.lua:30, FloatingChatFrame.xml:742). The stock ChatFrame.lua:1545 defines
// the same global and replaces this one when it loads.
lua.globals().set(
"ChatFrame_OpenChat",
lua.create_function(|lua, (text, _chat_frame): (Option<String>, Value)| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.open_chat_requests.push(text.unwrap_or_default());
Ok(())
})?,
)?;
// ── The rest of the record: name, shown, message groups, channels ──────────────────────
//
// `SetChatWindowName` `0x4a13f0`, `SetChatWindowShown` `0x4a1730`,
@@ -930,21 +910,6 @@ mod tests {
s.run("SetChatWindowSize(1, 0)").unwrap();
assert!(s.take_chat_window_changes().is_empty());
}
#[test]
fn chat_frame_open_chat_queues_its_text_and_ignores_the_frame() {
let mut s = UiScript::new().unwrap();
s.run("ChatFrame_OpenChat('/w Bob ')").unwrap();
s.run("ChatFrame_OpenChat('', 'not even a frame')").unwrap();
assert_eq!(
s.take_open_chat_requests(),
vec!["/w Bob ".to_string(), String::new()]
);
assert!(
s.take_open_chat_requests().is_empty(),
"the drain is a take"
);
}
}
#[cfg(test)]
+2 -11
View File
@@ -9,7 +9,7 @@
use mlua::{Lua, MultiValue, Value, Variadic};
use super::binding_abi::number_arg;
use super::binding_abi::{self, number_arg};
use super::item_stats::item_link;
use super::Model;
@@ -177,15 +177,6 @@ fn opt_str(lua: &Lua, s: Option<&String>) -> mlua::Result<Value> {
})
}
/// A `bool` as the reference's `1` or nil.
fn era_bool(b: bool) -> Value {
if b {
Value::Integer(1)
} else {
Value::Nil
}
}
/// Register the craft globals.
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
@@ -362,7 +353,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
if let Some(r) = recipe(&model, index) {
for (name, have) in &r.tools {
out.push(Value::String(lua.create_string(name)?));
out.push(era_bool(*have));
out.push(binding_abi::flag(*have));
}
}
Ok(MultiValue::from_vec(out))
@@ -178,15 +178,16 @@ fn click_focuses_regardless_of_autofocus_and_transition_order_is_lost_then_gaine
s.run(
r#"
log = {}
local function wire(name, y)
local function wire(name, y, autoFocus)
local f = CreateFrame("EditBox", name)
f:SetAutoFocus(autoFocus)
f:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, y)
f:SetWidth(100); f:SetHeight(20)
f:SetScript("OnEditFocusGained", function() table.insert(log, "gained"..name) end)
f:SetScript("OnEditFocusLost", function() table.insert(log, "lost"..name) end)
end
wire("A", 0) -- rect bottom 0..20
wire("B", 100) -- rect bottom 100..120
wire("A", 0, true) -- rect bottom 0..20
wire("B", 100, false) -- rect bottom 100..120: a click focuses it without autoFocus
"#,
)
.unwrap();
+7 -16
View File
@@ -9,6 +9,7 @@
use mlua::{Lua, MultiValue, Value};
use super::binding_abi;
use super::Model;
/// The thirteen rank rights in `GuildControlGetRankFlags`' return order, which is also the
@@ -258,16 +259,6 @@ fn truthy(value: &Value) -> bool {
}
}
/// The 1.12 boolean, `1` or `nil`: the FrameXML stores what it tests, so a `false` would survive
/// where the reference's `nil` does not.
fn era_bool(on: bool) -> Value {
if on {
Value::Integer(1)
} else {
Value::Nil
}
}
/// 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
@@ -305,7 +296,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"IsInGuild",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(model.guild.in_guild))
Ok(binding_abi::flag(model.guild.in_guild))
})?,
)?;
@@ -314,7 +305,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"IsGuildLeader",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(model.guild.is_leader))
Ok(binding_abi::flag(model.guild.is_leader))
})?,
)?;
@@ -375,7 +366,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::String(lua.create_string(&m.zone)?),
Value::String(lua.create_string(&m.note)?),
Value::String(lua.create_string(&m.officer_note)?),
era_bool(m.online),
binding_abi::flag(m.online),
Value::String(lua.create_string(&m.status)?),
]))
})?,
@@ -444,7 +435,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"GetGuildRosterShowOffline",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(model.guild.show_offline))
Ok(binding_abi::flag(model.guild.show_offline))
})?,
)?;
// With no argument it turns show-offline on, unlike an explicit `nil`, hence `MultiValue`.
@@ -593,7 +584,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
global,
lua.create_function(move |lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(has_right(&model, index)))
Ok(binding_abi::flag(has_right(&model, index)))
})?,
)?;
}
@@ -653,7 +644,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(MultiValue::from_vec(
RANK_RIGHT_BITS
.iter()
.map(|bit| era_bool(rights & bit != 0))
.map(|bit| binding_abi::flag(rights & bit != 0))
.collect(),
))
})?,
+7 -42
View File
@@ -448,39 +448,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// BenillaGetItemStats(itemId) → name, quality, invType, class, subclass, dmgMin, dmgMax,
// dmgType, delayMs, armor, block, sellPrice, or nil and an ask.
lua.globals().set(
"BenillaGetItemStats",
lua.create_function(|lua, item_id: u32| {
let view = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let v = model.item_templates.get(&item_id).cloned();
if v.is_none() && item_id != 0 {
model.item_stat_asks.insert(item_id);
}
v
};
let Some(v) = view else {
return Ok(MultiValue::from_vec(vec![Value::Nil]));
};
let (dmg_min, dmg_max, dmg_type) = v.damages.first().copied().unwrap_or_default();
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(&v.name)?),
Value::Integer(i64::from(v.quality)),
Value::Integer(i64::from(v.inventory_type)),
Value::Integer(i64::from(v.class)),
Value::Integer(i64::from(v.subclass)),
Value::Number(f64::from(dmg_min)),
Value::Number(f64::from(dmg_max)),
Value::Integer(i64::from(dmg_type)),
Value::Integer(i64::from(v.delay_ms)),
Value::Integer(i64::from(v.armor)),
Value::Integer(i64::from(v.block)),
Value::Integer(i64::from(v.sell_price)),
]))
})?,
)
Ok(())
}
#[cfg(test)]
@@ -580,9 +548,7 @@ mod tests {
#[test]
fn miss_records_ask_and_push_serves_the_stats() {
let mut s = UiScript::new().unwrap();
assert!(s
.eval::<bool>("return BenillaGetItemStats(25) == nil")
.unwrap());
assert!(s.eval::<bool>("return GetItemInfo(25) == nil").unwrap());
assert_eq!(s.take_item_stat_asks(), vec![25]);
s.set_item_template(
@@ -598,14 +564,13 @@ mod tests {
..Default::default()
},
);
let (name, quality, inv): (String, i64, i64) =
s.eval("return BenillaGetItemStats(25)").unwrap();
assert_eq!((name.as_str(), quality, inv), ("Worn Shortsword", 1, 21));
let (name, quality): (String, i64) = s
.eval("local n, _, q = GetItemInfo(25) return n, q")
.unwrap();
assert_eq!((name.as_str(), quality), ("Worn Shortsword", 1));
assert!(s.take_item_stat_asks().is_empty(), "push cleared the ask");
// Id 0 records no ask.
assert!(s
.eval::<bool>("return BenillaGetItemStats(0) == nil")
.unwrap());
assert!(s.eval::<bool>("return GetItemInfo(0) == nil").unwrap());
assert!(s.take_item_stat_asks().is_empty());
}
}
+1 -5
View File
@@ -403,8 +403,6 @@ struct PreambleProf {
scroll: u128,
/// `region_resolved`'s liveness retain.
retain: u128,
/// Never set, so always 0: the frame roster is built inside `fp_frames`.
plan: u128,
/// `LayoutScope::begin_full`.
begin: u128,
/// The fingerprint and scope walk over every live frame.
@@ -447,14 +445,13 @@ impl PreambleProf {
+ self.ids
+ self.scroll
+ self.retain
+ self.plan
+ self.begin
+ self.fp_frames
+ self.fp_regions
+ self.seed;
eprintln!(
"[layout-pre] skips={} incr={} frames={frames} anchored={regions} total_us={total} \
tooltip={} watched={} ids={} scroll={} retain={} plan={} begin={} \
tooltip={} watched={} ids={} scroll={} retain={} begin={} \
fp_frames={} fp_regions={} seed={}",
u8::from(skips),
u8::from(self.incremental),
@@ -463,7 +460,6 @@ impl PreambleProf {
self.ids,
self.scroll,
self.retain,
self.plan,
self.begin,
self.fp_frames,
self.fp_regions,
@@ -10,6 +10,7 @@
use crate::layout::Rect;
use crate::order::ZTarget;
use crate::script::object::frame_handle_of;
use crate::widget::{FrameHandle, InsertMode, KindState, MessageLine};
use super::layout::FramePaint;
@@ -38,8 +39,6 @@ pub(super) fn message_text(lua: &mlua::Lua, v: &mlua::Value) -> Option<String> {
/// Install `SetJustifyH`, `GetJustifyH`, `SetJustifyV` and `GetJustifyV`, which both classes carry,
/// under [`crate::justify`]'s rules: an unknown token raises the usage line, and a token of the
/// other axis clears this one, so `GetJustifyH()` then answers `"UNKNOWN"`.
use crate::script::object::frame_handle_of;
pub(super) fn install_justify(
lua: &mlua::Lua,
m: &mlua::Table,
-6
View File
@@ -280,10 +280,6 @@ pub(crate) struct Model {
pub(crate) petition: petition::PetitionState,
/// Charter calls (`BuyGuildCharter`, `SignPetition`, `TurnInGuildCharter`, …) queued.
pub(crate) petition_requests: Vec<petition::PetitionRequest>,
/// Names `ChatFrame_SendTell` queued; the app opens the chat box prefilled `/w <name> `.
pub(crate) tell_requests: Vec<String>,
/// Draft lines `ChatFrame_OpenChat` queued, for the app to prefill the chat box.
pub(crate) open_chat_requests: Vec<String>,
/// Per chat window from `ChatFrame1`, the tint, alpha and font size its tab menu can change.
pub(crate) chat_window_looks: [chat_window::ChatWindowLook; chat_window::NUM_CHAT_WINDOWS],
/// 0-based windows whose look Lua changed, the persist cue.
@@ -1080,8 +1076,6 @@ impl Model {
guild_requests: Vec::new(),
petition: petition::PetitionState::default(),
petition_requests: Vec::new(),
tell_requests: Vec::new(),
open_chat_requests: Vec::new(),
// Per window, as the stock dock differs: 1 and 2 are the dock, the rest undocked.
chat_window_looks: std::array::from_fn(chat_window::ChatWindowLook::stock),
chat_window_changes: HashSet::new(),
-27
View File
@@ -232,12 +232,6 @@ impl super::UiScript {
pub fn set_saved_instances(&mut self, saved: Vec<SavedInstanceInfo>) {
self.model_mut().saved_instances = saved;
}
/// Drain the names `ChatFrame_SendTell` queued; the app opens the chat edit box on
/// `/w <name> ` for each.
pub fn take_tell_requests(&mut self) -> Vec<String> {
std::mem::take(&mut self.model_mut().tell_requests)
}
}
/// Whether the player leads the group. `IsRaidLeader` (`0x4bb8c0`) and `IsPartyLeader`
@@ -703,18 +697,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// ChatFrame_SendTell(name) is FrameXML in the reference (`ChatFrame.lua:1606`, opening the
// edit box on `/w name `); the chat edit box is app-side here, so this queues the name for
// `UiScript::take_tell_requests`.
g.set(
"ChatFrame_SendTell",
lua.create_function(|lua, name: String| {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.tell_requests.push(name);
Ok(())
})?,
)?;
Ok(())
}
@@ -1167,15 +1149,6 @@ mod tests {
assert!(s.eval::<bool>("return IsRaidOfficer() == nil").unwrap());
}
#[test]
fn chat_frame_send_tell_queues_the_name() {
let mut s = UiScript::new().unwrap();
assert!(s.take_tell_requests().is_empty());
s.run(r#"ChatFrame_SendTell("Alice")"#).unwrap();
assert_eq!(s.take_tell_requests(), vec!["Alice".to_string()]);
assert!(s.take_tell_requests().is_empty());
}
// ── The identity predicates ─────────────────────────────────────────────
fn unit(exists: bool, guid: u64) -> crate::script::UnitState {
+3 -11
View File
@@ -6,6 +6,7 @@
use mlua::{Lua, MultiValue, Value};
use super::binding_abi;
use super::Model;
/// `GetPetitionInfo`'s first return when the record's charter bit is set (`0x84cf8c`, selected at
@@ -131,15 +132,6 @@ pub fn validate_guild_name(name: &str) -> Result<(), &'static str> {
Ok(())
}
/// 1.12's `1` or nil, never `true`/`false`: this client has no `lua_pushboolean`.
fn era_bool(on: bool) -> Value {
if on {
Value::Integer(1)
} else {
Value::Nil
}
}
/// A cached name as a string, an uncached one as nil.
fn name_value(lua: &Lua, name: Option<&String>) -> mlua::Result<Value> {
match name {
@@ -176,7 +168,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::String(lua.create_string(&r.body_text)?),
Value::Integer(i64::from(r.max_signatures)),
name_value(lua, r.originator.as_ref())?,
era_bool(r.is_originator),
binding_abi::flag(r.is_originator),
]))
})?,
)?;
@@ -215,7 +207,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"CanSignPetition",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(model.petition.can_sign))
Ok(binding_abi::flag(model.petition.can_sign))
})?,
)?;
+1 -1
View File
@@ -379,7 +379,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::Integer(i64::from(it.count)),
Value::Integer(i64::from(it.quality)),
Value::Boolean(it.usable),
// The item id, `BenillaGetItemStats`'s key, a sixth return that is not 1.12's.
// The item id, a sixth return that is not 1.12's.
Value::Integer(i64::from(it.item_id)),
]))
})?,
+6 -14
View File
@@ -12,6 +12,7 @@ use std::collections::HashMap;
use mlua::{Lua, MultiValue, Value};
use super::binding_abi;
use super::Model;
/// The synthetic header key for factions the player moved to the inactive bucket.
@@ -146,15 +147,6 @@ fn collate(a: &str, b: &str) -> std::cmp::Ordering {
.then_with(|| a.as_bytes().cmp(b.as_bytes()))
}
/// `1` or nil, never `0`: `0` is truthy in Lua, and the stock pane tests `if ( atWarWith )`.
fn era_bool(b: bool) -> Value {
if b {
Value::Integer(1)
} else {
Value::Nil
}
}
/// Build the header groups: the visible non-header rows by [`header_key`], sorted by name. A group
/// is named by its pushed header row (the reference synthesizes a missing one, `0x4d5a70`), or left
/// empty for a synthetic key; a childless header is no group, as the client's count drops it.
@@ -305,7 +297,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::Nil, // atWarWith
Value::Nil, // canToggleAtWar
Value::Integer(1), // isHeader
era_bool(collapsed), // isCollapsed
binding_abi::flag(collapsed), // isCollapsed
Value::Nil, // isWatched
]))
}
@@ -318,11 +310,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::Integer(i64::from(e.bar_min)),
Value::Integer(i64::from(e.bar_max)),
Value::Integer(i64::from(e.standing)),
era_bool(e.at_war),
era_bool(e.can_toggle_at_war),
binding_abi::flag(e.at_war),
binding_abi::flag(e.can_toggle_at_war),
Value::Nil, // isHeader
Value::Nil, // isCollapsed
era_bool(model.reputation.watched == Some(e.rep_list_id)),
binding_abi::flag(model.reputation.watched == Some(e.rep_list_id)),
]))
}
}
@@ -412,7 +404,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"IsFactionInactive",
lua.create_function(|lua, index: usize| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(
Ok(binding_abi::flag(
entry_at(&model, index).is_some_and(|e| e.inactive),
))
})?,
+3 -11
View File
@@ -16,6 +16,7 @@ use std::collections::HashMap;
use mlua::{Lua, MultiValue, Value};
use super::binding_abi;
use super::Model;
/// One known skill line off the player's `PLAYER_SKILL_INFO` block, resolved by the app.
@@ -261,15 +262,6 @@ fn displayed_ranks(e: &SkillEntry) -> (i64, i64) {
(rank, max)
}
/// A `bool` as the 1.12 `1`/nil shape.
fn era_bool(b: bool) -> Value {
if b {
Value::Integer(1)
} else {
Value::Nil
}
}
/// Register the skills-pane globals.
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
@@ -303,7 +295,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(&grp.name)?),
Value::Integer(1), // isHeader
era_bool(expanded),
binding_abi::flag(expanded),
Value::Integer(0), // skillRank
Value::Integer(0), // numTempPoints
Value::Integer(0), // skillModifier
@@ -326,7 +318,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::Integer(0), // numTempPoints
Value::Integer(i64::from(e.temp_bonus)), // skillModifier: temp only
Value::Integer(max),
era_bool(e.abandonable), // isAbandonable
binding_abi::flag(e.abandonable), // isAbandonable
// stepCost, rankCost: nil, since Lua reads `0` as true.
Value::Nil,
Value::Nil,
-5
View File
@@ -95,11 +95,6 @@ impl super::UiScript {
pub fn take_social_requests(&mut self) -> Vec<SocialRequest> {
std::mem::take(&mut self.model_mut().social_requests)
}
/// Queue an intent from the app side.
pub fn queue_social_request(&mut self, request: SocialRequest) {
self.model_mut().social_requests.push(request);
}
}
/// Register the social globals against the snapshot store.
@@ -846,7 +846,6 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
render_view(lua, &this, &v, BuilderFlags::default(), None)?;
// Nothing to arm: the vendor tab's row seats `ShoppingTooltip1/2` itself
// (`MerchantFrame.xml:67-80`), and the buyback tab seats none.
if buyback {}
}
None => {
// Template in flight: the row's own stat head as a minimal view, the
+9 -18
View File
@@ -2,7 +2,7 @@
use mlua::{Lua, MultiValue, Value};
use crate::script::binding_abi::number_arg;
use crate::script::binding_abi::{self, number_arg};
use crate::script::item_stats::item_link;
use crate::script::Model;
@@ -19,15 +19,6 @@ fn opt_str(lua: &Lua, s: Option<&String>) -> mlua::Result<Value> {
})
}
/// A `bool` as the 1.12 API's `1` or `nil`.
fn era_bool(b: bool) -> Value {
if b {
Value::Integer(1)
} else {
Value::Nil
}
}
/// Register the tradeskill globals.
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
@@ -139,7 +130,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
Value::String(lua.create_string(&name)?),
Value::String(lua.create_string("header")?),
Value::Integer(0),
era_bool(expanded),
binding_abi::flag(expanded),
]))
}
Row::Entry(ei) => {
@@ -276,7 +267,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
if let Some(r) = recipe_at(&model, index) {
for (name, have) in &r.tools {
out.push(Value::String(lua.create_string(name)?));
out.push(era_bool(*have));
out.push(binding_abi::flag(*have));
}
}
Ok(MultiValue::from_vec(out))
@@ -349,16 +340,16 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(|lua, index: usize| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
let Some(t) = model.trade_skill.as_ref() else {
return Ok(era_bool(false));
return Ok(binding_abi::flag(false));
};
let groups = build_groups(&t.recipes);
Ok(match index.checked_sub(1) {
None => era_bool(
None => binding_abi::flag(
!groups
.iter()
.any(|g| model.trade_skill_subclass_hidden.contains(&g.key)),
),
Some(n) => era_bool(
Some(n) => binding_abi::flag(
groups
.get(n)
.is_some_and(|g| !model.trade_skill_subclass_hidden.contains(&g.key)),
@@ -425,16 +416,16 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(|lua, index: usize| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
if model.trade_skill.is_none() {
return Ok(era_bool(false));
return Ok(binding_abi::flag(false));
}
let bits = present_inv_slots(&model);
Ok(match index.checked_sub(1) {
// Everything shown: (present & mask) == present (`0x4fffd0`).
None => era_bool(
None => binding_abi::flag(
bits.iter()
.all(|&b| model.trade_skill_invslot_mask & (1 << b) != 0),
),
Some(n) => era_bool(
Some(n) => binding_abi::flag(
bits.get(n)
.is_some_and(|&b| model.trade_skill_invslot_mask & (1 << b) != 0),
),
+12 -17
View File
@@ -6,6 +6,7 @@
use mlua::{Lua, MultiValue, Value};
use super::binding_abi;
use super::Model;
/// A service's state (the wire's `TrainerSpellState`): green learnable, red gated, gray known.
@@ -424,15 +425,6 @@ fn opt_str(lua: &Lua, s: Option<&String>) -> mlua::Result<Value> {
})
}
/// A `bool` as the reference's `1` or nil.
fn era_bool(b: bool) -> Value {
if b {
Value::Integer(1)
} else {
Value::Nil
}
}
/// Register the trainer globals.
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
@@ -468,7 +460,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Value::String(lua.create_string(&g.name)?),
Value::Nil,
Value::String(lua.create_string("header")?),
era_bool(expanded),
binding_abi::flag(expanded),
]))
}
Row::Service(si) => {
@@ -560,7 +552,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(&req.name)?),
Value::Integer(i64::from(req.rank)),
era_bool(req.met),
binding_abi::flag(req.met),
]))
})?,
)?;
@@ -584,7 +576,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
};
Ok(MultiValue::from_vec(vec![
Value::String(lua.create_string(&req.name)?),
era_bool(req.met),
binding_abi::flag(req.met),
]))
})?,
)?;
@@ -599,7 +591,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"IsTrainerServiceTradeSkill",
lua.create_function(|lua, index: usize| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(
Ok(binding_abi::flag(
service(&model, index).is_some_and(|s| s.is_trade_skill),
))
})?,
@@ -612,7 +604,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(|lua, index: usize| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
let learn = service(&model, index).is_some_and(|s| !s.is_trade_skill);
Ok(MultiValue::from_vec(vec![era_bool(learn), Value::Nil]))
Ok(MultiValue::from_vec(vec![
binding_abi::flag(learn),
Value::Nil,
]))
})?,
)?;
@@ -621,7 +616,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"IsTradeskillTrainer",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(model.trainer.as_ref().is_some_and(|t| {
Ok(binding_abi::flag(model.trainer.as_ref().is_some_and(|t| {
t.trainer_type == TRAINER_TYPE_TRADESKILL
})))
})?,
@@ -632,7 +627,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
"IsTalentTrainer",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(era_bool(
Ok(binding_abi::flag(
model.trainer.as_ref().is_some_and(|t| t.trainer_type == 1),
))
})?,
@@ -656,7 +651,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
lua.create_function(|lua, kind: String| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(match TrainerServiceCategory::from_filter_str(&kind) {
Some(c) => era_bool(model.trainer_filter[c.filter_slot()]),
Some(c) => binding_abi::flag(model.trainer_filter[c.filter_slot()]),
None => Value::Nil,
})
})?,
+3 -3
View File
@@ -114,10 +114,10 @@ fn level_shift_saturates_at_zero() {
let root = a.create(FrameKind::Frame, None, None);
let child = a.create(FrameKind::Frame, None, Some(root));
a.set_frame_level(root, 10, true);
a.set_frame_level(child, 12, true);
// Root drops by 10, taking the child from 12 to 2.
a.set_frame_level(child, 3, true);
// Root drops by 10, which would take the child to -7: it stops at 0.
a.set_frame_level(root, 0, true);
assert_eq!(a.frame(child).unwrap().level, 2);
assert_eq!(a.frame(child).unwrap().level, 0);
}
// ── Scale ────────────────────────────────────────────────────────────────────────────────────