superwow: TrackUnit/UntrackUnit, and the four chat-bubble CVars

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-25 12:58:31 -06:00
co-authored by Claude Opus 5
parent ac857ada36
commit 400457ff82
8 changed files with 302 additions and 8 deletions
+51 -4
View File
@@ -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<bool> {
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<crate::cvars::CvarChanged>, mut bubbles: ResMut<Bub
// 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(),
"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();
+46
View File
@@ -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));
+68 -1
View File
@@ -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<u32>,
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<u64, u32>,
self_guid: Option<u64>,
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 {
+10 -1
View File
@@ -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<u64> = 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,
@@ -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<String>| {
let mut model = lua.app_data_mut::<Model>().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<String> {
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<u64> {
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]
+6
View File
@@ -796,6 +796,11 @@ pub(crate) struct Model {
pub(crate) uninvite_requests: Vec<String>,
/// `ConsoleExec` lines that were not CVar writes.
pub(crate) console_lines: Vec<String>,
/// 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<u64>,
/// 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<String>,
@@ -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,
@@ -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
+24 -2
View File
@@ -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\<name>.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`,