cvars: the later clients' per-kind nameplate switches, so an addon's rows are editable

Cinematic's Nameplates page asks `GetCVar(name) ~= nil` before it will offer a
row (`Effects.lua`'s `HasPlateSettings`). A nil answer is not "off" to an
addon written for a later client — it is "this client has no such setting" —
and the whole row goes grey, which is why grom could not touch Friendly NPCs.
The two rows that worked were the two that happen to name
`nameplateShowEnemies`/`nameplateShowFriends`, the only nameplate CVars this
client had, because 1.12 has none at all.

Seventeen rows registered. Sixteen are live: friendly players, friendly NPCs,
hostile players, and each side's pets/guardians/totems/minions — eight names on
the friendly side and four on the hostile one, each side driving one field,
because a 1.12 object update carries an owner and not a kind of owner and an
addon writes the whole set together. `nameplateMaxDistance` joins
`NameplateRange` and `NP_NameplateDistance` as a third name for one reach.

All default "1", the least-restrictive value, for the reason `NameplateRange`
gives: the two masters already decide whether plates show at all, and a
refinement shipped off would take away plates a player has today. Paired
against `VPlateMode::default()` in the defaults test, the pairing the
derived-Default trap forced on `occlusion`.

Five are registered and inert, each saying so on its own row and in the orphan
test's list: `nameplateShowAll` (1.12 has no in-combat-only mode),
`nameplateShowSelf` (no plate over the player),
`nameplateShowOnlyNameForFriendlyPlayerUnits` and
`nameplateForceShowUnitName` (no such plate mode here), and
`nameplateShowEnemyMinus` (the plate path has no creature rank).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-02 21:15:38 -07:00
co-authored by Claude Opus 5
parent 36f504820d
commit d39d3ec5cf
4 changed files with 307 additions and 0 deletions
+205
View File
@@ -412,6 +412,155 @@ pub(crate) const REGISTERED: &[Registered] = &[
// boot off (`UIOptionsFrame.lua:180-183`, `:769-775`): no plates until V.
same(crate::vplates::CVAR_ENEMIES, "0"),
same(crate::vplates::CVAR_FRIENDS, "0"),
// ── The later clients' per-kind plate switches ───────────────────────────────────────────
//
// 1.12 has the two masters above and no notion of what a unit *is*; every client after it
// splits them by kind, and an addon written against those clients drives the split names. It
// asks for them first, and a nil answer is not "off" to such an addon — it is "this client
// has no such setting", and the whole row goes grey. Cinematic's Nameplates page does exactly
// that (`Effects.lua`'s `HasPlateSettings` → `GetCVar(name) ~= nil`), which is why grom could
// not touch its Friendly NPCs row: that row's only name is `nameplateShowFriendlyNPCs`, and
// the rows that worked were the two that happen to name the masters.
//
// All "1", the least-restrictive value, for the reason `NameplateRange` gives: the masters
// already decide whether plates show at all, and a refinement shipped off would take away
// plates a 1.12 player has today. The pets/guardians/totems/minions names share one knob each
// side — this client cannot tell those four apart (1.12's object fields carry an owner, not a
// kind of owner) and an addon writes the whole set together, so the last write wins.
// `crate::vplates::VPlateMode::friendly_players` and its siblings.
ours(
"nameplateShowFriendlyPlayers",
"1",
"later-client addon-compat, LIVE: friendly players' plates under nameplateShowFriends — \
see crate::vplates::VPlateMode::friendly_players",
),
ours(
"nameplateShowFriendlyNPCs",
"1",
"later-client addon-compat, LIVE: friendly NPCs' plates under nameplateShowFriends — see \
crate::vplates::VPlateMode::friendly_npcs",
),
ours(
"nameplateShowFriendlyMinions",
"1",
"later-client addon-compat, LIVE: friendly pets, guardians, totems and minions — one knob \
here, eight names; crate::vplates::PLATE_FRIENDLY_MINION_CVARS",
),
ours(
"nameplateShowFriendlyPets",
"1",
"later-client addon-compat, LIVE: see nameplateShowFriendlyMinions",
),
ours(
"nameplateShowFriendlyGuardians",
"1",
"later-client addon-compat, LIVE: see nameplateShowFriendlyMinions",
),
ours(
"nameplateShowFriendlyTotems",
"1",
"later-client addon-compat, LIVE: see nameplateShowFriendlyMinions",
),
ours(
"nameplateShowFriendlyPlayerMinions",
"1",
"later-client addon-compat, LIVE: the newer spelling of nameplateShowFriendlyMinions",
),
ours(
"nameplateShowFriendlyPlayerPets",
"1",
"later-client addon-compat, LIVE: the newer spelling of nameplateShowFriendlyPets",
),
ours(
"nameplateShowFriendlyPlayerGuardians",
"1",
"later-client addon-compat, LIVE: the newer spelling of nameplateShowFriendlyGuardians",
),
ours(
"nameplateShowFriendlyPlayerTotems",
"1",
"later-client addon-compat, LIVE: the newer spelling of nameplateShowFriendlyTotems",
),
ours(
"nameplateShowEnemyPlayers",
"1",
"later-client addon-compat, LIVE: hostile players' plates under nameplateShowEnemies — \
see crate::vplates::VPlateMode::enemy_players",
),
ours(
"nameplateShowEnemyMinions",
"1",
"later-client addon-compat, LIVE: hostile pets, guardians, totems and minions — one knob \
here, four names; crate::vplates::PLATE_ENEMY_MINION_CVARS",
),
ours(
"nameplateShowEnemyPets",
"1",
"later-client addon-compat, LIVE: see nameplateShowEnemyMinions",
),
ours(
"nameplateShowEnemyGuardians",
"1",
"later-client addon-compat, LIVE: see nameplateShowEnemyMinions",
),
ours(
"nameplateShowEnemyTotems",
"1",
"later-client addon-compat, LIVE: see nameplateShowEnemyMinions",
),
// `nameplateMaxDistance` is the later clients' name for the reach SuperWoW calls
// `NameplateRange` and nampower calls `NP_NameplateDistance`. One knob, three names, last
// writer wins — the file already carries that arrangement for the other two. The clamp is
// nampower's wider 5..200 rather than SuperWoW's 10..80, so a write through this name is not
// narrowed by which name it came through.
ours(
"nameplateMaxDistance",
"80",
"later-client addon-compat, LIVE: the nameplate reach, the same knob NameplateRange and \
NP_NameplateDistance drive — see crate::nameplates::NameConfig::range_sq",
),
// ── Registered so the question has an answer; nothing here reads them ─────────────────────
//
// Each of these is a row an addon asks for before it will show a control, and each names
// something this client does not have. Registering is not a lie as long as the default is the
// client's own behaviour, which is what every value below is.
//
// `nameplateShowAll` is "1" because 1.12 plates whatever the master allows whether or not you
// are fighting: "always" is already what happens, and there is no combat gate to turn off.
ours(
"nameplateShowAll",
"1",
"later-client addon-compat, STORED ONLY: 1.12 has no in-combat-only plate mode — plates \
follow the two masters at all times, which is what \"1\" says",
),
ours(
"nameplateShowSelf",
"0",
"later-client addon-compat, STORED ONLY: benilla draws no plate over the player \
(crate::vplates seats plates on other units only), so \"0\" is the client's behaviour",
),
ours(
"nameplateShowOnlyNameForFriendlyPlayerUnits",
"0",
"later-client addon-compat, STORED ONLY: there is no name-only plate here; a plate is a \
bar and a name together (crate::vplates)",
),
ours(
"nameplateForceShowUnitName",
"0",
"later-client addon-compat, STORED ONLY: a plate already carries its unit's name, so \
there is nothing for this to force",
),
// The one per-kind name with no gate of its own: a minus (trivial) creature is told apart by
// its `Creature.dbc` rank, which the plate path never looks up — it reads reaction and owner
// off the object update and nothing else. Registered so Cinematic's Enemy-minor row is
// editable like its neighbours; it will not change what is plated.
ours(
"nameplateShowEnemyMinus",
"1",
"later-client addon-compat, STORED ONLY: the plate path has no creature rank, so a minus \
creature is plated like any other hostile NPC",
),
// `WorldDetail` is no 1.12 CVar but the `GetWorldDetail`/`SetWorldDetail` verb name
// (`OptionsFrame.lua:27`); stops 0/1/2 are `frillDensity` 16/32/48. `SetWorldDetail 0x488dd0`
// also writes `SmallCull` {0.07, 0.04, 0.01}, and `GetWorldDetail` reads only `SmallCull`,
@@ -3392,6 +3541,34 @@ mod tests {
!plates.enemies && !plates.friends,
"a fresh 1.12 client draws no plates until V is pressed"
);
// The per-kind refinements, paired the same way: a derived `false` here would take a whole
// kind's plates away on a client whose `config.toml` carries no such row — which is every
// client until someone touches the setting, so nothing would ever correct it.
for (cvar, field) in [
("nameplateShowFriendlyPlayers", plates.friendly_players),
("nameplateShowFriendlyNPCs", plates.friendly_npcs),
("nameplateShowEnemyPlayers", plates.enemy_players),
] {
assert_eq!(d[cvar] != 0.0, field, "{cvar} and its field must agree");
}
for cvar in crate::vplates::PLATE_FRIENDLY_MINION_CVARS {
assert_eq!(
d[cvar] != 0.0,
plates.friendly_minions,
"{cvar} is one of the eight names for one knob and must carry its default"
);
}
for cvar in crate::vplates::PLATE_ENEMY_MINION_CVARS {
assert_eq!(
d[cvar] != 0.0,
plates.enemy_minions,
"{cvar} is one of the four names for one knob and must carry its default"
);
}
// One reach, three names: a player moving any of the three sliders must not find the
// value jump because the other two disagree about where it starts.
assert_eq!(d["nameplateMaxDistance"], d["NameplateRange"]);
assert_eq!(d["nameplateMaxDistance"], d["NP_NameplateDistance"]);
// `ClutterConfig::default()` reads `$WOW_CLUTTER_DENSITY`; stop 1 is its env-less ×2.
assert_eq!(d["WorldDetail"], 1.0);
assert_eq!(
@@ -5583,6 +5760,34 @@ mod tests {
"ShowVKeyCastbar",
"pfUI modules/nameplates.lua:3 writes it; benilla draws no plate cast bar",
),
// The five per-kind plate rows with no gate of their own. They exist because an addon
// written for a later client greys a whole row on a nil `GetCVar` — registering is the
// answer to "does this client have this setting", and each default below is this
// client's own behaviour, so the answer is not a lie. The other sixteen nameplate rows
// are deliberately NOT here: `vplates.rs` reads every one and must keep proving so.
(
"nameplateShowAll",
"Cinematic's Nameplates page; 1.12 has no in-combat-only plate mode, so the \
registered 1 is already what happens",
),
(
"nameplateShowSelf",
"Cinematic's Nameplates page; nothing here draws a plate over the player",
),
(
"nameplateShowOnlyNameForFriendlyPlayerUnits",
"Cinematic's Nameplates page; there is no name-only plate here, a plate is a bar \
and a name together",
),
(
"nameplateForceShowUnitName",
"Cinematic's Nameplates page; a plate already carries its unit's name",
),
(
"nameplateShowEnemyMinus",
"Cinematic's Nameplates page; a minus creature needs a Creature.dbc rank the \
plate path never looks up",
),
];
let app_src = crate::test_support::src_dir();
let ui_src = app_src
+4
View File
@@ -642,6 +642,10 @@ pub(crate) fn on_cvar(ev: On<crate::cvars::CvarChanged>, mut names: ResMut<NameC
// 5..200 slider bounds (`NampowerSettings/settings.lua`), wider than SuperWoW's 10..80 —
// each CVar is held to the range its own mod documents.
"np_nameplatedistance" => names.range_sq = ev.num().clamp(5.0, 200.0).powi(2),
// The later clients' own name for that reach, the third of the three. Same knob, same
// last-writer-wins, and the same wider clamp, so a write is not narrowed by which name it
// arrived under.
"nameplatemaxdistance" => names.range_sq = ev.num().clamp(5.0, 200.0).powi(2),
"nameplateocclusion" => names.occlusion = ev.flag(),
_ => {}
}
@@ -1431,6 +1431,8 @@ fn a_world_entry_leaves_the_saved_nameplate_setting_alone() {
occlusion: false,
critters: false,
spread: true,
// The per-kind refinements are not what this test is about; their shipped defaults.
..crate::vplates::VPlateMode::default()
};
let saved = |s: &mut UiScript| {
s.set_cvar_host(crate::vplates::CVAR_ENEMIES, "1");
+96
View File
@@ -73,6 +73,33 @@ pub(crate) struct VPlateMode {
///
/// Defaults **on**, so an untouched client keeps the reference's spread.
pub(crate) spread: bool,
/// The later clients' per-kind plate switches, the refinements under `enemies`/`friends`.
///
/// 1.12 has two nameplate switches and no notion of a kind; every client after it splits them
/// by what the unit *is*, and the addons written against those clients drive the split names
/// rather than the two masters. Cinematic's Nameplates page is the case in hand: each of its
/// rows asks `GetCVar(name) ~= nil` first (`Effects.lua`'s `HasPlateSettings`), greys the
/// whole row when the answer is nil, and so left grom unable to touch Friendly NPCs — the row
/// whose only name is `nameplateShowFriendlyNPCs` — while the rows that happen to name
/// `nameplateShowEnemies`/`nameplateShowFriends` worked. The rows exist so the question has an
/// answer; they gate the plate as well, so the answer is not a lie.
///
/// A kind here is what this client can actually tell apart at the seat: a player, a unit owned
/// by someone (`CREATEDBY`/`SUMMONEDBY`: pet, guardian, totem and minion all at once, which is
/// why the eight later names for that one idea share one field — an addon writes the whole set
/// together, so last-writer-wins is what it meant), or any other NPC. All default **on**: the
/// masters decide whether plates show at all, and a refinement defaulting off would take away
/// plates a 1.12 player has today.
pub(crate) friendly_players: bool,
/// The friendly half of [`Self::friendly_players`] for NPCs nobody owns.
pub(crate) friendly_npcs: bool,
/// Pets, guardians, totems and minions on the friendly side — one field, eight CVar names.
pub(crate) friendly_minions: bool,
/// The hostile half of [`Self::friendly_players`]. (Hostile NPCs have no refinement of their
/// own: `nameplateShowEnemies` is the name every client uses for them, and it is the master.)
pub(crate) enemy_players: bool,
/// Pets, guardians, totems and minions on the hostile side — one field, four CVar names.
pub(crate) enemy_minions: bool,
/// UnitXP_SP3's "Proper Nameplates Occlusion" (`NameplateOcclusion`): hide a plate whose unit
/// the camera cannot see. 1.12 has no such test — a plate is drawn for anything in range,
/// wall or no wall. Seeded by the boot-time `CvarChanged` observer like the rest of this
@@ -108,6 +135,15 @@ impl Default for VPlateMode {
occlusion: true,
// Likewise `NameplateMotion`, whose registered default is `1`: the reference spreads.
spread: true,
// The per-kind refinements, every one registered `"1"`, and written out here for the
// same reason as `occlusion`: their rows are normally absent from `config.toml`, so no
// `CvarChanged` ever arrives to tell a derived `false` otherwise, and the plates of a
// whole kind would go missing with both masters on.
friendly_players: true,
friendly_npcs: true,
friendly_minions: true,
enemy_players: true,
enemy_minions: true,
}
}
}
@@ -636,6 +672,28 @@ fn drive_vplates(
if friendly && !mode.friends || !friendly && !mode.enemies {
continue;
}
// Then the kind, under whichever master let it through: the later clients' per-kind
// switches (`VPlateMode::friendly_players` and its siblings). A unit with an owner is a
// minion here whatever kind of minion the newer clients would call it — the distinction
// they draw between a pet, a guardian and a totem needs fields 1.12's `SMSG_UPDATE_OBJECT`
// does not carry, and an addon writes all of those names together anyway.
let owned = store.is_some_and(|s| {
s.0.unit_summoned_by().is_some_and(|g| g != 0)
|| s.0.unit_created_by().is_some_and(|g| g != 0)
});
let kind_shown = match (friendly, is_player, owned) {
(true, true, _) => mode.friendly_players,
(true, false, true) => mode.friendly_minions,
(true, false, false) => mode.friendly_npcs,
(false, true, _) => mode.enemy_players,
(false, false, true) => mode.enemy_minions,
// A hostile NPC nobody owns is what `nameplateShowEnemies` itself names, and that
// master has already answered above.
(false, false, false) => true,
};
if !kind_shown {
continue;
}
// Only on the hostile side, a dead-looking unit has no plate (`0x60f733`): a feigning
// hunter's plate hides from enemies and stays for friends.
if !friendly && store.is_some_and(|s| s.0.unit_reads_dead()) {
@@ -870,6 +928,30 @@ pub(crate) struct VPlateSet;
/// `nameplateShowCritters`, this client's own: see [`VPlateMode::critters`].
pub(crate) const CVAR_CRITTERS: &str = "nameplateShowCritters";
/// The eight later-client names for [`VPlateMode::friendly_minions`]. A pet, a guardian, a totem
/// and a "minion" are one thing to this client — a unit with an owner — and an addon that means
/// "show them" writes the whole set, so every name here moves the one field and the last write
/// wins. The `FriendlyPlayer*` four are the newer spelling of the other four; Cinematic writes
/// both sets in one go (`Effects.lua`'s `PLATE_KIND_CVARS.FriendlyMinions`).
pub(crate) const PLATE_FRIENDLY_MINION_CVARS: [&str; 8] = [
"nameplateShowFriendlyMinions",
"nameplateShowFriendlyPets",
"nameplateShowFriendlyGuardians",
"nameplateShowFriendlyTotems",
"nameplateShowFriendlyPlayerMinions",
"nameplateShowFriendlyPlayerPets",
"nameplateShowFriendlyPlayerGuardians",
"nameplateShowFriendlyPlayerTotems",
];
/// The hostile half of [`PLATE_FRIENDLY_MINION_CVARS`]: four names, one field.
pub(crate) const PLATE_ENEMY_MINION_CVARS: [&str; 4] = [
"nameplateShowEnemyMinions",
"nameplateShowEnemyPets",
"nameplateShowEnemyGuardians",
"nameplateShowEnemyTotems",
];
/// V-key nameplates: the toggles and the per-frame gate and draw.
pub(crate) struct VPlatesPlugin;
@@ -889,6 +971,20 @@ pub(crate) fn on_cvar(ev: On<crate::cvars::CvarChanged>, mut mode: ResMut<VPlate
// 0 is Overlap; 1, 2 and 3 all spread, since the mod's own two alternative algorithms are
// unknowable from here and the fallback is the reference's solver (see `VPlateMode::spread`).
mode.spread = ev.num() != 0.0;
} else if ev.is("nameplateShowFriendlyPlayers") {
mode.friendly_players = ev.flag();
} else if ev.is("nameplateShowFriendlyNPCs") {
mode.friendly_npcs = ev.flag();
} else if PLATE_FRIENDLY_MINION_CVARS.iter().any(|n| ev.is(n)) {
mode.friendly_minions = ev.flag();
} else if ev.is("nameplateShowEnemyPlayers") {
mode.enemy_players = ev.flag();
} else if PLATE_ENEMY_MINION_CVARS.iter().any(|n| ev.is(n)) {
mode.enemy_minions = ev.flag();
} else if ev.is("nameplateMaxDistance") {
// The later clients' name for the same reach, clamped as `NP_NameplateDistance` is so a
// write is not narrowed by which of the three names it arrived under.
mode.range_sq = ev.num().clamp(5.0, 200.0).powi(2);
} else if ev.is("NP_NameplateDistance") {
// nampower v2's own nameplate reach (`docs/ADDON_COMPAT.md`): the same knob SuperWoW's
// `NameplateRange` drives, last writer wins, clamped to that addon's 5..200 slider.