auras: the player's first buff of a session was swallowed as a first-time diff
DoiteAuras reads the whole aura block once at PLAYER_ENTERING_WORLD and from then on keeps its
table purely from nampower's four self-aura events (`Modules/DoitePlayerAuras.lua:403-490`;
`UpdateAuras` is called nowhere else). The arrival loop here seeds a unit's memory and fires
nothing, which is right for a unit streamed in mid-aura — but it only ever seeded the harmful
half, while the comment above it claimed it seeded the player's helpful half too ("without this
its first real edge would be swallowed as a first-time diff, which is one lost BUFF_ADDED_SELF
per login"). The guid was destructured away as `_guid` and `own_buffs_seeded` was set nowhere but
in the edge loop.
So the player's first buff edge of every session reported nothing. That does not heal: a later
cast of a buff that is still running refreshes its slot with the same spell id, which is not an
edge this watch sees at all, so nothing reports the aura again until it drops completely. A
warrior's Battle Shout, re-cast while it still has a minute to run, never drops — grom's rule for
it read "missing" with the buff plainly up, all session.
Every other link was measured clean first: `GetUnitField("player", "aura")` answers a table, the
arg vector matches what the handler reads (arg3 spell id, arg4 stacks, arg6 raw slot, arg7 state),
and all seven ranks of Battle Shout resolve through both `GetSpellRecField(id, "name")` and
`GetSpellNameAndRankForId`.
The pin asserts both halves — the first buff is an add, and a refresh is not an edge — and its
control is worth noting: with the seed removed the first buff reports `[]`, not a wrong value,
which is why no session log ever named this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -1079,10 +1079,22 @@ fn feed_auras(
|
||||
// a burst exactly where it hurts, on streaming into a crowd). The seed is what makes the next
|
||||
// real application an *add* rather than being swallowed as a first-time diff.
|
||||
for e in &arrived {
|
||||
if let Ok((store, _guid)) = watched.get(e) {
|
||||
// The player is seeded here too now that it has events of its own: without this its
|
||||
// first real edge would be swallowed as a first-time diff, which is one lost
|
||||
// `BUFF_ADDED_SELF` per login.
|
||||
if let Ok((store, guid)) = watched.get(e) {
|
||||
// The player's HELPFUL half is seeded here too, because it has events of its own.
|
||||
// This was described here before it was done: the loop below treats an unseeded
|
||||
// `own_buffs` as first sight and fires nothing, so without this seed the player's
|
||||
// first buff edge of the session is swallowed as a first-time diff.
|
||||
//
|
||||
// That loss does not heal. A later cast of a buff that is still up refreshes its slot
|
||||
// with the same spell id, which is not an edge this watch sees at all (see
|
||||
// `AURA_ID_SPAN`), so nothing ever reports the aura again until it drops completely.
|
||||
// DoiteAuras keeps its own table purely from these events, so the buff read as missing
|
||||
// for the rest of the session — and a warrior's Battle Shout, re-cast while it is
|
||||
// still running, is exactly the buff that never drops.
|
||||
if guid.0 == self_guid.0 && !*own_buffs_seeded {
|
||||
collect_aura_slots(store, own_buffs, false);
|
||||
*own_buffs_seeded = true;
|
||||
}
|
||||
if !debuffs.contains_key(&e) {
|
||||
collect_debuff_slots(store, debuff_scratch);
|
||||
debuffs.insert(e, debuff_scratch.clone());
|
||||
@@ -2110,6 +2122,97 @@ end)
|
||||
assert!(seen(&mut app).is_empty());
|
||||
}
|
||||
|
||||
/// The player's own HELPFUL half, which only it has events for.
|
||||
const SELF_BUFF_EVENTS: &str = r#"
|
||||
seen = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("BUFF_ADDED_SELF")
|
||||
f:RegisterEvent("BUFF_REMOVED_SELF")
|
||||
f:SetScript("OnEvent", function()
|
||||
table.insert(seen, string.format("%s|%d|%d|%d|%d", event, arg3, arg4, arg6, arg7))
|
||||
end)
|
||||
"#;
|
||||
|
||||
/// [`aura_update`] over the **helpful** slots: the edges the net path raises for a buff.
|
||||
fn buff_update(app: &mut App, entity: Entity, guid: u64, rows: &[(u8, u32, u8, u8)]) {
|
||||
*app.world_mut().get_mut::<ObjectStore>(entity).unwrap() = unit_with_debuffs(rows);
|
||||
for slot in 0..UNIT_AURA_POSITIVE_SLOTS {
|
||||
app.world_mut().write_message(crate::net::FieldChanged {
|
||||
entity,
|
||||
guid,
|
||||
kind: benilla_protocol::messages::ObjectType::Unit,
|
||||
index: F_AURA + u16::from(slot),
|
||||
old: 0,
|
||||
new: 0,
|
||||
});
|
||||
}
|
||||
app.update();
|
||||
}
|
||||
|
||||
/// The player's first buff edge of a session has to be an *add*, not a seed.
|
||||
///
|
||||
/// The arrival frame seeds the helpful half, so a buff applied afterwards reports
|
||||
/// `BUFF_ADDED_SELF`. This was described in the feed before it was done: with no seed, the
|
||||
/// first edge was taken for first sight and fired nothing.
|
||||
///
|
||||
/// And that loss does not heal, which is the second half of this test: a later cast of a buff
|
||||
/// that is still up refreshes its slot with the same spell id, and that is not an edge this
|
||||
/// watch sees at all — so nothing reports the aura again until it drops completely.
|
||||
/// DoiteAuras keeps its own aura table purely from these four events
|
||||
/// (`Modules/DoitePlayerAuras.lua:403-490`), so the buff read as missing for the rest of the
|
||||
/// session. A warrior's Battle Shout, re-cast while it is still running, never drops.
|
||||
#[test]
|
||||
fn the_players_first_buff_of_a_session_is_an_add_and_a_refresh_is_not_an_edge() {
|
||||
let mut app = aura_app();
|
||||
let me = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
crate::net::SelfPlayer,
|
||||
unit_with_debuffs(&[]),
|
||||
crate::net::Guid(PLAYER),
|
||||
))
|
||||
.id();
|
||||
app.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.insert(PLAYER, me);
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
guid: PLAYER,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
script.run(SELF_BUFF_EVENTS).unwrap();
|
||||
app.insert_non_send_resource(script);
|
||||
|
||||
// The arrival frame: the player is `Added<ObjectStore>` here, so the memory seeds.
|
||||
app.update();
|
||||
assert!(
|
||||
seen(&mut app).is_empty(),
|
||||
"arriving with no buffs is no edge"
|
||||
);
|
||||
|
||||
// Battle Shout, rank 1, in the first helpful slot.
|
||||
buff_update(&mut app, me, PLAYER, &[(0, 6673, 1, 60)]);
|
||||
let rows = seen(&mut app);
|
||||
assert_eq!(
|
||||
rows,
|
||||
vec!["BUFF_ADDED_SELF|6673|1|0|0".to_string()],
|
||||
"the session's first buff is an add, not a swallowed first-time diff"
|
||||
);
|
||||
|
||||
// Re-cast while it is still up: same slot, same id, no edge — so an add missed above
|
||||
// would never be reported again.
|
||||
buff_update(&mut app, me, PLAYER, &[(0, 6673, 1, 60)]);
|
||||
assert!(
|
||||
seen(&mut app).is_empty(),
|
||||
"a refresh that keeps the slot's spell is not an edge"
|
||||
);
|
||||
}
|
||||
|
||||
/// The cost property, stated as a test: no aura-id field edge, no work and no event — even
|
||||
/// though the descriptor itself changed. This is what keeps the pair off the per-frame budget.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user