Unit aura bindings resolve their token as 1.12.1 does, so mouseover, party1target and any casing read the unit they name (#497)

`UnitBuff`, `UnitDebuff`, `UnitAura`, `CancelUnitBuff` and the tooltip's `SetUnitBuff`/`SetUnitDebuff` now resolve their unit token as 1.12.1's `0x515970` does before reading any aura, so `mouseover`, `party1target`, `raid3target`, `pettarget`, `targettargettarget` and any casing (`"PLAYER"`) answer for the unit they name, where before they answered nil.

- The resolver matches the nine base names as prefixes in the reference's order, folding ASCII case (`SStrCmpI 0x64a4c0`), with `npc` whole; the number after `party`/`raid` wraps at 32 bits and counts from 1; `mouseover` counts only while the unit is loaded (`0x515bd2`); each `target` suffix hops to that unit's `UNIT_FIELD_TARGET` (`0x5159d3`-`0x515a2c`); a name none of the nine match raises `Unknown unit name: %s` (`0x515c14`), before any index check, as `UnitBuff` calls it at `0x519542`.
- The VM holds one aura list per guid, plus the player's own in cache order (`0xbc6040`); any token naming the player reads that list, so its order and durations are unchanged.
- The feed builds lists only for guids some token can name, through the existing per-guid cache. `UNIT_AURA` fires for the same tokens as before.

Fixes #488

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-28 12:46:33 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8d31e9e91e
commit 65a5e44d48
17 changed files with 1141 additions and 471 deletions
+12 -15
View File
@@ -69,7 +69,7 @@ fn aura(
/// Push the list and fire `PLAYER_AURAS_CHANGED`, as the app's feed does (`BuffFrame.lua:113`).
fn push(s: &mut UiScript, auras: Vec<AuraState>) {
s.set_auras("player", Some(auras));
s.set_player_auras(auras);
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
}
@@ -351,7 +351,7 @@ fn a_refreshed_duration_reaches_the_bar_with_no_aura_event() {
assert_eq!(text(&s, "BuffButton0Duration"), "19 s");
// The refresh: a new expiry and no event.
s.set_auras("player", Some(vec![mark(300.0)]));
s.set_player_auras(vec![mark(300.0)]);
frame(&mut s, 0.1);
assert_eq!(
text(&s, "BuffButton0Duration"),
@@ -367,14 +367,14 @@ fn a_refreshed_duration_reaches_the_bar_with_no_aura_event() {
// timed aura with no stamp yet shows "0 s" and pulses until the stamp joins.
let mut pending = mark(0.0);
pending.until_cancelled = false; // a timed spell, stamp not yet arrived
s.set_auras("player", Some(vec![pending]));
s.set_player_auras(vec![pending]);
frame(&mut s, 0.1);
assert_eq!(
text(&s, "BuffButton0Duration"),
"0 s",
"the reference draws the floor of a timed aura it has no stamp for, not a blank"
);
s.set_auras("player", Some(vec![mark(60.0)])); // the stamp joins, still no event
s.set_player_auras(vec![mark(60.0)]); // the stamp joins, still no event
frame(&mut s, 0.1);
assert_eq!(
text(&s, "BuffButton0Duration"),
@@ -885,17 +885,14 @@ fn the_duration_line_reads_the_real_global_strings() {
// `secs_left` seconds remaining at GetTime = 10, read back as the tooltip's last line.
let mut line = |secs_left: f64| -> String {
s.set_auras(
"player",
Some(vec![AuraState {
spell_id: 1459,
name: Some("Arcane Intellect".into()),
duration: 86_400.0,
expiration_time: 10.0 + secs_left,
helpful: true,
..Default::default()
}]),
);
s.set_player_auras(vec![AuraState {
spell_id: 1459,
name: Some("Arcane Intellect".into()),
duration: 86_400.0,
expiration_time: 10.0 + secs_left,
helpful: true,
..Default::default()
}]);
s.run(
r#"
BENILLA_TIP:SetOwner(BENILLA_ANCHOR, "ANCHOR_RIGHT")
@@ -260,8 +260,15 @@ fn the_debuff_row_fills_from_the_pets_own_auras() {
s.set_unit("pet", Some(pet("Grimjaw", 72, 45, 80, 0)));
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
s.set_auras(
"pet",
// `"pet"` resolves to the pet's guid, whose list the row reads.
const PET: u64 = 0xF140_0000_0000_0077;
s.set_unit_guids(&benilla_ui::script::UnitGuids {
player: 1,
pet: PET,
..Default::default()
});
s.set_unit_auras(
PET,
Some(vec![pet_buff(1000, "Rend", 1), pet_buff(1001, "Sunder", 3)]),
);
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("pet".into())]);
@@ -289,7 +296,7 @@ fn the_debuff_row_fills_from_the_pets_own_auras() {
);
assert!(draws(&mut s, "Interface\\Icons\\Spell_1001"));
s.set_auras("pet", Some(vec![]));
s.set_unit_auras(PET, Some(vec![]));
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("pet".into())]);
assert!(!s
.eval::<bool>("return PetFrameDebuff1:IsVisible()")
@@ -323,22 +323,23 @@ fn auto_shot_bar_drains_the_moment_an_ammo_lock_event_arrives() {
/// Seat one active player buff by name and announce it the way the app's aura feed does.
fn seat_a_buff(s: &mut UiScript, spell_id: u32, name: &str, icon: &str) {
use benilla_ui::script::AuraState;
s.set_auras(
"player",
Some(vec![AuraState {
spell_id,
name: Some(name.into()),
icon: Some(icon.into()),
count: 0,
debuff_type: None,
duration: 0.0,
expiration_time: 0.0,
helpful: true,
cancelable: true,
until_cancelled: true,
channeled: false,
}]),
);
s.set_unit_guids(&benilla_ui::script::UnitGuids {
player: 1,
..Default::default()
});
s.set_player_auras(vec![AuraState {
spell_id,
name: Some(name.into()),
icon: Some(icon.into()),
count: 0,
debuff_type: None,
duration: 0.0,
expiration_time: 0.0,
helpful: true,
cancelable: true,
until_cancelled: true,
channeled: false,
}]);
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
}
@@ -1,7 +1,7 @@
//! The stock target frame's aura rows (`TargetFrame.lua:263`) under a stubbed feed.
//! BuffFrame.xml loads first for the `DebuffTypeColor` and `RefreshBuffs` the frames use.
use benilla_ui::script::{AuraState, QuadContent, ScriptValue, UiScript, UnitState};
use benilla_ui::script::{AuraState, QuadContent, ScriptValue, UiScript, UnitGuids, UnitState};
use super::test_ui::load_ui as load_xml;
@@ -56,10 +56,18 @@ fn target(s: &mut UiScript, reaction: u8, auras: Vec<AuraState>) {
..UnitState::default()
}),
);
s.set_auras("target", Some(auras));
s.set_unit_guids(&UnitGuids {
player: 1,
target: TARGET,
..Default::default()
});
s.set_unit_auras(TARGET, Some(auras));
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
}
/// The target's guid, which `"target"` resolves to.
const TARGET: u64 = 0xF130_0000_0000_0001;
fn debuff(spell_id: u32, name: &str, count: u8, debuff_type: Option<&str>) -> AuraState {
AuraState {
spell_id,
@@ -235,7 +243,7 @@ fn reaching_the_wrap_shrinks_the_first_row_to_17px() {
assert_eq!(size(&s, "TargetFrameBuff1"), (17.0, 17.0));
// Below the wrap they grow back; the feed re-fires UNIT_AURA on the change.
s.set_auras("target", Some(vec![debuff(1000, "D0", 1, None)]));
s.set_unit_auras(TARGET, Some(vec![debuff(1000, "D0", 1, None)]));
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
assert_eq!(size(&s, "TargetFrameDebuff1"), (21.0, 21.0));
assert_eq!(size(&s, "TargetFrameDebuff1Border"), (23.0, 23.0));
@@ -249,7 +257,7 @@ fn clearing_the_list_or_the_target_hides_the_buttons() {
target(&mut s, 2, vec![debuff(589, "Pain", 1, Some("Magic"))]);
assert!(shown(&s, "TargetFrameDebuff1"));
s.set_auras("target", Some(vec![]));
s.set_unit_auras(TARGET, Some(vec![]));
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
assert!(
!shown(&s, "TargetFrameDebuff1"),
@@ -260,7 +268,7 @@ fn clearing_the_list_or_the_target_hides_the_buttons() {
target(&mut s, 2, vec![debuff(589, "Pain", 1, Some("Magic"))]);
assert!(shown(&s, "TargetFrameDebuff1"));
s.set_unit("target", None);
s.set_auras("target", None);
s.set_unit_auras(TARGET, None);
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
assert!(!shown(&s, "TargetFrameDebuff1"), "no target, no buttons");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -432,22 +432,19 @@ fn buff_hover_hangs_below_left_of_the_button() {
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\BuffFrame.xml",
]);
s.set_auras(
"player",
Some(vec![AuraState {
spell_id: 1459,
name: Some("Arcane Intellect".into()),
icon: Some("Interface\\Icons\\Spell_Holy_MagicalSentry".into()),
count: 1,
debuff_type: None,
duration: 1800.0,
expiration_time: 1800.0,
helpful: true,
cancelable: true,
until_cancelled: false,
channeled: false,
}]),
);
s.set_player_auras(vec![AuraState {
spell_id: 1459,
name: Some("Arcane Intellect".into()),
icon: Some("Interface\\Icons\\Spell_Holy_MagicalSentry".into()),
count: 1,
debuff_type: None,
duration: 1800.0,
expiration_time: 1800.0,
helpful: true,
cancelable: true,
until_cancelled: false,
channeled: false,
}]);
// The event the stock buff buttons register (`BuffFrame.lua:113`).
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
s.resolve();
@@ -402,8 +402,9 @@ fn the_debuff_row_draws_what_the_unit_carries() {
benilla_formats::wow_data_or_skip!();
let mut s = load_tot();
switch_on(&mut s);
s.set_auras(
"targettarget",
s.set_unit_guids(&unit_guids());
s.set_unit_auras(
TOT,
Some(vec![
debuff(1000, "Rend", None),
debuff(1001, "Curse of Agony", Some("Curse")),
@@ -437,7 +438,8 @@ fn the_target_rows_wrap_short_while_the_frame_stands_beside_them() {
benilla_formats::wow_data_or_skip!();
let mut s = load_tot();
// A hostile target, so the debuffs lead and the buffs hang off them.
s.set_auras("target", Some(vec![debuff(2000, "Sunder", None)]));
s.set_unit_guids(&unit_guids());
s.set_unit_auras(TARGET, Some(vec![debuff(2000, "Sunder", None)]));
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
let anchor = |s: &mut UiScript, frame: &str| -> String {
@@ -498,3 +500,17 @@ fn the_reconcile_takes_the_frame_down_when_the_token_goes_silent() {
assert!(shown(&mut s), "and brings it back");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
const TARGET: u64 = 0xF130_0000_0000_0001;
const TOT: u64 = 0xF130_0000_0000_0002;
/// The resolver's inputs: `"target"` is `TARGET`, held and targeting `TOT`, so `"targettarget"`
/// is `TOT`.
fn unit_guids() -> benilla_ui::script::UnitGuids {
benilla_ui::script::UnitGuids {
player: 1,
target: TARGET,
held: [(TARGET, TOT)].into_iter().collect(),
..Default::default()
}
}