From afc5379911d7cf746004117b11cdf2d687f1e462 Mon Sep 17 00:00:00 2001 From: grom Date: Tue, 3 Nov 2026 08:22:20 -0700 Subject: [PATCH] auras: the player's first buff of a session was swallowed as a first-time diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/ui_aura.rs | 111 ++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/crates/benilla-app/src/ui_aura.rs b/crates/benilla-app/src/ui_aura.rs index 4277318c..05e79bab 100644 --- a/crates/benilla-app/src/ui_aura.rs +++ b/crates/benilla-app/src/ui_aura.rs @@ -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::(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::() + .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` 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]