app: the rest of nampower's CVar table — 49/49, five of them live
`strings nampower.dll | grep -oE '\bNP_[A-Za-z]+\b'` is the DLL's whole CVar table and `comm -23` against `cvars.rs` now prints nothing. Fifteen names were missing, for the same reason the function list started wrong: the first pass took them off `NampowerSettings`' writes rather than off the binary. Live: `NP_ChatBubbleDistance` (the same `BubbleConfig::range_sq` SuperWoW's `ChatBubbleRange` writes — both DLLs hook one cull, so last write wins there too), the three `NP_ChatBubbles*` kind toggles, and `NP_EnableLocalSetRaidTarget`, which routes a solo `SetRaidTarget` to the local board instead of a packet the server has nobody to echo to. Two deliberate deviations, both about what a player who never touched a setting gets: `NP_ChatBubbleDistance` ships 20 yd, the reference's own cull, not upstream's 60. Tripling the distance bubbles appear at is a visible change to the game nobody asked for; writing 60 gets 60. The six `NP_EnableUnitEvents*` rows keep a default of 0, where upstream's README says 1 and its own `EVENTS.md` says 0. The disagreement costs nothing under the registration rule the event family uses: a family is wanted when its CVar is set or something registered the event. Nine stored-only rows each say what building them would take. One is a real gap rather than a shrug: `NP_PreventMountingWhenBuffCapped` defaults ON upstream, so a real install refuses a mount cast at 32 buffs instead of letting the server eat it. Everything it needs is here; the test belongs inside `send_spell_cast`, which already takes 22 parameters and none of them is the player's own descriptor — and it refuses a press, so it wants a live test rather than being bolted on at the end of a round. `KEY_DOWN`/`KEY_UP` stay unbuilt, which leaves nampower's event table at 57 of 59 with both absences explained. They need the client's numeric `KEY_*` enum, a table in `WoW.exe` this round did not read: the DLL publishes four of its values, enough to see the shape (ASCII in bank 0, specials at 0x200, function keys at 0x300) and not enough to name a key in bank 2 past the first. A guessed code is worse than no event, since an addon switching on arg1 would silently never match — and they are the keyboard hook the press-interception queue uses, so they belong with that decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
+77
-1
@@ -966,7 +966,83 @@ from the consumer side — it destructures `arg2`=spellId, `arg3`=casterGuid, `a
|
||||
| **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`), `SPELL_CAST_EVENT` (`ui_self_cast.rs`), and `SPELL_QUEUE_EVENT` (`ui_spellbook.rs`, off `spell/queue.rs`'s own rows — every mutation of the queue goes through `queue`/`pop`, so a code cannot be dropped by a caller that forgets to emit one) |
|
||||
| **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_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, 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` |
|
||||
| **To fire, needs its own producer** | `KEY_DOWN`/`KEY_UP`, and nothing else — see "nampower's combat-log family" below for the other thirty-five, which are now fired. The pair needs the client's own numeric `KEY_*` enum, which is a table in `WoW.exe` this round did not read: the DLL's strings publish four of its values (`KEY_NONE=-1`, `KEY_SHIFT=0`, `KEY_A=65`, `KEY_ESCAPE=512`, `KEY_F1=768`), enough to see the shape — ASCII in bank 0, specials at `0x200`, function keys at `0x300` — and not enough to name a key in bank 2 past the first. A guessed code is worse than no event: an addon switching on `arg1` would silently never match. They are also nampower's **keyboard hook**, the same hook the press-interception queue uses, so they belong with that decision rather than ahead of it |
|
||||
|
||||
|
||||
### nampower's combat-log family — 35 events off a decode that was already there
|
||||
|
||||
The whole of the "needs its own producer" bucket above, bar two keys. `ui_np_events.rs`.
|
||||
|
||||
**Why it was cheap, and why the earlier reasons were wrong.** The rows above said
|
||||
`AUTO_ATTACK_*` needed "a melee-swing edge no path here raises" and `SPELL_HEAL_*` needed "the
|
||||
heal amount off `SMSG_SPELLHEALLOG`, which nothing reads yet". Both were about the **cast** path,
|
||||
and both were false about the tree: `combat_log/` has parsed `SMSG_ATTACKERSTATEUPDATE`,
|
||||
`SMSG_SPELLHEALLOG`, `SMSG_SPELLENERGIZELOG`, `SMSG_SPELLLOGMISS`, `SMSG_SPELLDISPELLOG`,
|
||||
`SMSG_SPELLDAMAGESHIELD`, `SMSG_ENVIRONMENTALDAMAGELOG` and `SMSG_PERIODICAURALOG` since it was
|
||||
written — that is where the chat lines come from. This module is a second consumer of the same
|
||||
`SessionEvent`, which is also what stops its arguments drifting from the chat log's.
|
||||
|
||||
That is the general lesson of the last three rounds, now three for three: of the reasons written
|
||||
down for why a nampower feature was not built, the ones that turned out to be wrong were wrong in
|
||||
the same way — they described what one subsystem could not do, when the answer was in a different
|
||||
subsystem that already did it.
|
||||
|
||||
| Event | Source | Note |
|
||||
|---|---|---|
|
||||
| `SPELL_DAMAGE_EVENT_SELF`/`_OTHER` | `SpellDamageLog`, and a `PERIODIC_DAMAGE` tick | `arg5` is upstream's comma-joined `absorb,block,resist`; `arg8` its `effect1,effect2,effect3,auraType` off `Spell.dbc` |
|
||||
| `AUTO_ATTACK_SELF`/`_OTHER` | `AttackerState` | `sub_damage_count` was added to the struct for `arg6`: vmangos writes one block, a server splitting a swing across schools writes more |
|
||||
| `SPELL_HEAL_BY_SELF`/`_BY_OTHER`/`_ON_SELF` | `SpellHealLog`, `PERIODIC_HEAL` | A self-heal fires **two**, which is the only reading under which all three names mean something |
|
||||
| `SPELL_ENERGIZE_BY_SELF`/`_BY_OTHER`/`_ON_SELF` | `SpellEnergizeLog`, `PERIODIC_ENERGIZE` | Same two-of-three shape |
|
||||
| `SPELL_MISS_SELF`/`_OTHER` | `SpellLogMiss` | One event per row of the packet's own miss list |
|
||||
| `DAMAGE_SHIELD_SELF`/`_OTHER` | `DamageShield` | `arg1` is the shield's **bearer**, which is the packet's first guid — nampower passes a packet's fields in wire order, which is what makes these argument lists checkable rather than guessed |
|
||||
| `ENVIRONMENTAL_DMG_SELF`/`_OTHER` | `EnvironmentalDamageLog` | |
|
||||
| `SPELL_DISPEL_BY_SELF`/`_OTHER` | `SpellDispelLog` | One per aura removed |
|
||||
| `UNIT_DIED` | a `UNIT_FIELD_HEALTH` edge to zero | **The edge, not the dead flag**: the flag is also set by a create of an already-dead corpse, and `FieldChanged` deliberately raises nothing for a create — so the edge is what keeps "died" meaning died rather than "was first seen dead" |
|
||||
| `BUFF_UPDATE_DURATION_SELF` / `DEBUFF_UPDATE_DURATION_SELF` | `SMSG_UPDATE_AURA_DURATION` | The packet carries a slot and a time; the spell id comes from the player's own aura block and the half from the slot, at the same 32 the block splits at. `arg3` is `GetTime()`×1000, **not** the reference's `GetTickCount` — that counter is unreachable from Lua, so an addon can only compare against `GetTime`, and the difference the argument is used for (`expiration − duration` = now) is the same either way |
|
||||
| the thirteen `UNIT_*_GUID` / `PLAYER_GUILD_UPDATE_GUID` | `net::FieldChanged` | One shared producer over the descriptor-edge stream, one field to one name. `UNIT_NAME_UPDATE_GUID` is the exception: a name is not a descriptor field here, it arrives from `SMSG_NAME_QUERY_RESPONSE`, so it has its own handler on that packet |
|
||||
| `UNIT_COMBAT_GUID` | `UnitCombatFeedback` | The same stream 1.12's own `UNIT_COMBAT` drains, so the action word is the reference's own classification rather than a second reading of the hit bits. Stock fires only for the tokens naming a unit, which is why the GUID form exists |
|
||||
|
||||
**A registration is the switch.** Upstream's README calls every `NP_Enable*Events` CVar
|
||||
*deprecated* — "registering the event is preferred" — so `refresh_listeners` reads the VM's own
|
||||
registration table (the same one `GetFramesRegisteredForEvent` reads) and a family is wanted when
|
||||
its CVar is set **or** something is listening. Without that, three of the eleven families have no
|
||||
CVar at all and would be unreachable for every addon, and `SPELL_ENERGIZE_*` unreachable for all
|
||||
of them, since nothing installed writes its switch. It is also what keeps the cost honest: with
|
||||
nobody registered, a damage packet queues nothing rather than building eight arguments and two
|
||||
formatted strings for a fire that reaches no frame.
|
||||
|
||||
**The descriptor indices are written out and then cross-checked**, against `unit_rec::FIELDS`
|
||||
(derived from 1.12's own block sizes) and the `FIELD_UNIT_*` constants `benilla-protocol` exports.
|
||||
A hand-written descriptor index is exactly the number that is plausibly wrong and silently so: the
|
||||
event simply never fires, which no amount of play distinguishes from "the server never sent one".
|
||||
|
||||
**Nothing installed registers one of the thirty-five.** They are built because the surface is the
|
||||
deliverable, not because an addon here is waiting — so this round changes nothing visible in play,
|
||||
and that is the honest expectation to set before testing it.
|
||||
|
||||
### The CVar table, 49/49
|
||||
|
||||
`strings nampower.dll | grep -oE '\bNP_[A-Za-z]+\b'` is the DLL's whole CVar table, and
|
||||
`comm -23` against `cvars.rs`'s own rows now prints nothing. Fifteen names were missing, and the
|
||||
reason is the same mistake the function list started from: the first pass took the list off
|
||||
`NampowerSettings`' own writes rather than off the binary.
|
||||
|
||||
| Row | State | Note |
|
||||
|---|---|---|
|
||||
| `NP_ChatBubbleDistance` | **LIVE** | Writes the same `BubbleConfig::range_sq` SuperWoW's `ChatBubbleRange` writes — not a conflict to resolve, since both DLLs hook one cull and the last write wins there too. Ships **20** yd, the reference's own cull, not upstream's 60: a default is what a player who never touched the setting gets, and tripling the distance bubbles appear at is a visible change nobody asked for |
|
||||
| `NP_ChatBubblesWhisper` / `Raid` / `Battleground` | **LIVE** | Three kinds the reference bubbles never. `Raid` is the raid-**leader** and **warning** kinds, which is upstream's own wording — reading it as "raid chat" would bubble every `/raid` line |
|
||||
| `NP_EnableLocalSetRaidTarget` | **LIVE** | Solo, the wire leg marks nothing at all: the server answers by echoing to a group that is not there. This switch routes it to the local board instead — the path SuperWoW's `"local"` argument already built |
|
||||
| `NP_EnableSpellEnergizeEvents` | **LIVE** | The seventh `NP_Enable*`; nothing installed writes it, which is why it was missed |
|
||||
| `NP_SpamProtectionEnabled`, `NP_QuickcastOnDoubleCast` | stored | The press-interception half of the queue, which is a separate decision |
|
||||
| `NP_PreventRightClickPvPAttack` | stored | Needs a hook in the right-click interact path, which decides target, interact and attack in one pass; its sibling `NP_PreventRightClickTargetChange` is stored for the same reason |
|
||||
| `NP_PreventMountingWhenBuffCapped` | stored — **a real gap** | Upstream defaults it **on**, so a real install refuses a mount cast at 32 buffs rather than letting the server eat it. Everything it needs is here (`SPELL_AURA_MOUNTED`, all 32 positive slots occupied, `unit_mount_display_id() == 0`); the test belongs inside `send_spell_cast`, which already takes 22 parameters and none of them is the player's own descriptor. It also refuses a press, so it wants a live test rather than being bolted on |
|
||||
| `NP_EnableEnhancedTooltips` | stored | Authored content rather than a binding: the numbers are in `Spell.dbc` columns this client reads, but what the lines say and where they sit is upstream's design, and guessing it produces a tooltip that is neither ours nor theirs |
|
||||
| `NP_PreserveGreaterDemonAutocast` + the three `*AutocastData` | stored | Needs pet-autocast state keyed by demon rather than by the live pet guid. The three data rows are not settings at all — upstream's README calls them "managed automatically" — so they are registered to round-trip a write and nothing here will read one |
|
||||
|
||||
**Two upstream documents disagree** about the six `NP_EnableUnitEvents*` defaults: `EVENTS.md`
|
||||
says 0, the README says 1. Those rows keep 0, and the disagreement costs nothing here, because a
|
||||
default is not a behaviour under the registration rule above — every addon that wants the stream
|
||||
either writes the CVar or registers the event.
|
||||
|
||||
|
||||
**ClassicAPI's 70** are the remaining bulk and are tracked in `docs/classic-api-functions.md`
|
||||
|
||||
Reference in New Issue
Block a user