The nameplate verbs write two runtime bits, cleared at world entry, and the setting lives in FrameXML's saved variables, not two CVars (#571)

The nameplate verbs now answer as the reference's (`0x489450`-`0x489480`): they flip bits `0x1` (enemy) and `0x8` (friend) of runtime state, write no CVar, and world entry clears both (`0x401639` → `0x6033c0`, after `VARIABLES_LOADED`). The setting persists only through FrameXML's `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`, as on 1.12.1.

- The `nameplateShowEnemies`/`nameplateShowFriends` CVar rows and the host's plate push are gone. A player's saved plate setting in `config.toml` moves once into the saved variables at the first world entry.
- benilla's layer keeps what players see today. Its `UpdateNameplates` fix drops the stock show-then-hide (`UIOptionsFrame.lua:775-776`), and it registers `FRIENDNAMEPLATES_ON` for saving, which the stock file never does (`:180-183`). So plates turned on come back after login.
- With the layer off, benilla behaves as 1.12.1: friendly plates are off after entry.
- The V keys flip the verb and the saved global together. V and Shift-V keep their recorded deviation; #570 tracks moving the binding table onto the stock `Bindings.xml`.

Verified:
- New tests for the verbs, the entry clear, the stock-only and layered persistence across entry and reload, the V keys and the one-time migration, each failing with its change undone.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run with `WOW_GM=off` turned both kinds on, relogged and read both back, from the file too.

Built on macOS; no platform seam touched.

Fixes #552

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-29 01:21:17 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 05e204de10
commit 0a1f69466c
14 changed files with 666 additions and 392 deletions
@@ -1459,58 +1459,67 @@ fn the_pfui_hdgraphic_extended_arm_runs() {
/// `ShowNameplates 0x489450`, `HideNameplates 0x489460`, `ShowFriendNameplates 0x489470` and
/// `HideFriendNameplates 0x489480` read no argument and return nothing: four 10-byte bodies over
/// two setters, differing only in an `or`/`and` mask.
/// two setters, differing only in an `or`/`and` mask. They write bits `0x1` and `0x8` of the
/// runtime dword `[0xc4da34]` and no CVar: 1.12 registers none (no `0x63db90` site names a plate).
#[test]
fn the_nameplate_verbs_ignore_their_arguments_and_return_nothing() {
let s = script();
s.register_cvars([
(crate::script::CVAR_NAMEPLATE_ENEMIES, "1"),
(crate::script::CVAR_NAMEPLATE_FRIENDS, "0"),
]);
fn the_nameplate_verbs_write_two_runtime_bits_and_no_cvar() {
use crate::script::{PLATE_BIT_ENEMY as ENEMY, PLATE_BIT_FRIEND as FRIEND};
let mut s = script();
let get = |s: &crate::script::UiScript, n: &str| s.cvar(n);
assert_eq!(
get(&s, crate::script::CVAR_NAMEPLATE_ENEMIES).as_deref(),
Some("1")
s.run("ShowNameplates()").unwrap();
assert_eq!(s.take_nameplate_bit_writes().apply(0), ENEMY);
assert!(
s.take_nameplate_bit_writes().is_empty(),
"a drain empties the queue"
);
s.run("HideNameplates()").unwrap();
assert_eq!(
get(&s, crate::script::CVAR_NAMEPLATE_ENEMIES).as_deref(),
Some("0")
);
assert_eq!(
get(&s, crate::script::CVAR_NAMEPLATE_FRIENDS).as_deref(),
Some("0"),
s.take_nameplate_bit_writes().apply(ENEMY | FRIEND),
FRIEND,
"the friendly bit is a separate setter — hiding enemies must not touch it"
);
s.run("ShowFriendNameplates()").unwrap();
assert_eq!(
get(&s, crate::script::CVAR_NAMEPLATE_FRIENDS).as_deref(),
Some("1")
);
assert_eq!(
get(&s, crate::script::CVAR_NAMEPLATE_ENEMIES).as_deref(),
Some("0"),
s.take_nameplate_bit_writes().apply(0),
FRIEND,
"...and back the other way"
);
s.run("HideFriendNameplates()").unwrap();
assert_eq!(s.take_nameplate_bit_writes().apply(FRIEND), 0);
// In call order, the last write per bit standing, as the setters `or`/`and` the live dword.
s.run("ShowFriendNameplates() HideFriendNameplates() HideNameplates() ShowNameplates()")
.unwrap();
assert_eq!(s.take_nameplate_bit_writes().apply(FRIEND), ENEMY);
// The frame loop's latches, bits `0x2` and `0x4`, are no verb's to touch.
s.run("HideNameplates() HideFriendNameplates()").unwrap();
assert_eq!(s.take_nameplate_bit_writes().apply(0xff), !(ENEMY | FRIEND));
s.run("ShowNameplates(false)").unwrap();
assert_eq!(
get(&s, crate::script::CVAR_NAMEPLATE_ENEMIES).as_deref(),
Some("1"),
s.take_nameplate_bit_writes().apply(0),
ENEMY,
"the verb IS the value — a falsy argument does not invert it"
);
for call in ["HideNameplates(1, 2, 3)", "ShowFriendNameplates({})"] {
s.run(call)
.unwrap_or_else(|e| panic!("{call} must not raise: {e}"));
}
let _ = s.take_nameplate_bit_writes();
assert_eq!(
s.arity("ShowNameplates()").unwrap(),
0,
"zero values — observably different from nil for a caller that counts"
);
assert!(
s.take_cvar_changes().is_empty(),
"no CVar written: the dword is not a setting"
);
for name in ["nameplateShowEnemies", "nameplateShowFriends"] {
assert_eq!(s.cvar(name), None, "{name}: no plate CVar is registered");
}
}
/// The host seams benilla's own interface used are gone, now that it speaks 1.12 alone: the key