ui: register UnitCanAssist, the CanAssist gate callers like ShaguTweaks rely on

UnitCanAssist (0x516bb0) was a nil global, so ShaguTweaks libpredict raised whenever UseAction ran. It answers the player's CanAssist verdict: 1 for a unit whose reaction toward the player is friendly (5 and up), or for itself, else nil, like UnitCanAttack's shape.
This commit is contained in:
chairmanpow
2026-10-07 00:20:28 +01:00
parent ad8f6cce43
commit 779510bdb0
2 changed files with 67 additions and 0 deletions
@@ -220,6 +220,39 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// UnitCanAssist (`0x516bb0`) delegates to `CanAssist` (`0x6066f0`): the player can help a unit
// whose reaction toward it is friendly, or itself. The snapshot's reaction is the Lua
// `UnitReaction` scale (`0` unknown, 5 Friendly up), and the player token's own reaction stays
// 0, so a both-arguments-`"player"` call answers the self case.
g.set(
"UnitCanAssist",
lua.create_function(|lua, (a, b): (Value, Value)| {
// Both arguments are `lua_isstring`-gated, so either one nil raises.
let a = Some(crate::script::binding_abi::string_arg(
lua,
a,
r#"Usage: UnitCanAssist("unit", "otherUnit")"#,
)?);
let b = Some(crate::script::binding_abi::string_arg(
lua,
b,
r#"Usage: UnitCanAssist("unit", "otherUnit")"#,
)?);
let token = pick_unit_token(&a, &b);
// The player can always assist itself; `"player"` resolves only when both arguments are
// `"player"`, so it is asked outside the snapshot.
let ok = if token
.as_deref()
.is_some_and(|t| t.eq_ignore_ascii_case("player"))
{
true
} else {
with_unit(lua, &token, false, |u| u.exists && u.reaction >= 5)?
};
Ok(flag(ok))
})?,
)?;
// GetQuestGreenRange (`0x4e17d0`): the green-to-grey boundary `QuestLogFrame.lua:593` buckets
// by, from the table at `0x8076c0`. The reference answers 0 with no player object (`0x4e17f8`);
// here a level of 0 reads the table's first entry, 4.
@@ -293,6 +293,30 @@ fn corpse_can_attack_and_green_range_bindings() {
.unwrap(),
1
);
// `UnitCanAssist` answers nil for the attackable target above, 1 for a friendly one, and the
// player can always assist itself.
assert!(s
.eval::<bool>(r#"return UnitCanAssist("player", "target") == nil"#)
.unwrap());
s.set_unit(
"target",
Some(UnitState {
exists: true,
reaction: 5,
..Default::default()
}),
);
assert_eq!(
s.eval::<i64>(r#"return UnitCanAssist("player", "target")"#)
.unwrap(),
1
);
assert_eq!(
s.eval::<i64>(r#"return UnitCanAssist("player", "player")"#)
.unwrap(),
1,
"the player can assist itself"
);
assert!(s
.eval::<bool>(r#"return UnitIsCorpse("target") == nil"#)
.unwrap());
@@ -1435,6 +1459,7 @@ fn a_two_token_predicate_raises_on_either_nil_argument() {
"UnitIsFriend",
"UnitCanCooperate",
"UnitCanAttack",
"UnitCanAssist",
"UnitReaction",
] {
assert!(
@@ -1685,6 +1710,15 @@ fn every_unit_predicate_is_one_or_nil_and_never_a_boolean() {
..Default::default()
},
),
(
"UnitCanAssist",
r#"UnitCanAssist("player", "target")"#,
UnitState {
exists: true,
reaction: 5,
..Default::default()
},
),
(
"UnitIsUnit",
r#"UnitIsUnit("target", "target")"#,