Lua bindings return 1/nil and the reference's value counts (#1000)
Lua bindings now return what 1.12.1 pushes, in type and count: - Flags are 1 or nil, never a Lua boolean. This covers `SetInventoryItem`/`SetBagItem` hasCooldown, `GetQuestLogLeaderBoard` finished, `GetContainerItemInfo` locked/readable, `AtTop`/`AtBottom`, `IsOwned` and `SetAlphaGradient`. - Eleven verbs whose handlers end `xor eax,eax` return nothing. That covers `PickupAction`, `PlaceAction`, `PickupSpell`, `PickupInventoryItem`, `EquipCursorItem`, `SplitContainerItem`, `PickupMacro`, `DeleteMacro`, `ResetChatColors` and others. - The miss tuples: `GetInboxHeaderInfo` gives 13 values (`0x4aeff0`), `GetInboxText` four nils, and `GetQuestLogLeaderBoard` three. - `GetTrainerServiceDescription` is nil with no text (`0x4d9d44`). `GetZonePVPInfo`'s faction is `""`, not nil (`0x48d633`). The boolean gates now probe every global and widget method. Fixes #442 Fixes #574 Fixes #577 Fixes #578 Fixes #822 Fixes #823 Fixes #872 🤖 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
1b335717c3
commit
720dcf8188
@@ -219,10 +219,7 @@ fn feed_zone_events(
|
||||
subzone: signal.subzone_text.clone(),
|
||||
minimap: minimap_text.clone(),
|
||||
pvp_type: pvp.map(|(ty, _)| ty.to_string()),
|
||||
pvp_faction: pvp
|
||||
.map(|(_, faction)| faction)
|
||||
.filter(|f| !f.is_empty())
|
||||
.map(str::to_string),
|
||||
pvp_faction: pvp_faction_text(pvp),
|
||||
is_arena,
|
||||
});
|
||||
|
||||
@@ -245,6 +242,14 @@ fn feed_zone_events(
|
||||
cache.wmo_group = wmo_group;
|
||||
}
|
||||
|
||||
/// `GetZonePVPInfo`'s second value: with a type answered, the faction name is always a string,
|
||||
/// starting as the empty string (`0x48d633`, `0x882748`) and pushed by `lua_pushstring`
|
||||
/// (`0x48d68f`) whether or not a FactionGroup row supplied one; only the bail (`0x48d6b5`) pushes
|
||||
/// nil.
|
||||
fn pvp_faction_text(pvp: Option<(&str, &str)>) -> Option<String> {
|
||||
pvp.map(|(_, faction)| faction.to_string())
|
||||
}
|
||||
|
||||
/// `GetZonePVPInfo` (`0x48d540`): `(pvpType, isArena)`. isArena is the leaf's Flags `0x80`, read
|
||||
/// before any bail. The type is nil with no zone row, no faction template, or the display gate
|
||||
/// closed: it opens on a PvP realm ([`RealmPvp`]) or in a zone whose Flags carry `0x10`, the six
|
||||
@@ -433,6 +438,21 @@ mod tests {
|
||||
assert_eq!(pvp(true, &orgrimmar), Some("hostile"));
|
||||
}
|
||||
|
||||
/// An answered type always carries a faction string, `""` in an ownerless zone such as
|
||||
/// Stranglethorn Vale (`0x48d633`); no type, no string (`0x48d6b5`).
|
||||
#[test]
|
||||
fn an_answered_type_carries_an_empty_faction_name_not_nil() {
|
||||
assert_eq!(
|
||||
pvp_faction_text(Some(("contested", ""))),
|
||||
Some(String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
pvp_faction_text(Some(("friendly", "Alliance"))),
|
||||
Some("Alliance".to_string())
|
||||
);
|
||||
assert_eq!(pvp_faction_text(None), None);
|
||||
}
|
||||
|
||||
/// isArena is the leaf's `0x80`, read before any bail, so a closed gate keeps it.
|
||||
#[test]
|
||||
fn is_arena_survives_a_closed_gate() {
|
||||
|
||||
@@ -509,8 +509,20 @@ fn cursor_update_highlights_fitting_doll_slots_while_holding_a_bag_item() {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
// The predicate the arm branches on, pinned beside the effect.
|
||||
assert!(s.eval::<bool>("return CursorCanGoInSlot(1)").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorCanGoInSlot(2)").unwrap());
|
||||
// `CursorCanGoInSlot` (`0x4c8ff0`) pushes the number 1 (`0x4c9063`) or nil (`0x4c909e`) and
|
||||
// returns one value (`arg.n` counts the returns).
|
||||
let ask =
|
||||
"local function c(...) return arg.n .. ':' .. type(arg[1]) .. ':' .. tostring(arg[1]) end";
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("{ask} return c(CursorCanGoInSlot(1))"))
|
||||
.unwrap(),
|
||||
"1:number:1"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("{ask} return c(CursorCanGoInSlot(2))"))
|
||||
.unwrap(),
|
||||
"1:nil:nil"
|
||||
);
|
||||
|
||||
assert!(
|
||||
ring_lit(&mut s, "CharacterHeadSlot"),
|
||||
|
||||
@@ -724,13 +724,10 @@ const ARG_LADDER: &[&str] = &[
|
||||
"\"player\", \"player\"",
|
||||
];
|
||||
|
||||
/// 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)";
|
||||
|
||||
/// 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`).
|
||||
/// name or `arity_conf`. 1.12 has no boolean-returning global but `IsPetAttackActive`, so a
|
||||
/// `Pickup*` or `Place*` verb that hands back whether it moved something fails here as readily as
|
||||
/// a predicate that answers `true` (`LootSlotIsItem`, `Resurrect*`, `Check*Dist`).
|
||||
fn predicate_names() -> Vec<String> {
|
||||
let tsv = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
@@ -742,13 +739,8 @@ fn predicate_names() -> Vec<String> {
|
||||
.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())
|
||||
(f.len() >= 10 && f[4] == "global" && !NOT_PROBED.contains(&f[0]))
|
||||
.then(|| f[0].to_string())
|
||||
})
|
||||
.collect();
|
||||
names.sort_unstable();
|
||||
@@ -756,13 +748,24 @@ fn predicate_names() -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Globals the boolean gate leaves alone: they end the session or the VM, not answer a value.
|
||||
const NOT_PROBED: &[&str] = &[
|
||||
"Quit",
|
||||
"ForceQuit",
|
||||
"Logout",
|
||||
"CancelLogout",
|
||||
"ReloadUI",
|
||||
"RestartGx",
|
||||
];
|
||||
|
||||
/// 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.
|
||||
/// resolved bind-on-pickup one and one still in flight), a respec question in range, a quest log
|
||||
/// with one finished line. 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,
|
||||
LootRollEntry, LootRollsState, QuestLogEntryView, QuestLogObjectiveView, QuestLogState,
|
||||
TalentPrereqView, TalentTabView, TalentUiState, TalentView,
|
||||
};
|
||||
s.set_unit(
|
||||
"player",
|
||||
@@ -830,6 +833,22 @@ fn seed_boolean_probe_state(s: &mut benilla_ui::script::UiScript) {
|
||||
],
|
||||
});
|
||||
s.set_talent_master_pending(true);
|
||||
s.set_quest_log(QuestLogState {
|
||||
num_quests: 1,
|
||||
entries: vec![QuestLogEntryView {
|
||||
quest_id: 1,
|
||||
title: "Probe".into(),
|
||||
objectives: vec![QuestLogObjectiveView {
|
||||
text: "Probes slain: 1/1".into(),
|
||||
kind: "monster".into(),
|
||||
finished: true,
|
||||
cur: 1,
|
||||
req: 1,
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// 1.12 has no boolean query: of the table's trustworthy rows only `IsPetAttackActive` and Lua's
|
||||
@@ -920,8 +939,17 @@ fn no_widget_method_answers_a_lua_boolean() {
|
||||
}
|
||||
}
|
||||
|
||||
// A widget predicate takes a script or object-type name more often than a number.
|
||||
const WIDGET_ARGS: &[&str] = &["", "\"OnClick\"", "\"Frame\"", "1", "1, 1"];
|
||||
// A widget predicate takes a script or object-type name more often than a number, and
|
||||
// `IsOwned` a frame; `PGFrame` is the probe frame made above.
|
||||
const WIDGET_ARGS: &[&str] = &[
|
||||
"",
|
||||
"\"OnClick\"",
|
||||
"\"Frame\"",
|
||||
"1",
|
||||
"1, 1",
|
||||
"PGFrame",
|
||||
"1, 5",
|
||||
];
|
||||
|
||||
let mut checked = 0usize;
|
||||
let mut booleans: Vec<String> = Vec::new();
|
||||
@@ -934,13 +962,8 @@ fn no_widget_method_answers_a_lua_boolean() {
|
||||
continue;
|
||||
}
|
||||
let (name, table) = (f[0], f[3]);
|
||||
if !["Get", "Is", "Has", "Can", "Num"]
|
||||
.iter()
|
||||
.any(|p| name.starts_with(p))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// No widget method answers a boolean in the reference, so none is exempt here.
|
||||
// No widget method answers a boolean in the reference, so none is exempt here, whatever
|
||||
// its name: `AtBottom`, `SetAlphaGradient` and `IsOwned` push 1 or nil.
|
||||
let Some(obj) = made.get(table) else { continue };
|
||||
for args in WIDGET_ARGS {
|
||||
let probe = format!(
|
||||
@@ -957,8 +980,8 @@ fn no_widget_method_answers_a_lua_boolean() {
|
||||
checked += 1;
|
||||
if got.split(',').any(|k| k == "boolean") {
|
||||
booleans.push(format!("{obj}:{name}({args}) answers ({got})"));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ fn service(
|
||||
name: Some(name.into()),
|
||||
subtext: None,
|
||||
texture: Some("Interface\\Icons\\INV_Sword_04".into()),
|
||||
description: String::new(),
|
||||
description: None,
|
||||
cost,
|
||||
prof_first_rank: false,
|
||||
category,
|
||||
|
||||
@@ -17,7 +17,7 @@ fn text_of(s: &UiScript, fontstring: &str) -> String {
|
||||
}
|
||||
|
||||
/// The zone caches the app pushes on an area change: the minimap line is the subzone, else the
|
||||
/// zone; an empty PvP word or faction is nil.
|
||||
/// zone; an empty PvP word is nil, and an answered one carries its faction name, `""` included.
|
||||
fn area(zone: &str, sub: &str, pvp: &str, faction: &str) -> ZoneTexts {
|
||||
let some = |t: &str| (!t.is_empty()).then(|| t.to_string());
|
||||
ZoneTexts {
|
||||
@@ -26,7 +26,7 @@ fn area(zone: &str, sub: &str, pvp: &str, faction: &str) -> ZoneTexts {
|
||||
subzone: sub.into(),
|
||||
minimap: if sub.is_empty() { zone } else { sub }.into(),
|
||||
pvp_type: some(pvp),
|
||||
pvp_faction: some(faction),
|
||||
pvp_faction: (!pvp.is_empty()).then(|| faction.to_string()),
|
||||
is_arena: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,9 @@ pub(super) struct ServiceText<'a> {
|
||||
/// never expanded). A used row at a mount trainer skips its own text (`0x4d9c29`), and every
|
||||
/// failure falls back to the wire text again (`0x4d9d3b`, probing slot 0 alone for the caster).
|
||||
/// The expansion itself is the spell tooltip's description call (`0x52f717`): level override 0,
|
||||
/// the Description column, and the modifiers applied.
|
||||
/// the Description column, and the modifiers applied. `None` is the nil the binding pushes
|
||||
/// (`0x4d9d90`): the wire spell has no record (`0x4d9bd7`), or no arm found text and the wire
|
||||
/// spell's own is null or empty (`0x4d9d44`, `0x4d9d49`).
|
||||
pub(super) fn service_description(
|
||||
wire: &TrainerSpell,
|
||||
trainer_type: u32,
|
||||
@@ -119,10 +121,8 @@ pub(super) fn service_description(
|
||||
items: &Items,
|
||||
commands: &NetCommands,
|
||||
text: &ServiceText,
|
||||
) -> String {
|
||||
let Some(d) = spells.get(wire.spell) else {
|
||||
return String::new();
|
||||
};
|
||||
) -> Option<String> {
|
||||
let d = spells.get(wire.spell)?;
|
||||
let learn = (0..3).find(|&i| {
|
||||
matches!(
|
||||
d.effects[i],
|
||||
@@ -139,13 +139,13 @@ pub(super) fn service_description(
|
||||
let wire_text = d.description.as_deref().filter(|t| !t.is_empty());
|
||||
if !(trainer_type == TRAINER_TYPE_MOUNT && wire.state == trainer_spell_state::GRAY) {
|
||||
if let Some(t) = wire_text {
|
||||
return substitute(t, d, learn.map_or(text.player, caster));
|
||||
return Some(substitute(t, d, learn.map_or(text.player, caster)));
|
||||
}
|
||||
}
|
||||
if let Some(slot) = learn {
|
||||
if let Some(taught) = spells.get(d.effect_trigger_spell[slot]) {
|
||||
if let Some(t) = taught.description.as_deref().filter(|t| !t.is_empty()) {
|
||||
return substitute(t, taught, caster(slot));
|
||||
return Some(substitute(t, taught, caster(slot)));
|
||||
}
|
||||
// The recipe's product carries the only text a `CREATE_ITEM` spell has.
|
||||
if taught.attributes & SPELL_ATTR_IS_TRADESKILL != 0
|
||||
@@ -156,12 +156,12 @@ pub(super) fn service_description(
|
||||
.map(|item| item.description.clone())
|
||||
.filter(|t| !t.is_empty())
|
||||
{
|
||||
return t;
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wire_text.map_or_else(String::new, |t| substitute(t, d, caster(0)))
|
||||
wire_text.map(|t| substitute(t, d, caster(0)))
|
||||
}
|
||||
|
||||
/// The list builder `0x4d7560`'s group key (`0x4d7786`). Type 2 resolves no skill line, so no row
|
||||
|
||||
@@ -528,11 +528,40 @@ fn service_description_expands_the_wire_spells_own_text() {
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
"Increases melee damage by 11."
|
||||
Some("Increases melee damage by 11.".into())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// No text to show is `None`, the nil `0x4d9d90` pushes: the wire spell has no record
|
||||
/// (`0x4d9bd7`), or no arm found text and its own is empty (`0x4d9d44`, `0x4d9d49`).
|
||||
#[test]
|
||||
fn service_description_is_none_without_a_record_or_text() {
|
||||
let empty = SpellDisplay {
|
||||
description: Some(String::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let bare = SpellDisplay::default();
|
||||
let spells = SpellCatalog::from_displays(HashMap::from([(1, empty), (2, bare)]));
|
||||
let deps = Deps::new();
|
||||
with_text(&spells, 0, 0, None, |text| {
|
||||
for spell in [1, 2, 3] {
|
||||
assert_eq!(
|
||||
service_description(
|
||||
&wire(spell, trainer_spell_state::GREEN, 10, 1, 0),
|
||||
0,
|
||||
&spells,
|
||||
&deps.items,
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
None,
|
||||
"spell {spell}"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A learn wrapper has no text of its own, so the taught spell's Description is expanded
|
||||
/// (`0x4d9c6f`); the description is the taught record's, not the wrapper's.
|
||||
#[test]
|
||||
@@ -562,7 +591,7 @@ fn service_description_hops_to_the_taught_spell() {
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
"Causes 10 threat."
|
||||
Some("Causes 10 threat.".into())
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -597,7 +626,7 @@ fn service_description_takes_the_recipes_product_text_verbatim() {
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
"Keeps $s1 for later.",
|
||||
Some("Keeps $s1 for later.".into()),
|
||||
"the product's text is the item's own, tokens and all"
|
||||
);
|
||||
});
|
||||
@@ -633,11 +662,11 @@ fn service_description_skips_the_wire_text_for_a_used_mount_row() {
|
||||
};
|
||||
assert_eq!(
|
||||
description(0, trainer_spell_state::GREEN),
|
||||
"Summons a mount."
|
||||
Some("Summons a mount.".into())
|
||||
);
|
||||
assert_eq!(
|
||||
description(TRAINER_TYPE_MOUNT, trainer_spell_state::GRAY),
|
||||
"Riding skill."
|
||||
Some("Riding skill.".into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -682,7 +711,7 @@ fn service_description_expands_against_the_pet_for_a_learn_pet_row() {
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
"Causes 11 threat.",
|
||||
Some("Causes 11 threat.".into()),
|
||||
"the pet's level drives the taught spell's points"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -694,7 +723,7 @@ fn service_description_expands_against_the_pet_for_a_learn_pet_row() {
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
"Causes 10 threat.",
|
||||
Some("Causes 10 threat.".into()),
|
||||
"the same taught spell under LEARN_SPELL reads the player's"
|
||||
);
|
||||
});
|
||||
@@ -724,7 +753,7 @@ fn service_description_expands_z_to_the_bind_area() {
|
||||
&deps.commands,
|
||||
text,
|
||||
),
|
||||
want
|
||||
Some(want.to_string())
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -748,12 +777,14 @@ fn service_description_on_real_data_expands_the_taught_ability() {
|
||||
);
|
||||
assert!(
|
||||
svc.description
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.starts_with("A strong attack that increases melee damage by "),
|
||||
"the taught 78's description, not the wrapper's empty one: {:?}",
|
||||
svc.description
|
||||
);
|
||||
assert!(
|
||||
!svc.description.contains('$'),
|
||||
!svc.description.as_deref().unwrap_or_default().contains('$'),
|
||||
"every token resolved: {:?}",
|
||||
svc.description
|
||||
);
|
||||
@@ -783,9 +814,11 @@ fn service_description_on_real_data_reads_astral_recalls_bind_area() {
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
svc.description,
|
||||
"Yanks the caster through the twisting nether back to Razor Hill. Speak to an \
|
||||
Innkeeper in a different place to change your home location."
|
||||
svc.description.as_deref(),
|
||||
Some(
|
||||
"Yanks the caster through the twisting nether back to Razor Hill. Speak to an \
|
||||
Innkeeper in a different place to change your home location."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -811,9 +844,11 @@ fn service_description_on_real_data_reads_the_profession_learn_row() {
|
||||
&BTreeSet::new(),
|
||||
);
|
||||
assert_eq!(
|
||||
svc.description,
|
||||
"Allows a Blacksmith to make basic weapons and armor up to a maximum potential skill of \
|
||||
75. Requires stone and metal found with the mining skill."
|
||||
svc.description.as_deref(),
|
||||
Some(
|
||||
"Allows a Blacksmith to make basic weapons and armor up to a maximum potential skill \
|
||||
of 75. Requires stone and metal found with the mining skill."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -856,7 +891,7 @@ fn service_description_on_real_data_reaches_for_the_recipes_product() {
|
||||
let deps = Deps::new();
|
||||
assert_eq!(
|
||||
describe(&deps),
|
||||
"",
|
||||
None,
|
||||
"nothing to show until the product's template lands"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -873,7 +908,7 @@ fn service_description_on_real_data_reaches_for_the_recipes_product() {
|
||||
deps.items.insert_template(6338, Some(product));
|
||||
deps
|
||||
};
|
||||
assert_eq!(describe(&landed), "Needed by Enchanters.");
|
||||
assert_eq!(describe(&landed), Some("Needed by Enchanters.".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! `ResetChatColors`: the reference's runtime table (`0xb4e518`), which the `COLORS` block of
|
||||
//! `chat-cache.txt` overwrites in place at load; the app owns that file.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
use mlua::{Lua, MultiValue};
|
||||
|
||||
use super::Model;
|
||||
|
||||
@@ -247,7 +247,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
reset(&mut model.chat_colors);
|
||||
model.chat_colors_changed = true;
|
||||
Ok(Value::Nil)
|
||||
// `0x4a09e0` ends `xor eax,eax; ret`: no value.
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use super::binding_abi::flag;
|
||||
use super::cursor::{self, CursorItem, CursorPayload};
|
||||
use super::Model;
|
||||
|
||||
@@ -538,6 +539,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// GetContainerItemInfo(bag, slot) (`0x4f9670`) → texture, itemCount, locked, quality,
|
||||
// readable, the five `ContainerFrame.lua:241` reads; no values for an empty or unknown slot.
|
||||
// `locked` and `readable` are 1 or nil (`0x4f975d`/`0x4f976b`, `0x4f97e6`/`0x4f97d3`).
|
||||
lua.globals().set(
|
||||
"GetContainerItemInfo",
|
||||
lua.create_function(|lua, (bag, slot): (i64, u32)| {
|
||||
@@ -573,12 +575,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Value::Integer(i64::from(s.count)),
|
||||
// A held item's source slot reads locked, derived from the cursor so the app's
|
||||
// re-push cannot wipe it.
|
||||
Value::Boolean(s.locked || held_here),
|
||||
flag(s.locked || held_here),
|
||||
match s.quality {
|
||||
Some(q) => Value::Integer(i64::from(q)),
|
||||
None => Value::Nil,
|
||||
},
|
||||
Value::Boolean(s.readable),
|
||||
flag(s.readable),
|
||||
]))
|
||||
})?,
|
||||
)?;
|
||||
@@ -702,6 +704,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ContainerMove, ContainerSlot, ContainerState, PendingWrap, UiCursorMode};
|
||||
use crate::script::cursor::split_container_item;
|
||||
use crate::script::{MerchantItem, MerchantState, UiScript};
|
||||
|
||||
fn backpack() -> ContainerState {
|
||||
@@ -857,6 +860,35 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// `GetContainerItemInfo` (`0x4f9670`) pushes `locked` and `readable` as the number 1
|
||||
/// (`0x4f975d`, `0x4f97e6`) or nil (`0x4f976b`, `0x4f97d3`), five values in all.
|
||||
#[test]
|
||||
fn container_item_info_flags_are_one_or_nil() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
// `arg.n` counts the returns (`select` is 5.1's, not a 1.12 global).
|
||||
let shape = |s: &mut UiScript, slot: u32| {
|
||||
s.eval::<String>(&format!(
|
||||
"local function c(...) return arg.n .. ':' .. type(arg[3]) .. ':' .. tostring(arg[3]) \
|
||||
.. ':' .. type(arg[5]) .. ':' .. tostring(arg[5]) end \
|
||||
return c(GetContainerItemInfo(0, {slot}))"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(shape(&mut s, 1), "5:nil:nil:nil:nil", "a plain item");
|
||||
assert_eq!(
|
||||
shape(&mut s, 4),
|
||||
"5:nil:nil:number:1",
|
||||
"the readable letter"
|
||||
);
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
assert_eq!(
|
||||
shape(&mut s, 1),
|
||||
"5:number:1:nil:nil",
|
||||
"the held item reads locked"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_container_item_queues_intents() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1061,9 +1093,7 @@ mod tests {
|
||||
);
|
||||
s.set_container(0, Some(state));
|
||||
|
||||
assert!(s
|
||||
.eval::<bool>("return SplitContainerItem(0, 1, 3)")
|
||||
.unwrap());
|
||||
assert!(split_container_item(&mut s.model_mut(), 0, 1, 3));
|
||||
let held = s.cursor_item().expect("the split carry");
|
||||
assert_eq!(
|
||||
(held.bag, held.slot, held.item_id, held.count),
|
||||
@@ -1107,15 +1137,11 @@ mod tests {
|
||||
fn split_of_the_whole_stack_is_a_plain_pickup() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack())); // slot 1: a 5-stack
|
||||
assert!(s
|
||||
.eval::<bool>("return SplitContainerItem(0, 1, 5)")
|
||||
.unwrap());
|
||||
assert!(split_container_item(&mut s.model_mut(), 0, 1, 5));
|
||||
assert_eq!(s.cursor_item().unwrap().count, None);
|
||||
// A split of more than the stack is a plain pickup too.
|
||||
s.run("ClearCursor()").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return SplitContainerItem(0, 1, 99)")
|
||||
.unwrap());
|
||||
assert!(split_container_item(&mut s.model_mut(), 0, 1, 99));
|
||||
assert_eq!(s.cursor_item().unwrap().count, None);
|
||||
}
|
||||
|
||||
|
||||
@@ -484,7 +484,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"SplitContainerItem",
|
||||
lua.create_function(|lua, (bag, slot, count): (i64, u32, i64)| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(split_container_item(&mut model, bag, slot, count))
|
||||
split_container_item(&mut model, bag, slot, count);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
@@ -507,7 +508,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CursorAction, CursorItem, CursorPayload, CursorSpell};
|
||||
use super::{split_container_item, CursorAction, CursorItem, CursorPayload, CursorSpell};
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// A backpack holding a five-stack in slot 1.
|
||||
@@ -595,12 +596,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `CursorHasItem` (`0x4895d0`) and `CursorHasSpell` (`0x489600`) push the number 1
|
||||
/// (`0x4895ea`, `0x489609`) or nil (`0x4895df`, `0x48961b`) and return one value.
|
||||
#[test]
|
||||
fn the_has_checks_answer_per_arm() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
// `arg.n` counts the returns (`select` is 5.1's, not a 1.12 global).
|
||||
let ask = |s: &mut UiScript, f: &str| {
|
||||
s.eval::<String>(&format!(
|
||||
"local function c(...) return arg.n .. ':' .. type(arg[1]) .. ':' .. tostring(arg[1]) end \
|
||||
return c({f}())"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
const YES: &str = "1:number:1";
|
||||
const NO: &str = "1:nil:nil";
|
||||
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
assert_eq!(ask(&mut s, "CursorHasItem"), NO);
|
||||
assert_eq!(ask(&mut s, "CursorHasSpell"), NO);
|
||||
|
||||
s.set_cursor_for_test(CursorPayload::Item(CursorItem {
|
||||
bar_placeable: true,
|
||||
@@ -613,8 +626,8 @@ mod tests {
|
||||
quality: Some(3),
|
||||
equip_slots: Vec::new(),
|
||||
}));
|
||||
assert!(s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
assert_eq!(ask(&mut s, "CursorHasItem"), YES);
|
||||
assert_eq!(ask(&mut s, "CursorHasSpell"), NO);
|
||||
|
||||
s.set_cursor_for_test(CursorPayload::Spell(CursorSpell {
|
||||
passive: false,
|
||||
@@ -623,8 +636,8 @@ mod tests {
|
||||
spell_id: 133,
|
||||
texture: None,
|
||||
}));
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
assert_eq!(ask(&mut s, "CursorHasItem"), NO);
|
||||
assert_eq!(ask(&mut s, "CursorHasSpell"), YES);
|
||||
|
||||
s.set_cursor_for_test(CursorPayload::Action(CursorAction {
|
||||
src_slot: 12,
|
||||
@@ -632,8 +645,8 @@ mod tests {
|
||||
action: 133,
|
||||
texture: None,
|
||||
}));
|
||||
assert!(!s.eval::<bool>("return CursorHasItem()").unwrap());
|
||||
assert!(!s.eval::<bool>("return CursorHasSpell()").unwrap());
|
||||
assert_eq!(ask(&mut s, "CursorHasItem"), NO);
|
||||
assert_eq!(ask(&mut s, "CursorHasSpell"), NO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -749,9 +762,7 @@ mod tests {
|
||||
fn split_of_the_whole_stack_degrades_to_a_plain_pickup() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(one_item_backpack())); // slot 1: a 5-stack
|
||||
assert!(s
|
||||
.eval::<bool>("return SplitContainerItem(0, 1, 5)")
|
||||
.unwrap());
|
||||
assert!(split_container_item(&mut s.model_mut(), 0, 1, 5));
|
||||
let held = s.cursor_item().expect("picked up");
|
||||
assert_eq!(held.count, None, "count>=stack ⇒ whole-stack pickup");
|
||||
}
|
||||
|
||||
@@ -110,14 +110,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"PickupAction",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(pickup_action(&mut model, id))
|
||||
pickup_action(&mut model, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"PlaceAction",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(place_action(&mut model, id))
|
||||
place_action(&mut model, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -126,6 +128,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{pickup_action, place_action};
|
||||
use crate::script::cursor::{CursorAction, CursorItem, CursorPayload, CursorSpell};
|
||||
use crate::script::{ActionSlot, UiScript};
|
||||
|
||||
@@ -144,7 +147,7 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 133)));
|
||||
|
||||
assert!(s.eval::<bool>("return PickupAction(1)").unwrap());
|
||||
assert!(pickup_action(&mut s.model_mut(), 1));
|
||||
assert!(
|
||||
!s.eval::<bool>("return HasAction(1)").unwrap(),
|
||||
"removed from the engine's optimistic mirror"
|
||||
@@ -160,7 +163,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pickup_action_on_an_empty_slot_is_a_no_op() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
assert!(!s.eval::<bool>("return PickupAction(5)").unwrap());
|
||||
assert!(!pickup_action(&mut s.model_mut(), 5));
|
||||
assert!(s.cursor_payload().is_none());
|
||||
assert!(s.take_action_sets().is_empty());
|
||||
}
|
||||
@@ -175,7 +178,7 @@ mod tests {
|
||||
texture: Some("Interface\\Icons\\Spell_A".into()),
|
||||
}));
|
||||
|
||||
assert!(s.eval::<bool>("return PlaceAction(7)").unwrap());
|
||||
assert!(place_action(&mut s.model_mut(), 7));
|
||||
assert!(s.cursor_payload().is_none(), "empty destination clears");
|
||||
assert!(s.eval::<bool>("return HasAction(7)").unwrap());
|
||||
assert_eq!(
|
||||
@@ -192,10 +195,10 @@ mod tests {
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 111)));
|
||||
s.set_action(2, Some(action_slot("Interface\\Icons\\Spell_B", 0x00, 222)));
|
||||
|
||||
assert!(s.eval::<bool>("return PickupAction(1)").unwrap());
|
||||
assert!(pickup_action(&mut s.model_mut(), 1));
|
||||
assert_eq!(s.take_action_sets(), vec![(1, 0)]);
|
||||
|
||||
assert!(s.eval::<bool>("return PlaceAction(2)").unwrap());
|
||||
assert!(place_action(&mut s.model_mut(), 2));
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetActionTexture(2)").unwrap(),
|
||||
"Interface\\Icons\\Spell_A"
|
||||
@@ -227,7 +230,7 @@ mod tests {
|
||||
equip_slots: Vec::new(),
|
||||
}));
|
||||
|
||||
assert!(s.eval::<bool>("return PlaceAction(4)").unwrap());
|
||||
assert!(place_action(&mut s.model_mut(), 4));
|
||||
assert!(s.cursor_payload().is_none());
|
||||
// 0x80<<24 | 117.
|
||||
assert_eq!(s.take_action_sets(), vec![(4, 0x8000_0000 | 117)]);
|
||||
@@ -248,7 +251,7 @@ mod tests {
|
||||
texture: Some("Interface\\Icons\\Spell_A".into()),
|
||||
}));
|
||||
|
||||
assert!(s.eval::<bool>("return PlaceAction(9)").unwrap());
|
||||
assert!(place_action(&mut s.model_mut(), 9));
|
||||
assert_eq!(s.take_action_sets(), vec![(9, 133)]); // 0x00<<24 | 133
|
||||
}
|
||||
|
||||
@@ -256,7 +259,7 @@ mod tests {
|
||||
fn place_action_with_an_empty_cursor_is_a_no_op() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 111)));
|
||||
assert!(!s.eval::<bool>("return PlaceAction(1)").unwrap());
|
||||
assert!(!place_action(&mut s.model_mut(), 1));
|
||||
assert!(s.eval::<bool>("return HasAction(1)").unwrap(), "untouched");
|
||||
assert!(s.take_action_sets().is_empty());
|
||||
}
|
||||
@@ -270,7 +273,7 @@ mod tests {
|
||||
action: 111,
|
||||
texture: Some("Interface\\Icons\\Spell_A".into()),
|
||||
}));
|
||||
assert!(s.eval::<bool>("return PickupAction(5)").unwrap());
|
||||
assert!(pickup_action(&mut s.model_mut(), 5));
|
||||
assert!(s.cursor_payload().is_none(), "placed onto the empty slot 5");
|
||||
assert!(s.eval::<bool>("return HasAction(5)").unwrap());
|
||||
assert_eq!(s.take_action_sets(), vec![(5, 111)]);
|
||||
@@ -326,7 +329,7 @@ mod tests {
|
||||
equip_slots: Vec::new(),
|
||||
}));
|
||||
|
||||
assert!(!s.eval::<bool>("return PlaceAction(4)").unwrap());
|
||||
assert!(!place_action(&mut s.model_mut(), 4));
|
||||
assert!(!s.eval::<bool>("return HasAction(4)").unwrap(), "no store");
|
||||
assert!(s.take_action_sets().is_empty(), "no packet");
|
||||
assert!(
|
||||
@@ -351,7 +354,7 @@ mod tests {
|
||||
passive: true,
|
||||
}));
|
||||
|
||||
assert!(!s.eval::<bool>("return PlaceAction(4)").unwrap());
|
||||
assert!(!place_action(&mut s.model_mut(), 4));
|
||||
assert!(!s.eval::<bool>("return HasAction(4)").unwrap());
|
||||
assert!(s.take_action_sets().is_empty());
|
||||
assert!(matches!(s.cursor_payload(), Some(CursorPayload::Spell(_))));
|
||||
@@ -377,7 +380,7 @@ mod tests {
|
||||
passive: false,
|
||||
}));
|
||||
|
||||
assert!(s.eval::<bool>("return PlaceAction(4)").unwrap());
|
||||
assert!(place_action(&mut s.model_mut(), 4));
|
||||
assert!(s.eval::<bool>("return HasAction(4)").unwrap());
|
||||
assert!(s.take_ui_errors().is_empty());
|
||||
}
|
||||
@@ -397,7 +400,7 @@ mod tests {
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
}));
|
||||
|
||||
assert!(s.eval::<bool>("return PlaceAction(4)").unwrap());
|
||||
assert!(place_action(&mut s.model_mut(), 4));
|
||||
assert_eq!(s.take_action_sets(), vec![(4, 0x8000_0000u32 | 4496)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,14 +196,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"PickupInventoryItem",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(pickup_inventory_item(&mut model, id))
|
||||
pickup_inventory_item(&mut model, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"EquipCursorItem",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(equip_cursor_item(&mut model, id))
|
||||
equip_cursor_item(&mut model, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
@@ -217,7 +219,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"AutoEquipCursorItem",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(auto_equip_cursor_item(&mut model))
|
||||
auto_equip_cursor_item(&mut model);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
@@ -241,6 +244,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{auto_equip_cursor_item, equip_cursor_item, pickup_inventory_item};
|
||||
use crate::script::cursor::{CursorAction, CursorPayload, CursorSpell, EQUIPMENT_BAG};
|
||||
use crate::script::{ContainerMove, ContainerSlot, ContainerState, InvSlotView, UiScript};
|
||||
|
||||
@@ -341,7 +345,7 @@ mod tests {
|
||||
s.set_inventory_slots(doll_slots());
|
||||
|
||||
assert!(!s.eval::<bool>("return IsInventoryItemLocked(1)").unwrap());
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
let held = s.cursor_item().expect("picked up");
|
||||
assert_eq!(
|
||||
(held.bag, held.slot, held.item_id),
|
||||
@@ -356,8 +360,8 @@ mod tests {
|
||||
fn pickup_same_doll_slot_cancels_no_move() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_inventory_slots(doll_slots());
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
assert!(!s.eval::<bool>("return IsInventoryItemLocked(1)").unwrap());
|
||||
@@ -372,7 +376,7 @@ mod tests {
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
assert_eq!(s.cursor_item().unwrap().equip_slots, vec![1]);
|
||||
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
assert!(s.cursor_item().is_none(), "a plain clear, no hop");
|
||||
assert_eq!(
|
||||
s.take_container_moves(),
|
||||
@@ -394,7 +398,7 @@ mod tests {
|
||||
s.set_container(0, Some(one_fitting_bag_item()));
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert!(!s.eval::<bool>("return PickupInventoryItem(2)").unwrap());
|
||||
assert!(!pickup_inventory_item(&mut s.model_mut(), 2));
|
||||
let held = s.cursor_item().expect("kept — doesn't fit slot 2");
|
||||
assert_eq!(held.item_id, 2000);
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
@@ -405,10 +409,10 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_inventory_slots(doll_slots());
|
||||
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(11)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 11));
|
||||
assert_eq!(s.cursor_item().unwrap().equip_slots, vec![11, 12]);
|
||||
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(12)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 12));
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert_eq!(
|
||||
s.take_container_moves(),
|
||||
@@ -434,7 +438,7 @@ mod tests {
|
||||
let held = s.cursor_item().expect("a split carry");
|
||||
assert_eq!(held.count, Some(2));
|
||||
|
||||
assert!(!s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(!pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
let held = s.cursor_item().expect("kept — can't equip a partial stack");
|
||||
assert_eq!(held.count, Some(2));
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
@@ -473,8 +477,8 @@ mod tests {
|
||||
s.set_inventory_slots(slots);
|
||||
|
||||
// Ammo (0) is refused; Bag0Slot (20) picks up the equipped bag.
|
||||
assert!(!s.eval::<bool>("return PickupInventoryItem(0)").unwrap());
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(20)").unwrap());
|
||||
assert!(!pickup_inventory_item(&mut s.model_mut(), 0));
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 20));
|
||||
let held = s.cursor_item().expect("bag picked up from the bar");
|
||||
assert_eq!(
|
||||
(held.bag, held.slot, held.item_id),
|
||||
@@ -520,7 +524,7 @@ mod tests {
|
||||
s.run("PickupContainerItem(0, 3)").unwrap();
|
||||
assert!(s.eval::<bool>("return CursorCanGoInSlot(21)").unwrap());
|
||||
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(21)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 21));
|
||||
assert!(s.cursor_item().is_none(), "a plain clear on place");
|
||||
assert_eq!(
|
||||
s.take_container_moves(),
|
||||
@@ -570,7 +574,7 @@ mod tests {
|
||||
s.set_container(0, Some(one_fitting_bag_item()));
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert!(s.eval::<bool>("return AutoEquipCursorItem()").unwrap());
|
||||
assert!(auto_equip_cursor_item(&mut s.model_mut()));
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert_eq!(s.take_container_autoequips(), vec![(0, 1)]);
|
||||
}
|
||||
@@ -581,7 +585,7 @@ mod tests {
|
||||
s.set_inventory_slots(doll_slots());
|
||||
|
||||
s.run("PickupInventoryItem(1)").unwrap();
|
||||
assert!(!s.eval::<bool>("return AutoEquipCursorItem()").unwrap());
|
||||
assert!(!auto_equip_cursor_item(&mut s.model_mut()));
|
||||
assert!(s.cursor_item().is_some(), "kept");
|
||||
assert!(s.take_container_autoequips().is_empty());
|
||||
s.run("ClearCursor()").unwrap();
|
||||
@@ -590,7 +594,7 @@ mod tests {
|
||||
state.slots.get_mut(&1).unwrap().count = 5;
|
||||
s.set_container(0, Some(state));
|
||||
s.run("SplitContainerItem(0, 1, 2)").unwrap();
|
||||
assert!(!s.eval::<bool>("return AutoEquipCursorItem()").unwrap());
|
||||
assert!(!auto_equip_cursor_item(&mut s.model_mut()));
|
||||
assert!(s.cursor_item().is_some(), "kept");
|
||||
assert!(s.take_container_autoequips().is_empty());
|
||||
}
|
||||
@@ -633,8 +637,8 @@ mod tests {
|
||||
}));
|
||||
s.run("ShowRepairCursor()").unwrap();
|
||||
|
||||
assert!(!s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(!s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(!pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
assert!(!pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
s.run("UseInventoryItem(1)").unwrap();
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert_eq!(s.take_inventory_repairs(), vec![1, 1, 1]);
|
||||
@@ -723,7 +727,7 @@ mod tests {
|
||||
s.set_container(0, Some(one_fitting_bag_item()));
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert!(s.eval::<bool>("return EquipCursorItem(1)").unwrap());
|
||||
assert!(equip_cursor_item(&mut s.model_mut(), 1));
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert_eq!(
|
||||
s.take_container_moves(),
|
||||
@@ -746,7 +750,7 @@ mod tests {
|
||||
s.set_container(0, Some(one_fitting_bag_item()));
|
||||
|
||||
// Unarmed, both seams do their ordinary gesture and queue nothing.
|
||||
assert!(s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
assert!(s.cursor_item().is_some());
|
||||
assert!(s.take_item_picks().is_empty());
|
||||
|
||||
@@ -761,7 +765,7 @@ mod tests {
|
||||
// Armed with an empty cursor: both seams bind and leave the cursor empty.
|
||||
s.run("ClearCursor()").unwrap();
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert!(!s.eval::<bool>("return PickupInventoryItem(1)").unwrap());
|
||||
assert!(!pickup_inventory_item(&mut s.model_mut(), 1));
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
assert_eq!(
|
||||
s.take_item_picks(),
|
||||
|
||||
@@ -221,13 +221,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.globals().set(
|
||||
"PickupPetAction",
|
||||
lua.create_function(|lua, slot: u32| {
|
||||
let took = {
|
||||
{
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_pet_action(&mut model, slot)
|
||||
};
|
||||
pickup_pet_action(&mut model, slot);
|
||||
}
|
||||
// Both the pickup and a drop's displaced occupant can put a token on the cursor.
|
||||
resolve_token_icon(lua);
|
||||
Ok(took)
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -236,6 +236,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::script::cursor::bar::place_action;
|
||||
use crate::script::{PetActionView, UiScript};
|
||||
|
||||
/// A hunter's bar words. `FILLER` is vmangos's unused slot, `ACT_DISABLED` with spell 0: type
|
||||
@@ -340,7 +341,7 @@ mod tests {
|
||||
#[test]
|
||||
fn picking_up_a_spell_blanks_its_slot_and_sends() {
|
||||
let mut s = bar_script();
|
||||
assert!(s.eval::<bool>("return PickupPetAction(4)").unwrap());
|
||||
assert!(pickup_pet_action(&mut s.model_mut(), 4));
|
||||
assert!(
|
||||
matches!(s.cursor_payload(), Some(crate::script::CursorPayload::PetAction(c)) if c.src_slot == 4),
|
||||
"{:?}",
|
||||
@@ -388,7 +389,7 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_pet_actions(true, false, true, hunter_bar());
|
||||
|
||||
assert!(s.eval::<bool>("return PickupPetAction(4)").unwrap());
|
||||
assert!(pickup_pet_action(&mut s.model_mut(), 4));
|
||||
assert!(
|
||||
matches!(s.cursor_payload(), Some(crate::script::CursorPayload::PetAction(c))
|
||||
if c.src_slot == 4 && c.packed == CLAW),
|
||||
@@ -440,7 +441,7 @@ mod tests {
|
||||
#[test]
|
||||
fn picking_up_a_token_leaves_the_slot_alone() {
|
||||
let mut s = bar_script();
|
||||
assert!(s.eval::<bool>("return PickupPetAction(1)").unwrap());
|
||||
assert!(pickup_pet_action(&mut s.model_mut(), 1));
|
||||
assert!(
|
||||
s.take_pet_set_actions().is_empty(),
|
||||
"nothing was written, so nothing is sent"
|
||||
@@ -506,8 +507,8 @@ mod tests {
|
||||
#[test]
|
||||
fn slot_eleven_is_refused_rather_than_written_past_the_array() {
|
||||
let mut s = bar_script();
|
||||
assert!(!s.eval::<bool>("return PickupPetAction(11)").unwrap());
|
||||
assert!(!s.eval::<bool>("return PickupPetAction(0)").unwrap());
|
||||
assert!(!pickup_pet_action(&mut s.model_mut(), 11));
|
||||
assert!(!pickup_pet_action(&mut s.model_mut(), 0));
|
||||
assert!(s.take_pet_set_actions().is_empty());
|
||||
assert!(s.cursor_payload().is_none());
|
||||
}
|
||||
@@ -517,7 +518,7 @@ mod tests {
|
||||
fn a_possessed_bar_takes_no_drag_at_all() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_pet_actions(true, true, false, hunter_bar());
|
||||
assert!(!s.eval::<bool>("return PickupPetAction(4)").unwrap());
|
||||
assert!(!pickup_pet_action(&mut s.model_mut(), 4));
|
||||
assert!(s.take_pet_set_actions().is_empty());
|
||||
}
|
||||
|
||||
@@ -558,7 +559,7 @@ mod tests {
|
||||
"the action bar's grid stays down — nothing can land there"
|
||||
);
|
||||
// PlaceAction refuses it outright and leaves it held.
|
||||
assert!(!s.eval::<bool>("return PlaceAction(1)").unwrap());
|
||||
assert!(!place_action(&mut s.model_mut(), 1));
|
||||
assert!(matches!(
|
||||
s.cursor_payload(),
|
||||
Some(crate::script::CursorPayload::PetAction(_))
|
||||
|
||||
@@ -406,7 +406,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"DeleteMacro",
|
||||
lua.create_function(|lua, index: usize| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(delete_macro(&mut model, index))
|
||||
delete_macro(&mut model, index);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -414,7 +415,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"PickupMacro",
|
||||
lua.create_function(|lua, index: usize| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(pickup_macro(&mut model, index))
|
||||
pickup_macro(&mut model, index);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -500,7 +502,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// The character macro keeps its own base after the delete.
|
||||
assert!(s.eval::<bool>("return DeleteMacro(1)").unwrap());
|
||||
assert!(delete_macro(&mut s.model_mut(), 1));
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>("local a, c = GetNumMacros() return a, c")
|
||||
.unwrap(),
|
||||
@@ -620,11 +622,8 @@ mod tests {
|
||||
s.run(r#"CreateMacro("Ambush", 1, "/cast Ambush")"#)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!s.eval::<bool>("return PickupMacro(5)").unwrap(),
|
||||
"empty slot"
|
||||
);
|
||||
assert!(s.eval::<bool>("return PickupMacro(1)").unwrap());
|
||||
assert!(!pickup_macro(&mut s.model_mut(), 5), "empty slot");
|
||||
assert!(pickup_macro(&mut s.model_mut(), 1));
|
||||
assert_eq!(
|
||||
s.cursor_payload(),
|
||||
Some(CursorPayload::Macro(CursorMacro {
|
||||
@@ -633,7 +632,7 @@ mod tests {
|
||||
}))
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return PickupMacro(1)").unwrap(),
|
||||
!pickup_macro(&mut s.model_mut(), 1),
|
||||
"a macro button is a source, never a drop target"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetInboxHeaderInfo(index): the 13 values `MailFrame.lua:105` reads; nil out of range.
|
||||
// GetInboxHeaderInfo(index): the 13 values `MailFrame.lua:105` reads. Out of range (or index
|
||||
// 0, which `OpenMail_Update` asks after a letter closes) the reference still returns 13
|
||||
// (`0x4aeff0`..`0x4af05e`): four nils, 0 for money, COD amount and days left, six nils.
|
||||
g.set(
|
||||
"GetInboxHeaderInfo",
|
||||
lua.create_function(|lua, index: usize| {
|
||||
@@ -296,7 +298,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
row_at(&model, index)
|
||||
};
|
||||
let Some(r) = row else {
|
||||
return Ok(MultiValue::from_vec(vec![Value::Nil]));
|
||||
let mut miss = vec![Value::Nil; 4];
|
||||
miss.extend(vec![Value::Integer(0); 3]);
|
||||
miss.extend(vec![Value::Nil; 6]);
|
||||
return Ok(MultiValue::from_vec(miss));
|
||||
};
|
||||
let opt_str = |s: &Option<String>| -> mlua::Result<Value> {
|
||||
Ok(match s {
|
||||
@@ -350,8 +355,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
row
|
||||
};
|
||||
// A miss pushes four nils (`0x4af311`..`0x4af334`).
|
||||
let Some(r) = row else {
|
||||
return Ok(MultiValue::from_vec(vec![Value::Nil]));
|
||||
return Ok(MultiValue::from_vec(vec![Value::Nil; 4]));
|
||||
};
|
||||
Ok(MultiValue::from_vec(vec![
|
||||
// An invoice's body is nil (`0x4af1cf`): its text is the auction house's
|
||||
@@ -1040,6 +1046,52 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// A row that does not exist still answers the full tuple: `GetInboxHeaderInfo` 13 values,
|
||||
/// four nils, `0` money, COD and days left, six nils (`0x4aeff0`..`0x4af05e`), so a closed
|
||||
/// letter's `GetInboxHeaderInfo(0)` can be compared with `CODAmount > 0` (`MailFrame.lua:399`);
|
||||
/// `GetInboxText` four nils (`0x4af311`..`0x4af334`).
|
||||
#[test]
|
||||
fn a_missing_inbox_row_answers_the_references_miss_tuples() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_mail(Some(state()));
|
||||
for index in [0, 3, 99] {
|
||||
assert_eq!(
|
||||
s.arity(&format!("GetInboxHeaderInfo({index})")).unwrap(),
|
||||
13,
|
||||
"header {index}"
|
||||
);
|
||||
assert_eq!(
|
||||
s.arity(&format!("GetInboxText({index})")).unwrap(),
|
||||
4,
|
||||
"text {index}"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!(
|
||||
"local t = {{GetInboxHeaderInfo({index})}} \
|
||||
local k = '' \
|
||||
for i = 1, 13 do k = k .. type(t[i]) .. ',' end \
|
||||
local _, _, _, _, m, c, d = GetInboxHeaderInfo({index}) \
|
||||
return k .. m .. ',' .. c .. ',' .. d .. ',' .. tostring(c > 0)"
|
||||
))
|
||||
.unwrap(),
|
||||
"nil,nil,nil,nil,number,number,number,nil,nil,nil,nil,nil,nil,0,0,0,false",
|
||||
"header {index}"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(&format!(
|
||||
"local a, b, c, d = GetInboxText({index}) \
|
||||
return a == nil and b == nil and c == nil and d == nil"
|
||||
))
|
||||
.unwrap(),
|
||||
"text {index}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
s.take_mail_opens().is_empty(),
|
||||
"a miss opens nothing for the app to fetch"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_inbox_text_returns_body_and_queues_open() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -154,13 +154,23 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
scroll(lua, &this, ScrollingMessageState::page_down)
|
||||
})?,
|
||||
)?;
|
||||
// AtTop (`0x793130`) pushes 1 (`0x7931c3`) or nil (`0x7931d6`); AtBottom (`0x7931f0`) pushes 1
|
||||
// (`0x793283`) or nil (`0x793296`).
|
||||
m.set(
|
||||
"AtTop",
|
||||
lua.create_function(|lua, this: Table| with_smf(lua, &this, |smf| smf.at_top()))?,
|
||||
lua.create_function(|lua, this: Table| {
|
||||
with_smf(lua, &this, |smf| {
|
||||
crate::script::binding_abi::flag(smf.at_top())
|
||||
})
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"AtBottom",
|
||||
lua.create_function(|lua, this: Table| with_smf(lua, &this, |smf| smf.at_bottom()))?,
|
||||
lua.create_function(|lua, this: Table| {
|
||||
with_smf(lua, &this, |smf| {
|
||||
crate::script::binding_abi::flag(smf.at_bottom())
|
||||
})
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetMaxLines wipes the ring (`0x7938a0`).
|
||||
|
||||
@@ -302,7 +302,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `text, type, finished` of line `i`, for any row as above (`QuestLogFrame.lua:635`).
|
||||
// `text, type, finished` of line `i`, for any row as above (`QuestLogFrame.lua:635`). Three
|
||||
// values on every path (`mov eax,3` at `0x4e02ba`, `0x4e079b`, `0x4e07c8`): `finished` is the
|
||||
// number 1 (`0x4e02ae`, `0x4e078f`) or nil (`0x4e07bc`), and a row or line that does not exist
|
||||
// takes the tail at `0x4e07ac`, which pushes three nils.
|
||||
g.set(
|
||||
"GetQuestLogLeaderBoard",
|
||||
lua.create_function(|lua, (i, quest): (usize, Option<u32>)| {
|
||||
@@ -316,12 +319,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.cloned()
|
||||
};
|
||||
let Some(l) = line else {
|
||||
return Ok(MultiValue::from_vec(vec![Value::Nil]));
|
||||
return Ok(MultiValue::from_vec(vec![
|
||||
Value::Nil,
|
||||
Value::Nil,
|
||||
Value::Nil,
|
||||
]));
|
||||
};
|
||||
Ok(MultiValue::from_vec(vec![
|
||||
Value::String(lua.create_string(&l.text)?),
|
||||
Value::String(lua.create_string(&l.kind)?),
|
||||
Value::Boolean(l.finished),
|
||||
flag(l.finished),
|
||||
]))
|
||||
})?,
|
||||
)?;
|
||||
@@ -1042,15 +1049,38 @@ mod tests {
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local t, k, f = GetQuestLogLeaderBoard(1, 2)\n\
|
||||
return t == 'Kobold Worker slain: 10/10' and k == 'monster' and f == true"
|
||||
return t == 'Kobold Worker slain: 10/10' and k == 'monster' and f == 1"
|
||||
)
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local t, k, f = GetQuestLogLeaderBoard(1)\n\
|
||||
return t == 'Kobold Vermin slain: 3/10' and k == 'monster' and f == false"
|
||||
return t == 'Kobold Vermin slain: 3/10' and k == 'monster' and f == nil"
|
||||
)
|
||||
.unwrap());
|
||||
// Three values on every path; a row or line with nothing answers three nils (`0x4e07ac`).
|
||||
for call in [
|
||||
"GetQuestLogLeaderBoard(1)",
|
||||
"GetQuestLogLeaderBoard(1, 2)",
|
||||
"GetQuestLogLeaderBoard(2)",
|
||||
"GetQuestLogLeaderBoard(1, 9)",
|
||||
"GetQuestLogLeaderBoard(0)",
|
||||
] {
|
||||
assert_eq!(s.arity(call).unwrap(), 3, "{call}");
|
||||
}
|
||||
for call in [
|
||||
"GetQuestLogLeaderBoard(2)",
|
||||
"GetQuestLogLeaderBoard(1, 9)",
|
||||
"GetQuestLogLeaderBoard(0)",
|
||||
] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!(
|
||||
"local a, b, c = {call} return a == nil and b == nil and c == nil"
|
||||
))
|
||||
.unwrap(),
|
||||
"{call}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetQuestLogRewardMoney()").unwrap(),
|
||||
40
|
||||
|
||||
@@ -276,8 +276,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetAlphaGradient(start, length), a FontString's write-on reveal: answers whether `start` is
|
||||
// still inside the text in chars, and stock `QuestFrame.lua:558` advances until it is not.
|
||||
// SetAlphaGradient(start, length) (`0x79cc80`), a FontString's write-on reveal: answers 1
|
||||
// (`0x79cd58`) or nil (`0x79cd6d`) for whether `start` is still inside the text in chars, and
|
||||
// stock `QuestFrame.lua:558` advances until it is not.
|
||||
m.set(
|
||||
"SetAlphaGradient",
|
||||
lua.create_function(|lua, (this, start, length): (Table, f32, f32)| {
|
||||
@@ -286,7 +287,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.alpha_gradient = Some((start, length));
|
||||
let chars = data.text.as_deref().map_or(0, |t| t.chars().count());
|
||||
Ok(start < chars as f32)
|
||||
Ok(crate::script::binding_abi::flag(start < chars as f32))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -582,7 +582,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, (id, book_type): (Value, Value)| {
|
||||
let (id, book_type) = spell_slot_args(id, book_type, "PickupSpell")?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
Ok(pickup_spell(&mut model, id, &book_type))
|
||||
pickup_spell(&mut model, id, &book_type);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -655,7 +656,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PetBookState, SpellBookState, SpellSlotView, SpellTabView};
|
||||
use super::{pickup_spell, PetBookState, SpellBookState, SpellSlotView, SpellTabView};
|
||||
use crate::script::cursor::{CursorAction, CursorPayload};
|
||||
use crate::script::UiScript;
|
||||
|
||||
@@ -912,8 +913,7 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_spellbook(book());
|
||||
|
||||
s.run(r#"picked = PickupSpell(1, BOOKTYPE_SPELL)"#).unwrap();
|
||||
assert!(s.eval::<bool>("return picked").unwrap());
|
||||
s.run(r#"PickupSpell(1, BOOKTYPE_SPELL)"#).unwrap();
|
||||
assert!(s.cursor_payload().is_some());
|
||||
assert!(
|
||||
matches!(
|
||||
@@ -967,9 +967,7 @@ mod tests {
|
||||
texture: None,
|
||||
}));
|
||||
|
||||
assert!(!s
|
||||
.eval::<bool>(r#"return PickupSpell(1, BOOKTYPE_SPELL)"#)
|
||||
.unwrap());
|
||||
assert!(!pickup_spell(&mut s.model_mut(), 1, "spell"));
|
||||
// The original (action) payload survives untouched.
|
||||
assert!(
|
||||
matches!(
|
||||
@@ -1093,9 +1091,7 @@ mod tests {
|
||||
assert!(s.take_spell_casts().is_empty(), "pet cast is a no-op");
|
||||
assert!(s.take_pet_spell_casts().is_empty());
|
||||
|
||||
assert!(!s
|
||||
.eval::<bool>(r#"return PickupSpell(1, BOOKTYPE_PET)"#)
|
||||
.unwrap());
|
||||
assert!(!pickup_spell(&mut s.model_mut(), 1, "pet"));
|
||||
assert!(s.cursor_payload().is_none(), "pet pickup is a no-op");
|
||||
}
|
||||
|
||||
@@ -1283,9 +1279,7 @@ mod tests {
|
||||
s.set_spellbook(book());
|
||||
s.set_pet_book(pet_book());
|
||||
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return PickupSpell(1, BOOKTYPE_PET)"#)
|
||||
.unwrap());
|
||||
assert!(pickup_spell(&mut s.model_mut(), 1, "pet"));
|
||||
let Some(CursorPayload::PetAction(p)) = s.cursor_payload() else {
|
||||
panic!(
|
||||
"expected a pet action payload, got {:?}",
|
||||
@@ -1297,9 +1291,7 @@ mod tests {
|
||||
|
||||
// The player book still produces a spell payload.
|
||||
s.run("ClearCursor()").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return PickupSpell(1, BOOKTYPE_SPELL)"#)
|
||||
.unwrap());
|
||||
assert!(pickup_spell(&mut s.model_mut(), 1, "spell"));
|
||||
assert!(matches!(s.cursor_payload(), Some(CursorPayload::Spell(_))));
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,23 @@ fn the_getters_answer_the_pushed_state_in_their_1_12_shapes() {
|
||||
s.eval::<(f64, f64)>("return GetGameTime()").unwrap(),
|
||||
(21.0, 7.0)
|
||||
);
|
||||
|
||||
// An ownerless zone: the type is answered and the faction name is the empty string
|
||||
// (`0x48d633`, pushed at `0x48d68f`), not nil.
|
||||
s.set_zone_texts(crate::script::ZoneTexts {
|
||||
pvp_type: Some("contested".into()),
|
||||
pvp_faction: Some(String::new()),
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(s.arity("GetZonePVPInfo()").unwrap(), 3);
|
||||
assert_eq!(
|
||||
s.eval::<String>(
|
||||
"local t, f, a = GetZonePVPInfo() \
|
||||
return t .. '|' .. type(f) .. ':' .. f .. '|' .. tostring(a)"
|
||||
)
|
||||
.unwrap(),
|
||||
"contested|string:|nil"
|
||||
);
|
||||
}
|
||||
|
||||
/// The reference's handler slot starts empty (`[0x8722cc]` = -1 at `0x7039e0`), so
|
||||
|
||||
@@ -52,6 +52,7 @@ mod name_targets;
|
||||
mod nameplate;
|
||||
mod numeric_shape_gate;
|
||||
mod object_model;
|
||||
mod one_or_nil_methods;
|
||||
mod reference_surface;
|
||||
mod region_map;
|
||||
mod regions;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Widget methods that push the number 1 or nil, never a Lua boolean, and the cursor, action and
|
||||
//! chat-colour verbs that push nothing at all. Counted through the implicit vararg table's `arg.n` (`select`
|
||||
//! is 5.1's, not a 1.12 global).
|
||||
|
||||
use super::common::script;
|
||||
|
||||
/// `ScrollingMessageFrame:AtTop` (`0x793130`) and `:AtBottom` (`0x7931f0`) test one flag and push
|
||||
/// 1 (`0x7931c3`, `0x793283`) or nil (`0x7931d6`, `0x793296`), one value each.
|
||||
#[test]
|
||||
fn scrolling_message_frame_at_top_and_bottom_answer_one_or_nil() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local function shape(...) return arg.n .. ':' .. type(arg[1]) .. ':' .. tostring(arg[1]) end
|
||||
local f = CreateFrame("ScrollingMessageFrame", "SMF")
|
||||
f:SetMaxLines(128)
|
||||
for i = 1, 40 do f:AddMessage("line " .. i) end
|
||||
assert(shape(f:AtBottom()) == "1:number:1", shape(f:AtBottom()))
|
||||
assert(shape(f:AtTop()) == "1:nil:nil", shape(f:AtTop()))
|
||||
f:ScrollToTop()
|
||||
assert(shape(f:AtTop()) == "1:number:1", shape(f:AtTop()))
|
||||
assert(shape(f:AtBottom()) == "1:nil:nil", shape(f:AtBottom()))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `GameTooltip:IsOwned` (`0x530fe0`) pushes 1 (`0x5310ad`) or nil (`0x5310c0`), one value.
|
||||
#[test]
|
||||
fn tooltip_is_owned_answers_one_or_nil() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
local function shape(...) return arg.n .. ':' .. type(arg[1]) .. ':' .. tostring(arg[1]) end
|
||||
local a = CreateFrame("Button", "A"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
local b = CreateFrame("Button", "B"); b:SetPoint("CENTER", 50, 0); b:SetWidth(10); b:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
assert(shape(tt:IsOwned(a)) == "1:number:1", shape(tt:IsOwned(a)))
|
||||
assert(shape(tt:IsOwned(b)) == "1:nil:nil", shape(tt:IsOwned(b)))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `FontString:SetAlphaGradient` (`0x79cc80`) pushes 1 (`0x79cd58`) while `start` is inside the
|
||||
/// text and nil (`0x79cd6d`) once it is past, one value.
|
||||
#[test]
|
||||
fn set_alpha_gradient_answers_one_or_nil() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local function shape(...) return arg.n .. ':' .. type(arg[1]) .. ':' .. tostring(arg[1]) end
|
||||
local f = CreateFrame("Frame", "F")
|
||||
local fs = f:CreateFontString("FS")
|
||||
fs:SetText("hello")
|
||||
assert(shape(fs:SetAlphaGradient(0, 5)) == "1:number:1", shape(fs:SetAlphaGradient(0, 5)))
|
||||
assert(shape(fs:SetAlphaGradient(5, 5)) == "1:nil:nil", shape(fs:SetAlphaGradient(5, 5)))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The cursor, action and chat-colour verbs push no value: `xor eax,eax; ret` ends each handler. A bare
|
||||
/// `return F()` hands back zero values, not a boolean for whether anything moved.
|
||||
#[test]
|
||||
fn the_pickup_and_place_verbs_return_nothing() {
|
||||
let s = script();
|
||||
for call in [
|
||||
"PickupInventoryItem(1)",
|
||||
"EquipCursorItem(1)",
|
||||
"AutoEquipCursorItem()",
|
||||
"SplitContainerItem(0, 1, 1)",
|
||||
"PickupAction(1)",
|
||||
"PlaceAction(1)",
|
||||
"PickupPetAction(1)",
|
||||
"PickupSpell(1, BOOKTYPE_SPELL)",
|
||||
"PickupMacro(1)",
|
||||
"DeleteMacro(1)",
|
||||
"ResetChatColors()",
|
||||
] {
|
||||
assert_eq!(s.arity(call).unwrap(), 0, "{call}");
|
||||
}
|
||||
}
|
||||
@@ -758,3 +758,79 @@ fn item_bindings_return_the_pushed_repair_cost() {
|
||||
.unwrap();
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// `SetInventoryItem` (`0x532ee0`) and `SetBagItem` (`0x534620`) push `hasCooldown` as the number 1
|
||||
/// (`0x5332d3`, `0x534958`) or nil (`0x5332da`, `0x534966`), never a Lua boolean, and the counts
|
||||
/// stay 3 and 2.
|
||||
#[test]
|
||||
fn item_bindings_push_has_cooldown_as_one_or_nil() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
inv[16] = Some(InvSlotView {
|
||||
item_id: 8200,
|
||||
name: Some("Timed Blade".into()),
|
||||
quality: 2,
|
||||
duration_ms: Some(60_000),
|
||||
..Default::default()
|
||||
});
|
||||
inv[17] = Some(InvSlotView {
|
||||
item_id: 8200,
|
||||
name: Some("Timed Blade".into()),
|
||||
quality: 2,
|
||||
..Default::default()
|
||||
});
|
||||
s.set_inventory_slots(inv);
|
||||
let slot = |cooldown, duration_ms| ContainerSlot {
|
||||
item_id: 8200,
|
||||
count: 1,
|
||||
quality: Some(2),
|
||||
link: Some("|cff1eff00|Hitem:8200:0:0:0|h[Timed Blade]|h|r".into()),
|
||||
cooldown,
|
||||
duration_ms,
|
||||
..Default::default()
|
||||
};
|
||||
s.set_container(
|
||||
0,
|
||||
Some(ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 24,
|
||||
slots: HashMap::from([
|
||||
(1, slot(Some((1000, 30_000, true)), None)),
|
||||
(2, slot(None, None)),
|
||||
(3, slot(None, Some(60_000))),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
s.set_item_template(
|
||||
8200,
|
||||
ItemTemplateView {
|
||||
name: "Timed Blade".into(),
|
||||
quality: 2,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0)
|
||||
a:SetWidth(10); a:SetHeight(10)
|
||||
local tt = CreateFrame("GameTooltip", "TT")
|
||||
tt:SetOwner(a, "ANCHOR_RIGHT")
|
||||
-- `arg.n` counts the returns (`select` is 5.1's, not a 1.12 global).
|
||||
local function r(...) return arg.n, arg[2] end
|
||||
local function b(...) return arg.n, arg[1] end
|
||||
local n, cd = r(tt:SetInventoryItem("player", 16))
|
||||
assert(n == 3 and cd == 1 and type(cd) == "number", "timed item: " .. tostring(cd))
|
||||
n, cd = r(tt:SetInventoryItem("player", 17))
|
||||
assert(n == 3 and cd == nil, "plain item: " .. tostring(cd))
|
||||
n, cd = b(tt:SetBagItem(0, 1))
|
||||
assert(n == 2 and cd == 1 and type(cd) == "number", "running cooldown: " .. tostring(cd))
|
||||
n, cd = b(tt:SetBagItem(0, 2))
|
||||
assert(n == 2 and cd == nil, "cold item is nil, not false: " .. tostring(cd))
|
||||
n, cd = b(tt:SetBagItem(0, 3))
|
||||
assert(n == 2 and cd == 1, "timed item: " .. tostring(cd))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
@@ -87,14 +87,15 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(anchor.name())
|
||||
})?,
|
||||
)?;
|
||||
// IsOwned(frame): the hover loop's OnUpdate gate (`ContainerFrame.lua:657`); the owner drops
|
||||
// on hide, so a stale OnUpdate never brings a hidden tooltip back.
|
||||
// IsOwned(frame) (`0x530fe0`): 1 (`0x5310ad`) or nil (`0x5310c0`), the hover loop's OnUpdate
|
||||
// gate (`ContainerFrame.lua:657`); the owner drops on hide, so a stale OnUpdate never brings a
|
||||
// hidden tooltip back.
|
||||
m.set(
|
||||
"IsOwned",
|
||||
lua.create_function(|lua, (this, frame): (Table, Table)| {
|
||||
let owner = with_tip(lua, &this, |t| t.owner)?;
|
||||
let fh = frame_handle_of(lua, &frame)?;
|
||||
Ok(owner == Some(fh))
|
||||
Ok(crate::script::binding_abi::flag(owner == Some(fh)))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
use super::binding_abi::flag;
|
||||
use super::object::frame_handle_of;
|
||||
use super::tooltip::{append_line, clear, clear_silent, show_or_hide_empty};
|
||||
use super::{ItemTemplateView, Model};
|
||||
@@ -409,11 +410,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// hasCooldown is the builder's `[ebp-0x38]`, set by four lines:
|
||||
// LOCKED_WITH_ITEM, the temporary-enchant countdown, the item's duration
|
||||
// (`0x52ce0d`) and ITEM_COOLDOWN_TIME. Only the duration is answered here.
|
||||
if inst.duration_ms.is_some() {
|
||||
Value::Boolean(true)
|
||||
} else {
|
||||
Value::Nil
|
||||
},
|
||||
flag(inst.duration_ms.is_some()),
|
||||
Value::Integer(i64::from(repair_cost)),
|
||||
]))
|
||||
},
|
||||
@@ -505,7 +502,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
show_or_hide_empty(lua, h);
|
||||
Ok(MultiValue::from_vec(vec![
|
||||
// The builder's `[ebp-0x38]`: a running cooldown or the item's duration line.
|
||||
Value::Boolean(has_cd || inst.duration_ms.is_some()),
|
||||
flag(has_cd || inst.duration_ms.is_some()),
|
||||
Value::Integer(i64::from(repair_cost)),
|
||||
]))
|
||||
})?,
|
||||
|
||||
@@ -111,7 +111,8 @@ pub struct TrainerService {
|
||||
/// The wire spell's rank text, such as "Rank 2".
|
||||
pub subtext: Option<String>,
|
||||
pub texture: Option<String>,
|
||||
pub description: String,
|
||||
/// The expanded text; `None` answers nil (`0x4d9d90`).
|
||||
pub description: Option<String>,
|
||||
/// In copper, already reputation-discounted by the server.
|
||||
pub cost: u32,
|
||||
/// A primary profession's first rank: the stock window's `cpCost2 > 0`, which asks to confirm.
|
||||
@@ -510,10 +511,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"GetTrainerServiceDescription",
|
||||
lua.create_function(|lua, index: usize| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let text = service(&model, index)
|
||||
.map(|s| s.description.clone())
|
||||
.unwrap_or_default();
|
||||
lua.create_string(&text)
|
||||
opt_str(
|
||||
lua,
|
||||
service(&model, index).and_then(|s| s.description.as_ref()),
|
||||
)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ fn svc(
|
||||
name: Some(name.into()),
|
||||
subtext: Some("Rank 1".into()),
|
||||
texture: Some(format!("Interface\\Icons\\Spell_{spell_id}")),
|
||||
description: format!("Teaches {name}."),
|
||||
description: Some(format!("Teaches {name}.")),
|
||||
cost: 100,
|
||||
prof_first_rank: false,
|
||||
category: cat,
|
||||
@@ -107,6 +107,31 @@ fn tree_interleaves_headers_and_ordered_services() {
|
||||
assert_eq!(info(&mut s, 5), ("Bloodrage".into(), "unavailable".into()));
|
||||
}
|
||||
|
||||
/// `GetTrainerServiceDescription` (`0x4d9b40`) pushes one value, nil (`0x4d9d90`) where there is
|
||||
/// no text to show: no row at the index (`0x4d9ba8` to `0x4d9d93`), a service with no spell record
|
||||
/// (`0x4d9bd7`), or a service whose spell has none (`0x4d9d44`, `0x4d9d49`). A header row has no service either.
|
||||
#[test]
|
||||
fn service_description_is_nil_without_text() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let mut t = trainer();
|
||||
t.services[1].description = None;
|
||||
s.set_trainer(Some(t));
|
||||
|
||||
// `arg.n` counts the returns (`select` is 5.1's, not a 1.12 global).
|
||||
let shape = |s: &mut UiScript, i: i64| {
|
||||
s.eval::<String>(&format!(
|
||||
"local function c(...) return arg.n .. ':' .. type(arg[1]) end \
|
||||
return c(GetTrainerServiceDescription({i}))"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(shape(&mut s, 2), "1:string");
|
||||
assert_eq!(shape(&mut s, 3), "1:nil", "a service with no text");
|
||||
assert_eq!(shape(&mut s, 4), "1:nil", "a header row");
|
||||
assert_eq!(shape(&mut s, 99), "1:nil", "past the last row");
|
||||
assert_eq!(shape(&mut s, 0), "1:nil", "before the first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_getters_read_the_row_at_a_visible_index() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -21,8 +21,8 @@ pub struct ZoneTexts {
|
||||
/// `GetZonePVPInfo`'s type, `"friendly"`, `"hostile"` or `"contested"`; `None` when the
|
||||
/// player, the zone or its faction template is missing, or on a PvE realm outside a capital.
|
||||
pub pvp_type: Option<String>,
|
||||
/// `GetZonePVPInfo`'s faction, `FactionGroup.dbc`'s name for the zone's mask bit; `None` for
|
||||
/// none.
|
||||
/// `GetZonePVPInfo`'s faction, `FactionGroup.dbc`'s name for the zone's mask bit, `""` when no
|
||||
/// row names one (`0x48d633`); `None` only where `pvp_type` is `None` (`0x48d6b5`).
|
||||
pub pvp_faction: Option<String>,
|
||||
/// `GetZonePVPInfo`'s isArena: the leaf area's flag `0x80`, the free-for-all pit.
|
||||
pub is_arena: bool,
|
||||
|
||||
Reference in New Issue
Block a user