Flag predicates answer 1 or nil, and the gate now sees them all (#904)
Fourteen bindings answered Lua `true`/`false` where 1.12.1 pushes the number 1 or nil: the loot slot, resurrect and distance predicates, the talent flags (`isExceptional` read 0, which is true, for every ordinary talent), the cursor predicates, and `GetLootRollItemInfo`'s bind flag, whose count also echoed the stack size where the reference answers 1. `GetRepairAllCost` returned two values with no vendor open, and `GetLootRollItemInfo` a sixth value. The reference pushes 1 or nil for each and returns exactly the values its catalogue row records. All of them now answer through `binding_abi::flag`, and `GetRepairAllCost` answers the one value 0 unless a repairing merchant is open. The boolean gate, which missed these, now probes every row whose kinds are nil or number whatever its name or arity confidence, over seeded talent and roll state. The widened gate fails on the parent for all fourteen. Fixes #159 Fixes #900 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a1130c693a
commit
9be2802f39
@@ -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::<Model>().expect("model app_data");
|
||||
Ok(model.binder_pending)
|
||||
Ok(flag(model.binder_pending))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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::<Model>().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::<Model>().expect("model app_data");
|
||||
Ok(matches!(model.cursor, Some(CursorPayload::Spell(_))))
|
||||
Ok(flag(matches!(model.cursor, Some(CursorPayload::Spell(_)))))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
|
||||
@@ -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::<Model>().expect("model app_data");
|
||||
Ok(cursor_can_go_in_slot(&model, id))
|
||||
Ok(flag(cursor_can_go_in_slot(&model, id)))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
|
||||
@@ -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::<Model>().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::<Model>().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::<Model>().expect("model app_data");
|
||||
Ok(model.death.spirit_healer_in_range)
|
||||
Ok(flag(model.death.spirit_healer_in_range))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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::<Model>().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::<Model>().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),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
/// Icon texture path (`Interface\Icons\…`). `None` only if the display catalog had no icon.
|
||||
pub texture: Option<String>,
|
||||
/// 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<u32>,
|
||||
/// 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::<i64>("return GetLootRollTimeLeft(7)").unwrap(),
|
||||
42_000
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("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::<mlua::MultiValue>(&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::<bool>(
|
||||
"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::<bool>(
|
||||
"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::<bool>("return GetLootRollItemLink(99) == nil")
|
||||
|
||||
@@ -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::<Model>().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::<Model>().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::<bool>("return GetRepairAllCost() == 0").unwrap(),
|
||||
"no vendor"
|
||||
);
|
||||
assert_eq!(
|
||||
s.arity("GetRepairAllCost()").unwrap(),
|
||||
1,
|
||||
"no vendor: `0x4fbd76`"
|
||||
);
|
||||
s.set_merchant(Some(stock()));
|
||||
assert!(
|
||||
s.eval::<bool>("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::<bool>("return CanMerchantRepair() == 1").unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("local c, can = GetRepairAllCost()\nreturn c == 1234 and can == true",)
|
||||
.eval::<bool>("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::<bool>("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());
|
||||
|
||||
@@ -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::<Model>().expect("model app_data");
|
||||
Ok(model.talent_master_pending)
|
||||
Ok(flag(model.talent_master_pending))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user