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:
@@ -49,7 +49,20 @@ pub(crate) struct BubbleConfig {
|
||||
/// hardcoded 20 yd (see [`MAX_DIST_SQ`]'s comment) made tunable. No shipped default is
|
||||
/// published for this one either (only the mod's documented 10-200 yd valid range), so this
|
||||
/// keeps today's exact 20 yd rather than guess at a different one.
|
||||
///
|
||||
/// **nampower's `NP_ChatBubbleDistance` writes the same field**, and that is not a conflict
|
||||
/// to resolve: both DLLs hook this one cull, so with both injected the last write wins, which
|
||||
/// is what happens here too. Its own documented default is 60 yd, deliberately *not* applied
|
||||
/// — see the row in `cvars.rs`.
|
||||
pub(crate) range_sq: f32,
|
||||
/// nampower's `NP_ChatBubblesWhisper`: whispers and whisper-informs, which the reference
|
||||
/// bubbles never.
|
||||
pub(crate) whisper: bool,
|
||||
/// nampower's `NP_ChatBubblesRaid` — the raid *leader* and *warning* kinds, which is what
|
||||
/// upstream's own wording names, not plain raid chat.
|
||||
pub(crate) raid: bool,
|
||||
/// nampower's `NP_ChatBubblesBattleground`: the battleground and battleground-leader kinds.
|
||||
pub(crate) battleground: bool,
|
||||
}
|
||||
|
||||
impl Default for BubbleConfig {
|
||||
@@ -58,6 +71,10 @@ impl Default for BubbleConfig {
|
||||
all: true,
|
||||
party: false,
|
||||
range_sq: MAX_DIST_SQ,
|
||||
// nampower's own defaults for the three: off.
|
||||
whisper: false,
|
||||
raid: false,
|
||||
battleground: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +119,13 @@ fn bubble_cvar(kind: ChatEventKind, cfg: &BubbleConfig) -> Option<bool> {
|
||||
match kind {
|
||||
K::Party => Some(cfg.party),
|
||||
K::Say | K::Yell | K::MonsterSay | K::MonsterYell => Some(cfg.all),
|
||||
// nampower's three extra kinds. Each stays `Some(false)` rather than `None` when its
|
||||
// switch is off, because the two answers differ where it matters: `None` is "this kind
|
||||
// never bubbles", which is what the trace prints and what a future reader would take as
|
||||
// settled, and these kinds now can.
|
||||
K::Whisper | K::WhisperInform => Some(cfg.whisper),
|
||||
K::RaidLeader | K::RaidWarning => Some(cfg.raid),
|
||||
K::Battleground | K::BattlegroundLeader => Some(cfg.battleground),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -672,6 +696,12 @@ pub(crate) fn on_cvar(ev: On<crate::cvars::CvarChanged>, mut bubbles: ResMut<Bub
|
||||
"chatbubbles" => bubbles.all = ev.flag(),
|
||||
"chatbubblesparty" => bubbles.party = ev.flag(),
|
||||
"chatbubblerange" => bubbles.range_sq = ev.num().clamp(10.0, 200.0).powi(2),
|
||||
// nampower's four. The distance shares `range_sq` with SuperWoW's name above and takes
|
||||
// the same clamp: a bubble cull of 0 or of a mile is not a setting either mod offers.
|
||||
"np_chatbubbledistance" => bubbles.range_sq = ev.num().clamp(10.0, 200.0).powi(2),
|
||||
"np_chatbubbleswhisper" => bubbles.whisper = ev.flag(),
|
||||
"np_chatbubblesraid" => bubbles.raid = ev.flag(),
|
||||
"np_chatbubblesbattleground" => bubbles.battleground = ev.flag(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -739,6 +769,7 @@ mod tests {
|
||||
all: true,
|
||||
party: true,
|
||||
range_sq: MAX_DIST_SQ,
|
||||
..Default::default()
|
||||
};
|
||||
for k in [K::Say, K::Yell, K::MonsterSay, K::MonsterYell] {
|
||||
assert_eq!(bubble_cvar(k, &on), Some(true));
|
||||
@@ -751,7 +782,6 @@ mod tests {
|
||||
for k in [
|
||||
K::Guild,
|
||||
K::Officer,
|
||||
K::Whisper,
|
||||
K::Emote,
|
||||
K::TextEmote,
|
||||
K::System,
|
||||
@@ -760,6 +790,33 @@ mod tests {
|
||||
] {
|
||||
assert_eq!(bubble_cvar(k, &on), None, "{k:?} must not bubble in v1");
|
||||
}
|
||||
// nampower's three extra kinds: off by its own default, and `Some(false)` rather than
|
||||
// `None` — the distinction matters, since `None` is the answer for a kind that can never
|
||||
// bubble. `K::Whisper` was in the list above until this family was built.
|
||||
let np = BubbleConfig {
|
||||
whisper: true,
|
||||
raid: true,
|
||||
battleground: true,
|
||||
..Default::default()
|
||||
};
|
||||
for k in [
|
||||
K::Whisper,
|
||||
K::WhisperInform,
|
||||
K::RaidLeader,
|
||||
K::RaidWarning,
|
||||
K::Battleground,
|
||||
K::BattlegroundLeader,
|
||||
] {
|
||||
assert_eq!(bubble_cvar(k, &on), Some(false), "{k:?} is off by default");
|
||||
assert_eq!(bubble_cvar(k, &np), Some(true), "{k:?} with its switch on");
|
||||
}
|
||||
// Plain raid chat is *not* one of them: upstream's `NP_ChatBubblesRaid` names the leader
|
||||
// and warning kinds, and reading it as "raid chat" would bubble every /raid line.
|
||||
assert_eq!(
|
||||
bubble_cvar(K::Raid, &np),
|
||||
None,
|
||||
"plain /raid still never bubbles"
|
||||
);
|
||||
|
||||
// Party off with say on is the default pair, so this also pins the boot state.
|
||||
let no_party = BubbleConfig::default();
|
||||
@@ -778,6 +835,7 @@ mod tests {
|
||||
all: false,
|
||||
party: true,
|
||||
range_sq: MAX_DIST_SQ,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(bubble_cvar(K::Say, &no_say), Some(false));
|
||||
assert_eq!(
|
||||
|
||||
@@ -1349,6 +1349,125 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
"nampower v2 addon-compat, LIVE: the gate on `SPELL_HEAL_BY_SELF`/`_BY_OTHER`/`_ON_SELF`, \
|
||||
off `SMSG_SPELLHEALLOG` and a `PERIODIC_HEAL` tick",
|
||||
),
|
||||
// ── The rest of the shipped DLL's CVar table ──────────────────────────────────────────────
|
||||
//
|
||||
// Fifteen names the first pass missed, and the reason it missed them is worth keeping: it
|
||||
// took the list off `NampowerSettings`' own writes rather than off the DLL. `strings
|
||||
// nampower.dll | grep -oE '\bNP_[A-Za-z]+\b'` is the whole table, and `comm -23` against
|
||||
// this file's own rows is the check. Defaults below are upstream's README, which is also the
|
||||
// one source that disagrees with `EVENTS.md`: the README gives the six `NP_EnableUnitEvents*`
|
||||
// rows a default of **1** where `EVENTS.md` says 0. Those rows keep 0, because a default is
|
||||
// not a behaviour here — `ui_np_events.rs` fires a family when its CVar is set OR something
|
||||
// registered the event, and every addon that wants the stream does one or the other.
|
||||
ours(
|
||||
"NP_ChatBubbleDistance",
|
||||
"20",
|
||||
"nampower addon-compat, LIVE: the chat-bubble cull in yards, writing the same \
|
||||
`BubbleConfig::range_sq` SuperWoW's `ChatBubbleRange` writes. Upstream's own default is \
|
||||
**60**; this row ships **20**, the reference's own hardcoded cull, because a default is \
|
||||
what a player who has never touched the setting gets, and tripling the distance bubbles \
|
||||
appear at is a visible change to the game nobody asked for. Writing 60 gets 60",
|
||||
),
|
||||
ours(
|
||||
"NP_ChatBubblesWhisper",
|
||||
"0",
|
||||
"nampower addon-compat, LIVE: bubbles for whisper and whisper-inform, which the \
|
||||
reference bubbles never — see crate::chat_bubble::bubble_cvar",
|
||||
),
|
||||
ours(
|
||||
"NP_ChatBubblesRaid",
|
||||
"0",
|
||||
"nampower addon-compat, LIVE: bubbles for the raid-leader and raid-warning kinds, which \
|
||||
is what upstream's own wording names rather than plain raid chat",
|
||||
),
|
||||
ours(
|
||||
"NP_ChatBubblesBattleground",
|
||||
"0",
|
||||
"nampower addon-compat, LIVE: bubbles for the battleground and battleground-leader kinds",
|
||||
),
|
||||
ours(
|
||||
"NP_EnableLocalSetRaidTarget",
|
||||
"1",
|
||||
"nampower addon-compat, LIVE: `SetRaidTarget` falls back to the local-only marker table \
|
||||
when the player is in no group, instead of sending a packet the server drops. The local \
|
||||
path itself is SuperWoW's `\"local\"` argument, already built \
|
||||
(`PartyRequest::SetRaidTarget.local`); this switch is what makes it the solo default, as \
|
||||
upstream's own default of 1 does — see crate::ui_party::feed::LocalRaidTarget",
|
||||
),
|
||||
// ── Stored only, each with what it would take ─────────────────────────────────────────────
|
||||
ours(
|
||||
"NP_SpamProtectionEnabled",
|
||||
"1",
|
||||
"nampower addon-compat, STORED ONLY: blocks repeat presses while the server has not yet \
|
||||
answered the first cast. That is the press-interception half of the queue, which is not \
|
||||
built — see `NP_QueueCastTimeSpells` and the queue section of docs/ADDON_COMPAT.md",
|
||||
),
|
||||
ours(
|
||||
"NP_QuickcastOnDoubleCast",
|
||||
"0",
|
||||
"nampower addon-compat, STORED ONLY: a second press casts a terrain-targeting spell at \
|
||||
the cursor. Same press-interception half; `NP_QuickcastTargetingSpells` beside it is \
|
||||
the one-press form of the same idea and is stored for the same reason",
|
||||
),
|
||||
ours(
|
||||
"NP_PreventRightClickPvPAttack",
|
||||
"0",
|
||||
"nampower addon-compat, STORED ONLY: right-click on a PvP-flagged player does not start \
|
||||
an attack. The sibling `NP_PreventRightClickTargetChange` is stored for the same reason \
|
||||
— both need a hook in the right-click interact path, which here decides target, \
|
||||
interact and attack in one pass (`target::cursor_mode`)",
|
||||
),
|
||||
ours(
|
||||
"NP_PreventMountingWhenBuffCapped",
|
||||
"1",
|
||||
"nampower addon-compat, STORED ONLY, and this one is a real gap rather than a shrug: \
|
||||
upstream defaults it ON, so a real install refuses a mount cast at 32 buffs instead of \
|
||||
letting the server eat it. Everything it needs is here — `SPELL_AURA_MOUNTED` in the \
|
||||
spell's `effect_apply_aura`, all 32 positive aura slots occupied, \
|
||||
`unit_mount_display_id() == 0` — but the test belongs inside \
|
||||
`spell::cast_send::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",
|
||||
),
|
||||
ours(
|
||||
"NP_EnableEnhancedTooltips",
|
||||
"1",
|
||||
"nampower addon-compat, STORED ONLY: upstream's own tooltip additions — a proc's chance, \
|
||||
an internal cooldown, an item's cooldown as text. Authored content rather than a \
|
||||
binding: the numbers come from `Spell.dbc` columns this client reads, but what the \
|
||||
lines say and where they sit in the tooltip is upstream's design, and guessing it would \
|
||||
produce a tooltip that is neither ours nor theirs",
|
||||
),
|
||||
ours(
|
||||
"NP_PreserveGreaterDemonAutocast",
|
||||
"1",
|
||||
"nampower addon-compat, STORED ONLY: remembers a Felguard's or Doomguard's autocast \
|
||||
toggles across a resummon. Needs pet-autocast state keyed by demon rather than by the \
|
||||
live pet guid, and the three `*AutocastData` rows below are where upstream keeps it",
|
||||
),
|
||||
// The three data rows are not settings at all: upstream's own README calls them "managed
|
||||
// automatically". They are registered so a write round-trips and persists — the same reason
|
||||
// every stored-only row exists — and nothing here will ever read one, because the blob's
|
||||
// shape is the DLL's private business.
|
||||
ours(
|
||||
"NP_FelguardAutocastData",
|
||||
"",
|
||||
"nampower addon-compat, STORED ONLY: upstream's own saved-state blob for \
|
||||
`NP_PreserveGreaterDemonAutocast`, not a setting a player or an addon sets",
|
||||
),
|
||||
ours(
|
||||
"NP_DoomguardAutocastData",
|
||||
"",
|
||||
"nampower addon-compat, STORED ONLY: see `NP_FelguardAutocastData`",
|
||||
),
|
||||
ours(
|
||||
"NP_InfernalAutocastData",
|
||||
"",
|
||||
"nampower addon-compat, STORED ONLY: see `NP_FelguardAutocastData`. The one name in the \
|
||||
DLL's CVar table that upstream's README does not document at all — it is in the binary, \
|
||||
so it is registered; its default is this client's guess at nothing rather than a \
|
||||
documented value",
|
||||
),
|
||||
// The seventh `NP_Enable*` row, registered a round after the other six: upstream has it and
|
||||
// nothing installed here writes it, which is why it was missed — the first pass took the
|
||||
// names off `NampowerSettings`' own writes rather than off the DLL's CVar table.
|
||||
@@ -4568,6 +4687,11 @@ mod tests {
|
||||
assert_eq!(
|
||||
strings,
|
||||
vec![
|
||||
// nampower's three saved-state blobs. Not settings: upstream's README calls them
|
||||
// "managed automatically", and they hold a pet's autocast toggles as text.
|
||||
"NP_DoomguardAutocastData",
|
||||
"NP_FelguardAutocastData",
|
||||
"NP_InfernalAutocastData",
|
||||
"graphicsQuality",
|
||||
"gxApi",
|
||||
"gxResolution",
|
||||
@@ -4941,6 +5065,46 @@ mod tests {
|
||||
// `ui_cast_events.rs` or `ui_np_events.rs` as a real event gate and must keep
|
||||
// proving so — which is the whole point of the orphan test: the day one of them
|
||||
// stops being read, it belongs back here with a reason, not silently.
|
||||
// The nine `NP_*` rows registered for the DLL's own table rather than for a reader
|
||||
// here. Each row in `REGISTERED` says what building it would take; the reader named
|
||||
// below is whoever actually writes it today, which for most of these is the settings
|
||||
// panel and nothing else.
|
||||
(
|
||||
"NP_SpamProtectionEnabled",
|
||||
"NampowerSettings panel writes it; the press-interception queue is not built",
|
||||
),
|
||||
(
|
||||
"NP_QuickcastOnDoubleCast",
|
||||
"NampowerSettings panel writes it; same press-interception half",
|
||||
),
|
||||
(
|
||||
"NP_PreventRightClickPvPAttack",
|
||||
"NampowerSettings panel writes it; no hook in the right-click interact path",
|
||||
),
|
||||
(
|
||||
"NP_PreventMountingWhenBuffCapped",
|
||||
"NampowerSettings panel writes it; the cast path has no refusal for it yet",
|
||||
),
|
||||
(
|
||||
"NP_EnableEnhancedTooltips",
|
||||
"NampowerSettings panel writes it; upstream's tooltip lines are not authored here",
|
||||
),
|
||||
(
|
||||
"NP_PreserveGreaterDemonAutocast",
|
||||
"NampowerSettings panel writes it; no per-demon autocast memory here",
|
||||
),
|
||||
(
|
||||
"NP_FelguardAutocastData",
|
||||
"upstream's own saved-state blob, managed automatically; nothing here reads it",
|
||||
),
|
||||
(
|
||||
"NP_DoomguardAutocastData",
|
||||
"upstream's own saved-state blob; see NP_FelguardAutocastData",
|
||||
),
|
||||
(
|
||||
"NP_InfernalAutocastData",
|
||||
"upstream's own saved-state blob; see NP_FelguardAutocastData",
|
||||
),
|
||||
// SuperWoW-era plate cast bar: pfUI's nameplates module writes 0 at load. benilla's
|
||||
// V-key plates draw no cast bar, so nothing here reads it — see the row's own note.
|
||||
(
|
||||
|
||||
@@ -704,6 +704,36 @@ fn member_for_token<'a>(group: &'a GroupState, token: &str) -> Option<&'a GroupM
|
||||
}
|
||||
|
||||
/// Drain the Lua party intents into their `CMSG_*` sends.
|
||||
/// nampower's `NP_EnableLocalSetRaidTarget`, upstream's own default **on**: with no group,
|
||||
/// `SetRaidTarget` marks this client's own board instead of sending a packet the server has
|
||||
/// nobody to echo it to.
|
||||
///
|
||||
/// Worth one resource rather than a bool on [`GroupState`]: that resource is the roster the
|
||||
/// server sends, and a client setting does not belong in it.
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct LocalRaidTarget(pub(crate) bool);
|
||||
|
||||
impl Default for LocalRaidTarget {
|
||||
fn default() -> Self {
|
||||
Self(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_cvar(ev: On<crate::cvars::CvarChanged>, mut local: ResMut<LocalRaidTarget>) {
|
||||
if ev.is("NP_EnableLocalSetRaidTarget") {
|
||||
local.0 = ev.flag();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a `SetRaidTarget` marks this client's own board instead of sending.
|
||||
///
|
||||
/// `explicit` is SuperWoW's `"local"` argument, which always wins; the other two are nampower's
|
||||
/// solo fallback. Split out as a function because it is the whole of the rule and the drain it
|
||||
/// sits in needs a VM and seven resources to stand up — the rule is worth a test either way.
|
||||
fn marks_locally(explicit: bool, solo_fallback: bool, in_group: bool) -> bool {
|
||||
explicit || (solo_fallback && !in_group)
|
||||
}
|
||||
|
||||
pub(super) fn drain_party(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
mut group: ResMut<GroupState>,
|
||||
@@ -711,6 +741,7 @@ pub(super) fn drain_party(
|
||||
names: Res<NameCache>,
|
||||
self_q: Query<&Guid, With<SelfPlayer>>,
|
||||
commands: Res<NetCommands>,
|
||||
solo_marks: Res<LocalRaidTarget>,
|
||||
mut tutorials: Option<MessageWriter<crate::tutorial::TutorialEvent>>,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
@@ -823,6 +854,11 @@ pub(super) fn drain_party(
|
||||
t => member_for_token(&group, t).map(|m| m.guid),
|
||||
};
|
||||
let Some(guid) = guid else { continue };
|
||||
// Solo, the wire leg marks nothing: the server answers `MSG_RAID_TARGET_UPDATE`
|
||||
// by echoing to the group, and there is no group to echo to — so the mark simply
|
||||
// never appears. nampower's switch turns that into the local path, which is the
|
||||
// one this client already had for SuperWoW's `"local"` argument.
|
||||
let local = marks_locally(local, solo_marks.0, group.in_group);
|
||||
if local {
|
||||
// SuperWoW's `"local"`: this client's own board only, nothing on the wire —
|
||||
// the same mark the server's own echo would otherwise apply.
|
||||
@@ -1350,6 +1386,27 @@ pub(crate) fn synthetic_raid(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::marks_locally;
|
||||
|
||||
/// nampower's `NP_EnableLocalSetRaidTarget`, which upstream defaults on: solo, the wire leg
|
||||
/// marks nothing at all, because the server answers by echoing to a group that is not there.
|
||||
#[test]
|
||||
fn a_solo_raid_target_marks_the_local_board_and_a_grouped_one_still_sends() {
|
||||
// Solo, switch on: local.
|
||||
assert!(marks_locally(false, true, false));
|
||||
// Solo, switch off: sent, and the mark never appears — upstream's pre-switch behaviour.
|
||||
assert!(!marks_locally(false, false, false));
|
||||
// In a group: sent, whatever the switch says, because the echo is what marks everyone.
|
||||
assert!(!marks_locally(false, true, true));
|
||||
assert!(!marks_locally(false, false, true));
|
||||
// SuperWoW's explicit `"local"` wins in every combination; it is a different request.
|
||||
for group in [false, true] {
|
||||
for switch in [false, true] {
|
||||
assert!(marks_locally(true, switch, group));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -26,7 +26,9 @@ impl Plugin for UiPartyPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
net::register(app);
|
||||
// A UI-only harness has no sound stack; the list handler queues the join chime.
|
||||
app.add_observer(feed::on_cvar);
|
||||
app.init_resource::<GroupState>()
|
||||
.init_resource::<feed::LocalRaidTarget>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.add_systems(
|
||||
Update,
|
||||
|
||||
+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