From 400457ff824897f6f39d00a9211bf95b5fed8b2c Mon Sep 17 00:00:00 2001 From: grom Date: Sun, 25 Oct 2026 12:58:31 -0600 Subject: [PATCH] superwow: TrackUnit/UntrackUnit, and the four chat-bubble CVars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TrackUnit is the one with a live consumer and a real feature behind it: 1.12 draws party members on the minimap and raid members NOWHERE, and pfUI puts the whole raid on it with these two — for every party and raid token on PARTY_MEMBERS_CHANGED/RAID_ROSTER_UPDATE/PLAYER_ENTERING_WORLD, each inside a pcall, so on a client without the function the feature is simply absent, which is what it has been here. The set is guids, not tokens, so a tracked raid member keeps its dot while the roster shuffles its token out from under it. The dot itself is drawn by a fourth always-show clause beside the reference's own three (`shows_tracking_dot`) — `tracked_creature` stays exactly `0x5ed210`, so the predicate that file documents is still the client's, and a tracked unit gets the ordinary cell, hover name and distance cull with nothing new drawn. The set is read off the VM handle the blip system already holds rather than behind a resource of its own: `BlipInputs` is at the 16-param ceiling and its last element is already a nested pair for that very reason. The bubble four come straight off the DLL's string pool. Three are nampower's `NP_ChatBubbles*` under SuperWoW's unprefixed names, writing the same fields — two mods, one knob, last writer wins, as the nameplate reach already is — each keeping 0 because the reference bubbles none of those kinds. `ChatBubblesCreatures` is SuperWoW's alone and the only behaviour of the four: it NARROWS `ChatBubbles` rather than standing beside it, because the mod hooks the reference's own gate and a switch added at that site can only subtract; the other reading would bubble creatures with the master switch off. It ships 1, so an untouched client bubbles monster say and yell exactly as 1.12 does. All four are welded to their struct literals in the defaults-mirror test. CREATE_CHATBUBBLE is deliberately NOT fired, and the row says why: its payload can only be the bubble, and this client's bubbles are not Lua frames — the same wall `C_ChatBubbles.GetAllChatBubbles` already answers an empty list for. Argless would be worse than silence, since an addon written against upstream reaches for arg1's regions and raises once per bubble. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/chat_bubble.rs | 55 ++++++++++- crates/benilla-app/src/cvars.rs | 46 ++++++++++ crates/benilla-app/src/minimap/blips/dots.rs | 69 +++++++++++++- crates/benilla-app/src/minimap/mod.rs | 11 ++- .../benilla-ui/src/script/compat/superwow.rs | 91 +++++++++++++++++++ crates/benilla-ui/src/script/model.rs | 6 ++ .../src/script/tests/reference_surface.rs | 6 ++ docs/ADDON_COMPAT.md | 26 +++++- 8 files changed, 302 insertions(+), 8 deletions(-) diff --git a/crates/benilla-app/src/chat_bubble.rs b/crates/benilla-app/src/chat_bubble.rs index 4ada2df9..1ccbf48a 100644 --- a/crates/benilla-app/src/chat_bubble.rs +++ b/crates/benilla-app/src/chat_bubble.rs @@ -63,6 +63,16 @@ pub(crate) struct BubbleConfig { pub(crate) raid: bool, /// nampower's `NP_ChatBubblesBattleground`: the battleground and battleground-leader kinds. pub(crate) battleground: bool, + /// SuperWoW's `ChatBubblesCreatures`: the monster say and yell kinds, which the reference + /// bubbles under `ChatBubbles` with everything else. Defaults **on**, so a player who never + /// touched it keeps the reference's behaviour; see [`bubble_cvar`] for why it narrows + /// `ChatBubbles` rather than replacing it. + /// + /// The three kinds above take SuperWoW's unprefixed spellings too — `ChatBubblesWhisper`, + /// `ChatBubblesRaid`, `ChatBubblesBattleground` — on these same fields. Two mods, one knob, + /// last writer wins, which is what both mods injected does and what a player moving either + /// slider expects (the nameplate reach already works this way). + pub(crate) creatures: bool, } impl Default for BubbleConfig { @@ -75,6 +85,7 @@ impl Default for BubbleConfig { whisper: false, raid: false, battleground: false, + creatures: true, } } } @@ -118,7 +129,13 @@ fn bubble_cvar(kind: ChatEventKind, cfg: &BubbleConfig) -> Option { use ChatEventKind as K; match kind { K::Party => Some(cfg.party), - K::Say | K::Yell | K::MonsterSay | K::MonsterYell => Some(cfg.all), + K::Say | K::Yell => Some(cfg.all), + // SuperWoW's `ChatBubblesCreatures` **narrows** `ChatBubbles` rather than standing beside + // it: the mod hooks this same gate, and the gate the reference has here is `ChatBubbles` + // (`0x608b0d`), so a switch added at the site can only subtract. The alternative reading — + // `Some(cfg.creatures)` alone — would bubble creatures with the master switch off, which + // is a surprise no setting advertises. + K::MonsterSay | K::MonsterYell => Some(cfg.all && cfg.creatures), // 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 @@ -699,9 +716,13 @@ pub(crate) fn on_cvar(ev: On, mut bubbles: ResMut 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(), + "np_chatbubbleswhisper" | "chatbubbleswhisper" => bubbles.whisper = ev.flag(), + "np_chatbubblesraid" | "chatbubblesraid" => bubbles.raid = ev.flag(), + "np_chatbubblesbattleground" | "chatbubblesbattleground" => { + bubbles.battleground = ev.flag(); + } + // SuperWoW's own, with no nampower twin. + "chatbubblescreatures" => bubbles.creatures = ev.flag(), _ => {} } } @@ -817,6 +838,32 @@ mod tests { None, "plain /raid still never bubbles" ); + // SuperWoW's `ChatBubblesCreatures` narrows `ChatBubbles`: off, the monster kinds stop + // and the player kinds do not; and with the master switch off it cannot turn them back on. + let no_creatures = BubbleConfig { + creatures: false, + ..on + }; + for k in [K::MonsterSay, K::MonsterYell] { + assert_eq!(bubble_cvar(k, &no_creatures), Some(false), "{k:?} silenced"); + } + for k in [K::Say, K::Yell] { + assert_eq!( + bubble_cvar(k, &no_creatures), + Some(true), + "{k:?} is a player kind and keeps bubbling" + ); + } + let creatures_only = BubbleConfig { + all: false, + creatures: true, + ..on + }; + assert_eq!( + bubble_cvar(K::MonsterSay, &creatures_only), + Some(false), + "the master switch wins: a new switch at the reference's own gate can only subtract" + ); // Party off with say on is the default pair, so this also pins the boot state. let no_party = BubbleConfig::default(); diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index fec83580..ac5b68ad 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -502,6 +502,34 @@ pub(crate) const REGISTERED: &[Registered] = &[ "20", "SuperWoW addon-compat: see crate::chat_bubble::BubbleConfig::range_sq", ), + // SuperWoW's other four bubble rows, read off the DLL's own string pool beside that one. + // Three are nampower's `NP_ChatBubbles*` under their unprefixed names and write the SAME + // `BubbleConfig` fields — two mods, one knob, last writer wins, as the nameplate reach + // already is. Each keeps `0`: the reference bubbles none of those three kinds, and a default + // is what a player who never touched the setting gets. + ours( + "ChatBubblesWhisper", + "0", + "SuperWoW addon-compat, LIVE: nampower's `NP_ChatBubblesWhisper` under SuperWoW's own name, on the same field — see crate::chat_bubble::BubbleConfig::whisper", + ), + ours( + "ChatBubblesRaid", + "0", + "SuperWoW addon-compat, LIVE: nampower's `NP_ChatBubblesRaid` under SuperWoW's own name, on the same field — see crate::chat_bubble::BubbleConfig::raid", + ), + ours( + "ChatBubblesBattleground", + "0", + "SuperWoW addon-compat, LIVE: nampower's `NP_ChatBubblesBattleground` under SuperWoW's own name, on the same field — see crate::chat_bubble::BubbleConfig::battleground", + ), + // This one is SuperWoW's alone and is the only behaviour of the four: it splits the monster + // say and yell kinds out of `ChatBubbles`, which the reference gates them under. Ships `1`, + // so an untouched client still bubbles them exactly as 1.12 does. + ours( + "ChatBubblesCreatures", + "1", + "SuperWoW addon-compat, LIVE: the monster say and yell kinds, which 1.12 gates under `ChatBubbles` with everything else — see crate::chat_bubble::BubbleConfig::creatures", + ), // Registered "1" (`0x82e748`), category 4. `profanityFilter` (`0x402e68`, name `0x82e7f4`, // callback `0x403570`) masks `ChatProfanity.dbc` spans inside the shared masker `0x4a1a66`, // covering all thirteen call sites; `spamFilter` (`0x402e8e`, name `0x82e7d4`, callback @@ -3084,6 +3112,24 @@ mod tests { assert_eq!(d["ChatBubbles"] != 0.0, bubbles.all); assert_eq!(d["ChatBubblesParty"] != 0.0, bubbles.party); assert!(bubbles.all && !bubbles.party, "the binary's own pair"); + // The four SuperWoW rows and the three nampower twins that share their fields: every one + // of these IS the live configuration for a player who never wrote it, so a registered + // default that drifts from the struct literal is a setting that reads one way in + // `config.toml` and behaves another (see `benilla-cvar-default-trap`). + assert_eq!(d["ChatBubblesWhisper"] != 0.0, bubbles.whisper); + assert_eq!(d["ChatBubblesRaid"] != 0.0, bubbles.raid); + assert_eq!(d["ChatBubblesBattleground"] != 0.0, bubbles.battleground); + assert_eq!(d["ChatBubblesCreatures"] != 0.0, bubbles.creatures); + assert_eq!(d["NP_ChatBubblesWhisper"], d["ChatBubblesWhisper"]); + assert_eq!(d["NP_ChatBubblesRaid"], d["ChatBubblesRaid"]); + assert_eq!( + d["NP_ChatBubblesBattleground"], d["ChatBubblesBattleground"], + "one field cannot have two different defaults, whichever mod's name wrote it" + ); + assert!( + bubbles.creatures, + "1.12 bubbles monster say and yell; an untouched client must keep doing it" + ); let zoom = MinimapZoom::default(); assert_eq!(d["minimapZoom"], f32::from(zoom.outdoor)); assert_eq!(d["minimapInsideZoom"], f32::from(zoom.inside)); diff --git a/crates/benilla-app/src/minimap/blips/dots.rs b/crates/benilla-app/src/minimap/blips/dots.rs index 0c01130a..ab2459c4 100644 --- a/crates/benilla-app/src/minimap/blips/dots.rs +++ b/crates/benilla-app/src/minimap/blips/dots.rs @@ -162,6 +162,24 @@ fn tracked_creature( .is_some_and(|t| (1..=32).contains(&t) && tracking.creatures & (1u32 << (t - 1)) != 0) } +/// Whether a unit draws a tracking dot: the reference's own predicate, or SuperWoW's +/// `TrackUnit` set (`benilla-ui`'s `compat/superwow.rs`), which is a fourth always-show clause +/// the mod adds rather than a change to the three [`tracked_creature`] documents. +/// +/// `addon_tracked` is sorted by guid, so this is a binary search per candidate and not a set +/// build per frame. +fn shows_tracking_dot( + addon_tracked: &[u64], + guid: u64, + tracking: SelfTracking, + creature_type: Option, + dynamic_flags: u32, + unit_creeping: bool, +) -> bool { + addon_tracked.binary_search(&guid).is_ok() + || tracked_creature(tracking, creature_type, dynamic_flags, unit_creeping) +} + /// The creature-type resolver `0x605570`: a shapeshift form's `SpellShapeshiftForm.dbc` type /// first (`<= 0` reads Humanoid), else an NPC's cached template, else Humanoid for a player. fn creature_type_of( @@ -196,6 +214,9 @@ pub(in crate::minimap) fn emit_tracking_dots( ctx: &BlipCtx, tracking: SelfTracking, candidates: &TrackedCandidates, + // SuperWoW's `TrackUnit` set, sorted by guid (`benilla-ui`'s `compat/superwow.rs`): each of + // these draws the ordinary tracked-unit dot whatever the player's own tracking spell is. + addon_tracked: &[u64], statuses: &HashMap, self_guid: Option, names: &NameCache, @@ -283,7 +304,14 @@ pub(in crate::minimap) fn emit_tracking_dots( names, forms, ); - if tracked_creature(tracking, creature_type, dyn_flags, creeping) { + if shows_tracking_dot( + addon_tracked, + guid.0, + tracking, + creature_type, + dyn_flags, + creeping, + ) { dot(guid.0, tf, TRACKED_UNIT_CELL, DotName::Guid); } } @@ -461,6 +489,45 @@ mod tests { "a key-ITEM slot's index is an item entry, not a LockType" ); assert!(!tracked_resource(1 << 2, 99, &locks), "unknown lock id"); + } + + /// SuperWoW's `TrackUnit` set is a fourth always-show clause: a tracked guid draws its dot + /// with **no** tracking spell up and no dyn-flag help, which is the whole point — 1.12 draws + /// raid members on the minimap nowhere at all. An untracked guid is unaffected. + #[test] + fn the_addon_tracked_set_lights_a_dot_the_reference_would_not() { + let none = SelfTracking { + creatures: 0, + resources: 0, + stealthed: false, + }; + let tracked = [0x10u64, 0x20, 0x30]; + + assert!( + shows_tracking_dot(&tracked, 0x20, none, Some(7), 0, false), + "in the set, nothing else on: the dot the mod exists to draw" + ); + assert!( + !shows_tracking_dot(&tracked, 0x21, none, Some(7), 0, false), + "not in the set and nothing else on: no dot" + ); + assert!( + !shows_tracking_dot(&[], 0x20, none, Some(7), 0, false), + "an empty set changes nothing — the usual case" + ); + // The reference's own three clauses still stand on their own. + assert!( + shows_tracking_dot(&[], 0x20, none, Some(7), UNIT_DYNFLAG_TRACK_UNIT, false), + "the server's own track bit" + ); + let beasts = SelfTracking { + creatures: 1 << 0, + ..none + }; + assert!( + shows_tracking_dot(&[], 0x20, beasts, Some(1), 0, false), + "and the tracking spell's type mask" + ); // Track Beasts sets bit 0 (Beast is creature type 1). let beasts = SelfTracking { diff --git a/crates/benilla-app/src/minimap/mod.rs b/crates/benilla-app/src/minimap/mod.rs index a644fcf6..26201750 100644 --- a/crates/benilla-app/src/minimap/mod.rs +++ b/crates/benilla-app/src/minimap/mod.rs @@ -406,7 +406,15 @@ fn emit_minimap( *blip_hover = blips::MinimapBlipHover::None; // Drain the `Minimap:PingLocation` click before any early return: it is spent in the frame it // was made, and a click on a frame the map does not draw is not a ping. - let click = script.and_then(|mut s| s.take_minimap_ping_request()); + // SuperWoW's `TrackUnit` set, read off the VM handle this system already holds rather than + // behind a resource of its own: `BlipInputs` is at the 16-param ceiling (its last element is + // already a nested pair for exactly that reason), and the set is at most a raid of guids. + // Empty — the usual case — allocates nothing. + let mut tracked_units: Vec = Vec::new(); + let click = script.and_then(|mut s| { + tracked_units = s.tracked_units(); + s.take_minimap_ping_request() + }); let (Some(slot), Some(assets), Some(map), Some(catalog)) = (widget.0.as_ref(), assets, map, catalog) else { @@ -744,6 +752,7 @@ fn emit_minimap( ctx, tracking, &tracked, + &tracked_units, quest.statuses(), self_guid, &names, diff --git a/crates/benilla-ui/src/script/compat/superwow.rs b/crates/benilla-ui/src/script/compat/superwow.rs index 704b9d51..15882e9d 100644 --- a/crates/benilla-ui/src/script/compat/superwow.rs +++ b/crates/benilla-ui/src/script/compat/superwow.rs @@ -218,6 +218,45 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { // is dropped by the same rule every routed line follows, which is upstream's behaviour too — // the function appends to an open file. // + // TrackUnit("unit") / UntrackUnit("unit"): put a minimap tracking dot on a named unit, and + // take it off. The set is by **guid**, not by token, so a tracked raid member keeps its dot + // while the roster shuffles its token out from under it. + // + // Why this is a real feature rather than a cosmetic one: 1.12 draws party members on the + // minimap and **raid members nowhere** (`minimap::blips::emit_party_dots` walks + // `party_slots()`, which is what the reference's own placement does). pfUI calls `TrackUnit` + // for all four party and all forty raid tokens on every `PARTY_MEMBERS_CHANGED`, + // `RAID_ROSTER_UPDATE` and `PLAYER_ENTERING_WORLD` under `C.unitframes.track_group` + // (`modules/superwow.lua:96-127`), each inside a `pcall` — so on a client without the + // function the whole feature is simply absent, which is what it has been here. + // + // The dot is drawn by the same always-show clause the reference's own creature tracking uses + // (`UNIT_DYNFLAG_TRACK_UNIT`, `0x5ed210`), so a tracked unit gets the ordinary tracked-unit + // cell, hover name and distance cull, and nothing new had to be drawn. A token naming + // nothing live is ignored rather than raising: pfUI calls these on units that may have just + // left the group, which is exactly the case its `pcall` is defending against. + for (name, track) in [("TrackUnit", true), ("UntrackUnit", false)] { + g.set( + name, + lua.create_function(move |lua, token: Option| { + let mut model = lua.app_data_mut::().expect("model app_data"); + let guid = token + .as_deref() + .and_then(|t| model.unit(t)) + .map(|u| u.guid) + .filter(|g| *g != 0); + if let Some(guid) = guid { + if track { + model.tracked_units.insert(guid); + } else { + model.tracked_units.remove(&guid); + } + } + Ok(()) + })?, + )?; + } + // CanLootUnit("unit"): whether that unit's corpse holds loot for us. Needs no new feed — // [`UnitState`] already carries `dynamic_flags` raw and `dead`, so this is the same two-part // test the loot sparkle makes on the engine side (`creature_anim/spell_visual.rs`'s @@ -516,6 +555,16 @@ impl crate::script::UiScript { pub fn take_combat_log_adds(&mut self) -> Vec { std::mem::take(&mut self.model_mut().combat_log_adds) } + + /// The `TrackUnit` set as a sorted slice of guids, for the minimap's blip pass; empty when + /// nothing is tracked, which is the usual case and allocates nothing. + /// + /// A copy rather than a borrow because the caller holds the VM through a `NonSendMut` it then + /// spends elsewhere, and because the set is at most the player's own raid — forty-odd `u64`s. + /// Sorted, so the blip pass binary-searches it instead of keeping an index of its own. + pub fn tracked_units(&self) -> Vec { + self.model_mut().tracked_units.iter().copied().collect() + } } #[cfg(test)] @@ -764,6 +813,48 @@ mod tests { ); } + /// `TrackUnit`/`UntrackUnit` keep a set of **guids**, so a tracked raid member keeps its dot + /// while the roster shuffles its token; and a token naming nothing is ignored, not raised, + /// because pfUI calls these on units that may have just left the group. + #[test] + fn track_unit_keeps_a_sorted_guid_set_and_untrack_removes_from_it() { + let mut s = UiScript::new().unwrap(); + let unit = |guid: u64| { + Some(UnitState { + exists: true, + has_object: true, + guid, + ..Default::default() + }) + }; + s.set_unit("raid1", unit(0x30)); + s.set_unit("raid2", unit(0x10)); + s.set_unit("raid3", unit(0x20)); + assert!(s.tracked_units().is_empty()); + + s.run( + r#"TrackUnit("raid1") TrackUnit("raid2") TrackUnit("raid3") + TrackUnit("raid2") + TrackUnit("raid9") TrackUnit()"#, + ) + .unwrap(); + assert_eq!( + s.tracked_units(), + vec![0x10, 0x20, 0x30], + "sorted, deduplicated, and a token naming nothing added nothing" + ); + assert!(s.errors().is_empty(), "handler errors: {:?}", s.errors()); + + s.run(r#"UntrackUnit("raid3") UntrackUnit("raid9") UntrackUnit()"#) + .unwrap(); + assert_eq!(s.tracked_units(), vec![0x10, 0x30]); + assert!( + s.tracked_units().windows(2).all(|w| w[0] < w[1]), + "the blip pass binary-searches this, so the order is load-bearing" + ); + assert!(s.errors().is_empty(), "handler errors: {:?}", s.errors()); + } + /// `CanLootUnit("unit")` is the loot sparkle's own two-part test read through the token map: /// dead, and `UNIT_DYNAMIC_FLAGS` bit `0x1`. A unit with no descriptor answers nil, not 0. #[test] diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index 86a74ab3..625cac1c 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -796,6 +796,11 @@ pub(crate) struct Model { pub(crate) uninvite_requests: Vec, /// `ConsoleExec` lines that were not CVar writes. pub(crate) console_lines: Vec, + /// SuperWoW's `TrackUnit`/`UntrackUnit` set, by guid: the units the minimap draws a tracking + /// dot for on top of whatever the player's own tracking spell finds + /// (`benilla-app`'s `minimap::blips`). Sorted, so the app can binary-search it per candidate + /// without building an index of its own. + pub(crate) tracked_units: std::collections::BTreeSet, /// SuperWoW's `CombatLogAdd` lines, queued for the app to append to `WoWCombatLog.txt` /// (`compat/superwow.rs`). Not chat: nothing is printed to a window. pub(crate) combat_log_adds: Vec, @@ -1950,6 +1955,7 @@ impl Model { roll_requests: Vec::new(), uninvite_requests: Vec::new(), console_lines: Vec::new(), + tracked_units: std::collections::BTreeSet::new(), combat_log_adds: Vec::new(), logging_chat: false, logging_combat: false, diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index 084b7d2b..2dd0b512 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -284,6 +284,12 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { "CanLootUnit", "GetSpeed", "UnitNameplate", + // SuperWoW's `TrackUnit`/`UntrackUnit` (script/compat/superwow.rs): a minimap tracking dot + // on a named unit. 1.12 draws party members and raid members nowhere, and has no verb for + // tracking anything by hand — the dots it draws come from a tracking spell alone. pfUI + // puts the whole raid on the minimap with these. + "TrackUnit", + "UntrackUnit", // SuperWoW's `CombatLogAdd(text)` (script/compat/superwow.rs): one line appended to // `WoWCombatLog.txt`, which 1.12 writes C-side off its own chat router and gives Lua no // way into. It is also a detection name in its own right — BigWigs' Spell Requests gates diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index bedc7a95..27a9f025 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -38,7 +38,7 @@ nothing behind it. | `UNIT_CASTEVENT` | done | `crates/benilla-app/src/superwow_events.rs`: a `CastEventFeed` queue the cast handlers push into, drained once per tick by one VM-holding system that fires the event — the pattern `ui_mirror.rs` already uses, and the only way across, since `spell/net.rs`'s `Lifecycle`/`Scene` bundles deliberately hold no `UiScript` (`game_plugins.rs`'s `every_vm_holder_in_update_declares_its_side_of_the_tick` gates that). Edges wired: `START` from `SMSG_SPELL_START` with its wire cast time as `arg5` (an instant raises none — its only edge is `CAST`, which is what the wiki's own START/CAST split means), `CAST` from `SMSG_SPELL_GO`, `FAIL` from `SMSG_SPELL_FAILED_OTHER`, `CHANNEL` from `MSG_CHANNEL_START` with its duration. GUIDs go out in the same 16-hex-digit spelling every other `compat/` surface uses, so `arg1`/`arg2` compare directly against a `UnitExists`/`UnitGUID` return. Not wired: `MAINHAND`/`OFFHAND` — benilla's melee-swing path raises no cast edge at all, so there is nothing to forward. `MSG_CHANNEL_START` names no caster (1.12 sends it for the local player's own channel only), so `CHANNEL`'s `arg1` is self and its `arg2` is 0 | | `RAW_COMBATLOG` | **done** | `ui_chat/frames.rs`'s `route` — the one funnel every combat line already passes on its way to a window and to `WoWCombatLog.txt`. Two arguments, as its one consumer in grom's corpus destructures them: the printed sentence, and the same sentence with each unit name spelled as its GUID (`PendingCombat::guid_fills`, composed a second time through the same `compose_line`, so the two can only ever differ in the name slots). Fired **before** the `CHAT_MSG_*`, which is what "raw" means upstream — it hooks the combat-log parse, ahead of the client's own formatting. Gated on `has_listener`, the gate nampower's combat-log family already uses, and the `arg2` composition is skipped under the same test: a raid's log is tens of lines a second. A variant whose template says "you" keeps saying it, because that word is in the *template* and not in a fill — and its consumer depends on exactly that, reading the word and then asking `UnitExists("player")` for its own guid. **Why this row came first:** of SuperWoW's whole surface, it and `CombatLogAdd` are the only two names grom's addons call that this client lacked (`scripts/dll-surface.py --wanted`), and `SuperCleveRoidMacros/Utility.lua:10206` funnels **five** subsystems through this single event — immunity and resist learning, hidden-CC ("afflicted by") detection, debuff-duration learning off the GUID half, and the reactive-proc window that lights **Overpower** and **Revenge**. Its own comment there is why the last of those can come from nowhere else: "SPELL_GO only provides hit/miss COUNTS, not miss TYPES, so it cannot distinguish dodge (Overpower proc) from parry/block/resist". This does **not** by itself fix the Overpower complaint: that needs the dodged-*ability* sentence to be emitted at all, which is its own open item (`benilla-overpower-aura-spelldodged`) | | `CombatLogAdd(text)` | **done** | `compat/superwow.rs` queues the line; `ui_chat/logging.rs`'s `sync_chat_logging` appends it to `WoWCombatLog.txt` through the same `record` every routed line uses, so it is stamped identically and one writer owns the file. Dropped while the log is closed, which is upstream's behaviour too (it appends to an open file). A non-string argument is ignored rather than raising: its one real caller is a debug-trace helper (`BigWigs/Plugins/MageTools.lua:761`, under a `fileDebug` profile flag) and a raise there would take the addon down for something the player cannot act on. **It is also a detection name**: `BigWigs/Plugins/SpellRequests.lua:6` is `if CombatLogAdd then hasSuperWow = true`, which gates that whole module's SuperWoW path | -| `CREATE_CHATBUBBLE` | todo | needs the chat-bubble creation site in `chat_bubble.rs` (its `push`/`drive_bubbles`); now that `superwow_events.rs` exists with the queue-plus-drain pattern established, this is a small addition rather than new architecture. It is the **last** of SuperWoW's three events — `SUPERWOW_COMBAT_LOG_EVENT`, which this row used to carry, is a ghost: see the manifest below | +| `CREATE_CHATBUBBLE` | **blocked, deliberately not fired** | the last of SuperWoW's three events, and the one that cannot be made good: its payload can only be the bubble, and this client's bubbles are not Lua frames — they are a 2-D world overlay (`chat_bubble.rs`), as the reference's own `CGChatBubbleFrame` is. The same wall `C_ChatBubbles.GetAllChatBubbles` hit, and firing it argless would be *worse* than silence (an addon reaching for `arg1`'s regions raises, once per bubble). See the manifest section below for what the one piece of work behind it would close. `SUPERWOW_COMBAT_LOG_EVENT`, which this row used to carry, is a ghost | | `NameplateRange` | done | one CVar, two readers: `nameplates.rs`'s `NameConfig::range_sq` (the overhead name — introduces a cull where the reference has none at all) and `vplates.rs`'s `VPlateMode::range_sq` (replaces the reference's own hardcoded 20 yd V-key-plate range). Default `80`, the top of the mod's documented 10-80 range — no shipped default is published, so this is the least-restrictive honest choice, not a confirmed number | | `ChatBubbleRange` | done | `chat_bubble.rs`'s `BubbleConfig::range_sq`, replacing the reference's hardcoded 20 yd. Default `20` (today's exact value) — no shipped default published for this one either, within the mod's documented 10-200 yd range | | `NameplateMotion`, `NameplateVerticalSpacing` | todo | needs locating whatever (if anything) already staggers overlapping nameplates, not investigated | @@ -77,7 +77,7 @@ The usage strings, verbatim, for what is left: | Usage string, as the DLL prints it | Status | |---|---| | `CombatLogAdd("text" [, addToRawLog])` | **done** — the second argument is why this row is here: nothing in the corpus passes it, and it would have been missed | -| `TrackUnit("unit")` / `UntrackUnit("unit")` | todo — pfUI calls `TrackUnit` for every party and raid member under `C.unitframes.track_group` (`modules/superwow.lua:96-127`, inside `pcall`), to put the group on the minimap | +| `TrackUnit("unit")` / `UntrackUnit("unit")` | **done** — a set of **guids** in the VM (`compat/superwow.rs`), read by the minimap's blip pass as a **fourth always-show clause** beside the reference's own three (`shows_tracking_dot` in `minimap/blips/dots.rs`; `tracked_creature` stays exactly `0x5ed210`). So a tracked unit gets the ordinary tracked-unit cell, hover name and distance cull, and nothing new is drawn. Guids rather than tokens, so a tracked raid member keeps its dot while the roster shuffles its token. **Why it is a real feature and not a cosmetic one:** 1.12 draws party members on the minimap and raid members **nowhere** — `emit_party_dots` walks `party_slots()`, which is what the reference's own placement does — and pfUI puts the whole raid on it with these two, for every party and raid token on `PARTY_MEMBERS_CHANGED`/`RAID_ROSTER_UPDATE`/`PLAYER_ENTERING_WORLD` under `C.unitframes.track_group`, each inside a `pcall`. So on a client without the function the feature is simply absent, which is what it has been here. The set is read off the VM handle the blip system already holds rather than behind a resource of its own, because `BlipInputs` is at the 16-param ceiling | | `UnitPosition("unit")` | done | | `GetPlayerBuffID(buffIndex)` | done | | `UnitNameplate("unit")` | **done** — `compat/superwow.rs`, over `NamePlates::frame_for_guid`, through the **same** `plate_frame` wrapper ClassicAPI's `C_NamePlate.GetNamePlateForUnit` uses, so the two names cannot hand back different objects for one plate. Welded by a test that asserts exactly that, because an addon may feature-detect either | @@ -91,6 +91,28 @@ The usage strings, verbatim, for what is left: | `GetQuestLinkForID(questID)` | todo — the same link for a quest the log does not hold, which is a server round trip (`Requesting quest ID %d from server` is the DLL's own next string) | | `ImportFile("filename")` / `ExportFile("filename","text")` | done, under nampower's fence. **Upstream's own folder is `Imports\.txt`** (two more of its literals), where ours is `benilla-config/CustomFiles/` — the deviation is written up in the file-and-credential section below | +**The four chat-bubble rows are now live.** `ChatBubblesWhisper`, `ChatBubblesRaid` and +`ChatBubblesBattleground` are nampower's `NP_ChatBubbles*` under SuperWoW's own names, writing the +same `BubbleConfig` fields — two mods, one knob, last writer wins, as the nameplate reach already +is — and each keeps `0`, because the reference bubbles none of those three kinds. +`ChatBubblesCreatures` is SuperWoW's alone and the only *behaviour* of the four: it splits the +monster say and yell kinds out of `ChatBubbles`, which 1.12 gates them under. It ships `1`, so an +untouched client bubbles them exactly as 1.12 does, and it **narrows** `ChatBubbles` rather than +standing beside it — the mod hooks the reference's own gate (`0x608b0d`), and a switch added at +that site can only subtract; the other reading would bubble creatures with the master switch off, +which is a surprise no setting advertises. All four are welded to their struct literals in +`cvars.rs`'s own defaults-mirror test, the `benilla-cvar-default-trap` discipline. + +**`CREATE_CHATBUBBLE` cannot be made good, and the reason is already written up elsewhere in this +file.** Its payload can only be the bubble, and this client's bubbles are not Lua frames at all — +they are a 2-D world overlay drawn by `chat_bubble.rs`, the same way the reference's own +`CGChatBubbleFrame` is drawn. That is the identical wall `C_ChatBubbles.GetAllChatBubbles` hit, and +it answers an empty list for it. Firing the event argless would be worse than not firing: an addon +written against upstream would reach for `arg1`'s regions and raise, once per bubble. **The real +item behind both** is making the native bubbles addressable from Lua — a frame with a texture and a +`FontString` per live bubble, kept in step with the overlay's own spawn, stacking and expiry rules — +and that one piece of work closes the event, the namespace and pfUI's bubble skinning together. + And the CVars it registers, as names: `NameplateRange`, `NameplateMotion`, `ChatBubbleRange`, `ChatBubblesRaid`, `ChatBubblesBattleground`, `ChatBubblesWhisper`, `ChatBubblesCreatures`, `BackgroundSound`, `UncapSounds`, `InterfaceVolume`, `LootSparkle`, `HealingText`,