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
@@ -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