app+protocol: nampower's ten cast events, off the queue that already had them
All ten ride `ui_cast_events.rs`'s existing `CastEventFeed` rows — the same rows `UNIT_CASTEVENT` and the cast windows are folded from — with a new `NpCastColumns` carrying the fields nampower's shapes want and SuperWoW's one word has no place for. One row per packet, folded once, which is this file's own rule: an addon woken by one of these and then asking a getter must not be able to get a contradicting answer, and a parallel queue over the same packets is exactly how that happens. `NP_EnableSpellStartEvents` and `NP_EnableSpellGoEvents` go from stored-only to LIVE. Their rows said the shapes wanted "itemId/spellType/corpseOwnerGuid columns this client does not carry"; two of those three turned out to be a Spell.dbc lookup and a guid hop the item-cooldown path already made. `itemId` is the cast item's entry through `item_entry` (now shared with `item_cooldown`, which had the same two hops inline) — `SessionEvent::SpellStart` was dropping the guid the GO always carried, so it carries it now. `spellType` is `AttributesEx & 0x44` for a channel and the auto-repeat bit for a shot. `channel_ms` is `SpellDuration.dbc`. The GO's two counts are the packet's own lists, capped at 255 because the server writes both as `u8`s. `SPELL_FAILED_SELF`'s result byte is the failing `SMSG_CAST_RESULT`'s reason — the number 1.12 turns into a red line and then discards, so no addon on that surface can see why a cast failed. `corpseOwnerGuid` is the one column that really is absent, and it is not sent at all: a trailing nil in Lua is indistinguishable from an absent return, and upstream sends nil for every cast without a corpse target. For the handful that have one the gap is bounded and written down — the targets block reads the corpse guid and drops it, and the owner is a further hop through that corpse's own CORPSE_FIELD_OWNER. Two subtleties, both pinned by tests: - A channel's START is queued although its wire cast time is 0, and `UNIT_CASTEVENT`'s START is still suppressed for it. Without the row, `SPELL_START_SELF` would never fire for Mind Flay or Blizzard — pfUI's reader expects exactly that row and falls back to arg7 "only for true channels like Blizzard where arg6 is 0/nil". With the word, every cast-tracking addon on this realm would get a SuperWoW START whose arg5 is 0, which is a cast bar told to run for no time. So the suppression moved from the producer to the fire, and SuperWoW's word behaves exactly as it did before the row existed. - The `_SELF`/`_OTHER` split is by caster here and by *target* for the aura family next door. Neither is a guess: upstream's wording for the aura pair is "casts that land on the active player", and for this one pfUI handles both names in a single arm and then reads arg3 to learn the caster, which only makes sense if the name already settled it. A gate hole closed on the way past: `AURA_CAST_ON_SELF`/`_OTHER` were fired through one `&str` variable, and `reference_ui.rs`'s beyond-1.12 event gate reads the string literal *at the call* — so it had been blind to that pair for as long as they had existed. Both are literals now, with their rows, and the probe confirms it: renaming either row makes the gate name the event. Tests read pfUI's own `local` bindings back out rather than a list of values in push order, as the aura-cast block beside them does. A positional shape got backwards here fails in silence — the handler binds arg3 to casterGuid whatever arg3 happens to be — and this family is where that would be most expensive, since pfUI's swing timer resets off the START and its whole debuff bookkeeping gates on the GO's two counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -1260,26 +1260,27 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
which benilla folds out of `SMSG_SPELL_GO` plus `Spell.dbc`'s aura effects — see \
|
||||
crate::ui_cast_events::AuraCastFeed",
|
||||
),
|
||||
// The other four name event classes nothing here emits. They are stored so the write
|
||||
// `SPELL_START_*` and `SPELL_GO_*` are LIVE too, as of the round that read nampower's own
|
||||
// registration table: the columns their rows were said to be missing turned out to be one
|
||||
// `Spell.dbc` lookup (`spellType`, a channel's length) and one guid hop the item-cooldown
|
||||
// path already made (`itemId`) — see `ui_cast_events.rs`'s `NpCastColumns`, which names the
|
||||
// one column that really is absent (`corpseOwnerGuid`) and what it would take.
|
||||
//
|
||||
// The remaining two name event classes nothing here emits. They are stored so the write
|
||||
// round-trips and `GetCVar` answers, and consumed by nothing — the same deliberate,
|
||||
// documented shape as the 22 stored-only queue rows above, and for the same reason: the
|
||||
// alternative is not "they work", it is the engine warning this task started from.
|
||||
//
|
||||
// What each would need, stated so the gap is a gap and not a mystery:
|
||||
// - `SPELL_START_*`/`SPELL_GO_*` carry nampower's own 9- and 8-argument shapes (itemId,
|
||||
// spellId, caster, target, castFlags, …, corpseOwnerGuid). The packets are already folded
|
||||
// into `CastEdge::Start`/`Cast` (`ui_cast_events.rs`), so the data is largely there, but the
|
||||
// `itemId`/`spellType`/`corpseOwnerGuid` columns are not, and pfUI's consumers of those two
|
||||
// (libpredict's HoT sender, swingtimer, actionbar's prowl hook) key on arguments this
|
||||
// client would have to guess at.
|
||||
// - `AUTO_ATTACK_*` needs a melee-swing edge no path here raises (`CastEdge`'s own note: the
|
||||
// swing path produces no cast edge at all).
|
||||
// - `SPELL_HEAL_*` needs the heal amount off `SMSG_SPELLHEALLOG`, which nothing reads yet.
|
||||
ours(
|
||||
"NP_EnableSpellStartEvents",
|
||||
"0",
|
||||
"nampower v2 addon-compat, STORED ONLY: benilla fires no `SPELL_START_SELF`/`_OTHER` — \
|
||||
their shape wants itemId/spellType/corpseOwnerGuid columns this client does not carry",
|
||||
"nampower v2 addon-compat, LIVE: the gate on `SPELL_START_SELF`/`_OTHER`, fired off the \
|
||||
same `SMSG_SPELL_START` row `UNIT_CASTEVENT` reads — see \
|
||||
crate::ui_cast_events::NpCastGates",
|
||||
),
|
||||
// The six `NP_EnableUnitEventsX` switches (nampower v2.33+): which unit tokens fire the
|
||||
// GUID-keyed unit events, so an addon can cut the stream to the units it watches.
|
||||
@@ -1327,8 +1328,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
ours(
|
||||
"NP_EnableSpellGoEvents",
|
||||
"0",
|
||||
"nampower v2 addon-compat, STORED ONLY: benilla fires no `SPELL_GO_SELF`/`_OTHER` — same \
|
||||
missing columns as the start pair",
|
||||
"nampower v2 addon-compat, LIVE: the gate on `SPELL_GO_SELF`/`_OTHER`, whose two target \
|
||||
counts come off the same `SMSG_SPELL_GO` hit and miss lists",
|
||||
),
|
||||
ours(
|
||||
"NP_EnableAutoAttackEvents",
|
||||
@@ -4287,9 +4288,11 @@ mod tests {
|
||||
|app| {
|
||||
app.add_observer(crate::world_backdrop::on_cvar);
|
||||
},
|
||||
// nampower v2's `AURA_CAST_ON_*` switch (`docs/ADDON_COMPAT.md`).
|
||||
// nampower v2's three live event switches: `AURA_CAST_ON_*` and the START/GO pair
|
||||
// (`docs/ADDON_COMPAT.md`).
|
||||
|app| {
|
||||
app.init_resource::<crate::ui_cast_events::AuraCastFeed>();
|
||||
app.init_resource::<crate::ui_cast_events::NpCastGates>();
|
||||
app.add_observer(crate::ui_cast_events::on_cvar);
|
||||
},
|
||||
];
|
||||
@@ -4920,19 +4923,12 @@ mod tests {
|
||||
"NP_RetryServerRejectedSpells",
|
||||
"NampowerSettings' behaviour toggles",
|
||||
),
|
||||
// nampower v2's event-class switches. `NP_EnableAuraCastEvents` is deliberately NOT
|
||||
// in this list: `ui_cast_events.rs` reads it as the `AURA_CAST_ON_*` gate and must
|
||||
// keep proving so. These four name event classes benilla emits none of (each row in
|
||||
// `REGISTERED` says what it would take), so pfUI's libdebuff — which writes all five
|
||||
// at `PLAYER_LOGIN` — is their only reader.
|
||||
(
|
||||
"NP_EnableSpellStartEvents",
|
||||
"pfUI libs/libdebuff.lua writes it at PLAYER_LOGIN; no SPELL_START_* here",
|
||||
),
|
||||
(
|
||||
"NP_EnableSpellGoEvents",
|
||||
"pfUI libs/libdebuff.lua writes it at PLAYER_LOGIN; no SPELL_GO_* here",
|
||||
),
|
||||
// nampower v2's event-class switches. Three are deliberately NOT in this list —
|
||||
// `NP_EnableAuraCastEvents`, `NP_EnableSpellStartEvents` and
|
||||
// `NP_EnableSpellGoEvents` — because `ui_cast_events.rs` reads each as a real event
|
||||
// gate and must keep proving so. The two below name event classes benilla emits none
|
||||
// of (each row in `REGISTERED` says what it would take), so pfUI's libdebuff — which
|
||||
// writes all five at `PLAYER_LOGIN` — is their only reader.
|
||||
(
|
||||
"NP_EnableAutoAttackEvents",
|
||||
"pfUI libs/libdebuff.lua writes it at PLAYER_LOGIN; no AUTO_ATTACK_* here",
|
||||
|
||||
@@ -239,6 +239,7 @@ fn on_spell_start(In(ev): In<SessionEvent>, mut l: Lifecycle, mut sc: Scene) {
|
||||
cast_time_ms,
|
||||
target,
|
||||
ammo_display_id,
|
||||
item_caster,
|
||||
} = ev
|
||||
{
|
||||
spell_start(
|
||||
@@ -264,8 +265,51 @@ fn on_spell_start(In(ev): In<SessionEvent>, mut l: Lifecycle, mut sc: Scene) {
|
||||
// `spell_start` just used for the stock bar, read from the one body so the two bars cannot
|
||||
// disagree about which spells have one. The event itself is ungated by it — `arg5` is
|
||||
// documented as the wire's cast time, not as "the bar's length".
|
||||
if cast_time_ms > 0 {
|
||||
l.cast_events_lua.push(
|
||||
//
|
||||
// nampower's `SPELL_START_SELF`/`_OTHER` ride the same row, with the columns that event
|
||||
// carries and `UNIT_CASTEVENT` has no place for: the cast item's entry, the wire's own
|
||||
// cast flags, the spell's kind, and a channel's length. The kind and the length come off
|
||||
// `Spell.dbc` because the wire says nothing about either — a channeled spell's START
|
||||
// carries `cast_time_ms` 0 and its duration arrives a round trip later in
|
||||
// `MSG_CHANNEL_START`, which is exactly the case pfUI's reader falls back to `arg7` for.
|
||||
//
|
||||
// **A channel's START is queued although its cast time is 0.** vmangos sends
|
||||
// `SMSG_SPELL_START` for a channeled spell with `cast_time_ms` 0 — the channel's own
|
||||
// length arrives a round trip later in `MSG_CHANNEL_START` — so the `cast_time_ms > 0`
|
||||
// test alone would drop it, and nampower's `SPELL_START_*` would never fire for Mind
|
||||
// Flay or Blizzard. pfUI's reader expects exactly that row: it falls back to `arg7`
|
||||
// "only for true channels like Blizzard where arg6 is 0/nil"
|
||||
// (`libdebuff.lua:913-916`). `UNIT_CASTEVENT`'s own START is still suppressed for it, at
|
||||
// the fire — see `drain_cast_edges` — so SuperWoW's word behaves exactly as it did
|
||||
// before this row existed.
|
||||
let channeled = l
|
||||
.spells
|
||||
.as_ref()
|
||||
.and_then(|s| s.catalog.get(spell_id))
|
||||
.is_some_and(|d| d.tooltip_channeled());
|
||||
if cast_time_ms > 0 || channeled {
|
||||
let def = l.spells.as_ref().and_then(|s| s.catalog.get(spell_id));
|
||||
let np = crate::ui_cast_events::NpCastColumns {
|
||||
item_id: item_caster
|
||||
.and_then(|guid| item_entry(guid, &l.index, &sc.stores))
|
||||
.unwrap_or(0),
|
||||
cast_flags,
|
||||
spell_type: match def {
|
||||
Some(d) if d.tooltip_channeled() => 1,
|
||||
Some(d) if d.auto_repeat() => 2,
|
||||
_ => 0,
|
||||
},
|
||||
channel_ms: match def {
|
||||
Some(d) if d.tooltip_channeled() => crate::ui_cast_events::aura_duration_ms(
|
||||
l.spells
|
||||
.as_ref()
|
||||
.and_then(|s| s.durations.get(d.duration_index)),
|
||||
),
|
||||
_ => 0,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
l.cast_events_lua.push_detailed(
|
||||
caster,
|
||||
target,
|
||||
crate::ui_cast_events::CastEdge::Start {
|
||||
@@ -273,6 +317,7 @@ fn on_spell_start(In(ev): In<SessionEvent>, mut l: Lifecycle, mut sc: Scene) {
|
||||
},
|
||||
spell_id,
|
||||
cast_time_ms,
|
||||
np,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -298,6 +343,13 @@ fn on_spell_go(
|
||||
} = ev
|
||||
{
|
||||
let engaged = !sc.engaged_self.is_empty();
|
||||
// nampower's two `SPELL_GO_*` counts, taken before `spell_go` consumes the lists. Capped
|
||||
// at 255 because that is the wire's own width: the server writes both counts as `u8`s
|
||||
// (vmangos `Spell.cpp:4657`), so a longer list cannot have arrived.
|
||||
let (num_hit, num_missed) = (
|
||||
u8::try_from(hits.len()).unwrap_or(u8::MAX),
|
||||
u8::try_from(misses.len()).unwrap_or(u8::MAX),
|
||||
);
|
||||
// nampower v2's `AURA_CAST_ON_SELF`/`_OTHER`, queued before `spell_go` consumes the hit
|
||||
// list. Pushed, not fired: these handlers hold no `UiScript`. Nothing below writes this
|
||||
// queue, so the position is about borrowing `hits`, not about ordering.
|
||||
@@ -368,12 +420,27 @@ fn on_spell_go(
|
||||
);
|
||||
// SuperWoW's `UNIT_CASTEVENT` CAST: the cast completed. No duration on this edge — the
|
||||
// length was START's, and an instant never had one.
|
||||
l.cast_events_lua.push(
|
||||
//
|
||||
// nampower's `SPELL_GO_SELF`/`_OTHER` ride the same row. Their two counts come off the
|
||||
// packet's own hit and miss lists, and they matter: pfUI gates its whole debuff
|
||||
// bookkeeping on `numMissed > 0 or numHit == 0` (`libdebuff.lua:947`), so this is the one
|
||||
// column in that family where a wrong number changes what a player sees.
|
||||
let np = crate::ui_cast_events::NpCastColumns {
|
||||
item_id: item_caster
|
||||
.and_then(|guid| item_entry(guid, &l.index, &sc.stores))
|
||||
.unwrap_or(0),
|
||||
cast_flags,
|
||||
num_hit,
|
||||
num_missed,
|
||||
..Default::default()
|
||||
};
|
||||
l.cast_events_lua.push_detailed(
|
||||
caster,
|
||||
target,
|
||||
crate::ui_cast_events::CastEdge::Cast,
|
||||
spell_id,
|
||||
0,
|
||||
np,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -805,13 +872,21 @@ fn cast_result(
|
||||
// Before this edge existed the window outlived its cast: vmangos answers a mid-cast
|
||||
// deselect with `SendCastResult(INTERRUPTED)` and nothing else, so `UnitCastingInfo` kept
|
||||
// reporting a cast that had stopped, right up to its original end.
|
||||
//
|
||||
// nampower's `SPELL_FAILED_SELF` rides the same row, carrying the `SpellCastResult` byte
|
||||
// as `arg2`. `failedByServer` is 1 at the fire because this packet *is* the server's
|
||||
// reply; a client-side refusal of ours reaches no cast edge at all (see `fire_np_cast`).
|
||||
if let Some(me) = self_guid.0 {
|
||||
cast_windows.push(
|
||||
cast_windows.push_detailed(
|
||||
me,
|
||||
None,
|
||||
crate::ui_cast_events::CastEdge::ResultFail,
|
||||
spell_id,
|
||||
0,
|
||||
crate::ui_cast_events::NpCastColumns {
|
||||
spell_result: reason.unwrap_or(0),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
// Our cast died (vmangos never sends `SMSG_SPELL_FAILURE`): end the self avatar's cast
|
||||
@@ -1391,6 +1466,17 @@ fn spell_cooldowns(
|
||||
|
||||
/// `SMSG_ITEM_COOLDOWN` (`0x6e95d0`): the fixed 30 s use cooldown, keyed on the item instance's
|
||||
/// template entry, as the reference resolves it.
|
||||
/// An item instance guid to its template entry, the two hops every item-keyed read here makes
|
||||
/// (the guid index, then the object's own `OBJECT_FIELD_ENTRY`). `None` for an item this client
|
||||
/// holds no object for, which is what a cast item belonging to someone else looks like.
|
||||
fn item_entry(item_guid: u64, index: &GuidIndex, stores: &Query<&mut ObjectStore>) -> Option<u32> {
|
||||
index
|
||||
.0
|
||||
.get(&item_guid)
|
||||
.and_then(|&e| stores.get(e).ok())
|
||||
.and_then(|s| s.0.object_entry())
|
||||
}
|
||||
|
||||
fn item_cooldown(
|
||||
item_guid: u64,
|
||||
spell_id: u32,
|
||||
@@ -1399,12 +1485,7 @@ fn item_cooldown(
|
||||
cooldowns: &mut Cooldowns,
|
||||
) {
|
||||
debug!("net: item cooldown — item {item_guid:#x} spell {spell_id}");
|
||||
let entry = index
|
||||
.0
|
||||
.get(&item_guid)
|
||||
.and_then(|&e| stores.get(e).ok())
|
||||
.and_then(|s| s.0.object_entry());
|
||||
if let Some(entry) = entry {
|
||||
if let Some(entry) = item_entry(item_guid, index, stores) {
|
||||
cooldowns.apply_wire_item_cooldown(entry, spell_id, Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,46 @@ impl CastEdge {
|
||||
}
|
||||
}
|
||||
|
||||
/// nampower's own cast columns: the packet fields its `SPELL_START_*`/`SPELL_GO_*`/
|
||||
/// `SPELL_FAILED_SELF` family reports and `UNIT_CASTEVENT` has no place for.
|
||||
///
|
||||
/// **On the same row, not a second queue.** This file's header gives the rule: an addon that hears
|
||||
/// an event and then asks a getter must not be able to get an answer that contradicts it, and the
|
||||
/// only guarantee of that is one row per packet, folded once. A parallel queue for nampower's
|
||||
/// family would be a second reader of the same packets that can drift — the exact failure the one
|
||||
/// queue exists to prevent. `Default` is "nothing extra", which is what the edges with no nampower
|
||||
/// event of their own carry.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub(crate) struct NpCastColumns {
|
||||
/// `arg1` of `SPELL_START_*` and `SPELL_GO_*`: the entry of the item whose use cast this, 0
|
||||
/// for a spell. Read nowhere in grom's corpus — pfUI's libdebuff names it `itemId` and passes
|
||||
/// it straight to its hooks, none of which touch it (`libdebuff.lua:931,941`) — but it is
|
||||
/// `arg1`, so every later column's position depends on it being present.
|
||||
pub(crate) item_id: u32,
|
||||
/// `arg5`, the wire's own `castFlags`: always `0x2` on START and `0x100` on GO, plus
|
||||
/// `CAST_FLAG_AMMO` `0x20` for a ranged spell (vmangos `Spell.h:53`).
|
||||
pub(crate) cast_flags: u16,
|
||||
/// `arg8` of `SPELL_START_*`: `0` normal, `1` channel, `2` autorepeat — the spell's own kind,
|
||||
/// off `Spell.dbc`, since the wire says nothing about it.
|
||||
pub(crate) spell_type: u8,
|
||||
/// `arg7` of `SPELL_START_*`: a channel's length in ms, 0 otherwise.
|
||||
///
|
||||
/// Not on the wire — `SMSG_SPELL_START` for a channeled spell carries `cast_time_ms` 0 and
|
||||
/// the duration arrives a round trip later in `MSG_CHANNEL_START`. The consumer knows: pfUI
|
||||
/// reads `arg6` first and falls back to this "only for true channels like Blizzard where
|
||||
/// arg6 is 0/nil" (`libdebuff.lua:913-916`), so this is the `SpellDuration.dbc` length, under
|
||||
/// exactly the conditions [`aura_duration_ms`] will answer one at all.
|
||||
pub(crate) channel_ms: u32,
|
||||
/// `arg6`/`arg7` of `SPELL_GO_*`: how many targets the launch hit and missed, off the
|
||||
/// packet's own two lists. pfUI gates its whole debuff bookkeeping on these
|
||||
/// (`if numMissed > 0 or numHit == 0 then return end`, `libdebuff.lua:947`), which is the one
|
||||
/// place in this family where a wrong number changes what a player sees.
|
||||
pub(crate) num_hit: u8,
|
||||
pub(crate) num_missed: u8,
|
||||
/// `arg2` of `SPELL_FAILED_SELF`: the `SpellCastResult` byte the server replied with.
|
||||
pub(crate) spell_result: u8,
|
||||
}
|
||||
|
||||
/// One queued cast edge, in `UNIT_CASTEVENT`'s argument order where it has one.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct CastEventRow {
|
||||
@@ -192,6 +232,8 @@ pub(crate) struct CastEventRow {
|
||||
/// `arg5`, ms: the cast or channel length, 0 for an edge that has none. On [`CastEdge::Delay`]
|
||||
/// the pushback, on [`CastEdge::ChannelUpdate`] the time left.
|
||||
pub(crate) duration_ms: u32,
|
||||
/// What nampower's own family reports beyond this.
|
||||
pub(crate) np: NpCastColumns,
|
||||
}
|
||||
|
||||
/// The producers' queue, drained once per tick by [`drain_cast_edges`].
|
||||
@@ -226,6 +268,29 @@ impl CastEventFeed {
|
||||
edge: CastEdge,
|
||||
spell_id: u32,
|
||||
duration_ms: u32,
|
||||
) {
|
||||
self.push_detailed(
|
||||
caster,
|
||||
target,
|
||||
edge,
|
||||
spell_id,
|
||||
duration_ms,
|
||||
NpCastColumns::default(),
|
||||
);
|
||||
}
|
||||
|
||||
/// [`Self::push`] for the three edges that carry nampower's extra columns — `SMSG_SPELL_START`,
|
||||
/// `SMSG_SPELL_GO` and the failing `SMSG_CAST_RESULT`. Same queue, same row, same caster gate:
|
||||
/// the two entry points differ only in whether the caller has anything to put in
|
||||
/// [`NpCastColumns`].
|
||||
pub(crate) fn push_detailed(
|
||||
&mut self,
|
||||
caster: u64,
|
||||
target: Option<u64>,
|
||||
edge: CastEdge,
|
||||
spell_id: u32,
|
||||
duration_ms: u32,
|
||||
np: NpCastColumns,
|
||||
) {
|
||||
if caster == 0 {
|
||||
return;
|
||||
@@ -236,6 +301,7 @@ impl CastEventFeed {
|
||||
edge,
|
||||
spell_id,
|
||||
duration_ms,
|
||||
np,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -352,10 +418,20 @@ pub(crate) struct AuraCastFeed {
|
||||
|
||||
/// Apply a change of the event switch. The registry fires this at boot with the registered
|
||||
/// default, so `enabled` is never read before it has been seeded.
|
||||
pub(crate) fn on_cvar(ev: On<crate::cvars::CvarChanged>, mut feed: ResMut<AuraCastFeed>) {
|
||||
pub(crate) fn on_cvar(
|
||||
ev: On<crate::cvars::CvarChanged>,
|
||||
mut feed: ResMut<AuraCastFeed>,
|
||||
mut gates: ResMut<NpCastGates>,
|
||||
) {
|
||||
if ev.is(CVAR_AURA_CAST_EVENTS) {
|
||||
feed.enabled = ev.flag();
|
||||
}
|
||||
if ev.is(CVAR_SPELL_START_EVENTS) {
|
||||
gates.start = ev.flag();
|
||||
}
|
||||
if ev.is(CVAR_SPELL_GO_EVENTS) {
|
||||
gates.go = ev.flag();
|
||||
}
|
||||
}
|
||||
|
||||
impl AuraCastFeed {
|
||||
@@ -566,6 +642,154 @@ impl CastWindows {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── nampower's own cast-edge family ──────────────────────────────────────────────────────────
|
||||
|
||||
/// The two switches nampower's START and GO classes ride behind, its own spellings, both
|
||||
/// documented `default 0` (upstream `EVENTS.md`) and registered in `cvars.rs`.
|
||||
///
|
||||
/// Both are written to `"1"` at login by the addons that want them — `NampowerAPI.lua:2894-2902`
|
||||
/// and pfUI's `libs/libdebuff.lua:56-88` — so in a real session they are on, and the default
|
||||
/// matters only for a client with neither addon.
|
||||
pub(crate) const CVAR_SPELL_START_EVENTS: &str = "NP_EnableSpellStartEvents";
|
||||
pub(crate) const CVAR_SPELL_GO_EVENTS: &str = "NP_EnableSpellGoEvents";
|
||||
|
||||
/// The applied values of the two switches above.
|
||||
///
|
||||
/// A resource of its own, not a field on [`CastEventFeed`]: the gate here can only sit at the
|
||||
/// *fire*, because the START and GO rows are queued regardless — `UNIT_CASTEVENT` reads them and
|
||||
/// the cast windows are folded from them. That is the difference from [`AuraCastFeed::enabled`],
|
||||
/// which gates the *push* and so saves a whole catalogue walk when it is off.
|
||||
///
|
||||
/// The other five events in this family — `SPELL_FAILED_SELF`/`_OTHER`, `SPELL_DELAYED_SELF` and
|
||||
/// the channel pair — have no switch at all in the DLL's own CVar table, so they fire
|
||||
/// unconditionally, as they do upstream.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct NpCastGates {
|
||||
/// `pub(crate)` for `cvars.rs`'s end-to-end test, which drives the real registry.
|
||||
pub(crate) start: bool,
|
||||
pub(crate) go: bool,
|
||||
}
|
||||
|
||||
/// nampower's cast-edge events for one row, in upstream's own argument order (`EVENTS.md`, and
|
||||
/// corroborated column by column from the consumer side by pfUI's `libs/libdebuff.lua:909-945`,
|
||||
/// which destructures `arg2` as the spell id, `arg3` as the caster, `arg4` as the target, `arg6`
|
||||
/// as the cast time and `arg7` as the channel duration).
|
||||
///
|
||||
/// **The `_SELF`/`_OTHER` split is by caster**, which is where this family differs from the aura
|
||||
/// one next door — that one splits by *target* (see [`AuraCastRow::on_self`]) — and the two are
|
||||
/// not a guess either way: upstream's own wording for the aura events is "casts that land on the
|
||||
/// active player", while here pfUI handles `SPELL_START_SELF` and `SPELL_START_OTHER` in one arm
|
||||
/// and then reads `arg3` to learn the caster, which only makes sense if the name already settled
|
||||
/// it.
|
||||
///
|
||||
/// Four edges raise nothing here:
|
||||
/// - [`CastEdge::Abort`], the client's own cancel: the server answers it with
|
||||
/// `SMSG_SPELL_FAILED_OTHER` a round trip later, and that is what raises the FAIL — firing one
|
||||
/// now would double an event an addon counts. Same reasoning as its `UNIT_CASTEVENT` row.
|
||||
/// - An instant's START, which is never queued at all (`spell::net` skips `cast_time_ms == 0`).
|
||||
/// - `SPELL_DELAYED_OTHER`, which upstream states never fires either, because the server sends
|
||||
/// `SMSG_SPELL_DELAYED` only to the affected player.
|
||||
/// - A client-side cast failure as `SPELL_FAILED_SELF` with `failedByServer == 0`. This client's
|
||||
/// local refusals funnel through `CastErrors` (`ui_action/errors.rs`), which carries a spell id
|
||||
/// and a reason byte and reaches no cast edge at all — the same absent half `SelfCastSend`
|
||||
/// writes down for `SPELL_CAST_EVENT`'s `success == 0`, and for the same reason.
|
||||
///
|
||||
/// `corpseOwnerGuid`, the last argument of both START and GO, is never sent: a trailing nil in
|
||||
/// Lua is indistinguishable from an absent return, and upstream sends nil for every cast without
|
||||
/// a corpse target, which is all but a handful (resurrections and corpse-targeted hunter pets).
|
||||
/// For those, the gap is real and bounded — `read_spell_cast_targets` reads the corpse guid and
|
||||
/// drops it, and the *owner* is a further hop through that corpse object's own
|
||||
/// `CORPSE_FIELD_OWNER`. Nothing in grom's corpus reads the column.
|
||||
fn fire_np_cast(
|
||||
script: &mut UiScript,
|
||||
row: &CastEventRow,
|
||||
named: u32,
|
||||
me: u64,
|
||||
gates: &NpCastGates,
|
||||
) {
|
||||
let caster = || ScriptValue::Str(benilla_ui::script::guid_token(row.caster));
|
||||
let target = || ScriptValue::Str(benilla_ui::script::guid_token(row.target));
|
||||
let int = |n: u32| ScriptValue::Int(i64::from(n));
|
||||
let is_self = row.caster != 0 && row.caster == me;
|
||||
match row.edge {
|
||||
CastEdge::Start { .. } if gates.start => {
|
||||
let args = vec![
|
||||
int(row.np.item_id),
|
||||
int(row.spell_id),
|
||||
caster(),
|
||||
target(),
|
||||
int(u32::from(row.np.cast_flags)),
|
||||
int(row.duration_ms),
|
||||
int(row.np.channel_ms),
|
||||
int(u32::from(row.np.spell_type)),
|
||||
];
|
||||
// The names are LITERALS at each call so `reference_ui.rs`'s beyond-1.12 event gate
|
||||
// can see them: its walker reads the string argument here and cannot follow a name
|
||||
// through a variable. `ui_aura.rs`'s own fire sites carry the same note, which is the
|
||||
// house rule this file had previously broken for the aura pair below.
|
||||
if is_self {
|
||||
script.fire_event("SPELL_START_SELF", args);
|
||||
} else {
|
||||
script.fire_event("SPELL_START_OTHER", args);
|
||||
}
|
||||
}
|
||||
CastEdge::Cast if gates.go => {
|
||||
let args = vec![
|
||||
int(row.np.item_id),
|
||||
int(row.spell_id),
|
||||
caster(),
|
||||
target(),
|
||||
int(u32::from(row.np.cast_flags)),
|
||||
int(u32::from(row.np.num_hit)),
|
||||
int(u32::from(row.np.num_missed)),
|
||||
];
|
||||
if is_self {
|
||||
script.fire_event("SPELL_GO_SELF", args);
|
||||
} else {
|
||||
script.fire_event("SPELL_GO_OTHER", args);
|
||||
}
|
||||
}
|
||||
// `SMSG_SPELL_FAILED_OTHER` is the "other" packet whatever it names, and it names us too
|
||||
// when the server kills our own cast — upstream hooks that one packet and this is the
|
||||
// event it raises, so the caster rides as `arg1` rather than the name deciding it.
|
||||
CastEdge::Fail => {
|
||||
script.fire_event("SPELL_FAILED_OTHER", vec![caster(), int(row.spell_id)]);
|
||||
}
|
||||
// A failing `SMSG_CAST_RESULT`, which is only ever our own cast's reply: `failedByServer`
|
||||
// is 1 because that is where this row comes from.
|
||||
CastEdge::ResultFail => {
|
||||
script.fire_event(
|
||||
"SPELL_FAILED_SELF",
|
||||
vec![
|
||||
int(row.spell_id),
|
||||
int(u32::from(row.np.spell_result)),
|
||||
ScriptValue::Int(1),
|
||||
],
|
||||
);
|
||||
}
|
||||
CastEdge::Delay => {
|
||||
script.fire_event("SPELL_DELAYED_SELF", vec![caster(), int(row.duration_ms)]);
|
||||
}
|
||||
// The channel pair is self-only on the wire (`MSG_CHANNEL_*` is sent to the caster alone),
|
||||
// which is why upstream marks both "self only" and neither has an `_OTHER` sibling.
|
||||
// `named` rather than `row.spell_id`: `MSG_CHANNEL_UPDATE` carries no spell id, so the
|
||||
// live window names it — the same substitution the token events make.
|
||||
CastEdge::Channel => {
|
||||
script.fire_event(
|
||||
"SPELL_CHANNEL_START",
|
||||
vec![int(named), target(), int(row.duration_ms)],
|
||||
);
|
||||
}
|
||||
CastEdge::ChannelUpdate => {
|
||||
script.fire_event(
|
||||
"SPELL_CHANNEL_UPDATE",
|
||||
vec![int(named), target(), int(row.duration_ms)],
|
||||
);
|
||||
}
|
||||
CastEdge::Start { .. } | CastEdge::Cast | CastEdge::Abort => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold every queued edge into the cast windows and fire its events — SuperWoW's
|
||||
/// `UNIT_CASTEVENT`, then ClassicAPI's `UNIT_SPELLCAST_*` family — in packet order, each row's
|
||||
/// state reaching the VM before any of its own events do.
|
||||
@@ -586,6 +810,8 @@ pub(crate) fn drain_cast_edges(
|
||||
mut feed: ResMut<CastEventFeed>,
|
||||
mut windows: ResMut<CastWindows>,
|
||||
mut auras: ResMut<AuraCastFeed>,
|
||||
gates: Res<NpCastGates>,
|
||||
self_guid: Res<crate::net::SelfGuid>,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
// No VM (a capture or headless run): drop the rows so the queue stays bounded, as
|
||||
@@ -621,7 +847,14 @@ pub(crate) fn drain_cast_edges(
|
||||
if folded {
|
||||
script.set_unit_casts(windows.snapshot());
|
||||
}
|
||||
if let Some(word) = row.edge.word() {
|
||||
// A START whose wire cast time is 0 raises no `UNIT_CASTEVENT` word. That is this
|
||||
// client's existing behaviour preserved rather than a new rule: such a row used not to be
|
||||
// queued at all (`spell::net` tested `cast_time_ms > 0`), and it is queued now only so
|
||||
// nampower's `SPELL_START_*` can fire for a **channel**, whose START really does carry 0.
|
||||
// An instant is the other case, and the wiki's own START/CAST split says its only edge is
|
||||
// the CAST.
|
||||
let np_only_start = matches!(row.edge, CastEdge::Start { .. }) && row.duration_ms == 0;
|
||||
if let (Some(word), false) = (row.edge.word(), np_only_start) {
|
||||
script.fire_event(
|
||||
"UNIT_CASTEVENT",
|
||||
vec![
|
||||
@@ -633,6 +866,10 @@ pub(crate) fn drain_cast_edges(
|
||||
],
|
||||
);
|
||||
}
|
||||
// nampower's own cast family, behind SuperWoW's one word and ahead of ClassicAPI's
|
||||
// tokens. The three are independent hooks in a real install, so no addon can observe an
|
||||
// order between them; this one is placed second because it is the second that existed.
|
||||
fire_np_cast(&mut script, &row, named, self_guid.0.unwrap_or(0), &gates);
|
||||
// ClassicAPI's token events, once per live token per event — the fan-out and the
|
||||
// `(arg1..arg5)` shape are `benilla-ui`'s `script/cast_events.rs`, which is where the
|
||||
// token set and `spell_display` both live. Event-major rather than token-major: when one
|
||||
@@ -648,25 +885,28 @@ pub(crate) fn drain_cast_edges(
|
||||
// in a real install, so no addon can observe an order between them; what *is* observable is
|
||||
// that the cast edges fire exactly where they did before this queue existed.
|
||||
for row in auras.rows.drain(..) {
|
||||
let name = if row.on_self {
|
||||
"AURA_CAST_ON_SELF"
|
||||
let args = vec![
|
||||
ScriptValue::Int(i64::from(row.spell_id)),
|
||||
ScriptValue::Str(benilla_ui::script::guid_token(row.caster)),
|
||||
ScriptValue::Str(benilla_ui::script::guid_token(row.target)),
|
||||
ScriptValue::Int(i64::from(row.effect)),
|
||||
ScriptValue::Int(i64::from(row.aura_name)),
|
||||
ScriptValue::Int(i64::from(row.amplitude)),
|
||||
ScriptValue::Int(i64::from(row.misc_value)),
|
||||
ScriptValue::Int(i64::from(row.duration_ms)),
|
||||
ScriptValue::Int(row.aura_cap),
|
||||
];
|
||||
// Both names written out as LITERALS, for the reason `ui_aura.rs`'s own fire sites give:
|
||||
// `reference_ui.rs`'s beyond-1.12 event gate reads the string argument at the call and
|
||||
// cannot follow a name through a variable. Routing these two through one `&str` compiled
|
||||
// fine and left that gate blind to the pair for as long as they have existed — the same
|
||||
// mistake, in the same shape, that the aura-edge fire in `ui_aura.rs` already carries a
|
||||
// warning about.
|
||||
if row.on_self {
|
||||
script.fire_event("AURA_CAST_ON_SELF", args);
|
||||
} else {
|
||||
"AURA_CAST_ON_OTHER"
|
||||
};
|
||||
script.fire_event(
|
||||
name,
|
||||
vec![
|
||||
ScriptValue::Int(i64::from(row.spell_id)),
|
||||
ScriptValue::Str(benilla_ui::script::guid_token(row.caster)),
|
||||
ScriptValue::Str(benilla_ui::script::guid_token(row.target)),
|
||||
ScriptValue::Int(i64::from(row.effect)),
|
||||
ScriptValue::Int(i64::from(row.aura_name)),
|
||||
ScriptValue::Int(i64::from(row.amplitude)),
|
||||
ScriptValue::Int(i64::from(row.misc_value)),
|
||||
ScriptValue::Int(i64::from(row.duration_ms)),
|
||||
ScriptValue::Int(row.aura_cap),
|
||||
],
|
||||
);
|
||||
script.fire_event("AURA_CAST_ON_OTHER", args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,6 +918,7 @@ impl Plugin for SuperWowEventsPlugin {
|
||||
app.init_resource::<CastEventFeed>()
|
||||
.init_resource::<CastWindows>()
|
||||
.init_resource::<AuraCastFeed>()
|
||||
.init_resource::<NpCastGates>()
|
||||
// In `UnitFeed`, like the mirror-timer drain: the edges are produced by the net
|
||||
// handlers earlier in the frame and must reach Lua before this tick's `OnUpdate`.
|
||||
// After the local cancel, whose `Abort` edge this drains the same frame — the same
|
||||
@@ -752,6 +993,8 @@ mod tests {
|
||||
app.init_resource::<CastEventFeed>();
|
||||
app.init_resource::<CastWindows>();
|
||||
app.init_resource::<AuraCastFeed>();
|
||||
app.init_resource::<NpCastGates>();
|
||||
app.init_resource::<crate::net::SelfGuid>();
|
||||
app.insert_non_send_resource(UiScript::new().unwrap());
|
||||
// A frame that records every UNIT_CASTEVENT it hears, so the assertion reads the real
|
||||
// dispatch path rather than the queue.
|
||||
@@ -807,6 +1050,8 @@ mod tests {
|
||||
app.init_resource::<CastEventFeed>();
|
||||
app.init_resource::<CastWindows>();
|
||||
app.init_resource::<AuraCastFeed>();
|
||||
app.init_resource::<NpCastGates>();
|
||||
app.init_resource::<crate::net::SelfGuid>();
|
||||
app.world_mut()
|
||||
.resource_mut::<CastEventFeed>()
|
||||
.push(0x2a, None, CastEdge::Cast, 133, 0);
|
||||
@@ -1004,6 +1249,8 @@ mod tests {
|
||||
app.init_resource::<CastEventFeed>();
|
||||
app.init_resource::<CastWindows>();
|
||||
app.init_resource::<AuraCastFeed>();
|
||||
app.init_resource::<NpCastGates>();
|
||||
app.init_resource::<crate::net::SelfGuid>();
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_now(100.0);
|
||||
script.set_spellbook(SpellBookState {
|
||||
@@ -1063,6 +1310,8 @@ mod tests {
|
||||
app.init_resource::<CastEventFeed>();
|
||||
app.init_resource::<CastWindows>();
|
||||
app.init_resource::<AuraCastFeed>();
|
||||
app.init_resource::<NpCastGates>();
|
||||
app.init_resource::<crate::net::SelfGuid>();
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_now(100.0);
|
||||
script.set_spellbook(SpellBookState {
|
||||
@@ -1138,6 +1387,7 @@ mod tests {
|
||||
edge,
|
||||
spell_id,
|
||||
duration_ms,
|
||||
np: NpCastColumns::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,6 +1409,12 @@ mod tests {
|
||||
|
||||
/// The player's and the mob's GUIDs, as the feeds name them.
|
||||
const ME: u64 = 0x2a;
|
||||
|
||||
/// A GUID as every `compat/` event writes it, so a test asserts the spelling once rather than
|
||||
/// repeating sixteen digits.
|
||||
fn tok(guid: u64) -> String {
|
||||
benilla_ui::script::guid_token(guid)
|
||||
}
|
||||
const MOB: u64 = 0x63;
|
||||
/// A second mob, for the multi-hit fan-out of nampower's aura events.
|
||||
const MOB2: u64 = 0x64;
|
||||
@@ -1362,6 +1618,7 @@ end)
|
||||
app.init_resource::<CastEventFeed>()
|
||||
.init_resource::<CastWindows>()
|
||||
.init_resource::<AuraCastFeed>()
|
||||
.init_resource::<NpCastGates>()
|
||||
.init_resource::<crate::ui_cast::CastBarFeed>()
|
||||
.init_resource::<crate::spell::PendingCast>()
|
||||
.init_resource::<crate::spell::QueuedMeleeSpell>()
|
||||
@@ -2433,7 +2690,9 @@ end)
|
||||
let mut app = App::new();
|
||||
app.init_resource::<CastEventFeed>()
|
||||
.init_resource::<CastWindows>()
|
||||
.init_resource::<AuraCastFeed>();
|
||||
.init_resource::<AuraCastFeed>()
|
||||
.init_resource::<NpCastGates>()
|
||||
.init_resource::<crate::net::SelfGuid>();
|
||||
app.world_mut().resource_mut::<AuraCastFeed>().enabled = enabled;
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_unit(
|
||||
@@ -2636,11 +2895,349 @@ end)
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
// ── nampower's own cast-edge family ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Same discipline as the aura-cast block above: the assertion reads **pfUI's own `local`
|
||||
// bindings** back out, not a list of values in the order this file happens to push them. A
|
||||
// positional shape got backwards here fails in silence — the handler binds `arg3` to
|
||||
// `casterGuid` whatever `arg3` happens to be — and the START/GO pair is where that would be
|
||||
// most expensive, because pfUI's swing timer resets off one and its debuff bookkeeping gates
|
||||
// on the other's two counts.
|
||||
|
||||
/// `pfUI/libs/libdebuff.lua:909-945`, the START and GO arms transcribed, with its own comment
|
||||
/// about `arg6`/`arg7` kept because it states the rule the `channel_ms` column exists for.
|
||||
const PFUI_NP_CAST: &str = r#"
|
||||
seen = {}
|
||||
local f = CreateFrame("Frame")
|
||||
for _, e in ipairs({"SPELL_START_SELF", "SPELL_START_OTHER", "SPELL_GO_SELF", "SPELL_GO_OTHER",
|
||||
"SPELL_FAILED_SELF", "SPELL_FAILED_OTHER", "SPELL_DELAYED_SELF",
|
||||
"SPELL_CHANNEL_START", "SPELL_CHANNEL_UPDATE"}) do
|
||||
f:RegisterEvent(e)
|
||||
end
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then
|
||||
local spellId = arg2
|
||||
local casterGuid = arg3
|
||||
-- arg6=castTime (ms), arg7=channel duration (ms). Prefer arg6 when set.
|
||||
local castTime = (arg6 and arg6 > 0) and arg6 or arg7
|
||||
if not casterGuid or not spellId then return end
|
||||
table.insert(seen, event .. "|item=" .. arg1 .. "|spell=" .. spellId
|
||||
.. "|caster=" .. casterGuid .. "|target=" .. arg4 .. "|flags=" .. arg5
|
||||
.. "|cast=" .. castTime .. "|type=" .. arg8)
|
||||
elseif event == "SPELL_GO_SELF" or event == "SPELL_GO_OTHER" then
|
||||
local itemId = arg1
|
||||
local spellId = arg2
|
||||
local casterGuid = arg3
|
||||
local targetGuid = arg4
|
||||
local numHit = arg6 or 0
|
||||
local numMissed = arg7 or 0
|
||||
table.insert(seen, event .. "|item=" .. itemId .. "|spell=" .. spellId
|
||||
.. "|caster=" .. casterGuid .. "|target=" .. targetGuid
|
||||
.. "|hit=" .. numHit .. "|missed=" .. numMissed)
|
||||
elseif event == "SPELL_FAILED_SELF" then
|
||||
table.insert(seen, event .. "|spell=" .. arg1 .. "|result=" .. arg2 .. "|byServer=" .. arg3)
|
||||
elseif event == "SPELL_FAILED_OTHER" then
|
||||
table.insert(seen, event .. "|caster=" .. arg1 .. "|spell=" .. arg2)
|
||||
elseif event == "SPELL_DELAYED_SELF" then
|
||||
table.insert(seen, event .. "|caster=" .. arg1 .. "|delay=" .. arg2)
|
||||
else
|
||||
table.insert(seen, event .. "|spell=" .. arg1 .. "|target=" .. arg2 .. "|ms=" .. arg3)
|
||||
end
|
||||
end)
|
||||
"#;
|
||||
|
||||
/// An app with the drain, the spy, and both switches on as every addon that wants them leaves
|
||||
/// them at login.
|
||||
fn np_cast_app(start: bool, go: bool) -> App {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<CastEventFeed>()
|
||||
.init_resource::<CastWindows>()
|
||||
.init_resource::<AuraCastFeed>()
|
||||
.init_resource::<NpCastGates>()
|
||||
.init_resource::<crate::net::SelfGuid>();
|
||||
app.world_mut().resource_mut::<NpCastGates>().start = start;
|
||||
app.world_mut().resource_mut::<NpCastGates>().go = go;
|
||||
app.world_mut().resource_mut::<crate::net::SelfGuid>().0 = Some(ME);
|
||||
let script = UiScript::new().unwrap();
|
||||
script.run(PFUI_NP_CAST).unwrap();
|
||||
app.insert_non_send_resource(script);
|
||||
app
|
||||
}
|
||||
|
||||
fn np_seen(app: &App) -> Vec<String> {
|
||||
app.world()
|
||||
.non_send_resource::<UiScript>()
|
||||
.eval("return seen")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The START pair: every column in its documented place, the `_SELF`/`_OTHER` split by
|
||||
/// **caster**, and a channel's length in `arg7` where `arg6` is the cast time — which is the
|
||||
/// one pair pfUI's own reader picks between.
|
||||
#[test]
|
||||
fn the_start_pair_carries_every_column_and_splits_by_caster() {
|
||||
let mut app = np_cast_app(true, true);
|
||||
{
|
||||
let mut feed = app.world_mut().resource_mut::<CastEventFeed>();
|
||||
feed.push_detailed(
|
||||
ME,
|
||||
Some(0x63),
|
||||
CastEdge::Start { window: true },
|
||||
133,
|
||||
3_000,
|
||||
NpCastColumns {
|
||||
item_id: 13_444,
|
||||
cast_flags: 0x2,
|
||||
spell_type: 0,
|
||||
channel_ms: 0,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// A channel: the wire's cast time is 0, which is why the row is queued at all and
|
||||
// why `arg7` is what the consumer falls back to.
|
||||
feed.push_detailed(
|
||||
0x63,
|
||||
Some(ME),
|
||||
CastEdge::Start { window: true },
|
||||
5_143,
|
||||
0,
|
||||
NpCastColumns {
|
||||
spell_type: 1,
|
||||
channel_ms: 8_000,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert_eq!(
|
||||
np_seen(&app),
|
||||
vec![
|
||||
format!(
|
||||
"SPELL_START_SELF|item=13444|spell=133|caster={}|target={}|flags=2|cast=3000\
|
||||
|type=0",
|
||||
tok(ME),
|
||||
tok(0x63)
|
||||
),
|
||||
format!(
|
||||
"SPELL_START_OTHER|item=0|spell=5143|caster={}|target={}|flags=0|cast=8000\
|
||||
|type=1",
|
||||
tok(0x63),
|
||||
tok(ME)
|
||||
),
|
||||
],
|
||||
"the player's cast is SELF and the mob's is OTHER, and the channel's length reaches \
|
||||
the consumer through arg7 because arg6 is not a real cast time"
|
||||
);
|
||||
}
|
||||
|
||||
/// The GO pair's two counts, which gate pfUI's whole debuff bookkeeping
|
||||
/// (`if numMissed > 0 or numHit == 0 then return end`).
|
||||
#[test]
|
||||
fn the_go_pair_reports_the_hit_and_miss_counts() {
|
||||
let mut app = np_cast_app(true, true);
|
||||
app.world_mut()
|
||||
.resource_mut::<CastEventFeed>()
|
||||
.push_detailed(
|
||||
ME,
|
||||
Some(0x63),
|
||||
CastEdge::Cast,
|
||||
133,
|
||||
0,
|
||||
NpCastColumns {
|
||||
cast_flags: 0x100,
|
||||
num_hit: 3,
|
||||
num_missed: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert_eq!(
|
||||
np_seen(&app),
|
||||
vec![format!(
|
||||
"SPELL_GO_SELF|item=0|spell=133|caster={}|target={}|hit=3|missed=1",
|
||||
tok(ME),
|
||||
tok(0x63)
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
/// The five ungated events: both failure shapes, pushback, and the channel pair whose spell
|
||||
/// id comes from the live window because `MSG_CHANNEL_UPDATE` carries nothing but a duration.
|
||||
#[test]
|
||||
fn the_failure_pushback_and_channel_events_carry_their_own_shapes() {
|
||||
let mut app = np_cast_app(false, false);
|
||||
{
|
||||
let mut feed = app.world_mut().resource_mut::<CastEventFeed>();
|
||||
feed.push(ME, None, CastEdge::Channel, 5_143, 8_000);
|
||||
feed.push(0x63, None, CastEdge::Fail, 116, 0);
|
||||
feed.push(ME, None, CastEdge::Delay, 0, 500);
|
||||
feed.push_detailed(
|
||||
ME,
|
||||
None,
|
||||
CastEdge::ResultFail,
|
||||
133,
|
||||
0,
|
||||
NpCastColumns {
|
||||
spell_result: 0x23,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert_eq!(
|
||||
np_seen(&app),
|
||||
vec![
|
||||
format!("SPELL_CHANNEL_START|spell=5143|target={}|ms=8000", tok(0)),
|
||||
format!("SPELL_FAILED_OTHER|caster={}|spell=116", tok(0x63)),
|
||||
format!("SPELL_DELAYED_SELF|caster={}|delay=500", tok(ME)),
|
||||
"SPELL_FAILED_SELF|spell=133|result=35|byServer=1".to_string(),
|
||||
],
|
||||
"packet order; the result byte is the server's own SpellCastResult (0x23 = 35), and \
|
||||
failedByServer is 1 because this row is the server's reply"
|
||||
);
|
||||
|
||||
// The channel's own stop, which names its spell from the window it is about.
|
||||
app.world_mut().resource_mut::<CastEventFeed>().push(
|
||||
ME,
|
||||
None,
|
||||
CastEdge::ChannelUpdate,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert_eq!(
|
||||
np_seen(&app).last().map(String::as_str),
|
||||
Some(format!("SPELL_CHANNEL_UPDATE|spell=5143|target={}|ms=0", tok(0)).as_str()),
|
||||
"the packet names no spell, so the live window does"
|
||||
);
|
||||
}
|
||||
|
||||
/// The two switches. Off, the START and GO pair is silent and the rest is not — which is the
|
||||
/// whole shape of upstream's own gating, where only those two classes have a CVar.
|
||||
#[test]
|
||||
fn the_two_switches_gate_only_the_start_and_go_pair() {
|
||||
let mut app = np_cast_app(false, false);
|
||||
{
|
||||
let mut feed = app.world_mut().resource_mut::<CastEventFeed>();
|
||||
feed.push(ME, Some(0x63), CastEdge::Start { window: true }, 133, 3_000);
|
||||
feed.push(ME, Some(0x63), CastEdge::Cast, 133, 0);
|
||||
feed.push(0x63, None, CastEdge::Fail, 116, 0);
|
||||
}
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert_eq!(
|
||||
np_seen(&app),
|
||||
vec![format!("SPELL_FAILED_OTHER|caster={}|spell=116", tok(0x63))],
|
||||
"both switches off: only the ungated event fires"
|
||||
);
|
||||
|
||||
// One switch at a time, so neither is standing in for the other.
|
||||
app.world_mut().resource_mut::<NpCastGates>().go = true;
|
||||
app.world_mut().resource_mut::<CastEventFeed>().push(
|
||||
ME,
|
||||
Some(0x63),
|
||||
CastEdge::Cast,
|
||||
133,
|
||||
0,
|
||||
);
|
||||
app.world_mut().resource_mut::<CastEventFeed>().push(
|
||||
ME,
|
||||
Some(0x63),
|
||||
CastEdge::Start { window: true },
|
||||
133,
|
||||
3_000,
|
||||
);
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert_eq!(
|
||||
np_seen(&app).len(),
|
||||
2,
|
||||
"the GO fires, the START still does not"
|
||||
);
|
||||
assert!(np_seen(&app)[1].starts_with("SPELL_GO_SELF"));
|
||||
}
|
||||
|
||||
/// A channel's START is queued although its wire cast time is 0, so nampower's
|
||||
/// `SPELL_START_*` can fire for Mind Flay and Blizzard — and `UNIT_CASTEVENT`'s own START is
|
||||
/// still suppressed for it, which is this client's behaviour before that row existed.
|
||||
///
|
||||
/// The two halves are asserted together because the whole point is that adding one did not
|
||||
/// change the other: SuperWoW's word is what every cast-tracking addon on this realm already
|
||||
/// registers, and a START with `arg5 = 0` reaching one of those is a cast bar told to run for
|
||||
/// no time at all.
|
||||
#[test]
|
||||
fn a_channels_start_reaches_nampower_but_not_unit_castevent() {
|
||||
let mut app = np_cast_app(true, true);
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.run(
|
||||
r#"
|
||||
words = {}
|
||||
local g = CreateFrame("Frame")
|
||||
g:RegisterEvent("UNIT_CASTEVENT")
|
||||
g:SetScript("OnEvent", function() table.insert(words, arg3 .. "/" .. arg5) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
{
|
||||
let mut feed = app.world_mut().resource_mut::<CastEventFeed>();
|
||||
// The channel's START (cast time 0), then a real 3 s cast's.
|
||||
feed.push_detailed(
|
||||
ME,
|
||||
None,
|
||||
CastEdge::Start { window: true },
|
||||
5_143,
|
||||
0,
|
||||
NpCastColumns {
|
||||
spell_type: 1,
|
||||
channel_ms: 8_000,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
feed.push(ME, None, CastEdge::Start { window: true }, 133, 3_000);
|
||||
}
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
np_seen(&app).len(),
|
||||
2,
|
||||
"nampower hears both — the channel through arg7, the cast through arg6"
|
||||
);
|
||||
assert_eq!(
|
||||
app.world()
|
||||
.non_send_resource::<UiScript>()
|
||||
.eval::<Vec<String>>("return words")
|
||||
.unwrap(),
|
||||
vec!["START/3000".to_string()],
|
||||
"SuperWoW hears only the cast: a START with arg5 = 0 is not an edge this client has \
|
||||
ever raised"
|
||||
);
|
||||
// And the channel opened no window either, which is `MSG_CHANNEL_START`'s job.
|
||||
assert_eq!(
|
||||
app.world().resource::<CastWindows>().0.len(),
|
||||
1,
|
||||
"one window, the 3 s cast's"
|
||||
);
|
||||
}
|
||||
|
||||
/// An abort raises nothing in this family, for the same reason it raises no `UNIT_CASTEVENT`:
|
||||
/// the server answers the cancel with `SMSG_SPELL_FAILED_OTHER` a round trip later, and
|
||||
/// firing one now would double an event an addon counts.
|
||||
#[test]
|
||||
fn a_local_abort_raises_no_nampower_event() {
|
||||
let mut app = np_cast_app(true, true);
|
||||
app.world_mut()
|
||||
.resource_mut::<CastEventFeed>()
|
||||
.push(ME, None, CastEdge::Abort, 133, 0);
|
||||
app.world_mut().run_system_once(drain_cast_edges).unwrap();
|
||||
assert!(np_seen(&app).is_empty());
|
||||
}
|
||||
|
||||
/// The switch is a real `CvarChanged` observer, so pfUI's `SetCVar` reaches it.
|
||||
#[test]
|
||||
fn the_cvar_observer_moves_the_switch_both_ways() {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<AuraCastFeed>().add_observer(on_cvar);
|
||||
app.init_resource::<AuraCastFeed>()
|
||||
.init_resource::<NpCastGates>()
|
||||
.add_observer(on_cvar);
|
||||
let fire = |app: &mut App, new: &str| {
|
||||
app.world_mut().trigger(crate::cvars::CvarChanged {
|
||||
name: CVAR_AURA_CAST_EVENTS.to_string(),
|
||||
|
||||
@@ -2663,6 +2663,79 @@ mod tests {
|
||||
"DEBUFF_REMOVED_SELF",
|
||||
"nampower: the removal half of the pair above",
|
||||
),
|
||||
(
|
||||
"AURA_CAST_ON_SELF",
|
||||
"nampower: an aura landing on the player, with the applying effect's own \
|
||||
Spell.dbc columns. Fired here since this family was built; it reached this list \
|
||||
only when its two fire sites were changed from one `&str` variable to two \
|
||||
literals, because this gate's walker reads the string at the call. The same \
|
||||
blindness, in the same shape, as the one `ui_aura.rs` carries a warning about",
|
||||
),
|
||||
(
|
||||
"AURA_CAST_ON_OTHER",
|
||||
"nampower: the other half of the pair above, split by TARGET rather than caster — \
|
||||
upstream's own wording is \"casts that land on the active player\"",
|
||||
),
|
||||
(
|
||||
"SPELL_START_SELF",
|
||||
"nampower: the player's own cast beginning, with the spell id, both GUIDs, the \
|
||||
wire's cast flags, the cast time, a channel's length and the spell's kind. \
|
||||
1.12's SPELLCAST_START carries a label and a duration and names neither spell \
|
||||
nor target, so nothing on that surface can correlate a cast with what it was \
|
||||
cast at. pfUI's libdebuff and its swingtimer both register this one \
|
||||
(libdebuff.lua:863, swingtimer.lua:727), and libpredict hangs its heal and \
|
||||
resurrection prediction off a hook it fires",
|
||||
),
|
||||
(
|
||||
"SPELL_START_OTHER",
|
||||
"nampower: another unit's cast beginning, the `_OTHER` half of the pair above. \
|
||||
The split is by caster, which is how pfUI reads it — it handles both names in \
|
||||
one arm and then reads arg3 for the caster, which only makes sense if the name \
|
||||
already settled it",
|
||||
),
|
||||
(
|
||||
"SPELL_GO_SELF",
|
||||
"nampower: the player's own cast launching, carrying how many targets it hit and \
|
||||
missed. Those two counts are the gate on pfUI's whole debuff bookkeeping \
|
||||
(`if numMissed > 0 or numHit == 0 then return end`, libdebuff.lua:947), which \
|
||||
1.12 cannot express at all: SPELLCAST_STOP is argless",
|
||||
),
|
||||
(
|
||||
"SPELL_GO_OTHER",
|
||||
"nampower: another unit's cast launching, the `_OTHER` half of the pair above",
|
||||
),
|
||||
(
|
||||
"SPELL_FAILED_SELF",
|
||||
"nampower: the player's own cast refused, with the server's SpellCastResult byte \
|
||||
— the number 1.12 turns into a red line and then discards, so no addon can see \
|
||||
WHY a cast failed. SuperCleveRoidMacros registers it beside SPELL_START_SELF \
|
||||
(Core.lua:4904)",
|
||||
),
|
||||
(
|
||||
"SPELL_FAILED_OTHER",
|
||||
"nampower: a cast interrupted, named by caster. Same packet SuperWoW reports as \
|
||||
UNIT_CASTEVENT's FAIL word, under nampower's own name and shape, because an \
|
||||
addon registers whichever mod it was written against",
|
||||
),
|
||||
(
|
||||
"SPELL_DELAYED_SELF",
|
||||
"nampower: pushback on the player's own cast, in ms. 1.12's SPELLCAST_DELAYED \
|
||||
carries the same number but no caster, and upstream's own note is that \
|
||||
SPELL_DELAYED_OTHER never fires at all because the server sends the packet only \
|
||||
to the affected player",
|
||||
),
|
||||
(
|
||||
"SPELL_CHANNEL_START",
|
||||
"nampower: the player's own channel beginning, with the spell id 1.12's \
|
||||
SPELLCAST_CHANNEL_START does not carry (it gets a label and a duration). \
|
||||
Self-only, as MSG_CHANNEL_START is on the wire",
|
||||
),
|
||||
(
|
||||
"SPELL_CHANNEL_UPDATE",
|
||||
"nampower: the channel's remaining time re-quoted, 0 ending it. Self-only for the \
|
||||
same reason, and the spell id comes from the live window because the packet \
|
||||
carries nothing but the duration",
|
||||
),
|
||||
(
|
||||
"SPELL_CAST_EVENT",
|
||||
"nampower: the player's own cast as the client committed it, carrying the spell \
|
||||
|
||||
@@ -552,6 +552,10 @@ pub enum SessionEvent {
|
||||
target: Option<u64>,
|
||||
/// The `CAST_FLAG_AMMO` tail: nocked ammo for any caster (`0x60ba30` from `0x6e78b6`).
|
||||
ammo_display_id: Option<u32>,
|
||||
/// The cast item when the first guid is not the caster, as [`Self::SpellGo`] carries it —
|
||||
/// nampower's `SPELL_START_*` reports the item as `arg1`, so the START needs the same
|
||||
/// column the GO already had.
|
||||
item_caster: Option<u64>,
|
||||
},
|
||||
/// The cast launched (`SMSG_SPELL_GO`); the server times impact off `Spell.dbc` Speed itself.
|
||||
SpellGo {
|
||||
|
||||
@@ -203,14 +203,20 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
ServerPacket::AiReaction { unit, reaction } => {
|
||||
vec![SessionEvent::AiReaction { unit, reaction }]
|
||||
}
|
||||
ServerPacket::SpellStart(s) => vec![SessionEvent::SpellStart {
|
||||
caster: spell_caster(s.item_or_caster, s.caster),
|
||||
spell_id: s.spell_id,
|
||||
cast_flags: s.cast_flags,
|
||||
cast_time_ms: s.cast_time_ms,
|
||||
target: s.targets.unit_target,
|
||||
ammo_display_id: s.ammo_display_id,
|
||||
}],
|
||||
ServerPacket::SpellStart(s) => {
|
||||
let caster = spell_caster(s.item_or_caster, s.caster);
|
||||
vec![SessionEvent::SpellStart {
|
||||
caster,
|
||||
spell_id: s.spell_id,
|
||||
cast_flags: s.cast_flags,
|
||||
cast_time_ms: s.cast_time_ms,
|
||||
target: s.targets.unit_target,
|
||||
ammo_display_id: s.ammo_display_id,
|
||||
// Same test the GO arm below makes: a GameObject's own guid in slot 1 is not an
|
||||
// item.
|
||||
item_caster: (s.item_or_caster != caster).then_some(s.item_or_caster),
|
||||
}]
|
||||
}
|
||||
ServerPacket::SpellGo(s) => {
|
||||
let caster = spell_caster(s.item_or_caster, s.caster);
|
||||
vec![SessionEvent::SpellGo {
|
||||
|
||||
@@ -388,6 +388,11 @@ fn spell_visual_wire_golden() {
|
||||
cast_time_ms,
|
||||
target,
|
||||
ammo_display_id,
|
||||
// This body writes a distinct guid in slot 1, as an item-cast START does, so the
|
||||
// event surfaces it — the same `Some(1)` the GO arm below asserts off the same
|
||||
// shape. nampower's `SPELL_START_*` reports the cast item as `arg1`, which is why
|
||||
// the event carries it at all.
|
||||
item_caster,
|
||||
} => assert_eq!(
|
||||
(
|
||||
caster,
|
||||
@@ -396,8 +401,9 @@ fn spell_visual_wire_golden() {
|
||||
cast_time_ms,
|
||||
target,
|
||||
ammo_display_id,
|
||||
item_caster,
|
||||
),
|
||||
(creature, 133, 0x2, 3000, Some(0x55), None)
|
||||
(creature, 133, 0x2, 3000, Some(0x55), None, Some(1))
|
||||
),
|
||||
other => panic!("spell start event, got {other:?}"),
|
||||
}
|
||||
|
||||
+48
-2
@@ -632,6 +632,52 @@ Three decisions worth having written down, because each had a tempting wrong ans
|
||||
fields. Nothing in the corpus reads the failing branch — `Core.lua:5177` is one
|
||||
`if success == 1 then` with no else.
|
||||
|
||||
### nampower's cast events — the ten off the one queue
|
||||
|
||||
**Built.** All ten ride `ui_cast_events.rs`'s existing `CastEventFeed` rows — the same rows
|
||||
`UNIT_CASTEVENT` and the cast windows are folded from — with `NpCastColumns` carrying the fields
|
||||
nampower's shapes want and SuperWoW's word has no place for. One row per packet, folded once, which
|
||||
is this file's own rule: an addon woken by one of these and then asking a getter must not be able
|
||||
to get a contradicting answer, and a parallel queue reading the same packets is exactly how that
|
||||
happens.
|
||||
|
||||
`NP_EnableSpellStartEvents` and `NP_EnableSpellGoEvents` are now **live** (they were stored-only),
|
||||
gating the START and GO pairs; the other five have no switch in the DLL's own CVar table and fire
|
||||
unconditionally, as upstream's do. Both are written to `"1"` at login by `NampowerAPI.lua:2894-2902`
|
||||
and by pfUI's `libs/libdebuff.lua:56-88`.
|
||||
|
||||
The stored-only rows claimed their shapes wanted "itemId/spellType/corpseOwnerGuid columns this
|
||||
client does not carry". Two of those three turned out to be a `Spell.dbc` lookup and a guid hop the
|
||||
item-cooldown path already made:
|
||||
|
||||
| Column | Where it comes from |
|
||||
|---|---|
|
||||
| `itemId` (`arg1` of START and GO) | the cast item's guid through the same two hops `item_cooldown` makes (the guid index, then `OBJECT_FIELD_ENTRY`), now one shared `item_entry` helper. `SMSG_SPELL_START` carries the guid and `SessionEvent::SpellStart` was dropping it; it carries it now, as the GO's always did |
|
||||
| `spellType` (`arg8` of START: 0 normal, 1 channel, 2 autorepeat) | the spell's own attributes — `AttributesEx & 0x44` for a channel, the auto-repeat bit for a shot. The wire says nothing about either |
|
||||
| `channel_ms` (`arg7` of START) | `SpellDuration.dbc`, under exactly the conditions `aura_duration_ms` will answer one at all. Not on the wire: a channeled spell's START carries `cast_time_ms` 0 and the length arrives a round trip later in `MSG_CHANNEL_START` — which is the case pfUI's reader names ("only for true channels like Blizzard where arg6 is 0/nil") |
|
||||
| `numTargetsHit`/`numTargetsMissed` (`arg6`/`arg7` of GO) | the packet's own two lists, capped at 255 because the server writes both counts as `u8`s |
|
||||
| `spellResult` (`arg2` of `SPELL_FAILED_SELF`) | the failing `SMSG_CAST_RESULT`'s reason byte — the number 1.12 turns into a red line and discards |
|
||||
| `corpseOwnerGuid` | **not sent**, and this is the one that really is absent. A trailing nil in Lua is indistinguishable from an absent return, and upstream sends nil for every cast without a corpse target. For the handful that have one the gap is bounded: `read_spell_cast_targets` reads the corpse guid and drops it, and the *owner* is a further hop through that corpse object's `CORPSE_FIELD_OWNER`. Nothing in grom's corpus reads the column |
|
||||
|
||||
Two subtleties worth having written down:
|
||||
|
||||
- **A channel's START is queued although its wire cast time is 0, and `UNIT_CASTEVENT`'s START is
|
||||
still suppressed for it.** Without the row, `SPELL_START_SELF` would never fire for Mind Flay or
|
||||
Blizzard; with the word, every cast-tracking addon on this realm would get a SuperWoW START whose
|
||||
`arg5` is 0 — a cast bar told to run for no time. The suppression moved from the producer
|
||||
(`cast_time_ms > 0`) to the fire, so SuperWoW's word behaves exactly as it did before the row
|
||||
existed. An instant is the other zero case, and the wiki's own START/CAST split says its only
|
||||
edge is the CAST.
|
||||
- **The `_SELF`/`_OTHER` split is by caster here, and by *target* for the aura family next door.**
|
||||
Neither is a guess: upstream's wording for the aura pair is "casts that land on the active
|
||||
player", and for this one pfUI handles both names in a single arm and then reads `arg3` to learn
|
||||
the caster — which only makes sense if the name already settled it.
|
||||
|
||||
**A gate hole closed on the way past.** `AURA_CAST_ON_SELF`/`_OTHER` were fired through one `&str`
|
||||
variable, and `reference_ui.rs`'s beyond-1.12 event gate reads the string literal *at the call* — so
|
||||
it had been blind to that pair for as long as they had existed. Both fire sites are literals now,
|
||||
with their rows; verified by probe (renaming either row makes the gate name it).
|
||||
|
||||
### The whole registered list, accounted for
|
||||
|
||||
With the registration table read (the section above), the surface is no longer a count of what a
|
||||
@@ -665,9 +711,9 @@ from the consumer side — it destructures `arg2`=spellId, `arg3`=casterGuid, `a
|
||||
|
||||
| Bucket | Names |
|
||||
|---|---|
|
||||
| **Fired** | `BUFF_ADDED_SELF`, `BUFF_REMOVED_SELF`, `DEBUFF_ADDED_SELF`, `DEBUFF_REMOVED_SELF`, `DEBUFF_ADDED_OTHER`, `DEBUFF_REMOVED_OTHER`, `AURA_CAST_ON_SELF`, `AURA_CAST_ON_OTHER` (`ui_aura.rs`, `ui_cast_events.rs`), `SPELL_CAST_EVENT` (`ui_self_cast.rs`) |
|
||||
| **Fired** | the aura eight — `BUFF_ADDED_SELF`, `BUFF_REMOVED_SELF`, `DEBUFF_ADDED_SELF`, `DEBUFF_REMOVED_SELF`, `DEBUFF_ADDED_OTHER`, `DEBUFF_REMOVED_OTHER`, `AURA_CAST_ON_SELF`, `AURA_CAST_ON_OTHER` (`ui_aura.rs`, `ui_cast_events.rs`) — the cast ten: `SPELL_START_SELF`/`_OTHER`, `SPELL_GO_SELF`/`_OTHER`, `SPELL_FAILED_SELF`/`_OTHER`, `SPELL_DELAYED_SELF`, `SPELL_CHANNEL_START`, `SPELL_CHANNEL_UPDATE` (`ui_cast_events.rs`), and `SPELL_CAST_EVENT` (`ui_self_cast.rs`) |
|
||||
| **Deliberately absent** | `BUFF_ADDED_OTHER`/`BUFF_REMOVED_OTHER` — diffing the helpful half of every streamed unit, which nothing registers (the `AuraEdge` doc says so at the enum); `SPELL_QUEUE_EVENT` — the queue; `SPELL_DELAYED_OTHER` — upstream's own note is that the server only sends the packet to the affected player, so it never fires there either |
|
||||
| **To fire, off packets already parsed** | `SPELL_START_SELF`/`_OTHER`, `SPELL_GO_SELF`/`_OTHER`, `SPELL_FAILED_SELF`/`_OTHER`, `SPELL_DELAYED_SELF`, `SPELL_CHANNEL_START`, `SPELL_CHANNEL_UPDATE` — all of these are edges `ui_cast_events.rs`'s one queue already carries for `UNIT_CASTEVENT`; what each needs beyond today's row is named in that file |
|
||||
|
||||
| **To fire, needs its own producer** | `UNIT_DIED`, the thirteen `UNIT_*_GUID`/`PLAYER_GUILD_UPDATE_GUID` rows (one shape: `guid, isPlayer, isTarget, isMouseover, isPet, partyIndex, raidIndex`, gated by the `NP_EnableUnitEvents*` CVars already registered), `UNIT_COMBAT_GUID`, `SPELL_DAMAGE_EVENT_SELF`/`_OTHER`, `SPELL_HEAL_BY_SELF`/`_BY_OTHER`/`_ON_SELF`, `SPELL_ENERGIZE_*`, `SPELL_MISS_SELF`/`_OTHER`, `SPELL_DISPEL_BY_*`, `DAMAGE_SHIELD_*`, `ENVIRONMENTAL_DMG_*`, `AUTO_ATTACK_SELF`/`_OTHER`, `BUFF_UPDATE_DURATION_SELF`, `DEBUFF_UPDATE_DURATION_SELF`, `KEY_DOWN`/`KEY_UP` |
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user