Files
benilla-octo/crates/benilla-app/src/ui_pet/tests.rs
T
SamandClaude Fable 5.1 60138f5d5d every objective line shows, the detail follows the row, and 1.98 lints
- a quest objective that carries both a creature and an item is two lines in the quest log, as the reference's four passes enumerate it: Graverobbers shows its Embalming Ichor (2249)
- the quest log's detail follows the live selection inside a frame, so a quest addon's log walk reads the right quest; Questie no longer files a chain quest under its shortest sibling (2247)
- a UI tint change is not a material change: a run's colour rides the batch's instance tag, so the PlayerFrame glow no longer re-prepares a material every frame (2245)
- the workspace lints clean on rustc 1.98: constant-size chunks become as_chunks, one slice fill, two unused globs
- WOW_HOVER_PROBE aims the pick in a headless run, and the inspect card names the tooltip's ladder stage (2248, 2250)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 17:01:27 +07:00

962 lines
37 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The pet system's unit tests — one module for all three concerns, because they share one set of
//! fixtures (a packed word, a slot view, a `PetSpells` state) and splitting them per module would
//! fork those three ways.
use benilla_protocol::messages::{
PetActionEntry, PetSpells, PET_ACT_COMMAND, PET_ACT_DISABLED, PET_ACT_ENABLED, PET_ACT_PASSIVE,
PET_ACT_REACTION, PET_COMMAND_ATTACK, PET_COMMAND_DISMISS, PET_COMMAND_FOLLOW,
PET_COMMAND_STAY, PET_REACT_AGGRESSIVE, PET_REACT_DEFENSIVE, PET_REACT_PASSIVE,
PET_STATE_BAR_DISABLED,
};
use benilla_ui::script::PetActionView;
use crate::net::{ClientCommand, NetCommands, ObjectStore};
use super::bar::*;
use super::drain::*;
use super::menu::*;
use super::unit::*;
use super::*;
fn packed(action: u32, kind: u8) -> PetActionEntry {
PetActionEntry::from(action | (u32::from(kind) << 24))
}
/// [`slot_view`] for a slot that is **not** showing active — the ordinary case, and the only
/// one the token/spell/cooldown tests care about. The showing-active leg has its own tests,
/// which call `slot_view` directly so the flag is visible at the call.
fn view(
entry: PetActionEntry,
bar: &PetSpells,
spell: Option<&benilla_formats::SpellDisplay>,
cooldown: Option<(i64, u32, bool)>,
pet_attacking: bool,
) -> PetActionView {
slot_view(entry, bar, spell, cooldown, pet_attacking, false)
}
fn spell(name: &str, rank: Option<&str>) -> benilla_formats::SpellDisplay {
benilla_formats::SpellDisplay {
name: name.to_string(),
rank: rank.map(str::to_string),
icon: Some("Interface\\Icons\\Ability_Druid_Rake".into()),
..Default::default()
}
}
/// The state dword as the server packs it: react in byte 0, command in byte 1.
fn state(command: u32, react: u32) -> PetSpells {
PetSpells {
pet_guid: 0x2A,
state: react | (command << 8),
..Default::default()
}
}
/// A command token returns GLOBAL NAMES for both name and texture, and is lit exactly when it
/// IS the pet's current command — the whole reason three command buttons can share one state.
#[test]
fn command_tokens_light_on_the_current_command() {
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let follow = view(
packed(PET_COMMAND_FOLLOW, PET_ACT_COMMAND),
&bar,
None,
None,
false,
);
assert_eq!(follow.name.as_deref(), Some("PET_ACTION_FOLLOW"));
assert_eq!(follow.texture.as_deref(), Some("PET_FOLLOW_TEXTURE"));
assert!(follow.is_token && follow.active);
assert!(!follow.attack_active, "Follow is not the attack fork");
let stay = view(
packed(PET_COMMAND_STAY, PET_ACT_COMMAND),
&bar,
None,
None,
false,
);
assert!(!stay.active, "only the CURRENT command is lit");
}
/// The Attack button's second clause: the attack latch lights it and arms the call-off fork
/// **independently of the command state**, which is how a pet told to Follow can still show
/// Attack lit while it is on something.
#[test]
fn the_attack_latch_lights_attack_whatever_the_command_state_says() {
let following = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let attack = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
let idle = view(attack, &following, None, None, false);
assert!(!idle.active && !idle.attack_active);
let on = view(attack, &following, None, None, true);
assert!(on.active, "the latch lights it even on a FOLLOW command");
assert!(on.attack_active, "and the next press calls the pet off");
// The latch never reaches another command's button, however busy the pet is.
let follow = view(
packed(PET_COMMAND_FOLLOW, PET_ACT_COMMAND),
&following,
None,
None,
true,
);
assert!(!follow.attack_active);
assert!(follow.active, "…but Follow is still the standing command");
}
/// Reaction tokens use the `PET_MODE_*` keys — the state words — not the right-click menu's
/// `PET_AGGRESSIVE` imperatives. Identical in enUS, distinguishable only by key.
#[test]
fn reaction_tokens_use_the_mode_keys_and_light_on_the_current_react() {
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let def = view(
packed(PET_REACT_DEFENSIVE, PET_ACT_REACTION),
&bar,
None,
None,
false,
);
assert_eq!(def.name.as_deref(), Some("PET_MODE_DEFENSIVE"));
assert_eq!(def.texture.as_deref(), Some("PET_DEFENSIVE_TEXTURE"));
assert!(def.is_token && def.active);
assert!(
!view(
packed(PET_REACT_AGGRESSIVE, PET_ACT_REACTION),
&bar,
None,
None,
false
)
.active
);
}
/// A DISABLED bar (bit 27) changes what both token classes report, and neither is a special
/// case we wrote: the reaction compare's left side is forced to Passive, and the command
/// compare — unmasked `state >> 8` — is put out of range of every command at once.
#[test]
fn a_disabled_bar_reads_passive_and_lights_no_command() {
let mut bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
bar.state |= PET_STATE_BAR_DISABLED;
let passive = view(
packed(PET_REACT_PASSIVE, PET_ACT_REACTION),
&bar,
None,
None,
false,
);
assert!(passive.active, "a bar that cannot be ordered reads Passive");
assert!(
!view(
packed(PET_REACT_DEFENSIVE, PET_ACT_REACTION),
&bar,
None,
None,
false
)
.active
);
assert!(
!view(
packed(PET_COMMAND_FOLLOW, PET_ACT_COMMAND),
&bar,
None,
None,
false
)
.active,
"the command it IS on goes dark too"
);
}
/// `GetPetActionsUsable` — bit 27 OR the pet's own crowd-control flags. The second half is the
/// leg benilla was missing: a feared/stunned/confused pet's bar greys.
#[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");
}
// POSSESSED is deliberately not one of them — that IS the pet-bar case.
assert!(actions_usable(&bar, Some(0x0100_0000)));
bar.spells.state |= PET_STATE_BAR_DISABLED;
assert!(!actions_usable(&bar, Some(0)));
}
/// A spell slot: real name, rank subtext, icon PATH — and the autocast pair read off **bits
/// 31/30**, not the type byte. `isActive` is nil on every spell path.
#[test]
fn spell_slots_read_their_autocast_off_bits_31_and_30() {
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let claw = spell("Claw", Some("Rank 3"));
let on = view(
packed(3010, PET_ACT_ENABLED),
&bar,
Some(&claw),
None,
false,
);
assert_eq!(on.name.as_deref(), Some("Claw"));
assert_eq!(on.subtext.as_deref(), Some("Rank 3"));
assert_eq!(on.spell_id, Some(3010));
assert!(!on.is_token);
assert!(on.autocast_allowed && on.autocast_enabled);
assert!(!on.active, "a SPELL slot never reports isActive");
let off = view(
packed(3010, PET_ACT_DISABLED),
&bar,
Some(&claw),
None,
false,
);
assert!(off.autocast_allowed && !off.autocast_enabled);
// A passive pet spell shows, but can never autocast — no ring, no sparkle.
let passive = view(
packed(3010, PET_ACT_PASSIVE),
&bar,
Some(&claw),
None,
false,
);
assert!(!passive.autocast_allowed && !passive.autocast_enabled);
}
/// The two routes to an empty-looking button: the client's zero WORD, and vmangos's own
/// `(0, ACT_DISABLED)` filler, which takes the spell branch and misses the catalog. Both must
/// draw nothing — reading either one wrong puts four "?" buttons mid-bar.
#[test]
fn both_kinds_of_empty_slot_draw_nothing() {
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let zero = PetActionEntry::default();
assert!(zero.is_empty());
assert_eq!(
view(zero, &bar, None, None, false),
PetActionView::default()
);
let filler = packed(0, PET_ACT_DISABLED);
assert!(!filler.is_empty(), "the WORD is not zero");
assert_eq!(
view(filler, &bar, None, None, false),
PetActionView {
// Draws nothing and still CARRIES its word (decision 1010) — this is the exact
// slot the drop core hunts for as a relocation candidate (type 1, low 16 zero),
// so zeroing it here would both send the wrong word and lose the candidate.
packed: filler.packed,
..Default::default()
},
"…but spell id 0 resolves to nothing, so the button is still empty"
);
}
/// A spell the catalog cannot name draws nothing rather than a nameless icon — and with it
/// goes the autocast pair, which the client also gates on the record resolving.
#[test]
fn an_unresolvable_spell_draws_nothing() {
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let entry = packed(999, PET_ACT_ENABLED);
let v = view(entry, &bar, None, None, false);
assert_eq!(
v,
PetActionView {
packed: entry.packed,
..Default::default()
}
);
assert!(!v.autocast_allowed && !v.autocast_enabled);
}
/// A type outside 1–7 is inert. The client's own default arm under-pushes here (wow-re §2.5);
/// we answer the empty slot instead, and in particular never reach the spell catalog with an
/// arbitrary number.
#[test]
fn an_unknown_type_byte_is_inert() {
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let claw = spell("Claw", None);
let entry = packed(3010, 0x33);
assert_eq!(
view(entry, &bar, Some(&claw), None, false),
PetActionView {
packed: entry.packed,
..Default::default()
}
);
}
/// The slot index map is 1-based and bounded — a stale index from the VM cannot read past the
/// ten words or wrap into slot 10.
#[test]
fn slot_lookup_is_one_based_and_bounded() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
bar.spells.bar[0] = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
bar.spells.bar[9] = packed(PET_REACT_PASSIVE, PET_ACT_REACTION);
assert_eq!(slot_entry(&bar, 1).unwrap().action(), PET_COMMAND_ATTACK);
assert_eq!(slot_entry(&bar, 10).unwrap().action(), PET_REACT_PASSIVE);
assert!(slot_entry(&bar, 0).is_none());
assert!(slot_entry(&bar, 11).is_none());
assert!(slot_entry_mut(&mut bar, 0).is_none());
assert!(slot_entry_mut(&mut bar, 11).is_none());
}
/// The local latch, which exists because the server confirms none of it (module doc), written
/// with the client's own masks: a command press moves bits 8–15 and keeps byte 0 **and bit
/// 27**, a reaction press moves byte 0 only, a spell press moves neither — and DISMISS moves
/// nothing, because it ends the pet rather than becoming its standing order.
#[test]
fn a_press_latches_the_state_the_server_never_echoes() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
bar.spells.state |= PET_STATE_BAR_DISABLED;
latch_press(&mut bar, packed(PET_COMMAND_STAY, PET_ACT_COMMAND), false);
assert_eq!(bar.spells.command_state() & 0xFF, PET_COMMAND_STAY);
assert_eq!(bar.spells.react_state(), PET_REACT_DEFENSIVE, "untouched");
assert!(
bar.spells.bar_disabled(),
"the disabled bit is the SERVER's — a command press must preserve it"
);
latch_press(
&mut bar,
packed(PET_REACT_AGGRESSIVE, PET_ACT_REACTION),
false,
);
assert_eq!(bar.spells.react_state(), PET_REACT_AGGRESSIVE);
assert_eq!(bar.spells.command_state() & 0xFF, PET_COMMAND_STAY);
assert!(bar.spells.bar_disabled());
latch_press(&mut bar, packed(3010, PET_ACT_DISABLED), false);
assert_eq!(bar.spells.command_state() & 0xFF, PET_COMMAND_STAY);
assert_eq!(bar.spells.react_state(), PET_REACT_AGGRESSIVE);
latch_press(
&mut bar,
packed(PET_COMMAND_DISMISS, PET_ACT_COMMAND),
false,
);
assert_eq!(
bar.spells.command_state() & 0xFF,
PET_COMMAND_STAY,
"Dismiss ends the pet; it never becomes its standing command"
);
}
/// **The director's report: clicking a lit Follow or Passive un-toggles it.**
///
/// The CheckButton flips itself before `OnClick` runs and the reference's first line is
/// `this:SetChecked(0)`, so every press starts by taking the light off. `0x4bc940`/`0x4bc960`
/// put it back by signalling `PET_BAR_UPDATE` **unconditionally** — no old-vs-new compare — so
/// pressing the mode the pet is already in still repaints. Our feed dedups on what it pushed,
/// so without a counter in that key the light had no way back.
///
/// A press that the reference does NOT signal must not bump: DISMISS and a spell reach
/// `0x4bd444` straight, and a hunter's ATTACK skips `0x4bd429` with the latch it never raises.
#[test]
fn every_press_the_reference_signals_forces_a_repaint() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
let signals = |b: &PetBar| b.bar_signals;
// Pressing the mode the pet is ALREADY in: nothing about the state moves, and the signal
// still fires. This is the whole bug.
let before = signals(&bar);
latch_press(&mut bar, packed(PET_COMMAND_FOLLOW, PET_ACT_COMMAND), false);
assert_eq!(bar.spells.command_state(), PET_COMMAND_FOLLOW, "unmoved");
assert_ne!(signals(&bar), before, "and it repaints anyway");
let before = signals(&bar);
latch_press(
&mut bar,
packed(PET_REACT_DEFENSIVE, PET_ACT_REACTION),
false,
);
assert_eq!(bar.spells.react_state(), PET_REACT_DEFENSIVE, "unmoved");
assert_ne!(signals(&bar), before, "same for the reaction side");
// The presses the reference leaves silent.
let before = signals(&bar);
latch_press(
&mut bar,
packed(PET_COMMAND_DISMISS, PET_ACT_COMMAND),
false,
);
latch_press(&mut bar, packed(3010, PET_ACT_ENABLED), false);
latch_press(&mut bar, packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND), false);
assert_eq!(
signals(&bar),
before,
"Dismiss, a spell and a pet's Attack all reach 0x4bd444 without signalling"
);
// A POSSESSED unit's Attack does signal — `0x4bd429`, one instruction before the latch.
latch_press(&mut bar, packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND), true);
assert_ne!(signals(&bar), before);
}
/// ATTACK is the one command press that can raise the attack latch — **and only while we are
/// possessing the unit** (`0x4bd420`). On an ordinary pet bar the arm is unreachable, which is
/// the whole reason its button never checks.
#[test]
fn only_a_possessed_units_attack_press_raises_the_latch() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
let attack = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
assert!(!bar.attacking);
// A hunter's pet: the press goes out (the caller sends regardless) and latches nothing.
latch_press(&mut bar, attack, false);
assert!(
!bar.attacking,
"an ordinary pet can never raise the latch, so its button can never light"
);
latch_press(&mut bar, attack, true);
assert!(bar.attacking, "a possessed unit can");
bar.attacking = false;
latch_press(&mut bar, packed(PET_COMMAND_DISMISS, PET_ACT_COMMAND), true);
latch_press(&mut bar, packed(PET_REACT_PASSIVE, PET_ACT_REACTION), true);
assert!(!bar.attacking, "only ATTACK raises it");
}
/// `0x5ee5a0` itself — the compare that decides all of the above.
///
/// This is the fact the pet bar was missing: the reference is not asking "is this my pet", it
/// is asking "is this the unit I am *driving*". A hunter pet answers no on the flag, so no
/// hunter press ever reaches the latch.
#[test]
fn the_latch_gate_is_possession_not_ownership() {
const FLAGS: u16 = 46;
const CHARMEDBY: u16 = 10;
const CREATEDBY: u16 = 14;
let unit =
|pairs: &[(u16, u32)]| ObjectStore(benilla_protocol::ObjectFields::from_pairs(pairs));
let me = Some(0x77u64);
// An ordinary hunter pet: ours, in combat, not possessed. The answer is no.
let hunter_pet = unit(&[
(CREATEDBY, 0x77),
(CREATEDBY + 1, 0),
(FLAGS, UNIT_FLAG_PET_IN_COMBAT),
]);
assert!(!possessing(Some(&hunter_pet), me));
// The same pet under Eyes of the Beast — possessed, and now the latch arm is live.
let driven = unit(&[
(CREATEDBY, 0x77),
(CREATEDBY + 1, 0),
(FLAGS, UNIT_FLAG_POSSESSED),
]);
assert!(possessing(Some(&driven), me));
// A mind-controlled mob: CHARMEDBY is the primary leg, so it needs no CREATEDBY at all.
let charmed = unit(&[(CHARMEDBY, 0x77), (CHARMEDBY + 1, 0), (FLAGS, 0x0100_0000)]);
assert!(possessing(Some(&charmed), me));
// Somebody ELSE's possessed unit is not ours to latch for.
let theirs = unit(&[
(CHARMEDBY, 0x99),
(CHARMEDBY + 1, 0),
(FLAGS, UNIT_FLAG_POSSESSED),
]);
assert!(!possessing(Some(&theirs), me));
// No descriptor is no possession — the honest reading of missing data, and the same
// posture the rest of the bar takes.
assert!(!possessing(None, me));
}
/// **The director's report, twice over: the Attack button is never a toggle.**
///
/// Reported by eye against the reference client — *"there is no way in the ref client for the
/// attack button to stay lit up/toggled like the follow or stay… it's simply a button to send
/// the pet"* — and the first fix was wrong because it took the latch's *lifetime* to be the
/// question. It is not: the latch never rises at all on a pet bar, because `0x4bd420` gates it
/// on possession. This drives a whole pet's worth of ATTACK presses and asserts the button
/// stays dark through every one.
#[test]
fn a_pets_attack_button_never_lights() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
let attack = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
let follow = packed(PET_COMMAND_FOLLOW, PET_ACT_COMMAND);
for _ in 0..3 {
assert!(
commit_press(&mut bar, attack, false, false),
"the order goes"
);
let lit = view(attack, &bar.spells, None, None, bar.attacking);
assert!(!lit.active, "and the button does not light — ever");
assert!(
!lit.attack_active,
"so the next press orders again, never calls off"
);
}
assert_eq!(
bar.spells.command_state(),
PET_COMMAND_FOLLOW,
"an attack order leaves the standing command alone"
);
assert!(
view(follow, &bar.spells, None, None, bar.attacking).active,
"and Follow keeps the light that is actually a mode's"
);
// Stay is still reachable as a mode — the command byte was never hijacked.
latch_press(&mut bar, packed(PET_COMMAND_STAY, PET_ACT_COMMAND), false);
assert_eq!(bar.spells.command_state(), PET_COMMAND_STAY);
}
/// The **possess** bar is the other half of the same button, and it does light — which is why
/// the `isActive` clause, `IsPetAttackActive` and `PetStopAttack` are all still modelled rather
/// than deleted as dead weight.
#[test]
fn a_possessed_units_attack_button_does_light() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
let attack = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
let follow = packed(PET_COMMAND_FOLLOW, PET_ACT_COMMAND);
assert!(commit_press(&mut bar, attack, false, true));
assert!(view(attack, &bar.spells, None, None, bar.attacking).active);
assert!(view(attack, &bar.spells, None, None, bar.attacking).attack_active);
assert!(
view(follow, &bar.spells, None, None, bar.attacking).active,
"the latch is not the command byte — Follow keeps its own light"
);
// The call-off: `PetStopAttack` clears the latch, and the latch was the ONLY thing lighting
// the button — so it goes out.
bar.attacking = false;
let called_off = view(attack, &bar.spells, None, None, bar.attacking);
assert!(!called_off.active);
assert!(!called_off.attack_active);
}
/// **A refused ATTACK costs everything** — no packet, no latch, no light. The reference's veto
/// jumps to the function epilogue rather than the shared send (`0x4bd414 je 0x4bd4c6`), so a
/// pet that is dead, stunned, feared, confused, pacified or charmed away simply does not take
/// the order, and the button must not report that it did.
///
/// This is the join decision 0998 could not test: the gate and the latch, composed.
#[test]
fn a_refused_attack_neither_sends_nor_lights_the_button() {
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
let attack = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
assert!(
!commit_press(&mut bar, attack, true, true),
"a vetoed order never reaches the wire"
);
assert!(!bar.attacking, "and never raises the latch");
assert!(
!view(attack, &bar.spells, None, None, bar.attacking).active,
"so the button stays dark, even on the one bar where it could light"
);
assert_eq!(
bar.spells.command_state(),
PET_COMMAND_FOLLOW,
"a refusal cannot move the standing command either"
);
// The same press with the gate clear is the ordinary attack, unchanged. The two gates are
// NOT symmetric: this one costs the packet, possession costs only the latch.
assert!(commit_press(&mut bar, attack, false, true));
assert!(bar.attacking);
assert!(view(attack, &bar.spells, None, None, bar.attacking).active);
}
/// **Touching your target calls the pet off** — `0x493910`'s `0x493a18`, the second call site
/// of `PetStopAttack`'s core and the one benilla was missing.
///
/// This is the mechanism behind the director's report that Attack is not a toggle. Without it
/// the latch had only two ways down (the lit button's own second press and a new pet), so an
/// order lit the button for the rest of the pet's life and read exactly like the Stay/Follow
/// modes it is supposed to contrast with.
#[test]
fn touching_your_target_calls_the_pet_off() {
let (tx, rx) = crossbeam_channel::unbounded();
let commands = NetCommands(tx);
let mut bar = PetBar {
spells: PetSpells {
pet_guid: 0xF14,
..state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE)
},
attacking: true,
..Default::default()
};
// Selecting when nothing was selected is NOT a clear — `0x493937` returns before anything.
assert!(!old_target_cleared(None, Some(7)));
// Replacing one target with another IS, and so is dropping it: the guard is on the OLD
// selection existing, never on the new one being empty.
assert!(old_target_cleared(Some(7), Some(9)));
assert!(old_target_cleared(Some(7), None));
// A no-op re-select never reaches the clear at all (`0x493540`'s own dedup).
assert!(!old_target_cleared(Some(7), Some(7)));
assert!(stop_pet_attack(&mut bar, &commands));
assert!(!bar.attacking, "the latch is down");
assert!(
matches!(rx.try_recv(), Ok(ClientCommand::PetStopAttack { pet_guid }) if pet_guid == 0xF14),
"and the server is told, with the BAR's guid"
);
// The button is dark now, because the latch was the only thing lighting it.
let attack = packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND);
assert!(!view(attack, &bar.spells, None, None, bar.attacking).active);
// Re-clearing is a no-op: `0x4bd65e` returns before the send when the latch is already
// down, so a target change per second does not spray packets.
assert!(!stop_pet_attack(&mut bar, &commands));
assert!(rx.try_recv().is_err());
}
/// `PET_ATTACK_START`/`STOP` read the pet's **server-owned** in-combat flag, and only for a
/// unit we own — the correction to decision 0990, which derived them from the local click
/// latch. The owner test uses SUMMONEDBY as its fallback, which is the callback's own choice
/// and not the one `0x5ee5a0` makes for the same-shaped read.
#[test]
fn the_attack_events_read_the_pets_combat_flag_not_the_click_latch() {
const FLAGS: u16 = 46;
const CHARMEDBY: u16 = 10;
const SUMMONEDBY: u16 = 12;
let unit =
|pairs: &[(u16, u32)]| ObjectStore(benilla_protocol::ObjectFields::from_pairs(pairs));
let me = Some(0x77u64);
// Ours by SUMMONEDBY, fighting / not fighting.
let mine = |flags: u32| unit(&[(SUMMONEDBY, 0x77), (SUMMONEDBY + 1, 0), (FLAGS, flags)]);
assert_eq!(
pet_combat_flag(&mine(UNIT_FLAG_PET_IN_COMBAT), me),
Some(true)
);
assert_eq!(pet_combat_flag(&mine(0), me), Some(false));
// An unrelated flag bit is not combat — the callback tests one bit.
assert_eq!(pet_combat_flag(&mine(0x1000), me), Some(false));
// Somebody else's minion never fires, however hard it is fighting.
let theirs = unit(&[
(SUMMONEDBY, 0x99),
(SUMMONEDBY + 1, 0),
(FLAGS, UNIT_FLAG_PET_IN_COMBAT),
]);
assert_eq!(pet_combat_flag(&theirs, me), None);
// CHARMEDBY wins over SUMMONEDBY: a mob WE mind-controlled is ours even though it was
// summoned by nobody, and a minion charmed AWAY from us stops being ours.
let charmed_by_me = unit(&[
(CHARMEDBY, 0x77),
(CHARMEDBY + 1, 0),
(FLAGS, UNIT_FLAG_PET_IN_COMBAT),
]);
assert_eq!(pet_combat_flag(&charmed_by_me, me), Some(true));
let stolen = unit(&[
(CHARMEDBY, 0x99),
(CHARMEDBY + 1, 0),
(SUMMONEDBY, 0x77),
(SUMMONEDBY + 1, 0),
(FLAGS, UNIT_FLAG_PET_IN_COMBAT),
]);
assert_eq!(pet_combat_flag(&stolen, me), None);
}
/// Only the ATTACK order consults the validator. The reference's type-7 arm branches exactly
/// twice, and every other action — the two modes, DISMISS, and anything `>= 4` — leaves down a
/// path that sends unconditionally. Gating them too would make a stunned pet impossible to
/// dismiss or to put back on Follow, which is not what the binary does.
#[test]
fn only_the_attack_order_is_gated() {
assert!(is_attack_order(packed(PET_COMMAND_ATTACK, PET_ACT_COMMAND)));
for action in [
PET_COMMAND_STAY,
PET_COMMAND_FOLLOW,
PET_COMMAND_DISMISS,
4,
9,
] {
assert!(
!is_attack_order(packed(action, PET_ACT_COMMAND)),
"command {action} sends unconditionally"
);
}
// A REACTION slot whose action happens to equal ATTACK's 2 (that is Aggressive) is a
// different word entirely — the type byte decides, never the action alone.
assert!(!is_attack_order(packed(
PET_REACT_AGGRESSIVE,
PET_ACT_REACTION
)));
}
/// A pet with one aura in slot 0. `AURAFLAGS` is nibble-packed 8 slots to the u32, so slot 0's
/// nibble is the low four bits: `0x2` is an effect-index bit (what makes the slot *live*) and
/// `0x1` is `AFLAG_CANCELABLE` — the bit `0x4bcea0` actually tests.
fn pet_running(spell_id: u32, nibble: u32) -> ObjectStore {
const AURA: u16 = 47;
const AURAFLAGS: u16 = 95;
ObjectStore(benilla_protocol::ObjectFields::from_pairs(&[
(AURA, spell_id),
(AURAFLAGS, nibble),
]))
}
/// A spell with an active icon — the shape the predicate needs before it can ever fire.
fn toggle_spell() -> benilla_formats::SpellDisplay {
benilla_formats::SpellDisplay {
active_icon_id: 122,
active_icon: Some("Interface\\Icons\\Ability_Druid_Cower".into()),
..spell("Cower", Some("Rank 1"))
}
}
/// The showing-active predicate, all four of its ways to say no. The pet-side twin of
/// `0x4e55f0` is the *same function* we already had, so what these assert is the wiring: the
/// pet's store goes in, and the three tests are the reference's three.
#[test]
fn a_pet_spell_shows_active_only_while_it_is_a_live_cancelable_aura_on_the_pet() {
let slot = packed(2645, benilla_protocol::messages::PET_TYPE_SPELL_FIRST);
let running = pet_running(2645, 0x3);
let d = toggle_spell();
assert_eq!(
active_aura_press(slot, Some(&running), Some(&d)),
Some(2645)
);
// No ActiveIconID: never a toggle — and that gate is on the SEND too, so this spell
// re-casts rather than cancelling however live its aura is (the reference's own quirk).
let plain = spell("Growl", None);
assert_eq!(active_aura_press(slot, Some(&running), Some(&plain)), None);
// Live but NOT cancelable (effect-index bit only).
let uncancelable = pet_running(2645, 0x2);
assert_eq!(active_aura_press(slot, Some(&uncancelable), Some(&d)), None);
// A different spell's aura, and no pet descriptor at all.
assert_eq!(
active_aura_press(slot, Some(&pet_running(768, 0x3)), Some(&d)),
None
);
assert_eq!(active_aura_press(slot, None, Some(&d)), None);
// A COMMAND slot whose action equals the spell id is a different word entirely — the type
// byte decides here exactly as it does for the ATTACK gate.
assert_eq!(
active_aura_press(packed(2645, PET_ACT_COMMAND), Some(&running), Some(&d)),
None
);
}
/// The icon swap itself: `ActiveIconID`'s texture replaces `SpellIconID`'s while the spell is
/// running, and the button keeps everything else it had.
#[test]
fn an_active_pet_spell_draws_its_active_icon() {
let slot = packed(2645, benilla_protocol::messages::PET_TYPE_SPELL_FIRST);
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let d = toggle_spell();
let idle = slot_view(slot, &bar, Some(&d), None, false, false);
assert_eq!(idle.texture, d.icon);
let active = slot_view(slot, &bar, Some(&d), None, false, true);
assert_eq!(active.texture, d.active_icon);
assert_eq!(
active.name, idle.name,
"only the icon swaps — the name, rank and autocast flags are untouched"
);
assert!(
!active.active,
"and it is still not `isActive`: a spell slot pushes nil there on every path"
);
}
/// An active spell whose `ActiveIconID` does not resolve in `SpellIcon.dbc` pushes **nil**,
/// not the inactive art. The reference looks up whichever id the predicate chose and gives up
/// if that lookup fails (`0x4bdd50`) — falling back would draw "not running" on a running
/// spell, which is worse than drawing nothing.
#[test]
fn an_unresolvable_active_icon_hides_rather_than_falling_back() {
let slot = packed(2645, benilla_protocol::messages::PET_TYPE_SPELL_FIRST);
let bar = state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE);
let d = benilla_formats::SpellDisplay {
active_icon_id: 9999,
active_icon: None,
..spell("Cower", None)
};
assert!(slot_view(slot, &bar, Some(&d), None, false, true)
.texture
.is_none());
}
/// **The menu's fork, and the direction of each mask** (decision 1066).
///
/// This is the test that exists because the two bits are one nibble apart and the failure is
/// silent both ways: a hunter offered *Dismiss* loses the taming chain's only exit, and a warlock
/// offered *Abandon* is offered a row the reference never shows them.
#[test]
fn the_menu_fork_reads_abandon_and_rename_off_the_right_bits() {
// A freshly tamed hunter pet carries both: abandon/rename/paperdoll show, dismiss hides.
assert_eq!(menu_predicates(0x30), (true, true));
// …and after its first rename the server clears only the rename bit.
assert_eq!(menu_predicates(0x20), (true, false));
// A warlock's demon carries neither — the whole menu flips to Dismiss.
assert_eq!(menu_predicates(0), (false, false));
// The masks are not interchangeable, which is the point of the test.
assert_eq!(menu_predicates(0x10), (false, true));
// Neighbouring flags are not these two: 0x8 is PLAYER_CONTROLLED (on every pet) and 0x40 is
// PLUS_MOB. Reading the pair as a byte rather than as two bits would swallow both.
assert_eq!(menu_predicates(0x8 | 0x40), (false, false));
}
/// The rename signal: a pet's cached name is dropped when *that pet's* timestamp moves, and on no
/// other transition. The guid half of the pair is what keeps the NEXT pet from reading as a
/// rename of the last one.
#[test]
fn only_the_same_pets_moving_timestamp_reads_as_a_rename() {
// Nothing seen before — a login, or the pet's object just streamed.
assert!(!was_renamed(None, (0xF14, Some(100))));
// The same pet, the same stamp: the ordinary frame, which is nearly all of them.
assert!(!was_renamed(Some((0xF14, Some(100))), (0xF14, Some(100))));
// The same pet, a moved stamp: the rename.
assert!(was_renamed(Some((0xF14, Some(100))), (0xF14, Some(200))));
// A field that had never been sent starting to arrive is also a move — a pet named for the
// first time goes from absent to a stamp, and its cached name is just as stale.
assert!(was_renamed(Some((0xF14, None)), (0xF14, Some(200))));
// A DIFFERENT pet is never a rename, whatever the stamps do — dismiss one pet, summon
// another, and the second must not have its name dropped on the first's account.
assert!(!was_renamed(Some((0xF14, Some(100))), (0xABC, Some(200))));
assert!(!was_renamed(Some((0xF14, Some(100))), (0xABC, Some(100))));
}
/// **Dismiss is a bar press with no button** — the correction the wow-re carve made to this build
/// (§11c). `PetDismiss 0x4be4d0` opens no packet: it stages the packed word `0x07000003` and hands
/// it to the same dispatcher every pet-bar click uses, so it leaves as `CMSG_PET_ACTION`.
///
/// The literal is pinned here against the word the constants build, because the two ways of saying
/// it are the two halves of the finding, and the first draft of this feature sent
/// `CMSG_PET_ABANDON` for both rows — which vmangos would have honoured, silently.
#[test]
fn the_dismiss_word_is_the_carved_literal() {
assert_eq!(
PET_COMMAND_DISMISS | (u32::from(PET_ACT_COMMAND) << 24),
0x0700_0003,
);
// …and it decodes back as the command token it is, through the client's own masked read.
let entry = PetActionEntry::from(0x0700_0003u32);
assert_eq!(entry.kind(), PET_ACT_COMMAND);
assert_eq!(entry.action(), PET_COMMAND_DISMISS);
assert!(!entry.is_spell(), "dismiss is a command, never a cast");
}
/// The bar's two events are two edges (1953): a change of the slots fires `PET_BAR_UPDATE`; a
/// change of the cooldown triples alone fires `PET_BAR_UPDATE_COOLDOWN` — the reference's
/// cooldown-subsystem fire for the pet bank (wow-re `pet-action-bar-api.md` §9) — and the
/// pushed state carries the new triple either way.
#[test]
fn a_cooldown_alone_fires_the_cooldown_event_and_not_the_bar_update() {
use bevy::prelude::*;
use std::collections::HashMap;
const CLAW: u32 = 3010;
let claw = || benilla_formats::SpellDisplay {
recovery_ms: 8_000,
..spell("Claw", None)
};
let mut bar = PetBar {
spells: state(PET_COMMAND_FOLLOW, PET_REACT_DEFENSIVE),
..Default::default()
};
bar.spells.bar[3] = packed(CLAW, PET_ACT_ENABLED);
let mut app = App::new();
app.init_resource::<crate::ui_script::UiClock>()
.init_resource::<crate::net::GuidIndex>()
.insert_resource(crate::ui_action::Spells {
catalog: benilla_formats::SpellCatalog::from_displays(HashMap::from([(CLAW, claw())])),
..crate::ui_action::Spells::empty_for_tests()
})
.insert_resource(bar)
.add_systems(Update, super::bar::feed_pet_bar);
let script = benilla_ui::script::UiScript::new().unwrap();
script
.run(
r#"
SEEN = {}
local f = CreateFrame("Frame")
f:RegisterEvent("PET_BAR_UPDATE")
f:RegisterEvent("PET_BAR_UPDATE_COOLDOWN")
f:SetScript("OnEvent", function() table.insert(SEEN, event) end)
"#,
)
.unwrap();
app.insert_non_send_resource(script);
let seen = |app: &mut App| -> Vec<String> {
app.update();
let mut s = app
.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>();
s.resolve();
let out = s.eval::<Vec<String>>("return SEEN").unwrap();
s.run("SEEN = {}").unwrap();
out
};
assert_eq!(
seen(&mut app),
vec!["PET_BAR_UPDATE".to_string()],
"the bar arrives"
);
assert_eq!(seen(&mut app), Vec::<String>::new(), "nothing moved");
app.world_mut()
.resource_mut::<PetBar>()
.cooldowns
.start_spell(CLAW, &claw(), 0, std::time::Instant::now());
assert_eq!(
seen(&mut app),
vec!["PET_BAR_UPDATE_COOLDOWN".to_string()],
"a cooldown alone is the bank's edge, not the bar's"
);
let (start, duration, enable): (f64, f64, i32) = app
.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>()
.eval("return GetPetActionCooldown(4)")
.unwrap();
assert!(
duration > 7.9 && duration < 8.1,
"the triple was pushed: {start} {duration}"
);
assert_eq!(enable, 1);
}