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:
co-authored by
Claude Opus 5.5
parent
05e204de10
commit
0a1f69466c
@@ -379,13 +379,6 @@ const USAGE_REGISTER_CVAR: &str = "Usage: RegisterCVar(\"cvar\" [, default])";
|
||||
/// `InitializeGame` does before `UI_Init`.
|
||||
pub const IN_WORLD_READ_ONLY_CVARS: [&str; 3] = ["realmList", "realmName", "scriptMemory"];
|
||||
|
||||
/// The enemy-plate toggle's CVar, which the app reads rather than re-spelling. Deviation: 1.12
|
||||
/// registers no nameplate CVar; the toggles persist under the later clients' names rather than
|
||||
/// invented ones.
|
||||
pub const CVAR_NAMEPLATE_ENEMIES: &str = "nameplateShowEnemies";
|
||||
/// The friendly-plate half of [`CVAR_NAMEPLATE_ENEMIES`].
|
||||
pub const CVAR_NAMEPLATE_FRIENDS: &str = "nameplateShowFriends";
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `RegisterCVar(name, value)` (`0x488b00`) declares a CVar the client does not ship, which is
|
||||
// how an addon persists a setting. Re-declaring a live name is a no-op: the reference never
|
||||
@@ -556,7 +549,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
install_nameplate_verbs(lua)?;
|
||||
install_world_detail_verbs(lua)?;
|
||||
super::video_pairs::install(lua)
|
||||
}
|
||||
@@ -898,44 +890,6 @@ const USAGE_SET_WORLD_DETAIL: &str = "Usage: SetWorldDetail(value)";
|
||||
/// `0x8423b8`, verbatim: the lowercase `v` and the odd comma are the reference's.
|
||||
const RANGE_SET_WORLD_DETAIL: &str = "value must be in the range 0, 2";
|
||||
|
||||
/// The four nameplate verbs: `ShowNameplates 0x489450`, `HideNameplates 0x489460`,
|
||||
/// `ShowFriendNameplates 0x489470`, `HideFriendNameplates 0x489480`. Each ignores its arguments
|
||||
/// (`ShowNameplates(false)` still shows) and returns zero values, and there is no getter; the
|
||||
/// state is bits `0x1` (enemy) and `0x8` (friend) of `[0xc4da34]`.
|
||||
///
|
||||
/// Deviation: the state lives in two CVars, so a player's plates survive a zone-in; the reference
|
||||
/// registers no nameplate CVar (none of `0x63db90`'s sites), clears both bits on every
|
||||
/// `EnterWorld`, and FrameXML replays its `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON` saved variables.
|
||||
fn install_nameplate_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
for (name, cvar, on) in [
|
||||
("ShowNameplates", CVAR_NAMEPLATE_ENEMIES, true),
|
||||
("HideNameplates", CVAR_NAMEPLATE_ENEMIES, false),
|
||||
("ShowFriendNameplates", CVAR_NAMEPLATE_FRIENDS, true),
|
||||
("HideFriendNameplates", CVAR_NAMEPLATE_FRIENDS, false),
|
||||
] {
|
||||
g.set(
|
||||
name,
|
||||
// Any arguments are ignored, as the reference reads none; the empty `MultiValue`
|
||||
// returns zero values, which is not nil.
|
||||
lua.create_function(move |lua, _: mlua::MultiValue| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let key = cvar.to_ascii_lowercase();
|
||||
if let Some(slot) = model.cvars.get_mut(&key) {
|
||||
let value = if on { "1" } else { "0" };
|
||||
if slot.value != value {
|
||||
slot.value = value.to_string();
|
||||
let reg_name = slot.name.clone();
|
||||
model.cvar_changes.push((reg_name, value.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(mlua::MultiValue::new())
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MultisampleFormat, SeededCvar};
|
||||
|
||||
@@ -89,7 +89,7 @@ mod merchant;
|
||||
mod messageframe;
|
||||
mod minimap;
|
||||
pub mod nameplate;
|
||||
pub use nameplate::{PlateGeometry, PlateState};
|
||||
pub use nameplate::{PlateBitWrites, PlateGeometry, PlateState, PLATE_BIT_ENEMY, PLATE_BIT_FRIEND};
|
||||
mod model;
|
||||
mod modelframe;
|
||||
mod net_stats;
|
||||
@@ -180,8 +180,8 @@ pub use cursor::{
|
||||
pub use cvars::{small_cull_text, sstr_to_int};
|
||||
pub use cvars::{
|
||||
MultisampleFormat, ScreenResolution, SeededCvar, VideoCaps, CVAR_FRILL_DENSITY, CVAR_GAMMA,
|
||||
CVAR_NAMEPLATE_ENEMIES, CVAR_NAMEPLATE_FRIENDS, CVAR_SMALL_CULL, CVAR_WORLD_DETAIL,
|
||||
IN_WORLD_READ_ONLY_CVARS, VIDEO_DEFAULT_CVARS, WORLD_DETAIL_STOPS,
|
||||
CVAR_SMALL_CULL, CVAR_WORLD_DETAIL, IN_WORLD_READ_ONLY_CVARS, VIDEO_DEFAULT_CVARS,
|
||||
WORLD_DETAIL_STOPS,
|
||||
};
|
||||
pub use death::{DeathAction, DeathUiState};
|
||||
pub use dressup::DressUpIntent;
|
||||
@@ -524,6 +524,7 @@ impl UiScript {
|
||||
death::install(&lua)?;
|
||||
aura::install(&lua)?;
|
||||
cvars::install(&lua)?;
|
||||
nameplate::install_verbs(&lua)?;
|
||||
saved::install(&lua)?;
|
||||
keybind::install(&lua)?;
|
||||
sound::install(&lua)?;
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! and read it positionally: six regions in creation order, then one child, the health bar (1.12
|
||||
//! has no cast bar). Addons take plates over, so static properties are written once, anchors
|
||||
//! when the window changes and state when it moves.
|
||||
//!
|
||||
//! Also the four verbs that switch plates on and off, which write two bits of the engine's
|
||||
//! runtime dword `[0xc4da34]` and nothing else.
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::order::DrawLayer;
|
||||
@@ -127,6 +132,9 @@ pub(crate) struct NamePlates {
|
||||
/// true, where freelook (`0x60f830`) clears the bit.
|
||||
hit_test_vetoed: bool,
|
||||
geometry: Option<PlateGeometry>,
|
||||
/// The verbs' writes to the two script bits, drained by
|
||||
/// [`UiScript::take_nameplate_bit_writes`].
|
||||
bit_writes: PlateBitWrites,
|
||||
}
|
||||
|
||||
impl NamePlates {
|
||||
@@ -154,7 +162,79 @@ struct SyncEffects {
|
||||
values: Vec<(FrameHandle, f32)>,
|
||||
}
|
||||
|
||||
/// Bit `0x1` of `[0xc4da34]`: plates on every unit that is not friendly, set by `ShowNameplates`
|
||||
/// and cleared by `HideNameplates` (setter `0x6054f0`).
|
||||
pub const PLATE_BIT_ENEMY: u8 = 0x1;
|
||||
/// Bit `0x8` of `[0xc4da34]`: plates on friendly units, set by `ShowFriendNameplates` and cleared
|
||||
/// by `HideFriendNameplates` (setter `0x605510`).
|
||||
pub const PLATE_BIT_FRIEND: u8 = 0x8;
|
||||
|
||||
/// The plate verbs' writes since the last drain. Each setter `or`s or `and`s its bit into the live
|
||||
/// dword in call order, so per bit the last write wins; bits `0x2` and `0x4` are the frame loop's
|
||||
/// own latches and no verb reaches them.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PlateBitWrites {
|
||||
set: u8,
|
||||
clear: u8,
|
||||
}
|
||||
|
||||
impl PlateBitWrites {
|
||||
fn write(&mut self, bit: u8, on: bool) {
|
||||
if on {
|
||||
self.set |= bit;
|
||||
self.clear &= !bit;
|
||||
} else {
|
||||
self.clear |= bit;
|
||||
self.set &= !bit;
|
||||
}
|
||||
}
|
||||
|
||||
/// `bits` with these writes applied.
|
||||
pub fn apply(self, bits: u8) -> u8 {
|
||||
(bits | self.set) & !self.clear
|
||||
}
|
||||
|
||||
/// No verb ran since the last drain.
|
||||
pub fn is_empty(self) -> bool {
|
||||
self.set | self.clear == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// The four nameplate verbs: `ShowNameplates 0x489450`, `HideNameplates 0x489460`,
|
||||
/// `ShowFriendNameplates 0x489470`, `HideFriendNameplates 0x489480`. Each reads no argument
|
||||
/// (`ShowNameplates(false)` still shows), returns zero values and writes its bit of `[0xc4da34]`,
|
||||
/// which the engine clears at every world entry (`0x60394b`, `0x603952`). There is no getter and
|
||||
/// no nameplate CVar: FrameXML keeps its own `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON` and replays
|
||||
/// them through these verbs.
|
||||
pub(super) fn install_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
for (name, bit, on) in [
|
||||
("ShowNameplates", PLATE_BIT_ENEMY, true),
|
||||
("HideNameplates", PLATE_BIT_ENEMY, false),
|
||||
("ShowFriendNameplates", PLATE_BIT_FRIEND, true),
|
||||
("HideFriendNameplates", PLATE_BIT_FRIEND, false),
|
||||
] {
|
||||
g.set(
|
||||
name,
|
||||
// The empty `MultiValue` returns zero values, which is not nil.
|
||||
lua.create_function(move |lua, _: mlua::MultiValue| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.nameplates.bit_writes.write(bit, on);
|
||||
Ok(mlua::MultiValue::new())
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl UiScript {
|
||||
/// Drain the plate verbs' writes for the engine's `[0xc4da34]`, which the app owns.
|
||||
pub fn take_nameplate_bit_writes(&mut self) -> PlateBitWrites {
|
||||
let lua = self.lua();
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
std::mem::take(&mut model.nameplates.bit_writes)
|
||||
}
|
||||
|
||||
/// Drive the pool from the app's per-frame states, one per shown plate; a unit with no state
|
||||
/// has its plate retired (hidden, never destroyed). An unchanged `geometry` costs nothing.
|
||||
pub fn sync_nameplates(&mut self, geometry: PlateGeometry, states: &[PlateState]) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user