Files
benilla-octo/crates/benilla-ui/src/script/calls.rs
T
Sam SungandClaude Opus 5.5 f768cf3e60 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>
2026-09-29 21:11:47 +07:00

100 lines
4.5 KiB
Rust

//! The script calls whose effects touch the selection, the player's cast or the targeting cursor,
//! held in one queue in the order the script made them. In the reference each such call is done
//! when it returns: the macro runner `0x4f14e0` fires every line in one pass, `TargetUnit`
//! (`0x4899d0`), `TargetByName` (`0x489d60`) and `ClearTarget` (`0x489ff0`) commit through
//! `SetSelection 0x493540` before returning, and `CastSpellByName` (`0x4b4ab0`) casts at the
//! selection as it stands then. So `/target Bob` then `/cast Flash Heal` heals Bob, and the app
//! applies this queue front to back at one point in the frame.
use super::{ActionUse, SelectionRequest, UiScript};
/// One queued call, in the reference's terms.
#[derive(Clone, Debug, PartialEq)]
pub enum ScriptCall {
/// `TargetUnit`, `AssistUnit`, `AssistByName` and `TargetLastEnemy`.
Select(SelectionRequest),
/// `TargetByName(name, exactMatch)` (`0x489d60`), the stock `/target`.
TargetByName { name: String, exact: bool },
/// `TargetNearestEnemy`, `TargetNearestFriend`, `TargetNearestPartyMember` and
/// `TargetNearestRaidMember` (`0x489a80`/`0x489aa0`/`0x489ac0`/`0x489ae0`): one TAB cycle,
/// `0x493f60(reverse, mode)`.
TargetNearest { mode: NearestMode, reverse: bool },
/// `TargetLastTarget()` (`0x489b00`): re-select the last-target pair, or deselect when it is
/// empty.
TargetLastTarget,
/// `ClearTarget()` (`0x489ff0` → `0x493540(0,0)`), a no-op when nothing is selected by then.
ClearTarget,
/// `AttackTarget()` (`0x489b50` → `0x6131a0(0,0)`): the attack validator's pick from the
/// selection as it stands then, which may move it to the nearest enemy, then the toggle.
AttackTarget,
/// `SpellTargetUnit(unit)` (`0x6e6d90`), made while the cursor was up.
SpellTargetUnit(String),
/// `SpellStopTargeting()` (`0x6e6e30`), made while the cursor was up.
SpellStopTargeting,
/// `SpellStopCasting()` (`0x6e6e80`), made with something to stop.
SpellStopCasting,
/// `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.
UseAction(ActionUse),
/// `UseContainerItem(bag, slot)` outside repair mode.
UseContainerItem { bag: i64, slot: u32 },
/// `UseInventoryItem(id)` outside repair mode.
UseInventoryItem(u32),
/// `CastPetAction(slot)`, a 1-based pet bar slot.
PetAction(u32),
/// `PetAttack` and the other one-shot orders, as their bar slot's packed word.
PetOrder(u32),
}
/// The mode the four `TargetNearest*` shims hand the TAB cycler `0x493f60`, the only byte that
/// differs between them; it reaches only the per-candidate filter `0x493e40`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NearestMode {
/// Mode 1, `TargetNearestEnemy`.
Enemy,
/// Mode 2, `TargetNearestFriend`.
Friend,
/// Mode 3, `TargetNearestPartyMember`: a party member other than us.
PartyMember,
/// Mode 4, `TargetNearestRaidMember`: a party or raid member other than us.
RaidMember,
}
impl UiScript {
/// Take every queued call, in call order.
pub fn take_script_calls(&mut self) -> Vec<ScriptCall> {
std::mem::take(&mut self.model_mut().script_calls)
}
/// Put calls back at the front of the queue, ahead of any queued since.
pub fn requeue_script_calls(&mut self, calls: Vec<ScriptCall>) {
let mut model = self.model_mut();
let later = std::mem::replace(&mut model.script_calls, calls);
model.script_calls.extend(later);
}
/// Take the queued calls `pick` answers for, in call order, leaving the rest queued.
pub(crate) fn take_calls_where<T>(
&mut self,
mut pick: impl FnMut(&ScriptCall) -> Option<T>,
) -> Vec<T> {
let mut model = self.model_mut();
let mut taken = Vec::new();
model.script_calls.retain(|call| match pick(call) {
Some(t) => {
taken.push(t);
false
}
None => true,
});
taken
}
}