app+protocol: nampower's combat-log family — 35 events off one decode
Every packet this family reports was already decoded: `combat_log/` builds its chat lines from exactly these structs. `ui_np_events.rs` is a second consumer of that decode, not a second decoder, which is also why its arguments cannot drift from the chat log's. Built: `SPELL_DAMAGE_EVENT_*`, `AUTO_ATTACK_*`, the three `SPELL_HEAL_*`, the three `SPELL_ENERGIZE_*`, `SPELL_MISS_*`, `DAMAGE_SHIELD_*`, `ENVIRONMENTAL_DMG_*`, `SPELL_DISPEL_BY_*`, `UNIT_DIED`, the `*_UPDATE_DURATION_SELF` pair, the thirteen `UNIT_*_GUID`/ `PLAYER_GUILD_UPDATE_GUID` rows and `UNIT_COMBAT_GUID`. **A registration is the switch.** Upstream's README calls every `NP_Enable*Events` CVar deprecated — "registering the event is preferred" — and three of the eleven families have no CVar at all, so a CVar-only reading would leave `SPELL_DAMAGE_EVENT_*` unreachable for every addon and `SPELL_ENERGIZE_*` unreachable for all of them, since nothing installed writes its CVar. `refresh_listeners` reads the VM's own registration table (the same one `GetFramesRegisteredForEvent` reads), so a family is wanted when its CVar is set OR something is listening. That is also what keeps the cost honest: with nobody registered a damage packet queues nothing, rather than building eight arguments for a fire that reaches no frame. Nine `NP_*` CVars move from stored-only to LIVE, and `NP_EnableSpellEnergizeEvents` is registered for the first time — the first pass took the names off `NampowerSettings`' writes rather than off the DLL's own CVar table. The two reasons written at the auto-attack and heal rows were both about the *cast* path, and both were answered by the combat-log decode that was already there. The descriptor indices the GUID-keyed family watches are written out and then cross-checked against the two tables the tree already keeps — `unit_rec::FIELDS` and the `FIELD_UNIT_*` constants. 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". `AttackerState` gains `sub_damage_count`, which is `AUTO_ATTACK_*`'s arg6; vmangos writes one block, but a server splitting a swing across schools sends more. Two test notes worth keeping: the edge tests run through the schedule, because `run_system_once` builds a fresh system each call and the `MessageReader` cursor restarts — a test written that way passes whatever the system does with the second edge. And the data-gated suite held at exactly 269, after the event gate correctly named all 35 new names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -2760,6 +2760,168 @@ mod tests {
|
||||
"BAG_NEW_ITEMS_UPDATED",
|
||||
"ClassicAPI's new-item marker, fired from its own callback registry",
|
||||
),
|
||||
// ── nampower's combat-log family (`ui_np_events.rs`) ──────────────────────────
|
||||
//
|
||||
// All of these exist for one reason, so it is written once here rather than
|
||||
// thirty-five times: 1.12 reports a hit by composing a **localized sentence** and
|
||||
// firing `CHAT_MSG_SPELL_*` with it, so every damage meter of the era is a parser of
|
||||
// grammar. nampower hooks the same packet handlers and fires the numbers. An addon
|
||||
// written against it registers the exact spelling below or hears nothing.
|
||||
(
|
||||
"SPELL_DAMAGE_EVENT_SELF",
|
||||
"nampower: a spell's damage as numbers — target, caster, spell id, amount, the \
|
||||
absorb/block/resist triple, hitInfo, school, and the spell's own effect columns. \
|
||||
The alternative on the 1.12 surface is parsing CHAT_MSG_SPELL_SELF_DAMAGE's \
|
||||
sentence, which is localized and lossy",
|
||||
),
|
||||
(
|
||||
"SPELL_DAMAGE_EVENT_OTHER",
|
||||
"nampower: the same hit when somebody else dealt it; the split is by caster",
|
||||
),
|
||||
(
|
||||
"AUTO_ATTACK_SELF",
|
||||
"nampower: a melee swing's own report off SMSG_ATTACKERSTATEUPDATE, with the \
|
||||
victim state and the sub-damage count. 1.12 fires UNIT_COMBAT for the player \
|
||||
and target tokens only, and names no attacker",
|
||||
),
|
||||
(
|
||||
"AUTO_ATTACK_OTHER",
|
||||
"nampower: the swing half above for every other attacker",
|
||||
),
|
||||
(
|
||||
"SPELL_HEAL_BY_SELF",
|
||||
"nampower: a heal's amount off SMSG_SPELLHEALLOG, which 1.12 publishes only as \
|
||||
chat text. pfUI's libpredict reads the family to correct its own prediction",
|
||||
),
|
||||
(
|
||||
"SPELL_HEAL_BY_OTHER",
|
||||
"nampower: a heal cast by anyone else",
|
||||
),
|
||||
(
|
||||
"SPELL_HEAL_ON_SELF",
|
||||
"nampower: a heal landing on the player, whoever cast it. A self-heal fires this \
|
||||
AND BY_SELF, which is the only reading under which all three names mean \
|
||||
something distinct",
|
||||
),
|
||||
(
|
||||
"SPELL_ENERGIZE_BY_SELF",
|
||||
"nampower: a power gain's type and amount off SMSG_SPELLENERGIZELOG. 1.12's own \
|
||||
UNIT_MANA says a bar moved and never says why",
|
||||
),
|
||||
(
|
||||
"SPELL_ENERGIZE_BY_OTHER",
|
||||
"nampower: a power gain somebody else caused",
|
||||
),
|
||||
(
|
||||
"SPELL_ENERGIZE_ON_SELF",
|
||||
"nampower: a power gain landing on the player; the same two-of-three shape as \
|
||||
the heal family",
|
||||
),
|
||||
(
|
||||
"SPELL_MISS_SELF",
|
||||
"nampower: one row of SMSG_SPELLLOGMISS, with the miss reason as a number rather \
|
||||
than as the word a chat line would have spelled it",
|
||||
),
|
||||
(
|
||||
"SPELL_MISS_OTHER",
|
||||
"nampower: the miss half above for another caster",
|
||||
),
|
||||
(
|
||||
"DAMAGE_SHIELD_SELF",
|
||||
"nampower: a Thorns-style return hit off SMSG_SPELLDAMAGESHIELD; arg1 is the \
|
||||
shield's bearer, which is the packet's own first guid",
|
||||
),
|
||||
(
|
||||
"DAMAGE_SHIELD_OTHER",
|
||||
"nampower: the return hit above when the shield was not the player's",
|
||||
),
|
||||
(
|
||||
"ENVIRONMENTAL_DMG_SELF",
|
||||
"nampower: falling, drowning, lava and fatigue as a type and three numbers, off \
|
||||
SMSG_ENVIRONMENTALDAMAGELOG",
|
||||
),
|
||||
(
|
||||
"ENVIRONMENTAL_DMG_OTHER",
|
||||
"nampower: the same for another unit",
|
||||
),
|
||||
(
|
||||
"SPELL_DISPEL_BY_SELF",
|
||||
"nampower: one event per aura a dispel removed, by spell id. 1.12's chat line \
|
||||
names the aura in the player's own locale and nothing else",
|
||||
),
|
||||
(
|
||||
"SPELL_DISPEL_BY_OTHER",
|
||||
"nampower: a dispel somebody else landed",
|
||||
),
|
||||
(
|
||||
"UNIT_DIED",
|
||||
"nampower: a unit's death by guid. 1.12 has PLAYER_DEAD for the player alone and \
|
||||
nothing at all for anyone else — a nameplate addon otherwise learns of a death \
|
||||
only by polling UnitIsDead. Ungated, as upstream gates it with nothing",
|
||||
),
|
||||
(
|
||||
"BUFF_UPDATE_DURATION_SELF",
|
||||
"nampower: SMSG_UPDATE_AURA_DURATION as a slot and a time. 1.12 exposes a \
|
||||
duration through GetPlayerBuffTimeLeft only by polling, so a bar either polls \
|
||||
every frame or drifts",
|
||||
),
|
||||
(
|
||||
"DEBUFF_UPDATE_DURATION_SELF",
|
||||
"nampower: the harmful half of the pair above, split at aura slot 32",
|
||||
),
|
||||
// The thirteen GUID-keyed unit events share one argument shape — `guid, isPlayer, \
|
||||
// isTarget, isMouseover, isPet, partyIndex, raidIndex` — and one reason: 1.12's own \
|
||||
// `UNIT_*` events carry a **unit token**, so they fire only for the handful of units \
|
||||
// the player happens to have a token for. An addon watching a crowd (a raid frame \
|
||||
// set, a nameplate stack) cannot hear about a unit it has no token for at all.
|
||||
(
|
||||
"UNIT_HEALTH_GUID",
|
||||
"nampower: UNIT_HEALTH for a unit the player holds no token for",
|
||||
),
|
||||
("UNIT_MANA_GUID", "nampower: UNIT_MANA by guid"),
|
||||
("UNIT_RAGE_GUID", "nampower: UNIT_RAGE by guid"),
|
||||
("UNIT_ENERGY_GUID", "nampower: UNIT_ENERGY by guid"),
|
||||
(
|
||||
"UNIT_PET_GUID",
|
||||
"nampower: UNIT_PET by guid, off the summon field",
|
||||
),
|
||||
("UNIT_FLAGS_GUID", "nampower: UNIT_FLAGS by guid"),
|
||||
(
|
||||
"UNIT_AURA_GUID",
|
||||
"nampower: UNIT_AURA by guid, on an edge anywhere in the unit's aura region",
|
||||
),
|
||||
(
|
||||
"UNIT_DYNAMIC_FLAGS_GUID",
|
||||
"nampower: UNIT_DYNAMIC_FLAGS by guid (tapped, lootable, feign death)",
|
||||
),
|
||||
(
|
||||
"UNIT_NAME_UPDATE_GUID",
|
||||
"nampower: UNIT_NAME_UPDATE by guid. The one name in the family whose edge is \
|
||||
not a descriptor field — it comes from SMSG_NAME_QUERY_RESPONSE",
|
||||
),
|
||||
(
|
||||
"UNIT_PORTRAIT_UPDATE_GUID",
|
||||
"nampower: UNIT_PORTRAIT_UPDATE by guid, off the native display id",
|
||||
),
|
||||
(
|
||||
"UNIT_MODEL_CHANGED_GUID",
|
||||
"nampower: UNIT_MODEL_CHANGED by guid, off the display id",
|
||||
),
|
||||
(
|
||||
"UNIT_INVENTORY_CHANGED_GUID",
|
||||
"nampower: UNIT_INVENTORY_CHANGED by guid, off another player's public \
|
||||
PLAYER_VISIBLE_ITEM block",
|
||||
),
|
||||
(
|
||||
"PLAYER_GUILD_UPDATE_GUID",
|
||||
"nampower: PLAYER_GUILD_UPDATE by guid, off PLAYER_GUILDID",
|
||||
),
|
||||
(
|
||||
"UNIT_COMBAT_GUID",
|
||||
"nampower: 1.12's UNIT_COMBAT with a guid and the hit's school, so a damage \
|
||||
display can follow a unit it has no token for. The action word is the \
|
||||
reference's own classification, shared with the stock event",
|
||||
),
|
||||
(
|
||||
"QUEST_TURNED_IN",
|
||||
"ClassicAPI. Carries the one fact reachable nowhere else on this surface: the \
|
||||
|
||||
@@ -22,6 +22,18 @@
|
||||
use benilla_protocol::ObjectFields;
|
||||
use benilla_ui::script::RecValue;
|
||||
|
||||
/// The descriptor index [`FIELDS`] gives for one name, for a reader that needs the index and
|
||||
/// not the kind — `ui_np_events.rs`'s own index table checks itself against this, which is the
|
||||
/// point of exposing it: two hand-written tables of the same numbers, each able to catch the
|
||||
/// other, rather than one table nothing disagrees with.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn index_of(name: &str) -> Option<u16> {
|
||||
FIELDS
|
||||
.iter()
|
||||
.find(|(n, _, _)| *n == name)
|
||||
.map(|(_, i, _)| *i)
|
||||
}
|
||||
|
||||
/// `OBJECT_END`: every `UnitFields` index is relative to this.
|
||||
const OBJECT_END: u16 = 6;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user