pet bar: 0x4bcf70 is one predicate, asked by GetPetActionsUsable, every press and order, Dismiss, both autocast toggles and the drag's writes, so an unusable pet greys the bar and does nothing (#687)
No pet-bar verb asked whether the pet's actions were usable. So a stunned, feared or confused pet, a pet out of view, a pet that is not yours, or a charmed player still did all of this: - pressed, latched Stay, Follow and the reactions, and swept the GCD; - sent `CMSG_PET_ACTION` and flipped autocast; - showed `GetPetActionsUsable()` as 1, which kept the stock bar in colour. `0x4bcf70` is the reference's one predicate. It returns 0 in any of these cases: - the active player does not resolve (`0x4bcf92`); - the player is charmed (`0x4bcf98`-`0x4bcfa4`); - the player's far-sight guid (`PLAYER_FARSIGHT`) is set and is not the pet (`0x4bcfbf`-`0x4bd012`); - the pet guid does not resolve to a unit (`0x4bd034`-`0x4bd03d`); - the pet's charmed-by guid, else its summoned-by guid, is not the player (`0x4bd03f`-`0x4bd067`); - the pet is stunned, confused or fleeing (`0x4bd06f`-`0x4bd08b`); - bar-state bit 27 is set (`0x4bd08d`). These callers ask it: - `GetPetActionsUsable` (`0x4be0b3`); - the dispatcher behind `CastPetAction`, the orders and Dismiss (`0x4bd1f2`, before its type switch); - `TogglePetAutocast` (`0x4bcbcf`); - the pet book's toggle (`0x4bccce`); - the bar write used by the drag's drop and the pickup's blank (`0x4bc9d0`). Changes: - `ui_pet/usable.rs` builds `0x4bcf70` once. The bar's feed pushes it as `GetPetActionsUsable`. `PetPress::press` asks it first, before the aura cancel. - Both autocast drains, Dismiss and the drag's two writes ask it too. A pickup still lifts the action onto the cursor, as the reference does. - The spell arm's later player and pet lookups, and the Attack arm's null branch, repeat the predicate's, so their dead handling goes. - `arm_pet_gcd`'s doc now names the three places the pet's GCD starts: the press (`0x4bd36e`), a pet-addressed `SMSG_SPELL_COOLDOWN` (`0x6e9598`) and the generic cast send (`0x6e58fb`). Tests: - `ui_pet/usable_tests.rs` tries each of 11 exits alone, and 5 usable lookalikes, against `GetPetActionsUsable`, a spell press, the orders, both autocast toggles, Dismiss and the drag. - A stock-UI test loads the default UI. A stunned pet's icon desaturates, and a click sends nothing until the stun ends. - The three press tests that pinned ungated sends are flipped. Fixes #672 Fixes #673 Fixes #674 🤖 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
8f6d337ad6
commit
7623cbae11
@@ -352,7 +352,8 @@ pub(crate) fn attack_actor_blocked(
|
||||
actor: Option<&ObjectStore>,
|
||||
self_guid: Option<u64>,
|
||||
) -> Option<&'static str> {
|
||||
// An unresolved actor skips the chain, as in the reference (`0x4bd403`).
|
||||
// An unresolved actor skips the chain, as in the reference (`0x4bd403`), though the pet bar's
|
||||
// own gate (`0x4bd1f2`) never lets an unresolved pet reach it.
|
||||
let fields = &actor?.0;
|
||||
// `0x605f30` (`0x612e10`): dead, or a player's ghost flag, since a ghost has health 1.
|
||||
let key = if fields.is_dead_or_ghost() {
|
||||
|
||||
@@ -11,18 +11,11 @@ use benilla_protocol::messages::{
|
||||
};
|
||||
use benilla_ui::script::{PetActionView, UiScript};
|
||||
|
||||
use crate::net::{GuidIndex, ObjectStore};
|
||||
use crate::net::ObjectStore;
|
||||
use crate::target::UNIT_FLAG_POSSESSED;
|
||||
use crate::ui_action::Spells;
|
||||
|
||||
use super::PetBar;
|
||||
|
||||
/// `GetPetActionsUsable()` (`0x4bcf70`): false while the state's bit 27 is set or the pet is
|
||||
/// stunned, confused or fleeing. Its other four steps test ownership, which a held bar implies.
|
||||
pub(super) fn actions_usable(bar: &PetBar, pet_flags: Option<u32>) -> bool {
|
||||
!bar.spells.bar_disabled()
|
||||
&& pet_flags.is_none_or(|f| f & benilla_protocol::messages::PET_UNUSABLE_UNIT_FLAGS == 0)
|
||||
}
|
||||
use super::{PetBar, PetUnit};
|
||||
|
||||
/// What the feed last pushed, so `PET_BAR_UPDATE` fires on a change; the key's leading `u32` is
|
||||
/// [`PetBar::bar_signals`], so a press that changes nothing still repaints.
|
||||
@@ -182,8 +175,7 @@ pub(super) fn feed_pet_bar(
|
||||
bar: Res<PetBar>,
|
||||
spells: Option<Res<Spells>>,
|
||||
clock: Res<crate::ui_script::UiClock>,
|
||||
index: Res<GuidIndex>,
|
||||
stores: Query<&ObjectStore>,
|
||||
pet: PetUnit,
|
||||
mut memory: Local<crate::ui_script::VmMemo<PetBarMemory>>,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
@@ -193,13 +185,10 @@ pub(super) fn feed_pet_bar(
|
||||
let now = Instant::now();
|
||||
let (anchor, ui_now) = (clock.anchor, clock.ui_now);
|
||||
let has_bar = bar.has_bar();
|
||||
// An unstreamed pet leaves usability to bit 27 alone, and no slot shows active.
|
||||
let pet_store = index
|
||||
.0
|
||||
.get(&bar.spells.pet_guid)
|
||||
.and_then(|&e| stores.get(e).ok());
|
||||
// An unstreamed pet is not usable (`0x4bd034`), and no slot shows active.
|
||||
let pet_store = pet.store(bar.spells.pet_guid);
|
||||
let pet_flags = pet_store.map(|s| s.0.unit_flags());
|
||||
let usable = actions_usable(&bar, pet_flags);
|
||||
let usable = pet.actions_usable(&bar);
|
||||
// `PickupPetAction`'s gate alone (`0x4be1c1`): a possessed unit's bar cannot be rearranged,
|
||||
// but its buttons work, so possession stays out of `usable`.
|
||||
let pickup_allowed = pet_flags.unwrap_or(0) & UNIT_FLAG_POSSESSED == 0;
|
||||
|
||||
@@ -62,7 +62,8 @@ impl PetPress<'_, '_> {
|
||||
/// Send under [`PetBar`]'s pet guid, never one from the VM, so a press queued as the pet
|
||||
/// leaves dies here. `CMSG_PET_ACTION` echoes the slot's word, which the server dispatches on
|
||||
/// its type, with our selection as the target; `HandlePetAction` drops a target the spell
|
||||
/// does not want.
|
||||
/// does not want. The dispatcher's first act is the usability predicate (`0x4bd1f2`), and a
|
||||
/// zero answer leaves for the epilogue (`0x4bd1f9`) ahead of everything below it.
|
||||
fn press(&mut self, slot: u32, entry: PetActionEntry) {
|
||||
let Self {
|
||||
bar,
|
||||
@@ -75,20 +76,19 @@ impl PetPress<'_, '_> {
|
||||
pick,
|
||||
seam,
|
||||
} = self;
|
||||
let pet_guid = bar.spells.pet_guid;
|
||||
if pet_guid == 0 {
|
||||
debug!("ui_pet: dropping a queued pet press — the bar is gone");
|
||||
let Some(pet_store) = pet.usable_pet(bar) else {
|
||||
debug!("ui_pet: slot {slot} refused, the pet's actions are not usable — no packet");
|
||||
return;
|
||||
}
|
||||
let pet_store = pet.store(pet_guid);
|
||||
let possessing = possessing(pet_store, pet.self_guid.0);
|
||||
};
|
||||
let pet_guid = bar.spells.pet_guid;
|
||||
let possessing = possessing(Some(pet_store), pet.self_guid.0);
|
||||
// A press on a spell the pet is running cancels its aura and sends no `CMSG_PET_ACTION`
|
||||
// (`0x4bd240`-`0x4bd2ad`); nothing latches, the icon follows the pet's aura field.
|
||||
let display = entry
|
||||
.is_spell()
|
||||
.then(|| spells.as_ref().and_then(|s| s.catalog.get(entry.action())))
|
||||
.flatten();
|
||||
if let Some(spell_id) = active_aura_press(entry, pet_store, display) {
|
||||
if let Some(spell_id) = active_aura_press(entry, Some(pet_store), display) {
|
||||
debug!("ui_pet: slot {slot} cancels its own aura (spell {spell_id}) — no PetAction");
|
||||
let _ = commands
|
||||
.0
|
||||
@@ -96,15 +96,15 @@ impl PetPress<'_, '_> {
|
||||
return;
|
||||
}
|
||||
// The spell arm leaves for the epilogue, before its GCD and its send, unless the slot's
|
||||
// `Spell.dbc` record, the active player and the pet's object all resolve (`0x4bd2e7`-
|
||||
// `0x4bd2fe`, `0x4bd31a`-`0x4bd324`, `0x4bd346`-`0x4bd34f`); an id past the table's
|
||||
// maximum has no record. Commands and reactions have no such exit (`0x4bd391`, `0x4bd3a3`).
|
||||
// `Spell.dbc` record resolves (`0x4bd2e7`-`0x4bd2fe`); an id past the table's maximum has
|
||||
// none. Its lookups of the active player and the pet (`0x4bd31a`-`0x4bd324`,
|
||||
// `0x4bd346`-`0x4bd34f`) repeat the predicate's, on the same guids and typemasks, and
|
||||
// cannot fail past it. Commands and reactions have no exit of their own (`0x4bd391`,
|
||||
// `0x4bd3a3`).
|
||||
let spell = if entry.is_spell() {
|
||||
let Some(spell) =
|
||||
display.filter(|_| pet_store.is_some() && pet.player_store().is_some())
|
||||
else {
|
||||
let Some(spell) = display else {
|
||||
debug!(
|
||||
"ui_pet: slot {slot} (spell {}) has no record, player or pet object — no packet",
|
||||
"ui_pet: slot {slot} (spell {}) has no record — no packet",
|
||||
entry.action()
|
||||
);
|
||||
return;
|
||||
@@ -117,7 +117,7 @@ impl PetPress<'_, '_> {
|
||||
// pick can move the selection; every other press sends the selection as is (`0x4bd212`).
|
||||
let mut target_guid = selection.guid.unwrap_or(0);
|
||||
let refused = if is_attack_order(entry) {
|
||||
crate::ui_action::attack_actor_refusal(pet_store, pet.self_guid.0, ui_errors)
|
||||
crate::ui_action::attack_actor_refusal(Some(pet_store), pet.self_guid.0, ui_errors)
|
||||
|| match pick.target(None, selection, seam, ui_errors) {
|
||||
Some(guid) => {
|
||||
target_guid = guid;
|
||||
@@ -155,14 +155,20 @@ impl PetPress<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The spell arm's tail (`0x4bd355`-`0x4bd36e`) for a press whose record, player and pet object
|
||||
/// resolved: one that neither cancels an aura nor takes the generic cast entry `0x6e4b60`
|
||||
/// (`AttributesEx4 & 0x20`, or the bar's unit possessed) calls `StartGlobalCooldown
|
||||
/// The spell arm's tail (`0x4bd355`-`0x4bd36e`) for a press past the usability gate whose
|
||||
/// `Spell.dbc` record resolved: one that neither cancels an aura nor takes the generic cast entry
|
||||
/// `0x6e4b60` (`AttributesEx4 & 0x20`, or the bar's unit possessed) calls `StartGlobalCooldown
|
||||
/// 0x6e2de0(spellId, 1)` before the send (`0x4bd444`): the spell's own `StartRecovery*` pair,
|
||||
/// under op 21, into the pet's list (`0xcecaec + 0x18`). The insert moves the list's generation,
|
||||
/// which carries the flush that follows it (`0x6e2e77`, `0x6e2e8e`) to
|
||||
/// `bar::fire_pet_cooldown_events`. This is the only place the pet's GCD starts: the
|
||||
/// `SMSG_SPELL_GO` pet leg inserts the spell's own timers and no GCD (`0x6e85f7`).
|
||||
/// `bar::fire_pet_cooldown_events`.
|
||||
///
|
||||
/// The pet's GCD starts in three places, and this arm is one. A `SMSG_SPELL_COOLDOWN` addressed
|
||||
/// to the pet's guid inserts the same pair for every entry whose spell lacks `Attributes` bit 25
|
||||
/// (`0x6e9553`-`0x6e9598`, `Cooldowns::apply_wire_cooldown`). The generic cast send starts it too
|
||||
/// (`0x6e58fb`), with the list picked by the caster's charmed-by, else summoned-by, guid
|
||||
/// (`0x6e58c2`-`0x6e58f3`); the possessed and client-targeted presses above reach it through
|
||||
/// `0x6e4b60`. The `SMSG_SPELL_GO` pet leg inserts the spell's own timers and no GCD (`0x6e85f7`).
|
||||
pub(super) fn arm_pet_gcd(
|
||||
bar: &mut PetBar,
|
||||
spell_id: u32,
|
||||
@@ -177,11 +183,14 @@ pub(super) fn arm_pet_gcd(
|
||||
}
|
||||
|
||||
/// Send the bar's other intents under [`PetBar`]'s pet guid: the autocast toggles, the stops and
|
||||
/// the drag writes.
|
||||
/// the drag writes. A toggle asks the usability predicate first (`0x4bcbcf`), so on an unusable
|
||||
/// pet it flips nothing and sends nothing. The stops do not (`0x4bd650`); the drag's writes were
|
||||
/// gated where the VM applied them (`0x4bc9d0`), on the answer the bar last pushed.
|
||||
pub(super) fn drain_pet_actions(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
mut bar: ResMut<PetBar>,
|
||||
commands: Res<NetCommands>,
|
||||
pet: PetUnit,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
@@ -201,6 +210,12 @@ pub(super) fn drain_pet_actions(
|
||||
// (`0x4bcbf7`: any right-click on a token) signals nothing, so after `OnClick`'s
|
||||
// `SetChecked(0)` that button stays unlit until the next repaint, as in the reference.
|
||||
for slot in toggles {
|
||||
if !pet.actions_usable(&bar) {
|
||||
debug!(
|
||||
"ui_pet: autocast toggle on slot {slot} refused, the pet's actions are not usable"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let Some(flipped) = toggle_slot_autocast(&mut bar, slot) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! `PetCanBeAbandoned()`, Rename when `PetCanBeRenamed()` too, and Dismiss otherwise. `PetAbandon`
|
||||
//! (`0x4be4c0`, `0x4bd740`) sends `CMSG_PET_ABANDON`; `PetDismiss` (`0x4be4d0`) sends no packet
|
||||
//! itself but hands the Dismiss command word to the bar's dispatcher (`0x4bd1d0`), so it leaves as
|
||||
//! `CMSG_PET_ACTION`. vmangos unsummons a summon on either, so sending the wrong one goes unseen.
|
||||
//! `CMSG_PET_ACTION`, and only past the dispatcher's usability predicate (`0x4bd1f2`). vmangos
|
||||
//! unsummons a summon on either, so sending the wrong one goes unseen.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
@@ -56,6 +57,7 @@ pub(super) fn drain_pet_menu(
|
||||
bar: Res<PetBar>,
|
||||
commands: Res<NetCommands>,
|
||||
mut ui_errors: ResMut<UiErrorKeys>,
|
||||
pet: PetUnit,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
@@ -76,9 +78,14 @@ pub(super) fn drain_pet_menu(
|
||||
debug!("ui_pet: abandoning pet {pet_guid:#x}");
|
||||
let _ = commands.0.send(ClientCommand::PetAbandon { pet_guid });
|
||||
}
|
||||
// `0x4bd6e0` stages `0x07000003` at target 0.
|
||||
// `0x4bd6e0` stages `0x07000003` at target 0 and enters the dispatcher, whose first act is the
|
||||
// usability predicate (`0x4bd1f2`): a pet that fails it is not sent the word.
|
||||
let dismiss_word = PET_COMMAND_DISMISS | (u32::from(PET_ACT_COMMAND) << 24);
|
||||
for _ in 0..dismisses {
|
||||
if !pet.actions_usable(&bar) {
|
||||
debug!("ui_pet: dismiss refused, the pet's actions are not usable — no packet");
|
||||
continue;
|
||||
}
|
||||
debug!("ui_pet: dismissing pet {pet_guid:#x} ({dismiss_word:#010x})");
|
||||
let _ = commands.0.send(ClientCommand::PetAction {
|
||||
pet_guid,
|
||||
|
||||
@@ -20,6 +20,7 @@ mod drain;
|
||||
mod menu;
|
||||
mod net;
|
||||
mod unit;
|
||||
mod usable;
|
||||
|
||||
use bar::{feed_pet_bar, fire_pet_cooldown_events};
|
||||
use drain::drain_pet_actions;
|
||||
@@ -35,6 +36,8 @@ mod flush_tests;
|
||||
mod press_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod usable_tests;
|
||||
|
||||
/// The pet action bar's state. A zero `spells.pet_guid` means no bar: the teardown packet carries
|
||||
/// only that guid.
|
||||
@@ -121,14 +124,13 @@ impl Plugin for UiPetPlugin {
|
||||
}
|
||||
|
||||
/// The unit behind [`PetBar`]'s cached guid, plus our own guid for the ownership tests the
|
||||
/// reference makes before trusting the pet's fields (`0x5ff780`, `0x612e33`).
|
||||
/// reference makes before trusting the pet's fields (`0x5ff780`, `0x612e33`), and the usability
|
||||
/// predicate every pet-bar verb the reference guards asks of them ([`PetUnit::usable_pet`]).
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct PetUnit<'w, 's> {
|
||||
index: Res<'w, GuidIndex>,
|
||||
stores: Query<'w, 's, &'static ObjectStore>,
|
||||
self_guid: Res<'w, crate::net::SelfGuid>,
|
||||
/// The per-field edges, for `fire_transitions`' watch-bridge arms.
|
||||
pub(super) edges: MessageReader<'w, 's, crate::net::FieldChanged>,
|
||||
}
|
||||
|
||||
impl PetUnit<'_, '_> {
|
||||
@@ -139,7 +141,7 @@ impl PetUnit<'_, '_> {
|
||||
}
|
||||
|
||||
/// The active player's descriptor, `None` while its object is not streamed: the lookup the
|
||||
/// pet bar's spell arm makes on the client's player guid (`0x4bd31a`, typemask `0x10`).
|
||||
/// usability predicate makes on the client's player guid (`0x4bcf89`, typemask `0x10`).
|
||||
pub(crate) fn player_store(&self) -> Option<&ObjectStore> {
|
||||
self.store(self.self_guid.0?)
|
||||
}
|
||||
|
||||
@@ -26,13 +26,15 @@ use super::PetBar;
|
||||
|
||||
pub(super) const ME: u64 = 0x10;
|
||||
pub(super) const PET: u64 = 0x2A;
|
||||
/// `UNIT_FIELD_CHARMEDBY` and `UNIT_FIELD_CREATEDBY`, low words; the high words stay 0.
|
||||
/// `UNIT_FIELD_CHARMEDBY`, `UNIT_FIELD_SUMMONEDBY` and `UNIT_FIELD_CREATEDBY`, low words; the high
|
||||
/// words stay 0.
|
||||
pub(super) const CHARMEDBY: u16 = 10;
|
||||
pub(super) const SUMMONEDBY: u16 = 12;
|
||||
pub(super) const CREATEDBY: u16 = 14;
|
||||
pub(super) const FLAGS: u16 = 46;
|
||||
/// `UNIT_FIELD_AURA` slot 0 and `UNIT_FIELD_AURAFLAGS`, a nibble per slot.
|
||||
const AURA: u16 = 47;
|
||||
const AURAFLAGS: u16 = 95;
|
||||
pub(super) const AURA: u16 = 47;
|
||||
pub(super) const AURAFLAGS: u16 = 95;
|
||||
|
||||
pub(super) const CLAW: u32 = 16829;
|
||||
pub(super) const BITE: u32 = 17258;
|
||||
@@ -70,7 +72,7 @@ pub(super) fn growl() -> SpellDisplay {
|
||||
}
|
||||
|
||||
/// Cower's shape here: a toggle with an active icon, so a press with its aura up cancels it.
|
||||
fn cower() -> SpellDisplay {
|
||||
pub(super) fn cower() -> SpellDisplay {
|
||||
SpellDisplay {
|
||||
name: "Cower".into(),
|
||||
active_icon_id: 122,
|
||||
@@ -78,28 +80,42 @@ fn cower() -> SpellDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
/// Our own pet: `CREATEDBY` us, no flags.
|
||||
/// Our own pet as vmangos fills it, `SUMMONEDBY` and `CREATEDBY` us (`Pet.cpp:264`, `:287`), no
|
||||
/// flags: the predicate's owner test reads `SUMMONEDBY` (`0x4bd054`).
|
||||
pub(super) fn owned_pet() -> ObjectStore {
|
||||
ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
|
||||
owned_pet_with(&[])
|
||||
}
|
||||
|
||||
/// [`owned_pet`] with more fields set, which win over its own.
|
||||
pub(super) fn owned_pet_with(extra: &[(u16, u32)]) -> ObjectStore {
|
||||
let mut pairs = vec![
|
||||
(SUMMONEDBY, ME as u32),
|
||||
(SUMMONEDBY + 1, 0),
|
||||
(CREATEDBY, ME as u32),
|
||||
(CREATEDBY + 1, 0),
|
||||
]))
|
||||
];
|
||||
pairs.retain(|(index, _)| !extra.iter().any(|(set, _)| set == index));
|
||||
pairs.extend_from_slice(extra);
|
||||
ObjectStore(benilla_protocol::ObjectFields::from_pairs(&pairs))
|
||||
}
|
||||
|
||||
/// Our own player: `OBJECT_FIELD_TYPE` 0x19, alive.
|
||||
fn player() -> ObjectStore {
|
||||
ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
|
||||
(2, 0x19),
|
||||
(22, 100),
|
||||
(28, 100),
|
||||
]))
|
||||
pub(super) fn player() -> ObjectStore {
|
||||
player_with(&[])
|
||||
}
|
||||
|
||||
/// [`player`] with more fields set.
|
||||
pub(super) fn player_with(extra: &[(u16, u32)]) -> ObjectStore {
|
||||
let mut pairs = vec![(2, 0x19), (22, 100), (28, 100)];
|
||||
pairs.extend_from_slice(extra);
|
||||
ObjectStore(benilla_protocol::ObjectFields::from_pairs(&pairs))
|
||||
}
|
||||
|
||||
pub(super) struct Rig {
|
||||
pub(super) app: App,
|
||||
commands: Receiver<ClientCommand>,
|
||||
/// The pet's feeds as a schedule of their own, so their `Local` memory lives across runs.
|
||||
feed: Schedule,
|
||||
pub(super) feed: Schedule,
|
||||
}
|
||||
|
||||
/// A VM whose listener records every event the pet's lists fire into `SEEN`, in order.
|
||||
@@ -200,8 +216,16 @@ impl Rig {
|
||||
.expect("the press applies as a one-shot system");
|
||||
}
|
||||
|
||||
/// `PetAttack` and the other one-shot orders, as their slot's packed word, applied.
|
||||
pub(super) fn order(&mut self, packed: u32) {
|
||||
self.app
|
||||
.world_mut()
|
||||
.run_system_once(move |mut press: PetPress| press.order(packed))
|
||||
.expect("the order applies as a one-shot system");
|
||||
}
|
||||
|
||||
/// The commands sent so far.
|
||||
fn sent(&self) -> Vec<ClientCommand> {
|
||||
pub(super) fn sent(&self) -> Vec<ClientCommand> {
|
||||
self.commands.try_iter().collect()
|
||||
}
|
||||
|
||||
@@ -226,7 +250,7 @@ impl Rig {
|
||||
}
|
||||
|
||||
/// Whether the pet's own list took no record at all, whichever category a reader asks in.
|
||||
fn pet_list_untouched(&self) -> bool {
|
||||
pub(super) fn pet_list_untouched(&self) -> bool {
|
||||
self.app.world().resource::<PetBar>().cooldowns.generation == 0
|
||||
}
|
||||
|
||||
@@ -236,7 +260,7 @@ impl Rig {
|
||||
}
|
||||
}
|
||||
|
||||
fn pet_actions(sent: &[ClientCommand]) -> usize {
|
||||
pub(super) fn pet_actions(sent: &[ClientCommand]) -> usize {
|
||||
sent.iter()
|
||||
.filter(|c| matches!(c, ClientCommand::PetAction { .. }))
|
||||
.count()
|
||||
@@ -342,12 +366,7 @@ fn a_client_targeted_spell_press_starts_no_pet_gcd() {
|
||||
/// `0x4bd24f`: a press on a running aura cancels it and returns before the arm.
|
||||
#[test]
|
||||
fn an_aura_cancel_press_starts_no_pet_gcd() {
|
||||
let running = ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
|
||||
(CREATEDBY, ME as u32),
|
||||
(CREATEDBY + 1, 0),
|
||||
(AURA, COWER),
|
||||
(AURAFLAGS, 0x3),
|
||||
]));
|
||||
let running = owned_pet_with(&[(AURA, COWER), (AURAFLAGS, 0x3)]);
|
||||
let mut rig = rig(vec![(COWER, cower())], &[COWER], Some(running));
|
||||
|
||||
rig.press(1);
|
||||
@@ -371,23 +390,23 @@ fn an_aura_cancel_press_starts_no_pet_gcd() {
|
||||
}
|
||||
|
||||
/// What a press latched: the mode word, its signal count and the attack latch.
|
||||
fn latched(rig: &Rig) -> (u32, u32, bool) {
|
||||
pub(super) fn latched(rig: &Rig) -> (u32, u32, bool) {
|
||||
let bar = rig.app.world().resource::<PetBar>();
|
||||
(bar.spells.state, bar.bar_signals, bar.attacking)
|
||||
}
|
||||
|
||||
/// Everything a refused spell press leaves untouched: no command on the wire, nothing latched, no
|
||||
/// GCD on the pet's list, no event.
|
||||
fn assert_press_refused(rig: &mut Rig, before: (u32, u32, bool)) {
|
||||
pub(super) fn assert_press_refused(rig: &mut Rig, before: (u32, u32, bool)) {
|
||||
assert!(rig.sent().is_empty(), "no packet of any kind");
|
||||
assert_eq!(latched(rig), before, "nothing latched");
|
||||
assert!(rig.pet_list_untouched(), "no GCD armed");
|
||||
assert!(rig.frame().is_empty(), "no event");
|
||||
}
|
||||
|
||||
/// `0x4bd346`-`0x4bd34f`: the arm leaves for the epilogue, before its GCD and its send, when the
|
||||
/// pet's object does not resolve, as it is while the pet is out of view and the bar stands on its
|
||||
/// guid.
|
||||
/// `0x4bd1f2`: the dispatcher leaves for the epilogue, before the arm, its GCD and its send, when
|
||||
/// the pet's object does not resolve, as it is while the pet is out of view and the bar stands on
|
||||
/// its guid.
|
||||
#[test]
|
||||
fn a_spell_press_with_no_pet_object_sends_nothing_and_arms_nothing() {
|
||||
let mut rig = rig(vec![(CLAW, claw())], &[CLAW], None);
|
||||
@@ -420,8 +439,7 @@ fn a_spell_press_with_no_catalog_row_sends_nothing() {
|
||||
assert_eq!(pet_actions(&rig.sent()), 1, "the control sends");
|
||||
}
|
||||
|
||||
/// `0x4bd31a`-`0x4bd324`: the arm also needs the active player's object, between the record and
|
||||
/// the pet.
|
||||
/// `0x4bd1f2`: the predicate also needs the active player's object (`0x4bcf92`).
|
||||
#[test]
|
||||
fn a_spell_press_with_no_player_object_sends_nothing() {
|
||||
let mut rig = rig(vec![(CLAW, claw())], &[CLAW], Some(owned_pet()));
|
||||
@@ -430,6 +448,8 @@ fn a_spell_press_with_no_player_object_sends_nothing() {
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.remove(&ME);
|
||||
// The bar greys as the player leaves, which is its own repaint.
|
||||
assert_eq!(rig.frame(), ["PET_BAR_UPDATE"]);
|
||||
let before = latched(&rig);
|
||||
|
||||
rig.press(1);
|
||||
@@ -437,68 +457,43 @@ fn a_spell_press_with_no_player_object_sends_nothing() {
|
||||
assert_press_refused(&mut rig, before);
|
||||
}
|
||||
|
||||
/// `0x4bd24a`: the aura leg comes first and needs neither the player nor a record beyond its own,
|
||||
/// so a running aura is cancelled with the player unresolved.
|
||||
/// `0x4bd1f2`: the aura leg (`0x4bd240`) sits behind the predicate, so with the active player
|
||||
/// unresolved a running aura is not cancelled either.
|
||||
#[test]
|
||||
fn an_aura_cancel_press_needs_no_player_object() {
|
||||
let running = ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
|
||||
(CREATEDBY, ME as u32),
|
||||
(CREATEDBY + 1, 0),
|
||||
(AURA, COWER),
|
||||
(AURAFLAGS, 0x3),
|
||||
]));
|
||||
fn an_aura_cancel_press_with_no_player_object_sends_nothing() {
|
||||
let running = owned_pet_with(&[(AURA, COWER), (AURAFLAGS, 0x3)]);
|
||||
let mut rig = rig(vec![(COWER, cower())], &[COWER], Some(running));
|
||||
rig.app
|
||||
.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.remove(&ME);
|
||||
assert_eq!(rig.frame(), ["PET_BAR_UPDATE"], "the bar greys");
|
||||
let before = latched(&rig);
|
||||
|
||||
rig.press(1);
|
||||
|
||||
let sent = rig.sent();
|
||||
assert_eq!(pet_actions(&sent), 0);
|
||||
assert!(sent
|
||||
.iter()
|
||||
.any(|c| matches!(c, ClientCommand::PetCancelAura { .. })));
|
||||
assert_press_refused(&mut rig, before);
|
||||
}
|
||||
|
||||
/// `0x4bd391`, `0x4bd3a3`: a reaction and the Follow and Stay commands have no exit before the
|
||||
/// send, so with the pet's object unresolved each still latches and sends.
|
||||
/// `0x4bd1f2`: a reaction and the Follow and Stay commands have no exit of their own (`0x4bd391`,
|
||||
/// `0x4bd3a3`), only the predicate's, so with the pet's object unresolved none latches or sends.
|
||||
#[test]
|
||||
fn a_command_or_reaction_press_with_no_pet_object_still_sends() {
|
||||
fn a_command_or_reaction_press_with_no_pet_object_sends_nothing() {
|
||||
let word = |kind: u8, action: u32| action | (u32::from(kind) << 24);
|
||||
let mut rig = rig(vec![(CLAW, claw())], &[CLAW], None);
|
||||
for (packed, state) in [
|
||||
(
|
||||
word(PET_ACT_COMMAND, PET_COMMAND_FOLLOW),
|
||||
PET_COMMAND_FOLLOW << 8,
|
||||
),
|
||||
(
|
||||
word(PET_ACT_COMMAND, PET_COMMAND_STAY),
|
||||
PET_COMMAND_STAY << 8,
|
||||
),
|
||||
(
|
||||
word(PET_ACT_REACTION, PET_REACT_AGGRESSIVE),
|
||||
PET_REACT_AGGRESSIVE,
|
||||
),
|
||||
for packed in [
|
||||
word(PET_ACT_COMMAND, PET_COMMAND_FOLLOW),
|
||||
word(PET_ACT_COMMAND, PET_COMMAND_STAY),
|
||||
word(PET_ACT_REACTION, PET_REACT_AGGRESSIVE),
|
||||
] {
|
||||
rig.app.world_mut().resource_mut::<PetBar>().spells.bar[0] = PetActionEntry::from(packed);
|
||||
let signals = latched(&rig).1;
|
||||
rig.frame();
|
||||
let before = latched(&rig);
|
||||
|
||||
rig.press(1);
|
||||
|
||||
let sent = rig.sent();
|
||||
assert!(
|
||||
matches!(
|
||||
sent.as_slice(),
|
||||
[ClientCommand::PetAction { pet_guid: PET, packed: p, .. }] if *p == packed
|
||||
),
|
||||
"{packed:#010x} sends: {sent:?}"
|
||||
);
|
||||
let (mode, now, _) = latched(&rig);
|
||||
assert_eq!(mode, state, "{packed:#010x} latches");
|
||||
assert_eq!(now, signals + 1, "{packed:#010x} signals the bar");
|
||||
assert_press_refused(&mut rig, before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,10 +521,12 @@ fn the_pets_gcd_takes_the_players_op_21() {
|
||||
assert_eq!(rig.pet_reads(CLAW, &shaved()).1, 1000);
|
||||
}
|
||||
|
||||
/// `0x4bd3c3`-`0x4bd405`: the Attack command resolves the pet before it validates, and with none
|
||||
/// jumps to the send (`0x4bd444`), the selection as its target.
|
||||
/// `0x4bd1f2`: the Attack command's own null-pet branch (`0x4bd403`-`0x4bd405`) repeats the lookup
|
||||
/// the predicate made (`0x4bd034`) on the same guid and typemask, so it never runs: with the pet's
|
||||
/// object unresolved the press leaves at the gate and sends nothing, at the selection or any
|
||||
/// other target.
|
||||
#[test]
|
||||
fn an_attack_press_with_no_pet_object_still_sends_at_the_selection() {
|
||||
fn an_attack_press_with_no_pet_object_sends_nothing() {
|
||||
const FOE: u64 = 0x77;
|
||||
let mut rig = rig(vec![(CLAW, claw())], &[CLAW], None);
|
||||
let world = rig.app.world_mut();
|
||||
@@ -561,6 +558,20 @@ fn an_attack_press_with_no_pet_object_still_sends_at_the_selection() {
|
||||
};
|
||||
let attack = PET_COMMAND_ATTACK | (u32::from(PET_ACT_COMMAND) << 24);
|
||||
world.resource_mut::<PetBar>().spells.bar[0] = PetActionEntry::from(attack);
|
||||
rig.frame();
|
||||
let before = latched(&rig);
|
||||
|
||||
rig.press(1);
|
||||
|
||||
assert_press_refused(&mut rig, before);
|
||||
|
||||
// The same press with the pet streamed in goes out at the selection, and an ordinary pet's
|
||||
// Attack raises no latch.
|
||||
let world = rig.app.world_mut();
|
||||
let pet = world
|
||||
.spawn((Guid(PET), owned_pet_with(&[(22, 100), (28, 100)])))
|
||||
.id();
|
||||
world.resource_mut::<GuidIndex>().0.insert(PET, pet);
|
||||
|
||||
rig.press(1);
|
||||
|
||||
|
||||
@@ -161,28 +161,6 @@ fn a_disabled_bar_reads_passive_and_lights_no_command() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usability_is_the_disabled_bit_and_the_pets_crowd_control() {
|
||||
let mut bar = PetBar {
|
||||
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(actions_usable(&bar, Some(0)));
|
||||
assert!(
|
||||
actions_usable(&bar, None),
|
||||
"a missing descriptor is not a no"
|
||||
);
|
||||
|
||||
for flag in [0x0004_0000, 0x0040_0000, 0x0080_0000] {
|
||||
assert!(!actions_usable(&bar, Some(flag)), "flag {flag:#x} disables");
|
||||
}
|
||||
// Those three are stunned, confused, fleeing; POSSESSED is not among them.
|
||||
assert!(actions_usable(&bar, Some(0x0100_0000)));
|
||||
|
||||
bar.spells.state |= PET_STATE_BAR_DISABLED;
|
||||
assert!(!actions_usable(&bar, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spell_slots_read_their_autocast_off_bits_31_and_30() {
|
||||
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
|
||||
@@ -781,6 +759,7 @@ fn a_cooldown_alone_is_pushed_with_no_event_from_the_bar_feed() {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<crate::ui_script::UiClock>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<crate::net::SelfGuid>()
|
||||
.insert_resource(crate::ui_action::Spells {
|
||||
catalog: benilla_formats::SpellCatalog::from_displays(HashMap::from([(CLAW, claw())])),
|
||||
..crate::ui_action::Spells::empty_for_tests()
|
||||
|
||||
@@ -39,7 +39,9 @@ pub(super) const UNIT_FLAG_PET_IN_COMBAT: u32 = 0x0000_0800;
|
||||
pub(super) fn feed_pet_unit(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
bar: Res<PetBar>,
|
||||
mut pet: PetUnit,
|
||||
pet: PetUnit,
|
||||
// The per-field edges, for `fire_transitions`' watch-bridge arms.
|
||||
mut field_edges: MessageReader<crate::net::FieldChanged>,
|
||||
changed_stores: Query<(), Changed<ObjectStore>>,
|
||||
mut removed_stores: RemovedComponents<ObjectStore>,
|
||||
mut names: ResMut<NameCache>,
|
||||
@@ -52,7 +54,7 @@ pub(super) fn feed_pet_unit(
|
||||
return;
|
||||
};
|
||||
let (memory, vm_reset) = memory.get_reset(&script);
|
||||
let edges = crate::net::FieldEdges::collect(&mut pet.edges);
|
||||
let edges = crate::net::FieldEdges::collect(&mut field_edges);
|
||||
let names_moved = memory.names_generation.moved(names.generation());
|
||||
let bar_changed = bar.is_changed();
|
||||
let stores_changed = !changed_stores.is_empty();
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! The pet-usability predicate `0x4bcf70`, asked once and by everything the reference lets it
|
||||
//! gate: `GetPetActionsUsable` (`0x4be0b3`), the dispatcher behind `CastPetAction` and the orders
|
||||
//! (`0x4bd1f2`), both autocast toggles (`0x4bcbcf`, `0x4bccce`) and the bar write the drag makes
|
||||
//! (`0x4bc9d0`, for the drop `0x4bce33` and the pickup's blank `0x4be27f`). A zero answer leaves
|
||||
//! each of them at its epilogue, before anything is latched, armed, cancelled or sent.
|
||||
|
||||
use benilla_protocol::messages::{OwnerFallback, PET_UNUSABLE_UNIT_FLAGS};
|
||||
|
||||
use crate::net::ObjectStore;
|
||||
|
||||
use super::{PetBar, PetUnit};
|
||||
|
||||
/// `0x4bcf70`, every exit in the order the bytes take them. `me` is the client's active player
|
||||
/// guid (`0x468550`); `player` is that guid's object and `pet` the bar's cached guid's (typemasks
|
||||
/// `0x10` and `8`), each `None` while it is not streamed.
|
||||
pub(super) fn actions_usable(
|
||||
bar: &PetBar,
|
||||
me: Option<u64>,
|
||||
player: Option<&ObjectStore>,
|
||||
pet: Option<&ObjectStore>,
|
||||
) -> bool {
|
||||
// `0x4bcf92`: the active player must resolve.
|
||||
let (Some(me), Some(player)) = (me, player) else {
|
||||
return false;
|
||||
};
|
||||
// `0x4bcf98`-`0x4bcfa4`: a charmed player, whose `UNIT_FIELD_CHARMEDBY` pair is nonzero, has
|
||||
// no pet bar to work.
|
||||
if player.0.unit_charmed_by().is_some() {
|
||||
return false;
|
||||
}
|
||||
// `0x4bcfbf`-`0x4bd012`: a view held on some unit, `PLAYER_FARSIGHT` (player field 712, at
|
||||
// `[[player+0xe68]+0x830]`), must be on the pet: Eyes of the Beast keeps the bar, Far Sight or
|
||||
// Mind Vision on anything else takes it. The two compares of the player's own guid against the
|
||||
// active guid (`0x4bcfb7`, `0x4bcfe0`) are the lookup's key against itself, so the branch that
|
||||
// zeroes the held guid (`0x4bcfe8`) has no way in.
|
||||
if player
|
||||
.0
|
||||
.player_farsight()
|
||||
.is_some_and(|held| held != bar.spells.pet_guid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// `0x4bd034`-`0x4bd03d`: the cached guid must resolve to a unit.
|
||||
let Some(pet) = pet else {
|
||||
return false;
|
||||
};
|
||||
// `0x4bd03f`-`0x4bd067`: the pet's charmed-by guid, or its summoned-by guid while that is
|
||||
// zero, must be the active player's.
|
||||
if pet.0.unit_owner(OwnerFallback::SummonedBy) != Some(me) {
|
||||
return false;
|
||||
}
|
||||
// `0x4bd06f`-`0x4bd08b`: stunned (`0x40000`), confused (`0x400000`) and fleeing (`0x800000`).
|
||||
// Health is never read.
|
||||
if pet.0.unit_flags() & PET_UNUSABLE_UNIT_FLAGS != 0 {
|
||||
return false;
|
||||
}
|
||||
// `0x4bd08d`: bit 27 of the bar state `[0xb71468]`, which only the server writes.
|
||||
!bar.spells.bar_disabled()
|
||||
}
|
||||
|
||||
impl PetUnit<'_, '_> {
|
||||
/// [`actions_usable`] for the bar as it stands: the pet's object when the predicate holds,
|
||||
/// `None` when it does not. A caller past the gate holds the pet the predicate resolved, which
|
||||
/// the dispatcher's own later lookups of the same guid find again (`0x4bd346`, `0x4bd403`).
|
||||
pub(crate) fn usable_pet(&self, bar: &PetBar) -> Option<&ObjectStore> {
|
||||
let pet = self.store(bar.spells.pet_guid);
|
||||
pet.filter(|_| actions_usable(bar, self.self_guid.0, self.player_store(), pet))
|
||||
}
|
||||
|
||||
/// `GetPetActionsUsable()`, and the gate every pet-bar verb the reference guards asks.
|
||||
pub(crate) fn actions_usable(&self, bar: &PetBar) -> bool {
|
||||
self.usable_pet(bar).is_some()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
//! The pet-usability predicate `0x4bcf70` and every verb it gates, whole client headless: each of
|
||||
//! its exits alone must grey `GetPetActionsUsable`, and must stop a press, an order, both autocast
|
||||
//! toggles and the drag's writes, while a pet that trips none of them takes all of it as before.
|
||||
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_protocol::messages::{
|
||||
PetActionEntry, PET_ACT_COMMAND, PET_ACT_ENABLED, PET_ACT_REACTION, PET_COMMAND_ATTACK,
|
||||
PET_COMMAND_FOLLOW, PET_COMMAND_STAY, PET_REACT_AGGRESSIVE, PET_STATE_BAR_DISABLED,
|
||||
};
|
||||
|
||||
use crate::net::{ClientCommand, Guid, GuidIndex};
|
||||
use crate::target::UNIT_FLAG_POSSESSED;
|
||||
|
||||
use super::press_tests::{
|
||||
claw, latched, owned_pet, owned_pet_with, pet_actions, player_with, rig, Rig, CHARMEDBY, CLAW,
|
||||
FLAGS, ME, PET, SUMMONEDBY,
|
||||
};
|
||||
use super::PetBar;
|
||||
|
||||
/// `PLAYER_FARSIGHT`, low word: the view's anchor, the guid `0x4bcfbf`-`0x4bd012` holds against the
|
||||
/// pet's.
|
||||
const FARSIGHT: u16 = 712;
|
||||
/// Some other guid than ours or the pet's.
|
||||
const OTHER: u64 = 0x99;
|
||||
/// A foe the Attack order can be aimed at.
|
||||
const FOE: u64 = 0x77;
|
||||
|
||||
fn guid_pair(field: u16, guid: u64) -> [(u16, u32); 2] {
|
||||
[(field, guid as u32), (field + 1, (guid >> 32) as u32)]
|
||||
}
|
||||
|
||||
/// The player, replaced: player-controlled, so the Attack order's validator needs no faction
|
||||
/// table, and carrying `extra`.
|
||||
fn set_player(rig: &mut Rig, extra: &[(u16, u32)]) {
|
||||
let world = rig.app.world_mut();
|
||||
let me = world.resource::<GuidIndex>().0[&ME];
|
||||
let mut fields = vec![(FLAGS, 0x8)];
|
||||
fields.extend_from_slice(extra);
|
||||
world.entity_mut(me).insert(player_with(&fields));
|
||||
}
|
||||
|
||||
/// The pet, replaced by [`owned_pet_with`] `extra`: alive, so the Attack order's validator passes.
|
||||
fn set_pet(rig: &mut Rig, extra: &[(u16, u32)]) {
|
||||
let world = rig.app.world_mut();
|
||||
let pet = world.resource::<GuidIndex>().0[&PET];
|
||||
let mut fields = vec![(22, 100), (28, 100)];
|
||||
fields.extend_from_slice(extra);
|
||||
world.entity_mut(pet).insert(owned_pet_with(&fields));
|
||||
}
|
||||
|
||||
const ATTACK: u32 = PET_COMMAND_ATTACK | ((PET_ACT_COMMAND as u32) << 24);
|
||||
const FOLLOW: u32 = PET_COMMAND_FOLLOW | ((PET_ACT_COMMAND as u32) << 24);
|
||||
const STAY: u32 = PET_COMMAND_STAY | ((PET_ACT_COMMAND as u32) << 24);
|
||||
const AGGRESSIVE: u32 = PET_REACT_AGGRESSIVE | ((PET_ACT_REACTION as u32) << 24);
|
||||
const CLAW_WORD: u32 = CLAW | ((PET_ACT_ENABLED as u32) << 24);
|
||||
|
||||
/// A usable pet on a bar with Claw (autocast on) in slot 1, and in the pet's book, with the foe
|
||||
/// selected for the Attack order. Fed once, so the VM holds what the bar last pushed.
|
||||
fn scene() -> Rig {
|
||||
let mut rig = rig(vec![(CLAW, claw())], &[CLAW], Some(owned_pet()));
|
||||
set_player(&mut rig, &[]);
|
||||
set_pet(&mut rig, &[]);
|
||||
let world = rig.app.world_mut();
|
||||
world.resource_mut::<PetBar>().spells.spells = vec![PetActionEntry::from(CLAW_WORD)];
|
||||
let foe = world
|
||||
.spawn((
|
||||
Guid(FOE),
|
||||
Transform::default(),
|
||||
crate::net::ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
|
||||
(22, 100),
|
||||
(28, 100),
|
||||
])),
|
||||
))
|
||||
.id();
|
||||
world.resource_mut::<GuidIndex>().0.insert(FOE, foe);
|
||||
*world.resource_mut::<crate::target::Selection>() = crate::target::Selection {
|
||||
target: Some(foe),
|
||||
guid: Some(FOE),
|
||||
..Default::default()
|
||||
};
|
||||
rig.frame();
|
||||
rig
|
||||
}
|
||||
|
||||
/// One state of the world to try every verb in.
|
||||
struct Case {
|
||||
name: &'static str,
|
||||
apply: fn(&mut Rig),
|
||||
/// What `0x4bcf70` answers.
|
||||
usable: bool,
|
||||
/// Whether `PickupPetAction`'s own gate lets the drag start: `UNIT_FLAG_POSSESSED` blocks it
|
||||
/// (`0x4be1c1`), which is no exit of the predicate.
|
||||
draggable: bool,
|
||||
}
|
||||
|
||||
/// One way `0x4bcf70` answers zero, and nothing else changed.
|
||||
type Exit = (&'static str, fn(&mut Rig));
|
||||
|
||||
/// Every exit of the predicate, each alone, in the order the bytes take them.
|
||||
const EXITS: [Exit; 11] = [
|
||||
("the active player does not resolve (0x4bcf92)", |rig| {
|
||||
rig.app
|
||||
.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.remove(&ME);
|
||||
}),
|
||||
("the player is charmed (0x4bcf98)", |rig| {
|
||||
set_player(rig, &guid_pair(CHARMEDBY, OTHER));
|
||||
}),
|
||||
(
|
||||
"the player's far sight is on another unit (0x4bcfbf)",
|
||||
|rig| set_player(rig, &guid_pair(FARSIGHT, OTHER)),
|
||||
),
|
||||
("the pet does not resolve (0x4bd034)", |rig| {
|
||||
rig.app
|
||||
.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.remove(&PET);
|
||||
}),
|
||||
("the pet is summoned by another (0x4bd054)", |rig| {
|
||||
set_pet(rig, &guid_pair(SUMMONEDBY, OTHER));
|
||||
}),
|
||||
(
|
||||
"the pet has no summoner, whatever created it (0x4bd054)",
|
||||
|rig| set_pet(rig, &guid_pair(SUMMONEDBY, 0)),
|
||||
),
|
||||
(
|
||||
"the pet is charmed by another, summoned by us (0x4bd048)",
|
||||
|rig| set_pet(rig, &guid_pair(CHARMEDBY, OTHER)),
|
||||
),
|
||||
("the pet is stunned (0x4bd075)", |rig| {
|
||||
set_pet(rig, &[(FLAGS, 0x0004_0000)]);
|
||||
}),
|
||||
("the pet is confused (0x4bd089)", |rig| {
|
||||
set_pet(rig, &[(FLAGS, 0x0040_0000)]);
|
||||
}),
|
||||
("the pet is fleeing (0x4bd081)", |rig| {
|
||||
set_pet(rig, &[(FLAGS, 0x0080_0000)]);
|
||||
}),
|
||||
("bit 27 of the bar state (0x4bd08d)", |rig| {
|
||||
rig.app.world_mut().resource_mut::<PetBar>().spells.state |= PET_STATE_BAR_DISABLED;
|
||||
}),
|
||||
];
|
||||
|
||||
/// Pets that trip none of the exits, however much like one: each still works.
|
||||
const USABLE: [Exit; 5] = [
|
||||
("an untouched pet", |_| {}),
|
||||
("far sight on the pet itself, Eyes of the Beast", |rig| {
|
||||
set_player(rig, &guid_pair(FARSIGHT, PET));
|
||||
}),
|
||||
(
|
||||
"the pet charmed by us and summoned by another: the charmed-by wins",
|
||||
|rig| {
|
||||
let mut fields = guid_pair(CHARMEDBY, ME).to_vec();
|
||||
fields.extend(guid_pair(SUMMONEDBY, OTHER));
|
||||
set_pet(rig, &fields);
|
||||
},
|
||||
),
|
||||
("the pet possessed, which greys nothing", |rig| {
|
||||
set_pet(rig, &[(FLAGS, UNIT_FLAG_POSSESSED)]);
|
||||
}),
|
||||
("the pet in combat", |rig| set_pet(rig, &[(FLAGS, 0x0800)])),
|
||||
];
|
||||
|
||||
/// Every exit alone, then every pet that trips none: the cases the rest of the file runs each
|
||||
/// verb over.
|
||||
fn all_cases() -> impl Iterator<Item = Case> {
|
||||
let usable = USABLE.iter().map(|&(name, apply)| Case {
|
||||
name,
|
||||
apply,
|
||||
usable: true,
|
||||
draggable: !name.contains("possessed"),
|
||||
});
|
||||
let exits = EXITS.iter().map(|&(name, apply)| Case {
|
||||
name,
|
||||
apply,
|
||||
usable: false,
|
||||
draggable: true,
|
||||
});
|
||||
usable.chain(exits)
|
||||
}
|
||||
|
||||
/// What a press did, by name, so one assert says which effect leaked and for which case.
|
||||
fn effects(rig: &Rig, sent: Vec<ClientCommand>) -> Vec<&'static str> {
|
||||
let mut out: Vec<&'static str> = sent
|
||||
.iter()
|
||||
.map(|c| match c {
|
||||
ClientCommand::PetAction { .. } => "PetAction",
|
||||
ClientCommand::PetCancelAura { .. } => "PetCancelAura",
|
||||
_ => "another command",
|
||||
})
|
||||
.collect();
|
||||
if !rig.pet_list_untouched() {
|
||||
out.push("a GCD armed");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn ui<R>(rig: &mut Rig, f: impl FnOnce(&mut benilla_ui::script::UiScript) -> R) -> R {
|
||||
let mut script = rig
|
||||
.app
|
||||
.world_mut()
|
||||
.non_send_resource_mut::<benilla_ui::script::UiScript>();
|
||||
f(&mut script)
|
||||
}
|
||||
|
||||
/// `GetPetActionsUsable()` as the Lua reads it after the feed ran.
|
||||
fn usable_answer(rig: &mut Rig) -> bool {
|
||||
rig.frame();
|
||||
ui(rig, |s| {
|
||||
s.eval::<bool>("return GetPetActionsUsable() ~= nil")
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
/// `0x4be0b3`: the display answers nil for each exit alone, and 1 for every pet that trips none.
|
||||
#[test]
|
||||
fn each_exit_alone_makes_get_pet_actions_usable_nil() {
|
||||
for Case {
|
||||
name,
|
||||
apply,
|
||||
usable,
|
||||
..
|
||||
} in all_cases()
|
||||
{
|
||||
let mut rig = scene();
|
||||
apply(&mut rig);
|
||||
assert_eq!(usable_answer(&mut rig), usable, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4bd1f2`: a spell press, and the orders: every one leaves at the gate, before the aura cancel,
|
||||
/// the latch, the GCD and the send.
|
||||
#[test]
|
||||
fn each_exit_alone_stops_a_press_and_every_order() {
|
||||
for Case {
|
||||
name,
|
||||
apply,
|
||||
usable,
|
||||
..
|
||||
} in all_cases()
|
||||
{
|
||||
let mut rig = scene();
|
||||
apply(&mut rig);
|
||||
rig.frame();
|
||||
let before = latched(&rig);
|
||||
|
||||
rig.press(1);
|
||||
let sent = rig.sent();
|
||||
let got = effects(&rig, sent);
|
||||
if usable {
|
||||
// A possessed unit's press takes the generic cast entry, which arms its own GCD.
|
||||
assert!(got.contains(&"PetAction"), "{name}: a spell press: {got:?}");
|
||||
} else {
|
||||
assert_eq!(got, Vec::<&str>::new(), "{name}: a spell press");
|
||||
assert_eq!(latched(&rig), before, "{name}: nothing latched");
|
||||
}
|
||||
|
||||
for (word, order) in [
|
||||
(FOLLOW, "Follow"),
|
||||
(STAY, "Stay"),
|
||||
(AGGRESSIVE, "Aggressive"),
|
||||
(ATTACK, "Attack"),
|
||||
] {
|
||||
let before = latched(&rig);
|
||||
rig.order(word);
|
||||
let sent = rig.sent();
|
||||
let went = sent
|
||||
.iter()
|
||||
.any(|c| matches!(c, ClientCommand::PetAction { packed, .. } if *packed == word));
|
||||
assert_eq!(went, usable, "{name}: {order} sends only on a usable pet");
|
||||
if !usable {
|
||||
assert_eq!(latched(&rig), before, "{name}: {order} latched nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4bcbcf`: the bar's `TogglePetAutocast` flips the word and sends only for a usable pet.
|
||||
#[test]
|
||||
fn each_exit_alone_stops_the_bars_autocast_toggle() {
|
||||
for Case {
|
||||
name,
|
||||
apply,
|
||||
usable,
|
||||
..
|
||||
} in all_cases()
|
||||
{
|
||||
let mut rig = scene();
|
||||
apply(&mut rig);
|
||||
rig.frame();
|
||||
let word = rig.app.world().resource::<PetBar>().spells.bar[0];
|
||||
|
||||
ui(&mut rig, |s| s.run("TogglePetAutocast(1)").unwrap());
|
||||
rig.app
|
||||
.world_mut()
|
||||
.run_system_once(super::drain::drain_pet_actions)
|
||||
.expect("the drain runs");
|
||||
|
||||
let sent = rig.sent();
|
||||
let flipped = rig.app.world().resource::<PetBar>().spells.bar[0] != word;
|
||||
assert_eq!(flipped, usable, "{name}: the word flips only when usable");
|
||||
assert_eq!(
|
||||
sent.iter()
|
||||
.filter(|c| matches!(c, ClientCommand::PetSetAction { .. }))
|
||||
.count(),
|
||||
usize::from(usable),
|
||||
"{name}: the toggle is sent only when usable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4bccce`: the pet book's `ToggleSpellAutocast` flips the word, repaints and sends only for a
|
||||
/// usable pet.
|
||||
#[test]
|
||||
fn each_exit_alone_stops_the_pet_books_autocast_toggle() {
|
||||
for Case {
|
||||
name,
|
||||
apply,
|
||||
usable,
|
||||
..
|
||||
} in all_cases()
|
||||
{
|
||||
let mut rig = scene();
|
||||
apply(&mut rig);
|
||||
rig.frame();
|
||||
let (word, signals) = {
|
||||
let bar = rig.app.world().resource::<PetBar>();
|
||||
(bar.spells.spells[0], bar.bar_signals)
|
||||
};
|
||||
|
||||
ui(&mut rig, |s| {
|
||||
s.run("ToggleSpellAutocast(1, BOOKTYPE_PET)").unwrap()
|
||||
});
|
||||
rig.app
|
||||
.world_mut()
|
||||
.run_system_once(crate::ui_pet_book::drain_pet_book)
|
||||
.expect("the drain runs");
|
||||
|
||||
let sent = rig.sent();
|
||||
let bar = rig.app.world().resource::<PetBar>();
|
||||
assert_eq!(
|
||||
bar.spells.spells[0] != word,
|
||||
usable,
|
||||
"{name}: the book's word flips only when usable"
|
||||
);
|
||||
assert_eq!(
|
||||
bar.bar_signals != signals,
|
||||
usable,
|
||||
"{name}: the bar repaints only when usable"
|
||||
);
|
||||
assert_eq!(
|
||||
sent.iter()
|
||||
.filter(|c| matches!(c, ClientCommand::PetSpellAutocast { .. }))
|
||||
.count(),
|
||||
usize::from(usable),
|
||||
"{name}: the toggle is sent only when usable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4bd6e0`: the pet menu's Dismiss hands its command word to the dispatcher, so it is refused
|
||||
/// at `0x4bd1f2` like any other order.
|
||||
#[test]
|
||||
fn each_exit_alone_stops_the_menus_dismiss() {
|
||||
const DISMISS: u32 = 0x0700_0003;
|
||||
for Case {
|
||||
name,
|
||||
apply,
|
||||
usable,
|
||||
..
|
||||
} in all_cases()
|
||||
{
|
||||
let mut rig = scene();
|
||||
apply(&mut rig);
|
||||
rig.frame();
|
||||
|
||||
ui(&mut rig, |s| s.run("PetDismiss()").unwrap());
|
||||
rig.app
|
||||
.world_mut()
|
||||
.run_system_once(super::menu::drain_pet_menu)
|
||||
.expect("the drain runs");
|
||||
|
||||
let went = rig
|
||||
.sent()
|
||||
.iter()
|
||||
.any(|c| matches!(c, ClientCommand::PetAction { packed, .. } if *packed == DISMISS));
|
||||
assert_eq!(went, usable, "{name}: Dismiss sends only on a usable pet");
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x4bc9d0`, from the drop `0x4bce33` and the pickup's blank `0x4be27f`: lifting Claw and
|
||||
/// dropping it on the next slot writes and sends nothing on an unusable pet, and two writes on a
|
||||
/// usable one.
|
||||
#[test]
|
||||
fn each_exit_alone_stops_the_drags_writes() {
|
||||
for Case {
|
||||
name,
|
||||
apply,
|
||||
usable,
|
||||
draggable,
|
||||
} in all_cases()
|
||||
{
|
||||
let writes = usable && draggable;
|
||||
let mut rig = scene();
|
||||
apply(&mut rig);
|
||||
rig.frame();
|
||||
let bar_before = rig.app.world().resource::<PetBar>().spells.bar;
|
||||
|
||||
ui(&mut rig, |s| {
|
||||
s.run("PickupPetAction(1) PickupPetAction(2)").unwrap();
|
||||
});
|
||||
rig.app
|
||||
.world_mut()
|
||||
.run_system_once(super::drain::drain_pet_actions)
|
||||
.expect("the drain runs");
|
||||
|
||||
let sent = rig.sent();
|
||||
let bar = rig.app.world().resource::<PetBar>().spells.bar;
|
||||
assert_eq!(
|
||||
sent.iter()
|
||||
.filter(|c| matches!(c, ClientCommand::PetSetAction { .. }))
|
||||
.count(),
|
||||
if writes { 2 } else { 0 },
|
||||
"{name}: the pickup's blank and the drop send only when usable"
|
||||
);
|
||||
assert_eq!(
|
||||
bar != bar_before,
|
||||
writes,
|
||||
"{name}: the bar's words move only when usable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole default UI on the rig's world in place of its listening VM: the stock pet bar, and
|
||||
/// the fed pet under it.
|
||||
fn with_stock_ui(rig: &mut Rig) {
|
||||
let mut script = benilla_ui::script::UiScript::new().expect("a VM");
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
// The in-game UI loads on world entry, so a player exists.
|
||||
script.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
name: Some("Probefour".into()),
|
||||
level: 60,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
let failures = crate::ui_script::load_default_ui(&script);
|
||||
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
|
||||
rig.app.world_mut().insert_non_send_resource(script);
|
||||
}
|
||||
|
||||
/// The feeds, then the VM's clock and layout, so the bar's slide and repaint have happened.
|
||||
fn settle(rig: &mut Rig) {
|
||||
rig.feed.run(rig.app.world_mut());
|
||||
ui(rig, |s| {
|
||||
for _ in 0..3 {
|
||||
s.tick(0.05);
|
||||
}
|
||||
s.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether the button's icon quad draws greyscale: `PetActionBar_Update`'s `SetDesaturation` on
|
||||
/// `PetActionButton1Icon` (`PetActionBarFrame.lua:129-134`).
|
||||
fn icon_is_grey(rig: &mut Rig, icon: &str) -> bool {
|
||||
ui(rig, |s| {
|
||||
s.extract()
|
||||
.iter()
|
||||
.find_map(|q| match &q.content {
|
||||
benilla_ui::script::QuadContent::Texture {
|
||||
path: Some(p),
|
||||
desaturated,
|
||||
..
|
||||
} if p == icon => Some(*desaturated),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("the icon {icon} is on screen"))
|
||||
})
|
||||
}
|
||||
|
||||
/// A left click on `PetActionButton1`, at its own centre, and the frame's script calls applied.
|
||||
fn click_first_button(rig: &mut Rig) {
|
||||
let (x, y): (f32, f32) = ui(rig, |s| {
|
||||
s.eval("return PetActionButton1:GetCenter()").unwrap()
|
||||
});
|
||||
ui(rig, |s| {
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
});
|
||||
rig.app
|
||||
.world_mut()
|
||||
.run_system_once(crate::script_calls::apply_script_calls)
|
||||
.expect("the frame's script calls apply");
|
||||
}
|
||||
|
||||
/// With the default UI loaded, the stock bar reads `GetPetActionsUsable` for its icons and clicks
|
||||
/// `CastPetAction` whatever it answered, so the engine's gate is the only one: a stunned pet's bar
|
||||
/// greys, and clicking it sends nothing (`0x4bd1f2`), where the same click on the pet before it was
|
||||
/// stunned went out.
|
||||
#[test]
|
||||
fn the_stock_bar_greys_a_stunned_pet_and_its_click_sends_nothing() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
const ICON: &str = "Interface\\Icons\\Ability_Druid_Rake";
|
||||
let display = benilla_formats::SpellDisplay {
|
||||
icon: Some(ICON.into()),
|
||||
..claw()
|
||||
};
|
||||
let mut rig = rig(vec![(CLAW, display)], &[CLAW], Some(owned_pet()));
|
||||
with_stock_ui(&mut rig);
|
||||
settle(&mut rig);
|
||||
|
||||
assert!(
|
||||
!icon_is_grey(&mut rig, ICON),
|
||||
"a usable pet's icon is in colour"
|
||||
);
|
||||
click_first_button(&mut rig);
|
||||
assert_eq!(
|
||||
pet_actions(&rig.sent()),
|
||||
1,
|
||||
"and its button presses, as before"
|
||||
);
|
||||
|
||||
set_pet(&mut rig, &[(FLAGS, 0x0004_0000)]);
|
||||
settle(&mut rig);
|
||||
|
||||
assert!(icon_is_grey(&mut rig, ICON), "a stunned pet's bar greys");
|
||||
let before = latched(&rig);
|
||||
click_first_button(&mut rig);
|
||||
assert!(
|
||||
rig.sent().is_empty(),
|
||||
"the click on the greyed button sends nothing"
|
||||
);
|
||||
assert_eq!(latched(&rig), before);
|
||||
|
||||
// The same click again once the stun ends.
|
||||
set_pet(&mut rig, &[]);
|
||||
settle(&mut rig);
|
||||
assert!(!icon_is_grey(&mut rig, ICON));
|
||||
click_first_button(&mut rig);
|
||||
assert_eq!(pet_actions(&rig.sent()), 1);
|
||||
}
|
||||
|
||||
/// `0x4bd06f`-`0x4bd08b` read three flags and never the pet's health: a dead pet's bar is usable,
|
||||
/// its press goes out, and only the Attack order's own validator refuses it (`0x612e10`).
|
||||
#[test]
|
||||
fn a_dead_pet_is_still_usable() {
|
||||
let mut rig = scene();
|
||||
set_pet(&mut rig, &[(22, 0)]);
|
||||
|
||||
assert!(usable_answer(&mut rig));
|
||||
rig.press(1);
|
||||
assert_eq!(pet_actions(&rig.sent()), 1, "the spell press goes out");
|
||||
rig.order(FOLLOW);
|
||||
assert_eq!(pet_actions(&rig.sent()), 1, "and so does Follow");
|
||||
rig.order(ATTACK);
|
||||
assert_eq!(
|
||||
pet_actions(&rig.sent()),
|
||||
0,
|
||||
"the attack validator refuses the dead actor, past the gate"
|
||||
);
|
||||
}
|
||||
|
||||
/// The predicate on its own, over every field it reads, so the exit table above is what the
|
||||
/// function says and not what the rig happens to build.
|
||||
#[test]
|
||||
fn the_predicate_reads_exactly_the_reference_fields() {
|
||||
use super::usable::actions_usable;
|
||||
let store = |fields: &[(u16, u32)]| {
|
||||
crate::net::ObjectStore(benilla_protocol::ObjectFields::from_pairs(fields))
|
||||
};
|
||||
let bar = PetBar {
|
||||
spells: benilla_protocol::messages::PetSpells {
|
||||
pet_guid: PET,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let me = Some(ME);
|
||||
let player = store(&[(2, 0x19)]);
|
||||
let pet = owned_pet();
|
||||
|
||||
assert!(actions_usable(&bar, me, Some(&player), Some(&pet)));
|
||||
assert!(!actions_usable(&bar, None, Some(&player), Some(&pet)));
|
||||
assert!(!actions_usable(&bar, me, None, Some(&pet)));
|
||||
assert!(!actions_usable(&bar, me, Some(&player), None));
|
||||
|
||||
// The owner is `CHARMEDBY` when set, else `SUMMONEDBY`, and never `CREATEDBY`.
|
||||
let created_only = store(&[(14, ME as u32), (15, 0)]);
|
||||
assert!(!actions_usable(
|
||||
&bar,
|
||||
me,
|
||||
Some(&player),
|
||||
Some(&created_only)
|
||||
));
|
||||
let unowned = store(&[]);
|
||||
assert!(!actions_usable(&bar, me, Some(&player), Some(&unowned)));
|
||||
|
||||
// A far-sight guid of zero is no view; the pet's own guid is the one that stays usable.
|
||||
let zero_view = store(&[(FARSIGHT, 0), (FARSIGHT + 1, 0)]);
|
||||
assert!(actions_usable(&bar, me, Some(&zero_view), Some(&pet)));
|
||||
let on_pet = store(&[(FARSIGHT, PET as u32), (FARSIGHT + 1, 0)]);
|
||||
assert!(actions_usable(&bar, me, Some(&on_pet), Some(&pet)));
|
||||
let elsewhere = store(&[(FARSIGHT, OTHER as u32), (FARSIGHT + 1, 0)]);
|
||||
assert!(!actions_usable(&bar, me, Some(&elsewhere), Some(&pet)));
|
||||
// The high word counts: a view whose low word is the pet's and whose high word is not.
|
||||
let high_only = store(&[(FARSIGHT, PET as u32), (FARSIGHT + 1, 1)]);
|
||||
assert!(!actions_usable(&bar, me, Some(&high_only), Some(&pet)));
|
||||
|
||||
// Each of the three flags alone, and no other.
|
||||
for flag in [0x0004_0000, 0x0040_0000, 0x0080_0000] {
|
||||
let hurt = owned_pet_with(&[(FLAGS, flag)]);
|
||||
assert!(
|
||||
!actions_usable(&bar, me, Some(&player), Some(&hurt)),
|
||||
"flag {flag:#x} disables"
|
||||
);
|
||||
}
|
||||
let possessed = owned_pet_with(&[(FLAGS, UNIT_FLAG_POSSESSED)]);
|
||||
assert!(actions_usable(&bar, me, Some(&player), Some(&possessed)));
|
||||
|
||||
let mut disabled = PetBar {
|
||||
spells: bar.spells.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
disabled.spells.state |= PET_STATE_BAR_DISABLED;
|
||||
assert!(!actions_usable(&disabled, me, Some(&player), Some(&pet)));
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use benilla_ui::script::{PetBookState, SpellSlotView, UiScript};
|
||||
|
||||
use crate::net::{ClientCommand, GuidIndex, NetCommands, ObjectStore, SelfPlayer};
|
||||
use crate::ui_action::Spells;
|
||||
use crate::ui_pet::PetBar;
|
||||
use crate::ui_pet::{PetBar, PetUnit};
|
||||
use crate::ui_script::UiInput;
|
||||
use crate::ui_unit::UnitFeed;
|
||||
|
||||
@@ -173,7 +173,8 @@ fn slot_view(
|
||||
/// `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.
|
||||
/// player. Unlike the bar's press it asks no usability predicate: no call site of `0x4bcf70` is
|
||||
/// in this path.
|
||||
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 {
|
||||
@@ -203,11 +204,14 @@ pub(crate) fn cast_pet_spell(p: &mut crate::ui_pet::PetPress, spell_id: u32, on_
|
||||
});
|
||||
}
|
||||
|
||||
/// Drains the pet book's `ToggleSpellAutocast` intents.
|
||||
fn drain_pet_book(
|
||||
/// Drains the pet book's `ToggleSpellAutocast` intents. Each asks the usability predicate before
|
||||
/// it looks anything up (`0x4bccce`), so an unusable pet's toggle flips, repaints and sends
|
||||
/// nothing.
|
||||
pub(crate) fn drain_pet_book(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
mut bar: ResMut<PetBar>,
|
||||
commands: Res<NetCommands>,
|
||||
pet: PetUnit,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
@@ -222,6 +226,10 @@ fn drain_pet_book(
|
||||
return;
|
||||
}
|
||||
for spell_id in autocasts {
|
||||
if !pet.actions_usable(&bar) {
|
||||
debug!("ui_pet_book: autocast {spell_id} refused, the pet's actions are not usable");
|
||||
continue;
|
||||
}
|
||||
let Some(on) = flip_autocast(&mut bar.spells, spell_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
//! payload, a word carried verbatim (`0x4bce00`), so a drag only rearranges the bar the server
|
||||
//! sent. A drop clears the cursor before deciding (`0x495190`) and only a write that relocated
|
||||
//! nothing refills it (`0x4bce38`), so a refused drop loses the payload, as in the reference.
|
||||
//! Both bar writes, the drop's (`0x4bce33`) and the pickup's blank (`0x4be27f`), ask the pet's
|
||||
//! usability predicate first (`0x4bc9d0`): on an unusable pet the drop writes nothing and refills
|
||||
//! nothing, and the pickup holds its action with the slot left as it was.
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
@@ -166,8 +169,14 @@ fn slot_index(model: &Model, slot: u32) -> Option<usize> {
|
||||
}
|
||||
|
||||
/// Run [`assign`] on the engine's optimistic mirror of the ten words and queue the send; the
|
||||
/// pickup's blank goes through it too, as in the reference (`0x4be268`).
|
||||
/// pickup's blank goes through it too, as in the reference (`0x4be268`). `0x4bc9a0` takes its
|
||||
/// send flag from both callers as 1, which makes it ask `0x4bcf70` before anything else
|
||||
/// (`0x4bc9c7`-`0x4bc9d7`); the app's answer, the one the bar's `GetPetActionsUsable` reads, is
|
||||
/// `PetBarState::actions_usable`.
|
||||
fn write_slot(model: &mut Model, target: usize, source: u32, passive: bool) -> Option<Assigned> {
|
||||
if !model.pet_bar.actions_usable {
|
||||
return None;
|
||||
}
|
||||
let mut words: Vec<u32> = model.pet_bar.slots.iter().map(|s| s.view.packed).collect();
|
||||
let assigned = assign(&mut words, target, source, passive)?;
|
||||
|
||||
@@ -369,6 +378,64 @@ mod tests {
|
||||
assert_eq!(p.texture, None);
|
||||
}
|
||||
|
||||
/// `0x4bc9d0`: the write asks the pet's usability predicate first, and the app's answer is the
|
||||
/// one the bar last pushed. The pickup still lifts its action (`0x4be25d`), but its blank
|
||||
/// (`0x4be27f`) is refused, so the slot keeps the word and nothing is sent; the drop clears the
|
||||
/// cursor before it decides (`0x4be220`), and its refused write refills nothing (`0x4bce38`).
|
||||
#[test]
|
||||
fn an_unusable_bar_writes_nothing_at_either_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_pet_actions(true, false, true, hunter_bar());
|
||||
|
||||
assert!(s.eval::<bool>("return PickupPetAction(4)").unwrap());
|
||||
assert!(
|
||||
matches!(s.cursor_payload(), Some(crate::script::CursorPayload::PetAction(c))
|
||||
if c.src_slot == 4 && c.packed == CLAW),
|
||||
"the pickup lifted Claw: {:?}",
|
||||
s.cursor_payload()
|
||||
);
|
||||
assert!(
|
||||
s.take_pet_set_actions().is_empty(),
|
||||
"but its blank was refused, so nothing is sent"
|
||||
);
|
||||
|
||||
s.run("PickupPetAction(5)").unwrap();
|
||||
assert!(
|
||||
s.cursor_payload().is_none(),
|
||||
"the drop cleared the cursor and nothing refilled it"
|
||||
);
|
||||
assert!(s.take_pet_set_actions().is_empty(), "and wrote nothing");
|
||||
|
||||
// Slot 4 was never blanked: a second pickup lifts the whole word, autocast bits and all,
|
||||
// where a blanked slot would give the type with id 0.
|
||||
s.run("PickupPetAction(4)").unwrap();
|
||||
assert!(
|
||||
matches!(s.cursor_payload(), Some(crate::script::CursorPayload::PetAction(c))
|
||||
if c.packed == CLAW),
|
||||
"{:?}",
|
||||
s.cursor_payload()
|
||||
);
|
||||
// Growl kept its slot too: the refused drop never displaced it.
|
||||
s.run("ClearCursor() PickupPetAction(5)").unwrap();
|
||||
assert!(matches!(
|
||||
s.cursor_payload(),
|
||||
Some(crate::script::CursorPayload::PetAction(c)) if c.packed == GROWL
|
||||
));
|
||||
assert!(s.take_pet_set_actions().is_empty());
|
||||
}
|
||||
|
||||
/// The same bar made usable takes the same two writes, which is what the test above stops.
|
||||
#[test]
|
||||
fn a_usable_bar_takes_both_writes() {
|
||||
let mut s = bar_script();
|
||||
s.run("PickupPetAction(4) PickupPetAction(5)").unwrap();
|
||||
assert_eq!(
|
||||
s.take_pet_set_actions().len(),
|
||||
2,
|
||||
"the pickup's blank and the drop"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picking_up_a_token_leaves_the_slot_alone() {
|
||||
let mut s = bar_script();
|
||||
|
||||
@@ -104,7 +104,8 @@ pub struct PetStats {
|
||||
pub(crate) struct PetBarState {
|
||||
/// `PetHasActionBar()`: there is a bar, even one of ten empty slots.
|
||||
pub(crate) has_bar: bool,
|
||||
/// `GetPetActionsUsable()`: false desaturates the whole bar.
|
||||
/// `GetPetActionsUsable()` (`0x4bcf70`): false desaturates the whole bar, and refuses the
|
||||
/// drag's bar writes with it (`0x4bc9d0`).
|
||||
pub(crate) actions_usable: bool,
|
||||
pub(crate) slots: Vec<StoredPetAction>,
|
||||
/// `HasPetUI`'s first return: a pet with a nonzero `UNIT_FIELD_PETNUMBER` (`0x4be697`). Not
|
||||
@@ -113,7 +114,8 @@ pub(crate) struct PetBarState {
|
||||
pub(crate) stats: PetStats,
|
||||
/// `PickupPetAction`'s gate, `UNIT_FLAG_POSSESSED` clear (`0x4be1c1`), which blocks the drop
|
||||
/// as well as the pick-up. Not `actions_usable`: possession does not grey the bar, and the
|
||||
/// flags that grey it do not block a drag.
|
||||
/// pick-up itself does not ask it. The writes at both ends do (`0x4bc9d0`), so a greyed bar
|
||||
/// still lifts an action onto the cursor and then writes nothing.
|
||||
pub(crate) pickup_allowed: bool,
|
||||
/// `PetCanBeAbandoned()`: a kept pet rather than a summon. It forks the pet menu: paperdoll,
|
||||
/// rename and abandon show when true, dismiss only when false (`UnitPopup.lua:402-417`).
|
||||
|
||||
Reference in New Issue
Block a user