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
@@ -493,3 +493,122 @@ fn every_global_the_layer_defines_takes_the_benilla_prefix() {
|
||||
"layer globals without the Benilla prefix: {unprefixed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A login as the reference runs one on `s`, over a saved-variables file in `home` holding
|
||||
/// `saved`: the chunk and `VARIABLES_LOADED` inside the UI load, then the world entry's clear
|
||||
/// (`0x401639`, after the load at `0x401602`), then `PLAYER_ENTERING_WORLD`; a `ReloadUI()`
|
||||
/// (`entry` false) skips the clear. The bits start as `before`. Returns the app holding the VM and
|
||||
/// the plate bits.
|
||||
fn log_in(
|
||||
s: UiScript,
|
||||
home: &std::path::Path,
|
||||
saved: Option<&str>,
|
||||
entry: bool,
|
||||
before: crate::vplates::VPlateMode,
|
||||
) -> bevy::app::App {
|
||||
use bevy::prelude::*;
|
||||
std::fs::create_dir_all(home).unwrap();
|
||||
if let Some(saved) = saved {
|
||||
std::fs::write(home.join("saved-variables.lua"), saved).unwrap();
|
||||
}
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, crate::vplates::apply_plate_verbs)
|
||||
.insert_resource(before)
|
||||
.insert_non_send_resource(s);
|
||||
crate::ui_saved::load_saved_variables(
|
||||
&mut app.world_mut().non_send_resource_mut::<UiScript>(),
|
||||
|_| {},
|
||||
);
|
||||
app.update();
|
||||
if entry {
|
||||
crate::vplates::clear_at_world_entry(app.world_mut());
|
||||
}
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
|
||||
fn plates(app: &bevy::app::App) -> (bool, bool) {
|
||||
let m = app.world().resource::<crate::vplates::VPlateMode>();
|
||||
(m.enemies, m.friends)
|
||||
}
|
||||
|
||||
fn plate_globals(app: &bevy::app::App) -> (Option<i64>, Option<i64>) {
|
||||
app.world()
|
||||
.non_send_resource::<UiScript>()
|
||||
.eval("return NAMEPLATES_ON, FRIENDNAMEPLATES_ON")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// On the stock UI alone benilla is 1.12.1: the stock `UpdateNameplates` shows the friendly
|
||||
/// plates and hides them at once (`UIOptionsFrame.lua:775-776`), so they are off after the entry
|
||||
/// though the saved variable says on, and the saved variable is left as it was.
|
||||
#[test]
|
||||
fn on_the_stock_ui_friendly_plates_are_off_after_entry_and_the_saved_value_stands() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (s, failures) = production_load_with("plates-stock", true, "", |_| {});
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
let home = std::env::temp_dir().join(format!("benilla-plates-stock-{}", std::process::id()));
|
||||
let _c = EnvGuard::unset("WOW_CAPTURE");
|
||||
let _h = EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
|
||||
|
||||
let app = log_in(
|
||||
s,
|
||||
&home,
|
||||
Some("NAMEPLATES_ON = 1\nFRIENDNAMEPLATES_ON = 1\n"),
|
||||
true,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(plates(&app), (true, false), "enemy on, friendly off");
|
||||
assert_eq!(
|
||||
plate_globals(&app),
|
||||
(Some(1), Some(1)),
|
||||
"both saved variables untouched"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&home);
|
||||
}
|
||||
|
||||
/// With the layer, plates the player turned on stay on: after the entry, through the logout's
|
||||
/// save, and after a `ReloadUI()` on the saved file.
|
||||
#[test]
|
||||
fn with_the_layer_plates_turned_on_stay_on_after_entry_and_reload() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (s, failures) = production_load_with("plates-layer", false, "", |_| {});
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
let home = std::env::temp_dir().join(format!("benilla-plates-layer-{}", std::process::id()));
|
||||
let _c = EnvGuard::unset("WOW_CAPTURE");
|
||||
let _h = EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
|
||||
|
||||
let mut app = log_in(
|
||||
s,
|
||||
&home,
|
||||
Some("NAMEPLATES_ON = 1\nFRIENDNAMEPLATES_ON = 1\n"),
|
||||
true,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(plates(&app), (true, true), "both on after the entry");
|
||||
|
||||
// The session's end writes the file, the friendly global included.
|
||||
crate::ui_saved::save(&mut app.world_mut().non_send_resource_mut::<UiScript>());
|
||||
let file = std::fs::read_to_string(home.join("saved-variables.lua")).unwrap();
|
||||
for line in ["NAMEPLATES_ON = 1", "FRIENDNAMEPLATES_ON = 1"] {
|
||||
assert!(file.contains(line), "{line} saved: {file}");
|
||||
}
|
||||
|
||||
// `ReloadUI()`: a new VM over the file just written, and no clear.
|
||||
let before = *app.world().resource::<crate::vplates::VPlateMode>();
|
||||
let (s, failures) = production_load_with("plates-reload", false, "", |_| {});
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
let app = log_in(s, &home, None, false, before);
|
||||
assert_eq!(plates(&app), (true, true), "both on after the reload");
|
||||
assert_eq!(plate_globals(&app), (Some(1), Some(1)));
|
||||
let _ = std::fs::remove_dir_all(&home);
|
||||
}
|
||||
|
||||
@@ -216,6 +216,8 @@ pub(crate) fn run_pending_entry_load(world: &mut World) {
|
||||
world.remove_resource::<PendingEntryUiLoad>();
|
||||
let start = std::time::Instant::now();
|
||||
load_ingame_ui_on_world_entry(world);
|
||||
// The world entry's own step after the UI load, which a `ReloadUI()` never takes.
|
||||
crate::vplates::clear_at_world_entry(world);
|
||||
// Every entry logs the load's cost, whether the cover hid it, and the session every
|
||||
// `VmMemo` keys on.
|
||||
info!(
|
||||
@@ -360,10 +362,17 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
let z = world.resource::<crate::minimap::MinimapZoom>();
|
||||
(z.outdoor, z.inside)
|
||||
};
|
||||
let plates = world
|
||||
.get_resource::<crate::vplates::VPlateMode>()
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
// A plate setting `config.toml` still holds, carried into FrameXML's saved variables once.
|
||||
// Only by a load with the layer, which registers `FRIENDNAMEPLATES_ON` for save; a stock-UI
|
||||
// load leaves it for the next layered one.
|
||||
let legacy_plates = if super::manifest::layer_enabled() {
|
||||
world
|
||||
.get_resource_mut::<crate::cvars::Cvars>()
|
||||
.map(|mut cvars| crate::vplates::take_legacy_settings(&mut cvars))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
// No sound during the load edge, as in the reference's `UI_Init` (`0x48fbfa` → `0x49016d`).
|
||||
silenced_ui_load(&mut script, |script| {
|
||||
let _ = load_ingame_ui(script, identity.as_ref(), &roster, version_check);
|
||||
@@ -378,9 +387,9 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
// events (`ChatFrame.lua:1261-1273`).
|
||||
finish_ui_load_with(
|
||||
script,
|
||||
// `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON` before `VARIABLES_LOADED`, whose
|
||||
// `UIParent_OnEvent` arm runs the first `UpdateNameplates()` (`UIParent.lua:231-234`).
|
||||
|script| crate::vplates::push_plate_globals(script, plates),
|
||||
// Before `VARIABLES_LOADED`, whose `UIParent_OnEvent` arm runs the first
|
||||
// `UpdateNameplates()` (`UIParent.lua:231-234`).
|
||||
|script| crate::vplates::seat_legacy_settings(script, &legacy_plates),
|
||||
|script| {
|
||||
crate::ui_chat::restore_chat_looks(world, script);
|
||||
},
|
||||
@@ -681,7 +690,8 @@ pub(crate) fn finish_ui_load(script: &mut UiScript) {
|
||||
}
|
||||
|
||||
/// [`finish_ui_load`] with its two host seams. `host_settings` runs between the saved-variables
|
||||
/// chunk and `VARIABLES_LOADED`, pushing the settings benilla keeps in `config.toml`. `between`
|
||||
/// chunk and `VARIABLES_LOADED`, carrying over a setting `config.toml` held before it moved into
|
||||
/// the saved variables. `between`
|
||||
/// is the chat-cache restore's `UPDATE_CHAT_WINDOWS` + `UPDATE_CHAT_COLOR` burst, after
|
||||
/// `VARIABLES_LOADED` and before `PLAYER_LOGIN`, where the reference registers its reader
|
||||
/// (`0x4900d6` → `0x498a20`). On a fresh login the reference's cache (`0x5afe50`) defers that
|
||||
@@ -818,6 +828,35 @@ mod tests {
|
||||
assert!(world.get_non_send_resource::<ParkedBootVm>().is_none());
|
||||
}
|
||||
|
||||
/// The plate bits clear at the world entry (`0x401639`, in the entry setup `0x401570`) and
|
||||
/// not at a `ReloadUI()`, which never runs that setup. A capture world, so no UI is built.
|
||||
#[test]
|
||||
fn the_world_entry_clears_the_plate_bits_and_a_reload_does_not() {
|
||||
use crate::vplates::VPlateMode;
|
||||
let on = VPlateMode {
|
||||
enemies: true,
|
||||
friends: true,
|
||||
};
|
||||
let world_with = |on: VPlateMode| {
|
||||
let mut world = World::new();
|
||||
world.insert_resource(State::new(crate::char_select::ClientState::InWorld));
|
||||
world.insert_resource(crate::run_mode::CaptureMode);
|
||||
world.insert_resource(on);
|
||||
world
|
||||
};
|
||||
|
||||
let mut world = world_with(on);
|
||||
world.insert_resource(PendingEntryUiLoad);
|
||||
run_pending_entry_load(&mut world);
|
||||
assert_eq!(*world.resource::<VPlateMode>(), VPlateMode::default());
|
||||
|
||||
let mut world = world_with(on);
|
||||
world.insert_resource(ReloadUiPending(true));
|
||||
run_pending_reload(&mut world);
|
||||
assert!(!world.resource::<ReloadUiPending>().0, "the reload ran");
|
||||
assert_eq!(*world.resource::<VPlateMode>(), on, "a reload keeps them");
|
||||
}
|
||||
|
||||
/// The reference re-makes its Lua state inside `UI_Init` (`0x48fe97`).
|
||||
#[test]
|
||||
fn the_world_entry_builds_its_own_vm_rather_than_adopting_the_glue_phases() {
|
||||
|
||||
@@ -1272,89 +1272,6 @@ fn the_world_detail_slider_writes_the_cvar_and_the_readout_names_its_stop() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// On the real manifest and CVar table: `UIParent_OnEvent` calls `UpdateNameplates()` on
|
||||
/// `VARIABLES_LOADED` and `PLAYER_ENTERING_WORLD` (`UIParent.lua:234`, `:367`), and here its verbs
|
||||
/// write the CVar pair that is the store. The setting survives only with both the host seeding
|
||||
/// `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON` first and our `UpdateNameplates` without the stock
|
||||
/// friendly show-then-hide (`UIOptionsFrame.lua:775-776`).
|
||||
#[test]
|
||||
fn a_world_entry_leaves_the_saved_nameplate_setting_alone() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The client never loads the manifest without a player.
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
name: Some("Probefour".into()),
|
||||
level: 60,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
let failures = super::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "loader errors: {failures:?}");
|
||||
let _ = s.errors();
|
||||
|
||||
// Both halves on, restored from `config.toml`: a host write, which queues nothing.
|
||||
let plates = crate::vplates::VPlateMode {
|
||||
enemies: true,
|
||||
friends: true,
|
||||
};
|
||||
let saved = |s: &mut UiScript| {
|
||||
s.set_cvar_host(crate::vplates::CVAR_ENEMIES, "1");
|
||||
s.set_cvar_host(crate::vplates::CVAR_FRIENDS, "1");
|
||||
assert!(s.take_cvar_changes().is_empty(), "the host write is silent");
|
||||
};
|
||||
|
||||
// The control: with the globals nil, the entry writes both halves off.
|
||||
saved(&mut s);
|
||||
s.fire_event("VARIABLES_LOADED", vec![]);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
assert_eq!(
|
||||
(
|
||||
s.cvar(crate::vplates::CVAR_ENEMIES),
|
||||
s.cvar(crate::vplates::CVAR_FRIENDS)
|
||||
),
|
||||
(Some("0".into()), Some("0".into())),
|
||||
"nil globals ⇒ the replay writes the store off — this is what the report saw"
|
||||
);
|
||||
let _ = s.take_cvar_changes();
|
||||
|
||||
// With the globals seeded by the host, the entry changes nothing.
|
||||
saved(&mut s);
|
||||
crate::vplates::push_plate_globals(&s, plates);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return NAMEPLATES_ON").unwrap(),
|
||||
1,
|
||||
"the reference's own truthiness — the NUMBER 1, never the truthy `0`"
|
||||
);
|
||||
s.fire_event("VARIABLES_LOADED", vec![]);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
assert_eq!(
|
||||
(
|
||||
s.cvar(crate::vplates::CVAR_ENEMIES),
|
||||
s.cvar(crate::vplates::CVAR_FRIENDS)
|
||||
),
|
||||
(Some("1".into()), Some("1".into())),
|
||||
"the player's setting survives the entry"
|
||||
);
|
||||
assert!(
|
||||
s.take_cvar_changes().is_empty(),
|
||||
"and queues nothing, so `config.toml` is never dirtied by a world entry"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// An addon that moves a global and calls the verb still writes the CVar.
|
||||
s.run("FRIENDNAMEPLATES_ON = nil UpdateNameplates()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_cvar_changes(),
|
||||
vec![(crate::vplates::CVAR_FRIENDS.to_string(), "0".to_string())],
|
||||
"UpdateNameplates keeps its contract — it is adapted, not stubbed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_nameplates_page_toggles_the_unit_name_cvars() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -1398,19 +1315,46 @@ fn the_nameplates_page_toggles_the_unit_name_cvars() {
|
||||
}
|
||||
let _ = s.take_sounds();
|
||||
|
||||
// The CVars the V and Shift-V bindings write, so the window and the keys agree.
|
||||
// The plate rows move the verbs and FrameXML's globals together, as the V and Shift-V
|
||||
// bindings do, so the window and the keys agree and the saved variables carry it; no CVar.
|
||||
use benilla_ui::script::{PLATE_BIT_ENEMY, PLATE_BIT_FRIEND};
|
||||
let _ = s.take_nameplate_bit_writes();
|
||||
let globals = |s: &UiScript| {
|
||||
s.eval::<(Option<i64>, Option<i64>)>("return NAMEPLATES_ON, FRIENDNAMEPLATES_ON")
|
||||
.unwrap()
|
||||
};
|
||||
s.run("BenillaOptionsFrameContainerBodyNameplatesRowFriendlyPlatesCheck:Click()")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_nameplate_bit_writes().apply(0), PLATE_BIT_FRIEND);
|
||||
assert_eq!(globals(&s), (None, Some(1)));
|
||||
s.run("BenillaOptionsFrameContainerBodyNameplatesRowEnemyPlatesCheck:Click()")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_nameplate_bit_writes().apply(0), PLATE_BIT_ENEMY);
|
||||
assert_eq!(globals(&s), (Some(1), Some(1)));
|
||||
s.run("BenillaOptionsFrameContainerBodyNameplatesRowFriendlyPlatesCheck:Click()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_cvar_changes(),
|
||||
vec![(crate::vplates::CVAR_FRIENDS.to_string(), "1".to_string())]
|
||||
s.take_nameplate_bit_writes()
|
||||
.apply(PLATE_BIT_ENEMY | PLATE_BIT_FRIEND),
|
||||
PLATE_BIT_ENEMY,
|
||||
"unticking hides"
|
||||
);
|
||||
s.run("BenillaOptionsFrameContainerBodyNameplatesRowEnemyPlatesCheck:Click()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_cvar_changes(),
|
||||
vec![(crate::vplates::CVAR_ENEMIES.to_string(), "1".to_string())]
|
||||
globals(&s),
|
||||
(Some(1), None),
|
||||
"the number 1 or nil, never a truthy \"0\""
|
||||
);
|
||||
assert!(s.take_cvar_changes().is_empty(), "no plate CVar");
|
||||
// A key moved the global behind the window's back: the rows read it.
|
||||
s.run("FRIENDNAMEPLATES_ON = 1 BenillaOptionsFrameCategoryListRowNameplates:Click()")
|
||||
.unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return BenillaOptionsFrameContainerBodyNameplatesRowFriendlyPlatesCheck:GetChecked()"
|
||||
)
|
||||
.unwrap());
|
||||
s.run("FRIENDNAMEPLATES_ON = nil").unwrap();
|
||||
let _ = s.take_nameplate_bit_writes();
|
||||
let _ = s.take_sounds();
|
||||
|
||||
// The interface panel's click kit (`PlayClickSound`, `OptionsFrame.lua:509-515`), not the
|
||||
|
||||
Reference in New Issue
Block a user