Party members had no buffs because nothing ever wrote their auras
grom reported it from play: the party frames show no buffs. pfUI's group header
reads `UnitBuff("party1", i)` and the stock `PartyMemberFrame_UpdateAuras` reads
the same verb, and nothing was wrong with the verb — `ui_aura`'s feed pushed six
tokens (player, target, pet, targettarget and ClassicAPI's focus pair) and not
one party token, so `nth_aura` found no list at all for `party1` and every buff
row stopped on the first nil.
The four tokens now get their own lists off `GroupState::party_slots`, through
the same `other_list` builder and the same `buffs_visible_on` gate the target,
pet and focus tokens use — so a party member shows exactly what a targeted party
member shows, and an empty slot clears its token. Each gets its own `UNIT_AURA`
fire keyed on guid and projection, because a party member's auras change on their
own clock and a `"target"` fire tells a party frame nothing.
Not the raid tokens: `raid1`..`raid40` is ten times this work every frame, each
list walking a descriptor and re-running the visibility gate, and that cost has
not been measured. The party's four are bounded and are what was reported.
The guid index and the roster share one system parameter because bevy's
`IntoSystem` tops out at sixteen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -304,6 +304,9 @@ struct AuraFeedMemo {
|
||||
/// ClassicAPI's focus pair (`docs/ADDON_COMPAT.md`), keyed like the three above.
|
||||
focus_last: Option<(u64, Vec<AuraProjection>)>,
|
||||
focus_target_last: Option<(u64, Vec<AuraProjection>)>,
|
||||
/// `party1`..`party4`, keyed like the rest: the guid makes a roster reshuffle re-fire even
|
||||
/// where two members carry the same auras.
|
||||
party_last: [Option<(u64, Vec<AuraProjection>)>; 4],
|
||||
}
|
||||
|
||||
// ─── nampower v2's DEBUFF_ADDED_OTHER / DEBUFF_REMOVED_OTHER ──────────────────────────────────
|
||||
@@ -682,7 +685,9 @@ fn feed_auras(
|
||||
stores: Query<&ObjectStore>,
|
||||
spells: Option<Res<Spells>>,
|
||||
pet: Res<crate::ui_pet::PetBar>,
|
||||
index: Res<GuidIndex>,
|
||||
// The guid index and the party roster, one parameter because bevy's `IntoSystem` tops out at
|
||||
// sixteen: both are "who is who" lookups this body only reads.
|
||||
(index, group): (Res<GuidIndex>, Res<crate::ui_party::GroupState>),
|
||||
// The `UnitBuff` gate's inputs, the same reaction the selection ring and nameplates use.
|
||||
factions: Option<Res<Factions>>,
|
||||
reputations: Res<Reputations>,
|
||||
@@ -961,6 +966,36 @@ fn feed_auras(
|
||||
focus_target_cur.is_some() && focus_target_cur != memo.focus_target_last;
|
||||
memo.focus_target_last = focus_target_cur;
|
||||
|
||||
// `party1`..`party4`. **The absence of these was a defect grom reported from play**: no buffs
|
||||
// on party members in the party frames, because pfUI's group header reads
|
||||
// `UnitBuff("party1", i)` (`api/unitframes.lua`) and this feed pushed six tokens, none of them
|
||||
// a party one, so `nth_aura` found no list at all and every party buff row stayed empty. The
|
||||
// stock party frames show buffs too (`PartyMemberFrame_UpdateAuras`), so nothing about this is
|
||||
// pfUI's.
|
||||
//
|
||||
// Nothing new is computed per unit: `other_list` is the same builder the target, pet and focus
|
||||
// tokens go through, with the same `buffs_visible_on` gate, so a party member shows exactly
|
||||
// what a targeted party member shows. Four slots in roster order
|
||||
// (`GroupState::party_slots`), which is the order the tokens themselves mean; an empty slot
|
||||
// clears its token.
|
||||
//
|
||||
// **Not the raid tokens.** `raid1`..`raid40` would be ten times this work every frame — each
|
||||
// list walks the unit's descriptor and re-runs the visibility gate — and the cost has not been
|
||||
// measured, where the party's four are bounded and are what was reported. Left as its own
|
||||
// piece of work with that measurement first; `ui_party::RAID_TOKENS` is the list.
|
||||
let party_guids: Vec<u64> = group.party_slots().map(|m| m.guid).collect();
|
||||
let party_lists: Vec<Option<Vec<AuraState>>> = crate::ui_party::PARTY_TOKENS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| {
|
||||
party_guids
|
||||
.get(i)
|
||||
.copied()
|
||||
.filter(|g| *g != 0)
|
||||
.and_then(other_list)
|
||||
})
|
||||
.collect();
|
||||
|
||||
script.set_auras("player", Some(list));
|
||||
// Clearing a token fires nothing: the frames react to `PLAYER_TARGET_CHANGED` and `UNIT_PET`,
|
||||
// and the focus pair to `PLAYER_FOCUS_CHANGED`.
|
||||
@@ -969,6 +1004,26 @@ fn feed_auras(
|
||||
script.set_auras("targettarget", tot_list);
|
||||
script.set_auras("focus", focus_list);
|
||||
script.set_auras("focustarget", focus_target_list);
|
||||
// The four party tokens, with their own edge keys: a party member's auras change on their
|
||||
// own clock, so a `"target"` fire tells a party frame nothing.
|
||||
let party_changed: Vec<bool> = party_lists
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, list)| {
|
||||
let cur = party_guids
|
||||
.get(i)
|
||||
.copied()
|
||||
.filter(|g| *g != 0)
|
||||
.zip(list.as_deref())
|
||||
.map(|(guid, l)| (guid, projection_of(l)));
|
||||
let moved = cur != mem.vm.get(&script).party_last[i];
|
||||
mem.vm.get(&script).party_last[i] = cur;
|
||||
moved
|
||||
})
|
||||
.collect();
|
||||
for (token, list) in crate::ui_party::PARTY_TOKENS.iter().zip(party_lists) {
|
||||
script.set_auras(token, list);
|
||||
}
|
||||
script.set_tracking(tracking);
|
||||
if changed {
|
||||
script.fire_event("UNIT_AURA", vec![ScriptValue::Str("player".into())]);
|
||||
@@ -991,6 +1046,11 @@ fn feed_auras(
|
||||
if focus_target_changed {
|
||||
script.fire_event("UNIT_AURA", vec![ScriptValue::Str("focustarget".into())]);
|
||||
}
|
||||
for (token, moved) in crate::ui_party::PARTY_TOKENS.iter().zip(&party_changed) {
|
||||
if *moved {
|
||||
script.fire_event("UNIT_AURA", vec![ScriptValue::Str((*token).to_string())]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── nampower v2's `DEBUFF_ADDED_OTHER` / `DEBUFF_REMOVED_OTHER` ──────────────────────────
|
||||
//
|
||||
@@ -1141,6 +1201,9 @@ fn end_session_aura_state(
|
||||
script.set_auras("targettarget", None);
|
||||
script.set_auras("focus", None);
|
||||
script.set_auras("focustarget", None);
|
||||
for token in crate::ui_party::PARTY_TOKENS {
|
||||
script.set_auras(token, None);
|
||||
}
|
||||
script.set_tracking(None);
|
||||
}
|
||||
cache.auras.clear();
|
||||
@@ -1180,6 +1243,8 @@ mod tests {
|
||||
.init_resource::<GuidIndex>()
|
||||
// A required system parameter, unlike `Factions`.
|
||||
.init_resource::<Reputations>()
|
||||
// Likewise: the feed reads the party roster for the four party tokens' lists.
|
||||
.init_resource::<crate::ui_party::GroupState>()
|
||||
.insert_resource(NetCommands(tx))
|
||||
.add_plugins(UiAuraPlugin);
|
||||
app
|
||||
@@ -1462,6 +1527,104 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The defect grom reported from play: **no buffs on party members in the party frames**.
|
||||
///
|
||||
/// This feed pushed six tokens — player, target, pet, targettarget and ClassicAPI's focus pair
|
||||
/// — and not one party token, so `UnitBuff("party1", i)` found no list at all and every party
|
||||
/// buff row stayed empty, for pfUI's group header and for the stock
|
||||
/// `PartyMemberFrame_UpdateAuras` alike. Nothing was wrong with `UnitBuff`; the state it reads
|
||||
/// was never written.
|
||||
#[test]
|
||||
fn a_party_member_gets_its_own_aura_list_and_its_own_unit_aura_fire() {
|
||||
const ME: u64 = 0x0000_0000_0000_0001;
|
||||
const MATE: u64 = 0xF130_0000_9700_0009;
|
||||
|
||||
let mut app = aura_app();
|
||||
let me = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
crate::net::SelfPlayer,
|
||||
ObjectStore(benilla_protocol::ObjectFields::default()),
|
||||
crate::net::Guid(ME),
|
||||
))
|
||||
.id();
|
||||
// Auras visible, so the helpful half enumerates and the list is not empty by gate.
|
||||
let mate = app
|
||||
.world_mut()
|
||||
.spawn(ridge_stalker_slots(UNIT_FLAG_AURAS_VISIBLE))
|
||||
.id();
|
||||
let mut index = app.world_mut().resource_mut::<GuidIndex>();
|
||||
index.0.insert(ME, me);
|
||||
index.0.insert(MATE, mate);
|
||||
|
||||
// One party member in slot 1: same subgroup as us, which is what `party_slots` filters on.
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::ui_party::GroupState>()
|
||||
.members = vec![benilla_protocol::messages::GroupMemberEntry {
|
||||
name: "Mate".into(),
|
||||
guid: MATE,
|
||||
status: 1,
|
||||
flags: 0,
|
||||
}];
|
||||
|
||||
let script = UiScript::new().unwrap();
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
SEEN = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UNIT_AURA")
|
||||
f:SetScript("OnEvent", function() table.insert(SEEN, arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
app.insert_non_send_resource(script);
|
||||
|
||||
app.update();
|
||||
|
||||
let mut script = app.world_mut().non_send_resource_mut::<UiScript>();
|
||||
// By spell id, as the focus test does: this app has no `Spells` catalog, so no aura
|
||||
// resolves an icon and `UnitBuff`'s texture return would be nil either way.
|
||||
let ids = |script: &mut UiScript, filter: &str| -> Vec<u32> {
|
||||
script
|
||||
.eval::<Vec<u32>>(&format!(
|
||||
r#"local out = {{}} local i = 1
|
||||
while true do
|
||||
local a = C_UnitAuras.GetAuraDataByIndex("party1", i, "{filter}")
|
||||
if not a then break end
|
||||
table.insert(out, a.spellId) i = i + 1
|
||||
end
|
||||
return out"#
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
(ids(&mut script, "HELPFUL"), ids(&mut script, "HARMFUL")),
|
||||
(vec![5916], vec![22766]),
|
||||
"party1 enumerates that member's own auras"
|
||||
);
|
||||
// And the verb the party frames actually call, which is what was empty in play. By
|
||||
// **arity**, not by the texture: `UnitBuff` answers `(texture, stacks, ...)` and the
|
||||
// texture comes from the spell catalog, which this app has no `Spells` for — so the icon
|
||||
// would be nil here whatever the list holds. Arity is the whole question anyway: nothing
|
||||
// at all is what `nth_aura` returns for a token with no list, and nothing at all is what
|
||||
// pfUI's `if texture then` and the stock `PartyMemberFrame_UpdateAuras` loop stop on.
|
||||
assert!(
|
||||
script.arity(r#"UnitBuff("party1", 1)"#).unwrap() > 0,
|
||||
"UnitBuff answers for party1 instead of returning nothing"
|
||||
);
|
||||
assert_eq!(
|
||||
script.arity(r#"UnitBuff("party2", 1)"#).unwrap(),
|
||||
0,
|
||||
"an empty slot still answers nothing"
|
||||
);
|
||||
let seen = script.eval::<Vec<String>>("return SEEN").unwrap();
|
||||
assert!(
|
||||
seen.iter().any(|t| t == "party1"),
|
||||
"the party frame hears only its own token: {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// ClassicAPI's focus (`docs/ADDON_COMPAT.md`): pfUI's focus frame draws debuff buttons and
|
||||
/// hears only `UNIT_AURA` with its OWN token — the `"target"` fire tells it nothing — so the
|
||||
/// list has to be pushed per token and the event fired per token.
|
||||
|
||||
Reference in New Issue
Block a user