diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index e4bc3674..a390905b 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -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 diff --git a/crates/benilla-app/src/nameplates.rs b/crates/benilla-app/src/nameplates.rs index 5dcfb418..8336872c 100644 --- a/crates/benilla-app/src/nameplates.rs +++ b/crates/benilla-app/src/nameplates.rs @@ -642,6 +642,10 @@ pub(crate) fn on_cvar(ev: On, mut names: ResMut 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(), _ => {} } diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs index 92061682..0021ef84 100644 --- a/crates/benilla-app/src/ui_script/options_tests.rs +++ b/crates/benilla-app/src/ui_script/options_tests.rs @@ -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"); diff --git a/crates/benilla-app/src/vplates.rs b/crates/benilla-app/src/vplates.rs index ebfbf330..0f176cd8 100644 --- a/crates/benilla-app/src/vplates.rs +++ b/crates/benilla-app/src/vplates.rs @@ -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, mut mode: ResMut