CastSpellByName's onSelf and CastSpell's third argument cast the spell at the player, the pet book's at the player too, read through GetBoolOrDefault as the reference does (#634)
`CastSpellByName(name, onSelf)` dropped its second argument, and `CastSpell(slot, book, onSelf)` read only two. So a self-cast macro line such as `CastSpellByName("Power Word: Fortitude", 1)` buffed the selected friend instead of you. With no friendly target and `autoSelfCast` off, it did not cast at all.
In the reference, `CastSpellByName` (`0x4b4ab0`) reads its second argument through `GetBoolOrDefault(L, 2, 0)` (`0x4b4aea`-`0x4b4af1`). `CastSpell` (`0x4b42f0`) reads its third the same way (`0x4b4333`-`0x4b433c`). When the value is nonzero, the active player's guid replaces the selection as the guid handed to the dispatcher `0x4b3300` (`0x4b4afa`, `0x4b4345`). The player's book then binds it through `ArmCast`'s explicit-guid leg (`0x6e5393`). The pet book sends it as `CMSG_PET_ACTION`'s target (`0x4b34ce`).
- `ScriptCall::CastSpell` and `CastPetSpell` now carry `on_self`, read with `binding_abi::bool_or_default`. That is not Lua truthiness: `"0"`, `"false"` and `0.5` are false; `"yes"`, `"on"` and `1` are true.
- The action bar's self-bind swap is now `CastContext::self_bound`, which `UseAction`'s `onSelf` and the two script casts share. `ui_spellbook::cast_spell` applies it and hands `on_self` to the attack pick.
- The pet book's `CMSG_PET_ACTION` aims at the player under `onSelf`.
Tests:
- `cast_spell_by_name_reads_on_self_as_the_reference_coerces_it` and `cast_spell_reads_on_self_from_its_third_argument` cover the coercion.
- `on_self_casts_at_the_player_and_leaves_the_selection`, `on_self_binds_the_player_where_the_selection_is_not_assistable` and `a_pet_book_cast_on_self_aims_at_the_player` check the wire target through `apply_script_calls`.
Fixes #446
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
666300c6a5
commit
f768cf3e60
@@ -960,8 +960,9 @@ pub(crate) enum ClientCommand {
|
||||
toggles: u8,
|
||||
},
|
||||
/// `CMSG_PET_ACTION`: `packed` is the slot's word as the server sent it, dispatched on its type
|
||||
/// byte; `target_guid` is our selection (`0x4bd212`). The server does not reply, so the
|
||||
/// caller applies the change locally first.
|
||||
/// byte; `target_guid` is our selection (`0x4bd212`), or the player under the pet book's
|
||||
/// `onSelf` (`0x4b4345`). The server does not reply, so the caller applies the change locally
|
||||
/// first.
|
||||
PetAction {
|
||||
pet_guid: u64,
|
||||
packed: u32,
|
||||
|
||||
@@ -49,8 +49,8 @@ pub(crate) fn apply_script_calls(
|
||||
ScriptCall::SpellTargetUnit(token) => appliers.p1().spell_target_unit(&token),
|
||||
ScriptCall::SpellStopTargeting => appliers.p1().stop_targeting(),
|
||||
ScriptCall::SpellStopCasting => appliers.p2().stop_casting(),
|
||||
ScriptCall::CastSpell(spell_id) => {
|
||||
crate::ui_spellbook::cast_spell(&mut appliers.p3(), spell_id);
|
||||
ScriptCall::CastSpell { spell_id, on_self } => {
|
||||
crate::ui_spellbook::cast_spell(&mut appliers.p3(), spell_id, on_self);
|
||||
}
|
||||
ScriptCall::CastShapeshiftForm(spell_id) => {
|
||||
crate::ui_shapeshift::cast_form(&mut appliers.p3(), spell_id);
|
||||
@@ -65,8 +65,8 @@ pub(crate) fn apply_script_calls(
|
||||
appliers.p5().use_container_item(script, bag, slot);
|
||||
}
|
||||
ScriptCall::UseInventoryItem(id) => appliers.p5().use_inventory_item(script, id),
|
||||
ScriptCall::CastPetSpell(spell_id) => {
|
||||
crate::ui_pet_book::cast_pet_spell(&mut appliers.p6(), spell_id);
|
||||
ScriptCall::CastPetSpell { spell_id, on_self } => {
|
||||
crate::ui_pet_book::cast_pet_spell(&mut appliers.p6(), spell_id, on_self);
|
||||
}
|
||||
ScriptCall::PetAction(slot) => appliers.p6().press_slot(slot),
|
||||
ScriptCall::PetOrder(packed) => appliers.p6().order(packed),
|
||||
@@ -123,7 +123,7 @@ mod tests {
|
||||
use benilla_protocol::messages::{
|
||||
ActionButton, GroupMemberEntry, ACTION_KIND_MACRO, ACTION_KIND_SPELL,
|
||||
};
|
||||
use benilla_ui::script::{SpellBookState, SpellSlotView, SpellTabView};
|
||||
use benilla_ui::script::{PetBookState, SpellBookState, SpellSlotView, SpellTabView};
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
@@ -352,6 +352,143 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `CastSpellByName(name, onSelf)` (`0x4b4ab0`) and `CastSpell(slot, book, onSelf)` (`0x4b42f0`)
|
||||
/// swap the target guid for the active player's (`0x4b4afa`, `0x4b4345`) when
|
||||
/// `GetBoolOrDefault` reads the flag true, so the cast goes to us whoever is selected, and the
|
||||
/// selection stays where it was. Without the flag it goes to the selection.
|
||||
#[test]
|
||||
fn on_self_casts_at_the_player_and_leaves_the_selection() {
|
||||
for (lua, target, selection) in [
|
||||
(r#"CastSpellByName("Heal", 1)"#, ME, OLD),
|
||||
(r#"CastSpellByName("Heal", "yes")"#, ME, OLD),
|
||||
(r#"CastSpell(1, "spell", 1)"#, ME, OLD),
|
||||
(
|
||||
r#"TargetUnit("party1") CastSpellByName("Heal", 1)"#,
|
||||
ME,
|
||||
NEW,
|
||||
),
|
||||
(r#"CastSpellByName("Heal")"#, OLD, OLD),
|
||||
(r#"CastSpellByName("Heal", 0.5)"#, OLD, OLD),
|
||||
(r#"CastSpellByName("Heal", "false")"#, OLD, OLD),
|
||||
(r#"CastSpell(1, "spell")"#, OLD, OLD),
|
||||
(r#"CastSpell(1, "spell", "0")"#, OLD, OLD),
|
||||
] {
|
||||
let mut f = frame(true);
|
||||
run(&mut f, lua);
|
||||
assert_eq!(casts(&f), vec![(HEAL, Some(target))], "{lua}");
|
||||
assert_eq!(selected(&f), Some(selection), "{lua}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Power Word: Fortitude's word, an assist unit (implicit target 21), in the catalog and
|
||||
/// the book.
|
||||
const FORTITUDE: u32 = 1243;
|
||||
|
||||
fn with_fortitude(f: &mut Frame) {
|
||||
let world = f.app.world_mut();
|
||||
let display = benilla_formats::SpellDisplay {
|
||||
implicit_target_a1: 21,
|
||||
..Default::default()
|
||||
};
|
||||
world.insert_resource(crate::ui_action::Spells {
|
||||
catalog: benilla_formats::SpellCatalog::from_displays([(FORTITUDE, display)].into()),
|
||||
..crate::ui_action::Spells::empty_for_tests()
|
||||
});
|
||||
world
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.set_spellbook(SpellBookState {
|
||||
tabs: vec![SpellTabView {
|
||||
name: "Priest".into(),
|
||||
texture: None,
|
||||
offset: 0,
|
||||
num_spells: 1,
|
||||
}],
|
||||
slots: vec![SpellSlotView {
|
||||
spell_id: FORTITUDE,
|
||||
name: "Fortitude".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
/// The bind runs the ordinary relation chain over the player: with a unit we cannot assist
|
||||
/// selected and `autoSelfCast` off, the plain call binds nothing (the cursor comes up), while
|
||||
/// `onSelf` binds us; with a friendly player selected the plain call binds that player.
|
||||
#[test]
|
||||
fn on_self_binds_the_player_where_the_selection_is_not_assistable() {
|
||||
for (selected_template, lua, target) in [
|
||||
(FOE, r#"CastSpellByName("Fortitude")"#, None),
|
||||
(FOE, r#"CastSpellByName("Fortitude", 1)"#, Some(ME)),
|
||||
(FOE, r#"CastSpell(1, "spell", 1)"#, Some(ME)),
|
||||
(FRIEND, r#"CastSpellByName("Fortitude")"#, Some(OLD)),
|
||||
(FRIEND, r#"CastSpellByName("Fortitude", 1)"#, Some(ME)),
|
||||
] {
|
||||
let mut f = frame(true);
|
||||
// `CanAssist` passes a friendly player-controlled unit (`UNIT_FLAG_PLAYER_CONTROLLED`).
|
||||
with_factions(&mut f, selected_template, &[(FLAGS, 0x8)]);
|
||||
with_fortitude(&mut f);
|
||||
run(&mut f, lua);
|
||||
let sent: Vec<_> = target.into_iter().map(|t| (FORTITUDE, Some(t))).collect();
|
||||
assert_eq!(casts(&f), sent, "{lua} at template {selected_template}");
|
||||
assert_eq!(selected(&f), Some(OLD), "{lua}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The pet's guid, and the pet book's Growl (`CMSG_PET_ACTION`'s type-1 word, `0x4b34ce`).
|
||||
const PET: u64 = 0x50;
|
||||
const GROWL: u32 = 2649;
|
||||
|
||||
/// The `CMSG_PET_ACTION`s sent, as `(pet, word, target)`.
|
||||
fn pet_actions(f: &Frame) -> Vec<(u64, u32, u64)> {
|
||||
f.rx.try_iter()
|
||||
.filter_map(|c| match c {
|
||||
ClientCommand::PetAction {
|
||||
pet_guid,
|
||||
packed,
|
||||
target_guid,
|
||||
} => Some((pet_guid, packed, target_guid)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `0x4b3300`'s pet fork sends the guid it was handed as the order's target (`0x4b34ce`), so
|
||||
/// `CastSpell(slot, "pet", 1)` aims the pet's spell at the player, not the selection.
|
||||
#[test]
|
||||
fn a_pet_book_cast_on_self_aims_at_the_player() {
|
||||
for (lua, target) in [
|
||||
(r#"CastSpell(1, "pet", 1)"#, ME),
|
||||
(r#"CastSpell(1, "pet", "on")"#, ME),
|
||||
(r#"CastSpell(1, "pet")"#, OLD),
|
||||
(r#"CastSpell(1, "pet", "0")"#, OLD),
|
||||
] {
|
||||
let mut f = frame(true);
|
||||
f.app
|
||||
.world_mut()
|
||||
.resource_mut::<crate::ui_pet::PetBar>()
|
||||
.spells
|
||||
.pet_guid = PET;
|
||||
f.app
|
||||
.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.set_pet_book(PetBookState {
|
||||
token: Some("PET".into()),
|
||||
slots: vec![SpellSlotView {
|
||||
spell_id: GROWL,
|
||||
name: "Growl".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
});
|
||||
run(&mut f, lua);
|
||||
assert_eq!(
|
||||
pet_actions(&f),
|
||||
vec![(PET, 0x0100_0000 | GROWL, target)],
|
||||
"{lua}"
|
||||
);
|
||||
assert_eq!(selected(&f), Some(OLD), "{lua}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `/target` then `/cast`: `TargetUnit` commits through `SetSelection 0x493540` before it
|
||||
/// returns, and `CastSpellByName` (`0x4b4ab0`) casts at the selection as it stands then.
|
||||
#[test]
|
||||
|
||||
@@ -131,6 +131,20 @@ pub(crate) struct CastContext<'a> {
|
||||
pub(crate) self_move_flags: u32,
|
||||
}
|
||||
|
||||
impl CastContext<'_> {
|
||||
/// Applies the self-cast modifier, `onSelf`: the caster replaces the selection as the bind
|
||||
/// candidate, guid and store both, the pair the autoSelfCast fallback swaps (`0x6e53d7`). It
|
||||
/// is the explicit guid `CastSpell` and `CastSpellByName` hand `ArmCast 0x6e5250` (`0x4b4345`,
|
||||
/// `0x4b4afa`, bound at `0x6e5393`); where `UseAction 0x4e5ee0` applies its own is untraced.
|
||||
pub(crate) fn self_bound(mut self, on_self: bool) -> Self {
|
||||
if on_self {
|
||||
self.selection_guid = self.self_guid;
|
||||
self.rel.target_store = self.rel.self_store;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Positions and combat reaches for the pre-send range refusal: the client's cast runs
|
||||
/// `CanTargetUnit 0x6e4440` and `IsTargetInRange 0x6e47b0` before the commit, so an out-of-range
|
||||
/// press refuses locally and none of the commit tail runs.
|
||||
|
||||
@@ -177,21 +177,6 @@ pub(super) fn drain_go_openers(
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the self-cast modifier, `UseAction`'s `onSelf` (`SELFACTIONBUTTON1`-`12`): the caster
|
||||
/// replaces the selection as the bind candidate, guid and store both, the pair the autoSelfCast
|
||||
/// fallback swaps (`0x6e53d7`). Where `UseAction 0x4e5ee0` applies `onSelf` is untraced; this
|
||||
/// fills `ArmCast 0x6e5250`'s explicit guid (`0x6e5393`); a forced-self leg would look the same.
|
||||
fn self_bound<'a>(
|
||||
mut ctx: cast_target::CastContext<'a>,
|
||||
press: benilla_ui::script::ActionUse,
|
||||
) -> cast_target::CastContext<'a> {
|
||||
if press.on_self {
|
||||
ctx.selection_guid = ctx.self_guid;
|
||||
ctx.rel.target_store = ctx.rel.self_store;
|
||||
}
|
||||
ctx
|
||||
}
|
||||
|
||||
/// What a `UseAction` press reads and writes.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct ActionPress<'w, 's> {
|
||||
@@ -279,7 +264,7 @@ pub(crate) fn use_action(
|
||||
);
|
||||
ladder.send_spell(
|
||||
b.action,
|
||||
&self_bound(targeting.context(), press),
|
||||
&targeting.context().self_bound(press.on_self),
|
||||
press.on_self,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,14 @@ impl PetPress<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The guid the pet book's `CastSpell(id, "pet", onSelf)` aims at: the selection, or with
|
||||
/// `onSelf` the active player (`0x4b4345`). A zero guid falls back to the selection
|
||||
/// (`0x4b34af`), so a missing player leaves it.
|
||||
pub(crate) fn spell_target(&self, on_self: bool) -> u64 {
|
||||
let caster = if on_self { self.pet.self_guid.0 } else { None };
|
||||
caster.or(self.selection.guid).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `PetAttack` and the other one-shot orders, as their slot's packed word. A slot press and
|
||||
/// an order are one press: both reach the reference's one dispatcher (`0x4bd1d0`). The slot
|
||||
/// number only labels logs.
|
||||
|
||||
@@ -168,16 +168,17 @@ fn slot_view(
|
||||
}
|
||||
}
|
||||
|
||||
/// `CastSpell(id, "pet")`: cancel first, as on the bar (`0x4b33af`-`0x4b3461`), a spell whose
|
||||
/// aura is on the pet sending `CMSG_PET_CANCEL_AURA` (0x26B) instead of the order; else
|
||||
/// `CMSG_PET_ACTION` at the selection as the calls before it left it.
|
||||
pub(crate) fn cast_pet_spell(p: &mut crate::ui_pet::PetPress, spell_id: u32) {
|
||||
/// `CastSpell(id, "pet", onSelf)`: cancel first, as on the bar (`0x4b33af`-`0x4b3461`), a spell
|
||||
/// whose aura is on the pet sending `CMSG_PET_CANCEL_AURA` (0x26B) instead of the order; else
|
||||
/// `CMSG_PET_ACTION` at the selection as the calls before it left it, or with `onSelf` at the
|
||||
/// player.
|
||||
pub(crate) fn cast_pet_spell(p: &mut crate::ui_pet::PetPress, spell_id: u32, on_self: bool) {
|
||||
let pet_guid = p.bar.spells.pet_guid;
|
||||
if pet_guid == 0 {
|
||||
debug!("ui_pet_book: dropping a queued pet book cast — the bar is gone");
|
||||
return;
|
||||
}
|
||||
let target_guid = p.selection.guid.unwrap_or(0);
|
||||
let target_guid = p.spell_target(on_self);
|
||||
let display = p.spells.as_ref().and_then(|s| s.catalog.get(spell_id));
|
||||
if let (Some(d), Some(store)) = (display, p.pet.store(pet_guid)) {
|
||||
if crate::ui_action::toggle::active_action_toggle(spell_id, d, store) {
|
||||
|
||||
@@ -195,7 +195,7 @@ fn shipped_spellbook_drives_end_to_end() {
|
||||
s.mouse_button(x1, y1, "LeftButton", true);
|
||||
s.mouse_button(x1, y1, "LeftButton", false);
|
||||
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
|
||||
assert_eq!(s.take_spell_casts(), vec![133]);
|
||||
assert_eq!(s.take_spell_casts(), vec![(133, false)]);
|
||||
assert!(s.cursor_payload().is_none(), "a cast never picks up");
|
||||
|
||||
s.set_modifiers(true, false, false);
|
||||
@@ -536,7 +536,7 @@ fn the_pet_tab_switches_books_and_renders_the_pets_spells() {
|
||||
// ── The clicks ────────────────────────────────────────────────────────────────────────────
|
||||
click(&mut s, "SpellButton1", "LeftButton");
|
||||
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
|
||||
assert_eq!(s.take_pet_spell_casts(), vec![2649]);
|
||||
assert_eq!(s.take_pet_spell_casts(), vec![(2649, false)]);
|
||||
assert!(s.take_spell_casts().is_empty());
|
||||
assert!(s.take_pet_spell_autocasts().is_empty());
|
||||
|
||||
@@ -710,7 +710,11 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
|
||||
"right-click errors: {:?}",
|
||||
s.errors()
|
||||
);
|
||||
assert_eq!(s.take_spell_casts(), vec![133], "a right-click casts");
|
||||
assert_eq!(
|
||||
s.take_spell_casts(),
|
||||
vec![(133, false)],
|
||||
"a right-click casts"
|
||||
);
|
||||
assert_eq!(
|
||||
body(&s),
|
||||
"",
|
||||
@@ -719,7 +723,7 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
|
||||
assert!(s.cursor_payload().is_none());
|
||||
|
||||
click(&mut s, "SpellButton1", "LeftButton");
|
||||
assert_eq!(s.take_spell_casts(), vec![133]);
|
||||
assert_eq!(s.take_spell_casts(), vec![(133, false)]);
|
||||
assert_eq!(body(&s), "");
|
||||
|
||||
// ── Shift-click appends a whole `/cast` line and never casts ──────────────────────────────
|
||||
|
||||
@@ -420,9 +420,10 @@ fn leading_number(s: &str) -> u32 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// `CastSpell(id, "spell")` and `CastSpellByName` (`0x4b3300`'s player leg), through the cast
|
||||
/// tail the action bar uses, at the selection as the calls before it left it.
|
||||
pub(crate) fn cast_spell(cast: &mut crate::spell::ScriptCast, spell_id: u32) {
|
||||
/// `CastSpell(id, "spell", onSelf)` and `CastSpellByName(name, onSelf)` (`0x4b3300`'s player leg),
|
||||
/// through the cast tail the action bar uses, at the selection as the calls before it left it, or
|
||||
/// with `onSelf` at the caster.
|
||||
pub(crate) fn cast_spell(cast: &mut crate::spell::ScriptCast, spell_id: u32, on_self: bool) {
|
||||
let crate::spell::ScriptCast { targeting, ladder } = cast;
|
||||
// `CastSpell`'s two cancel forks (`0x4b3300`), in order: the active-action toggle
|
||||
// (`0x4b36f0`), then the form match (`0x4b348b`), which `UseAction` lacks, with its silent
|
||||
@@ -457,10 +458,11 @@ pub(crate) fn cast_spell(cast: &mut crate::spell::ScriptCast, spell_id: u32) {
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"ui_spellbook: cast {spell_id} (target {:?})",
|
||||
targeting.selection.guid
|
||||
"ui_spellbook: cast {spell_id} (target {:?}{})",
|
||||
targeting.selection.guid,
|
||||
if on_self { ", on self" } else { "" }
|
||||
);
|
||||
ladder.send_spell(spell_id, &targeting.context(), false);
|
||||
ladder.send_spell(spell_id, &targeting.context().self_bound(on_self), on_self);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -33,10 +33,12 @@ pub enum ScriptCall {
|
||||
SpellStopTargeting,
|
||||
/// `SpellStopCasting()` (`0x6e6e80`), made with something to stop.
|
||||
SpellStopCasting,
|
||||
/// `CastSpell(id, "spell")` and `CastSpellByName`, through `0x4b3300`'s player leg.
|
||||
CastSpell(u32),
|
||||
/// `CastSpell(id, "pet")`, `0x4b3300`'s `CMSG_PET_ACTION` fork (`0x4b34ce`).
|
||||
CastPetSpell(u32),
|
||||
/// `CastSpell(id, "spell", onSelf)` and `CastSpellByName(name, onSelf)`, through `0x4b3300`'s
|
||||
/// player leg; `onSelf` swaps the target guid for the active player's (`0x4b4afa`, `0x4b4345`).
|
||||
CastSpell { spell_id: u32, on_self: bool },
|
||||
/// `CastSpell(id, "pet", onSelf)`, `0x4b3300`'s `CMSG_PET_ACTION` fork (`0x4b34ce`), with the
|
||||
/// same guid swap.
|
||||
CastPetSpell { spell_id: u32, on_self: bool },
|
||||
/// `CastShapeshiftForm(index)` (`0x4b4810`), as the form's spell.
|
||||
CastShapeshiftForm(u32),
|
||||
/// `UseAction(slot, …)` (`0x4e7140` → `0x4e5ee0`); a macro slot runs its lines inside it.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
use super::binding_abi::flag;
|
||||
use super::binding_abi::{bool_or_default, flag};
|
||||
use super::cursor::{queue_cursor_update, CursorPayload, CursorSpell};
|
||||
use super::{Model, ScriptCall};
|
||||
|
||||
@@ -94,11 +94,11 @@ impl super::UiScript {
|
||||
self.model_mut().pet_book = state;
|
||||
}
|
||||
|
||||
/// Take the `CastSpell(id, "pet")` calls out of the call stream: each is a `CMSG_PET_ACTION`
|
||||
/// with a type-1 word (`0x4b34ce`), not a player cast.
|
||||
pub fn take_pet_spell_casts(&mut self) -> Vec<u32> {
|
||||
/// Take the `CastSpell(id, "pet")` calls out of the call stream as `(spell id, onSelf)`: each
|
||||
/// is a `CMSG_PET_ACTION` with a type-1 word (`0x4b34ce`), not a player cast.
|
||||
pub fn take_pet_spell_casts(&mut self) -> Vec<(u32, bool)> {
|
||||
self.take_calls_where(|c| match c {
|
||||
ScriptCall::CastPetSpell(id) => Some(*id),
|
||||
ScriptCall::CastPetSpell { spell_id, on_self } => Some((*spell_id, *on_self)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -115,10 +115,11 @@ impl super::UiScript {
|
||||
self.model_mut().spellbook.clone()
|
||||
}
|
||||
|
||||
/// Take the `CastSpell` and `CastSpellByName` calls out of the call stream.
|
||||
pub fn take_spell_casts(&mut self) -> Vec<u32> {
|
||||
/// Take the `CastSpell` and `CastSpellByName` calls out of the call stream as
|
||||
/// `(spell id, onSelf)`.
|
||||
pub fn take_spell_casts(&mut self) -> Vec<(u32, bool)> {
|
||||
self.take_calls_where(|c| match c {
|
||||
ScriptCall::CastSpell(id) => Some(*id),
|
||||
ScriptCall::CastSpell { spell_id, on_self } => Some((*spell_id, *on_self)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -474,28 +475,34 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// CastSpell(id, bookType), the plain click, queues the slot's spell unless it is passive. On
|
||||
// the pet book `0x4b3300` forks at `0x4b34c8` (the player's cast is `0x6e5a90`) to send
|
||||
// `CMSG_PET_ACTION` (0x175), `{ u64 [0xb714a0], u32 (spellId & 0xFFFF) | 0x01000000, u64
|
||||
// target }` (`0x4b34ce`), so the pet book casts a spell not on the bar; the target falls back
|
||||
// to the selection (`0x4b34af`), as in `CastPetAction`. The app sends both at the drain.
|
||||
// CastSpell(id, bookType [, onSelf]), the plain click, queues the slot's spell unless it is
|
||||
// passive. The third argument goes through `GetBoolOrDefault(L, 3, 0)` (`0x4b4333`-`0x4b433c`),
|
||||
// and a nonzero one swaps the target guid for the active player's (`0x4b4345`) before the
|
||||
// dispatcher `0x4b3300`. On the pet book that forks at `0x4b34c8` (the player's cast is
|
||||
// `0x6e5a90`) to send `CMSG_PET_ACTION` (0x175), `{ u64 [0xb714a0], u32 (spellId & 0xFFFF) |
|
||||
// 0x01000000, u64 target }` (`0x4b34ce`), so the pet book casts a spell not on the bar; the
|
||||
// target falls back to the selection (`0x4b34af`), as in `CastPetAction`. The app sends both at
|
||||
// the drain.
|
||||
g.set(
|
||||
"CastSpell",
|
||||
lua.create_function(|lua, (id, book_type): (Value, Value)| {
|
||||
let (id, book_type) = spell_slot_args(id, book_type, "CastSpell")?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Some(slot) = book_slot(&model, id, &book_type) {
|
||||
if !slot.passive {
|
||||
let spell_id = slot.spell_id;
|
||||
model.script_calls.push(if is_pet_book(&book_type) {
|
||||
ScriptCall::CastPetSpell(spell_id)
|
||||
} else {
|
||||
ScriptCall::CastSpell(spell_id)
|
||||
});
|
||||
lua.create_function(
|
||||
|lua, (id, book_type, on_self): (Value, Value, Option<Value>)| {
|
||||
let (id, book_type) = spell_slot_args(id, book_type, "CastSpell")?;
|
||||
let on_self = bool_or_default(on_self.as_ref(), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Some(slot) = book_slot(&model, id, &book_type) {
|
||||
if !slot.passive {
|
||||
let spell_id = slot.spell_id;
|
||||
model.script_calls.push(if is_pet_book(&book_type) {
|
||||
ScriptCall::CastPetSpell { spell_id, on_self }
|
||||
} else {
|
||||
ScriptCall::CastSpell { spell_id, on_self }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// HasPetSpells() -> numPetSpells, petToken: always two returns (`0x4b4410`), `(nil, nil)` with
|
||||
@@ -552,15 +559,19 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// CastSpellByName(name [, onSelf]) (`0x4b4ab0`) shares the dispatcher `0x4b3300` with
|
||||
// `CastSpell`, so it queues the same call; `SlashCmdList["CAST"]` calls it. `onSelf` is
|
||||
// accepted and ignored: self-cast is not built.
|
||||
// `CastSpell`, so it queues the same call; `SlashCmdList["CAST"]` calls it. The target guid
|
||||
// defaults to the selection (`0x4b4adb`), and a nonzero `GetBoolOrDefault(L, 2, 0)`
|
||||
// (`0x4b4aea`-`0x4b4af1`) swaps it for the active player's (`0x4b4afa`).
|
||||
g.set(
|
||||
"CastSpellByName",
|
||||
lua.create_function(|lua, (name, _on_self): (String, MultiValue)| {
|
||||
lua.create_function(|lua, (name, on_self): (String, Option<Value>)| {
|
||||
let on_self = bool_or_default(on_self.as_ref(), false);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Some(slot) = resolve_spell_by_name(&model.spellbook, &name) {
|
||||
let spell_id = slot.spell_id;
|
||||
model.script_calls.push(ScriptCall::CastSpell(spell_id));
|
||||
model
|
||||
.script_calls
|
||||
.push(ScriptCall::CastSpell { spell_id, on_self });
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -968,7 +979,7 @@ mod tests {
|
||||
s.set_spellbook(book());
|
||||
|
||||
s.run(r#"CastSpell(1, BOOKTYPE_SPELL)"#).unwrap(); // Fireball: active
|
||||
assert_eq!(s.take_spell_casts(), vec![133]);
|
||||
assert_eq!(s.take_spell_casts(), vec![(133, false)]);
|
||||
|
||||
s.run(r#"CastSpell(2, BOOKTYPE_SPELL)"#).unwrap(); // Fire Blast: passive, refused
|
||||
assert!(s.take_spell_casts().is_empty());
|
||||
@@ -981,6 +992,75 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// `CastSpellByName`'s second argument goes through `GetBoolOrDefault(L, 2, 0)`
|
||||
/// (`0x4b4aea`-`0x4b4af1`), which is not Lua truthiness: `"0"`, `"false"` and `0.5` are false.
|
||||
#[test]
|
||||
fn cast_spell_by_name_reads_on_self_as_the_reference_coerces_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_spellbook(book());
|
||||
for (arg, on_self) in [
|
||||
("", false),
|
||||
(", nil", false),
|
||||
(", false", false),
|
||||
(", 0", false),
|
||||
(", 0.5", false),
|
||||
(r#", "0""#, false),
|
||||
(r#", "false""#, false),
|
||||
(r#", "off""#, false),
|
||||
(", 1", true),
|
||||
(", true", true),
|
||||
(", 1, 0", true),
|
||||
(r#", "1""#, true),
|
||||
(r#", "yes""#, true),
|
||||
(r#", "on""#, true),
|
||||
(r#", "enabled""#, true),
|
||||
] {
|
||||
s.run(&format!(r#"CastSpellByName("Fireball"{arg})"#))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_spell_casts(),
|
||||
vec![(133, on_self)],
|
||||
"CastSpellByName(\"Fireball\"{arg})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `CastSpell`'s third argument is `GetBoolOrDefault(L, 3, 0)` (`0x4b4333`-`0x4b433c`), on the
|
||||
/// pet book as well, and a passive is refused whatever it says.
|
||||
#[test]
|
||||
fn cast_spell_reads_on_self_from_its_third_argument() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_spellbook(book());
|
||||
s.set_pet_book(pet_book());
|
||||
for (arg, on_self) in [
|
||||
("", false),
|
||||
(", 0.5", false),
|
||||
(r#", "false""#, false),
|
||||
(", 1", true),
|
||||
(", 1, 0", true),
|
||||
(r#", "yes""#, true),
|
||||
(r#", "on""#, true),
|
||||
] {
|
||||
s.run(&format!("CastSpell(1, BOOKTYPE_SPELL{arg})"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_spell_casts(),
|
||||
vec![(133, on_self)],
|
||||
"CastSpell(1, BOOKTYPE_SPELL{arg})"
|
||||
);
|
||||
s.run(&format!("CastSpell(1, BOOKTYPE_PET{arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.take_pet_spell_casts(),
|
||||
vec![(2649, on_self)],
|
||||
"CastSpell(1, BOOKTYPE_PET{arg})"
|
||||
);
|
||||
}
|
||||
s.run("CastSpell(2, BOOKTYPE_SPELL, 1) CastSpell(3, BOOKTYPE_PET, 1)")
|
||||
.unwrap();
|
||||
assert!(s.take_spell_casts().is_empty(), "a passive is refused");
|
||||
assert!(s.take_pet_spell_casts().is_empty(), "a passive is refused");
|
||||
}
|
||||
|
||||
/// With no pet book, as when the reference's count `[0xb71174]` is 0, every pet arm answers
|
||||
/// nothing.
|
||||
#[test]
|
||||
@@ -1180,8 +1260,12 @@ mod tests {
|
||||
CastSpell(1, BOOKTYPE_SPELL)"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s.take_pet_spell_casts(), vec![2649], "the passive refused");
|
||||
assert_eq!(s.take_spell_casts(), vec![133]);
|
||||
assert_eq!(
|
||||
s.take_pet_spell_casts(),
|
||||
vec![(2649, false)],
|
||||
"the passive refused"
|
||||
);
|
||||
assert_eq!(s.take_spell_casts(), vec![(133, false)]);
|
||||
assert!(s.take_pet_spell_casts().is_empty(), "drain empties");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user