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:
2026-10-24 08:00:19 -06:00
co-authored by Claude Opus 5
parent ecee994ab6
commit dc59594389
8 changed files with 877 additions and 68 deletions
+48 -2
View File
@@ -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` |