diff --git a/crates/benilla-app/src/ui_loot_roll.rs b/crates/benilla-app/src/ui_loot_roll.rs index 00f826a6..70f6608f 100644 --- a/crates/benilla-app/src/ui_loot_roll.rs +++ b/crates/benilla-app/src/ui_loot_roll.rs @@ -445,9 +445,6 @@ fn snapshot( roll_id: r.roll_id, name: t.map(|t| catalogs.name(&t.name, r.random_property_id)), texture, - // The reference's `GetLootRollItemInfo` returns a literal 1 (`0x4c3160`); the - // packet carries no count. - quantity: 1, quality: t.map(|t| t.quality), bind_on_pickup: t.is_some_and(|t| t.bonding == BIND_WHEN_PICKED_UP), time_left_ms: r.remaining_ms, diff --git a/crates/benilla-app/src/ui_script/group_loot_tests.rs b/crates/benilla-app/src/ui_script/group_loot_tests.rs index eb41c100..49f6614e 100644 --- a/crates/benilla-app/src/ui_script/group_loot_tests.rs +++ b/crates/benilla-app/src/ui_script/group_loot_tests.rs @@ -99,7 +99,6 @@ fn rolls() -> LootRollsState { roll_id: 7, name: Some("Staff of Jordan".into()), texture: Some("Interface\\Icons\\INV_Staff_12".into()), - quantity: 1, quality: Some(4), // Epic -> purple bind_on_pickup: true, time_left_ms: 42_000, @@ -111,7 +110,6 @@ fn rolls() -> LootRollsState { roll_id: 8, name: Some("Worn Shortsword".into()), texture: Some("Interface\\Icons\\INV_Sword_04".into()), - quantity: 1, quality: Some(1), // Common -> white bind_on_pickup: false, time_left_ms: 60_000, @@ -124,7 +122,6 @@ fn rolls() -> LootRollsState { roll_id: 9, name: None, texture: None, - quantity: 1, quality: None, bind_on_pickup: false, time_left_ms: 55_000, diff --git a/crates/benilla-app/src/ui_script/shape_gate.rs b/crates/benilla-app/src/ui_script/shape_gate.rs index a8b4f1fc..f575c4e4 100644 --- a/crates/benilla-app/src/ui_script/shape_gate.rs +++ b/crates/benilla-app/src/ui_script/shape_gate.rs @@ -5,7 +5,8 @@ //! are gated with arity. The table says per row what to trust: `arity_conf = exact`, kinds only //! where `kinds_conf = agree`, never `argc_conf`. Only query verbs are probed, a call that raises //! is skipped, and a name registered from several tables (`GetBuildInfo` is 5 in the glue table -//! `0x8373b8`, 3 in-game at `0x83de68`) is read at its non-glue row. +//! `0x8373b8`, 3 in-game at `0x83de68`) is read at its non-glue row. The boolean gate alone reads every `global` row, whatever its +//! `arity_conf`, and adds each row whose kinds are `(nil) | (number)` to the query verbs. /// Globals known to answer the wrong arity: a name, the reference's arity and the reason. The /// list may only shrink. @@ -706,21 +707,63 @@ fn every_widget_method_answers_the_reference_s_return_kinds() { ); } -/// Argument tuples the boolean gate tries in order until a call returns. A plausible wrong -/// argument can change which kinds come back, which is why the kind gate passes only a unit -/// token, but it never turns `1` into `true`. -const ARG_LADDER: &[&str] = &["", "\"player\"", "1", "1, 1", "\"player\", 1"]; +/// Argument tuples the boolean gate tries, every one that returns being measured: a plausible +/// wrong argument can change which kinds come back, which is why the kind gate passes only a unit +/// token, but it never turns `1` into `true`. The ids and indices reach the rows +/// [`seed_boolean_probe_state`] pushes, each leg of a predicate on its own tuple. +const ARG_LADDER: &[&str] = &[ + "", + "\"player\"", + "1", + "2", + "3", + "1, 1", + "1, 2", + "2, 1", + "\"player\", 1", + "\"player\", \"player\"", +]; -/// 1.12 has no boolean query: of the table's trustworthy rows only `IsPetAttackActive` and Lua's -/// own `rawequal` answer a `boolean`, and every other predicate pushes `1` or `nil`, which -/// `x == 1`, `tostring(x)` or a saved variable tell apart from `true`. -#[test] -fn no_query_binding_answers_a_lua_boolean() { - // The reference's own two; the list may only shrink. - const REFERENCE_BOOLEANS: &[&str] = &["IsPetAttackActive", "rawequal"]; +/// The kinds a predicate row carries in the table: the `(nil) | (number)` of a flag, which the +/// gate probes whatever the binding's name or arity confidence. +const FLAG_KINDS: &str = "(nil) | (number)"; - let all = rows(); - let mut s = benilla_ui::script::UiScript::new().expect("VM"); +/// The global bindings the boolean gate probes: every `global` row of the table, whatever its +/// `arity_conf`, that is named like a query (`Get`, `Is`, `Has`, `Can`, `Unit`, `Num`) or whose +/// kinds are [`FLAG_KINDS`], whatever its name (`LootSlotIsItem`, `Resurrect*`, `Check*Dist`). +fn predicate_names() -> Vec { + let tsv = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../reference/1.12-shapes.tsv" + ); + let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv"); + let mut names: Vec = text + .lines() + .filter(|l| !l.starts_with('#') && !l.starts_with("name\t")) + .filter_map(|l| { + let f: Vec<&str> = l.split('\t').collect(); + if f.len() < 10 || f[4] != "global" { + return None; + } + let query = ["Get", "Is", "Has", "Can", "Unit", "Num"] + .iter() + .any(|p| f[0].starts_with(p)); + (query || f[9] == FLAG_KINDS).then(|| f[0].to_string()) + }) + .collect(); + names.sort_unstable(); + names.dedup(); + names +} + +/// The state the predicates that answer only against state read from: a seated player, two +/// talents (an ordinary one met and learnable, an exceptional one neither), two loot rolls (a +/// resolved bind-on-pickup one and one still in flight), a respec question in range. Each carries +/// both legs of its flags. +fn seed_boolean_probe_state(s: &mut benilla_ui::script::UiScript) { + use benilla_ui::script::{ + LootRollEntry, LootRollsState, TalentPrereqView, TalentTabView, TalentUiState, TalentView, + }; s.set_unit( "player", Some(benilla_ui::script::UnitState { @@ -730,24 +773,85 @@ fn no_query_binding_answers_a_lua_boolean() { ..Default::default() }), ); + s.set_talents(TalentUiState { + tabs: vec![TalentTabView { + name: "Probe".into(), + background: "Interface\\TalentFrame\\Probe-".into(), + points_spent: 0, + }], + talents: vec![vec![ + TalentView { + name: "Ordinary".into(), + tier: 1, + column: 1, + max_rank: 5, + meets_prereq: true, + ..Default::default() + }, + TalentView { + name: "Exceptional".into(), + tier: 2, + column: 1, + max_rank: 1, + exceptional: true, + prereqs: vec![ + TalentPrereqView { + tier: 1, + column: 1, + learnable: true, + }, + TalentPrereqView { + tier: 1, + column: 2, + learnable: false, + }, + ], + ..Default::default() + }, + ]], + points: (1, 0), + }); + s.set_loot_rolls(LootRollsState { + rolls: vec![ + LootRollEntry { + roll_id: 1, + name: Some("Bound".into()), + texture: Some("Interface\\Icons\\INV_Misc_QuestionMark".into()), + quality: Some(4), + bind_on_pickup: true, + item_id: 1, + ..Default::default() + }, + LootRollEntry { + roll_id: 2, + item_id: 2, + ..Default::default() + }, + ], + }); + s.set_talent_master_pending(true); +} + +/// 1.12 has no boolean query: of the table's trustworthy rows only `IsPetAttackActive` and Lua's +/// own `rawequal` answer a `boolean`, and every other predicate pushes `1` or `nil`, which +/// `x == 1`, `tostring(x)` or a saved variable tell apart from `true`. +#[test] +fn no_query_binding_answers_a_lua_boolean() { + // The reference's own two; the list may only shrink. + const REFERENCE_BOOLEANS: &[&str] = &["IsPetAttackActive", "rawequal"]; + + let mut s = benilla_ui::script::UiScript::new().expect("VM"); + seed_boolean_probe_state(&mut s); s.run(KIND_HELPER).expect("kind helper"); - let mut names: Vec<&str> = all - .iter() - .map(|r| r.name.as_str()) - .filter(|n| { - ["Get", "Is", "Has", "Can", "Unit", "Num"] - .iter() - .any(|p| n.starts_with(p)) - }) - .filter(|n| !REFERENCE_BOOLEANS.contains(n)) + let names: Vec = predicate_names() + .into_iter() + .filter(|n| !REFERENCE_BOOLEANS.contains(&n.as_str())) .collect(); - names.sort_unstable(); - names.dedup(); - let mut checked = 0usize; + let mut measured = std::collections::BTreeSet::new(); let mut booleans: Vec = Vec::new(); - for name in names { + for name in &names { for args in ARG_LADDER { let probe = format!( "if type({name}) ~= 'function' then return '?' end \ @@ -760,17 +864,23 @@ fn no_query_binding_answers_a_lua_boolean() { if got == "?" { continue; } - checked += 1; - if got.split(',').any(|k| k == "boolean") { + measured.insert(name.as_str()); + // One line per binding: the first rung that answers a boolean. + if got.split(',').any(|k| k == "boolean") + && !booleans + .last() + .is_some_and(|l| l.starts_with(&format!("{name}("))) + { booleans.push(format!("{name}({args}) answers ({got})")); } - break; } } assert!( - checked >= 300, - "the boolean gate measured only {checked} bindings — it has stopped covering anything" + // A floor, not a target: raise it when coverage rises, never lower it to fit a change. + measured.len() >= 500, + "the boolean gate measured only {} bindings — it has stopped covering anything", + measured.len() ); assert!( booleans.is_empty(), diff --git a/crates/benilla-ui/src/script/binder.rs b/crates/benilla-ui/src/script/binder.rs index 2487e217..1b588af2 100644 --- a/crates/benilla-ui/src/script/binder.rs +++ b/crates/benilla-ui/src/script/binder.rs @@ -4,6 +4,7 @@ use mlua::Lua; +use super::binding_abi::flag; use super::Model; impl super::UiScript { @@ -36,13 +37,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { })?, )?; - // `CheckBinderDist()`, polled by the dialog's OnUpdate. This answers a boolean; the - // reference (`0x48d210`) answers 1 or nil. + // `CheckBinderDist()`, polled by the dialog's OnUpdate: 1 or nil (`0x48d210`). g.set( "CheckBinderDist", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(model.binder_pending) + Ok(flag(model.binder_pending)) })?, )?; diff --git a/crates/benilla-ui/src/script/cursor.rs b/crates/benilla-ui/src/script/cursor.rs index e11b6194..c055ac24 100644 --- a/crates/benilla-ui/src/script/cursor.rs +++ b/crates/benilla-ui/src/script/cursor.rs @@ -5,6 +5,7 @@ use mlua::Lua; +use super::binding_abi::flag; use super::{Model, ScriptValue}; mod bag_verbs; @@ -460,14 +461,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "CursorHasItem", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(matches!(model.cursor, Some(CursorPayload::Item(_)))) + Ok(flag(matches!(model.cursor, Some(CursorPayload::Item(_))))) })?, )?; g.set( "CursorHasSpell", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(matches!(model.cursor, Some(CursorPayload::Spell(_)))) + Ok(flag(matches!(model.cursor, Some(CursorPayload::Spell(_))))) })?, )?; g.set( diff --git a/crates/benilla-ui/src/script/cursor/doll.rs b/crates/benilla-ui/src/script/cursor/doll.rs index a95e54cb..f842d95f 100644 --- a/crates/benilla-ui/src/script/cursor/doll.rs +++ b/crates/benilla-ui/src/script/cursor/doll.rs @@ -210,7 +210,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "CursorCanGoInSlot", lua.create_function(|lua, id: u32| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(cursor_can_go_in_slot(&model, id)) + Ok(flag(cursor_can_go_in_slot(&model, id))) })?, )?; g.set( diff --git a/crates/benilla-ui/src/script/death.rs b/crates/benilla-ui/src/script/death.rs index 5330a4d0..e50d0d78 100644 --- a/crates/benilla-ui/src/script/death.rs +++ b/crates/benilla-ui/src/script/death.rs @@ -1,11 +1,11 @@ //! The release, reclaim, resurrect and self-res verbs and the getters the `DEATH`, //! `RECOVER_CORPSE`, `RESURRECT` and `XP_LOSS` dialogs call, over a snapshot the app pushes. -//! The three predicates answer Lua booleans where the reference answers `1` or nil -//! (`ResurrectHasSickness` `0x48aa00`, `ResurrectHasTimer` `0x48aa30`, `CheckSpiritHealerDist` -//! `0x48d120`). +//! The three predicates answer `1` or nil, as the reference does (`ResurrectHasSickness` +//! `0x48aa00`, `ResurrectHasTimer` `0x48aa30`, `CheckSpiritHealerDist` `0x48d120`). use mlua::{Lua, Value}; +use super::binding_abi::flag; use super::Model; /// The death snapshot the app pushes each frame. @@ -106,21 +106,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "ResurrectHasSickness", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(model.death.resurrect_sickness) + Ok(flag(model.death.resurrect_sickness)) })?, )?; g.set( "ResurrectHasTimer", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(model.death.resurrect_has_timer) + Ok(flag(model.death.resurrect_has_timer)) })?, )?; g.set( "CheckSpiritHealerDist", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(model.death.spirit_healer_in_range) + Ok(flag(model.death.spirit_healer_in_range)) })?, )?; diff --git a/crates/benilla-ui/src/script/loot.rs b/crates/benilla-ui/src/script/loot.rs index 31cad993..82086792 100644 --- a/crates/benilla-ui/src/script/loot.rs +++ b/crates/benilla-ui/src/script/loot.rs @@ -219,10 +219,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "LootSlotIsItem", lua.create_function(|lua, slot: usize| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(slot - .checked_sub(1) - .and_then(|n| model.loot.as_ref()?.rows.get(n)?.as_ref()) - .is_some_and(|r| !r.is_coin)) + Ok(flag( + slot.checked_sub(1) + .and_then(|n| model.loot.as_ref()?.rows.get(n)?.as_ref()) + .is_some_and(|r| !r.is_coin), + )) })?, )?; @@ -230,10 +231,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "LootSlotIsCoin", lua.create_function(|lua, slot: usize| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(slot - .checked_sub(1) - .and_then(|n| model.loot.as_ref()?.rows.get(n)?.as_ref()) - .is_some_and(|r| r.is_coin)) + Ok(flag( + slot.checked_sub(1) + .and_then(|n| model.loot.as_ref()?.rows.get(n)?.as_ref()) + .is_some_and(|r| r.is_coin), + )) })?, )?; diff --git a/crates/benilla-ui/src/script/loot_roll.rs b/crates/benilla-ui/src/script/loot_roll.rs index 06429478..d13de001 100644 --- a/crates/benilla-ui/src/script/loot_roll.rs +++ b/crates/benilla-ui/src/script/loot_roll.rs @@ -6,6 +6,7 @@ use mlua::Lua; +use super::binding_abi::flag; use super::Model; /// `rollType` 0, the one vote the bind-on-pickup gate never intercepts. @@ -26,17 +27,13 @@ pub struct LootRollEntry { pub name: Option, /// Icon texture path (`Interface\Icons\…`). `None` only if the display catalog had no icon. pub texture: Option, - /// The stack size, answered as `GetLootRollItemInfo`'s `count`; the reference answers a literal - /// `1.0` there, a group roll being one stack (`0x4c3160`). - pub quantity: u32, /// Item quality 0..6; `None` while the template is in flight, answered as `CACHE_MISS_QUALITY`. pub quality: Option, /// Bind on pickup, which swaps in the gold backdrop; `false` while the template is in flight. pub bind_on_pickup: bool, /// Milliseconds left, from `SMSG_LOOT_START_ROLL`'s countdown; saturates at 0. pub time_left_ms: u32, - /// The item id, the key of the hover's item lookup; `GetLootRollItemInfo` also returns it as a - /// sixth value, which 1.12 does not. + /// The item id, the key of the hover's item lookup. pub item_id: u32, /// The link `GetLootRollItemLink` answers for the icon's ctrl/shift clicks /// (`LootFrame.xml:353-361`); `None` until the item template lands, since it embeds the name. @@ -78,8 +75,10 @@ fn find(model: &Model, roll_id: u32) -> Option<&LootRollEntry> { pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let g = lua.globals(); - // `0x4c3050`, read at `LootFrame.lua:261`. An unknown id takes the reference's miss tail - // `nil, nil, 1.0, 1.0, nil` (`0x4c31a3`), as a roll with no item template does there. + // `0x4c3050`, read at `LootFrame.lua:261`. `count` is the literal 1.0 (`0x4c3160`) and + // `bindOnPickup` 1.0 when `[template+0x194] == 1` else nil (`0x4c317e`). An unknown id takes + // the reference's miss tail `nil, nil, 1.0, 1.0, nil` (`0x4c31a3`), as a roll with no item + // template does there. g.set( "GetLootRollItemInfo", lua.create_function(|lua, roll_id: u32| { @@ -91,16 +90,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { 1, CACHE_MISS_QUALITY, mlua::Value::Nil, - mlua::Value::Nil, )); }; let vals = ( r.texture.clone(), r.name.clone(), - r.quantity, + 1, r.quality.map_or(CACHE_MISS_QUALITY, i64::from), - r.bind_on_pickup, - r.item_id, + flag(r.bind_on_pickup), ); lua.pack_multi(vals) })?, @@ -182,7 +179,6 @@ mod tests { roll_id: 7, name: Some("Staff of Jordan".into()), texture: Some("Interface\\Icons\\INV_Staff_12".into()), - quantity: 1, quality: Some(4), bind_on_pickup: true, time_left_ms: 42_000, @@ -195,7 +191,6 @@ mod tests { roll_id: 8, name: None, texture: None, - quantity: 2, quality: None, bind_on_pickup: false, time_left_ms: 60_000, @@ -218,26 +213,35 @@ mod tests { s.set_loot_rolls(rolls()); let (texture, name, count, quality, bop) = s - .eval::<(String, String, i64, i64, bool)>("return GetLootRollItemInfo(7)") + .eval::<(String, String, i64, i64, i64)>("return GetLootRollItemInfo(7)") .unwrap(); assert_eq!(texture, "Interface\\Icons\\INV_Staff_12"); assert_eq!(name, "Staff of Jordan"); - assert_eq!((count, quality, bop), (1, 4, true)); + assert_eq!((count, quality, bop), (1, 4, 1)); assert_eq!( s.eval::("return GetLootRollTimeLeft(7)").unwrap(), 42_000 ); - assert_eq!( - s.eval::("local _, _, _, _, _, id = GetLootRollItemInfo(7)\nreturn id") - .unwrap(), - 17182 - ); + // Five values, a hit and the miss tail alike (`0x4c31a3`). + for id in [7, 99] { + let vals = s + .eval::(&format!("return GetLootRollItemInfo({id})")) + .unwrap(); + assert_eq!(vals.len(), 5, "roll {id}"); + } - // In flight: the quality is the miss tail's 1, never nil. + // In flight: the quality is the miss tail's 1, never nil; the count is the literal 1 + // whatever the stack, and a roll that does not bind answers nil, not false. assert!(s .eval::( "local t, n, c, q, b = GetLootRollItemInfo(8)\n\ - return t == nil and n == nil and q == 1 and c == 2 and b == false", + return t == nil and n == nil and q == 1 and c == 1 and b == nil", + ) + .unwrap()); + assert!(s + .eval::( + "local _, _, _, _, b7 = GetLootRollItemInfo(7)\n\ + return type(b7) == 'number' and b7 == 1", ) .unwrap()); @@ -259,8 +263,8 @@ mod tests { assert_eq!((count, quality), (1, 1), "the miss tail is 1, 1"); assert_eq!( s.arity("GetLootRollItemInfo(99)").unwrap(), - 6, - "five reference returns plus benilla's trailing item id" + 5, + "the reference's five returns (`0x4c31a3`)" ); assert!(s .eval::("return GetLootRollItemLink(99) == nil") diff --git a/crates/benilla-ui/src/script/merchant.rs b/crates/benilla-ui/src/script/merchant.rs index c1878992..257f3dd2 100644 --- a/crates/benilla-ui/src/script/merchant.rs +++ b/crates/benilla-ui/src/script/merchant.rs @@ -9,7 +9,7 @@ use mlua::{Lua, MultiValue, Value}; -use super::binding_abi::number_arg; +use super::binding_abi::{flag, number_arg}; use super::container::UiCursorMode; use super::cursor::CursorPayload; use super::Model; @@ -414,21 +414,19 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { )?; // GetRepairAllCost() → cost, canRepair: whether there is damage to pay for, which enables the - // repair-all button (`MerchantFrame.lua:38`); 0 away from a vendor that repairs (`0x4fbd60`). + // repair-all button (`MerchantFrame.lua:38`). `canRepair` is 1 or nil (`0x4fbfa8`-`0x4fbfce`). + // With no open merchant that repairs (`0x4fadb0`) it answers the one value 0 (`0x4fbd76`). g.set( "GetRepairAllCost", lua.create_function(|lua, ()| { - let cost = { - let model = lua.app_data_ref::().expect("model app_data"); - if model.merchant.as_ref().is_some_and(|m| m.can_repair) { - model.repair_all_cost - } else { - 0 - } - }; + let model = lua.app_data_ref::().expect("model app_data"); + if !model.merchant.as_ref().is_some_and(|m| m.can_repair) { + return Ok(MultiValue::from_vec(vec![Value::Integer(0)])); + } + let cost = model.repair_all_cost; Ok(MultiValue::from_vec(vec![ Value::Integer(i64::from(cost)), - Value::Boolean(cost > 0), + flag(cost > 0), ])) })?, )?; @@ -783,11 +781,21 @@ mod tests { s.eval::("return GetRepairAllCost() == 0").unwrap(), "no vendor" ); + assert_eq!( + s.arity("GetRepairAllCost()").unwrap(), + 1, + "no vendor: `0x4fbd76`" + ); s.set_merchant(Some(stock())); assert!( s.eval::("return GetRepairAllCost() == 0").unwrap(), "a vendor that does not repair" ); + assert_eq!( + s.arity("GetRepairAllCost()").unwrap(), + 1, + "one value, 0, when `0x4fadb0` fails (`0x4fbd76`)" + ); } /// `RepairAllItems` returns before anything else unless the open merchant repairs @@ -814,8 +822,14 @@ mod tests { s.set_repair_all_cost(1234); assert!(s.eval::("return CanMerchantRepair() == 1").unwrap()); assert!(s - .eval::("local c, can = GetRepairAllCost()\nreturn c == 1234 and can == true",) + .eval::("local c, can = GetRepairAllCost()\nreturn c == 1234 and can == 1",) .unwrap()); + // No damage: `canRepair` is nil, not false (`0x4fbfce`). + s.set_repair_all_cost(0); + assert!(s + .eval::("local c, can = GetRepairAllCost()\nreturn c == 0 and can == nil",) + .unwrap()); + assert_eq!(s.arity("GetRepairAllCost()").unwrap(), 2); s.run("RepairAllItems()").unwrap(); assert!(s.take_repair_all()); diff --git a/crates/benilla-ui/src/script/talent.rs b/crates/benilla-ui/src/script/talent.rs index ba344327..71908440 100644 --- a/crates/benilla-ui/src/script/talent.rs +++ b/crates/benilla-ui/src/script/talent.rs @@ -19,6 +19,7 @@ use mlua::{Lua, MultiValue, Table, Value}; +use super::binding_abi::flag; use super::tooltip_spell::{spell_view_of, TalentLines}; use super::Model; @@ -172,8 +173,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { Value::Integer(i64::from(t.column)), Value::Integer(i64::from(t.rank)), Value::Integer(i64::from(t.max_rank)), - Value::Integer(i64::from(t.exceptional)), - Value::Boolean(t.meets_prereq), + flag(t.exceptional), + flag(t.meets_prereq), ])) })?, )?; @@ -188,7 +189,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { for p in &t.prereqs { out.push(Value::Integer(i64::from(p.tier))); out.push(Value::Integer(i64::from(p.column))); - out.push(Value::Boolean(p.learnable)); + out.push(flag(p.learnable)); } } Ok(MultiValue::from_vec(out)) @@ -217,14 +218,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { })?, )?; - // CheckTalentMasterDist(): polled by that dialog's OnUpdate, false hides it. The reference + // CheckTalentMasterDist(): polled by that dialog's OnUpdate, nil hides it. The reference // re-runs its interact-range test on the latched trainer (`0x5df980`, `d² <= [0xc4c28c]`); // no packet either way. g.set( "CheckTalentMasterDist", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); - Ok(model.talent_master_pending) + Ok(flag(model.talent_master_pending)) })?, )?; diff --git a/crates/benilla-ui/src/script/tests/talent.rs b/crates/benilla-ui/src/script/tests/talent.rs index 7920b405..56111db8 100644 --- a/crates/benilla-ui/src/script/tests/talent.rs +++ b/crates/benilla-ui/src/script/tests/talent.rs @@ -75,13 +75,13 @@ fn bindings_read_the_pushed_snapshot() { local n, icon, tier, col, rank, max, exc, meets = GetTalentInfo(1, 1) assert(n == "Improved Fireball" and tier == 1 and col == 1) - assert(rank == 3 and max == 5 and exc == 0 and meets == true) + assert(rank == 3 and max == 5 and exc == nil and meets == 1) assert(icon == "Interface\\Icons\\Spell_Fire_FlameBolt") assert(GetTalentInfo(1, 9) == nil) -- The prereq triplets, flat (the reference walks arg[5], arg[6], arg[7], ...). local pt, pc, pl = GetTalentPrereqs(1, 2) - assert(pt == 1 and pc == 1 and pl == false) + assert(pt == 1 and pc == 1 and pl == nil) assert(GetTalentPrereqs(1, 1) == nil, "no prereqs is empty") local cp1, cp2 = UnitCharacterPoints("player") @@ -92,6 +92,40 @@ fn bindings_read_the_pushed_snapshot() { assert!(s.take_errors().is_empty()); } +/// `isExceptional` is `1.0` when `[rec+0x4c] & 1` and nil otherwise (`0x4f33e8`): never `0`, which +/// Lua reads as true, and `meetsPrereq` and each prereq's `isLearnable` are 1 or nil too +/// (`0x4f3403`-`0x4f3443`, `0x4f3676`, `0x4f3644`). `GetTalentPrereqs` answers three values per edge. +#[test] +fn talent_flags_are_one_or_nil() { + let mut s = script(); + let mut state = one_tab_state(); + state.talents[0][0].exceptional = true; + state.talents[0][1].meets_prereq = false; + state.talents[0][1].prereqs.push(TalentPrereqView { + tier: 1, + column: 2, + learnable: true, + }); + s.set_talents(state); + s.run( + r#" + local _, _, _, _, _, _, exc, meets = GetTalentInfo(1, 1) + assert(exc == 1 and type(exc) == "number" and meets == 1) + local _, _, _, _, _, _, exc2, meets2 = GetTalentInfo(1, 2) + assert(exc2 == nil, "an ordinary talent is not exceptional") + assert(meets2 == nil, "an unmet prerequisite is nil, not false") + + local t1, c1, l1, t2, c2, l2 = GetTalentPrereqs(1, 2) + assert(t1 == 1 and c1 == 1 and l1 == nil) + assert(t2 == 1 and c2 == 2 and l2 == 1 and type(l2) == "number") + "#, + ) + .unwrap(); + assert_eq!(s.arity("GetTalentInfo(1, 2)").unwrap(), 8); + assert_eq!(s.arity("GetTalentPrereqs(1, 2)").unwrap(), 6); + assert!(s.take_errors().is_empty()); +} + #[test] fn learn_talent_queues_for_the_app_drain() { let mut s = script();