diff --git a/crates/benilla-app/assets/ui/FrameXMLFixes.xml b/crates/benilla-app/assets/ui/FrameXMLFixes.xml
index 26cfff83..461e6f9a 100644
--- a/crates/benilla-app/assets/ui/FrameXMLFixes.xml
+++ b/crates/benilla-app/assets/ui/FrameXMLFixes.xml
@@ -29,5 +29,23 @@
UIParent_ManageFramePositions()
end)
end
+
+ -- Deviation: friendly plates the player turned on come back at every world entry. The
+ -- stock UpdateNameplates (UIOptionsFrame.lua:768-780) shows them and hides them again at
+ -- once (l.775-776), and FRIENDNAMEPLATES_ON is never saved, since l.182 registers
+ -- NAMEPLATES_ON a second time.
+ function UpdateNameplates()
+ if ( NAMEPLATES_ON ) then
+ ShowNameplates()
+ else
+ HideNameplates()
+ end
+ if ( FRIENDNAMEPLATES_ON ) then
+ ShowFriendNameplates()
+ else
+ HideFriendNameplates()
+ end
+ end
+ RegisterForSave("FRIENDNAMEPLATES_ON")
]]>
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index 3b4a4219..924a22bd 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -63,23 +63,33 @@
end
end
- -- UIOptionsFrame.lua:768-780. Deviation: stock l.776 is dropped. On 1.12 its show-then-hide
- -- clears volatile bits (`[0xc4da34]`); here the verbs write the CVar pair that is the
- -- store, so it would erase the saved setting at every world entry.
- function UpdateNameplates()
- if ( NAMEPLATES_ON ) then
+ -- The two plate rows' store: FrameXML's own NAMEPLATES_ON/FRIENDNAMEPLATES_ON, the number 1
+ -- or nil (a "0" would be truthy), applied through the verbs as the bindings do.
+ function BenillaOptionsFrame_GetEnemyPlates()
+ return NAMEPLATES_ON
+ end
+ function BenillaOptionsFrame_SetEnemyPlates(value)
+ if ( value == "1" ) then
+ NAMEPLATES_ON = 1
ShowNameplates()
else
+ NAMEPLATES_ON = nil
HideNameplates()
end
- if ( FRIENDNAMEPLATES_ON ) then
+ end
+ function BenillaOptionsFrame_GetFriendlyPlates()
+ return FRIENDNAMEPLATES_ON
+ end
+ function BenillaOptionsFrame_SetFriendlyPlates(value)
+ if ( value == "1" ) then
+ FRIENDNAMEPLATES_ON = 1
ShowFriendNameplates()
else
+ FRIENDNAMEPLATES_ON = nil
HideFriendNameplates()
end
end
-
-- The category tree in list order; keys are the row name suffixes.
BENILLA_OPTIONS_CATEGORY_KEYS = {
"Controls", "Interface", "ActionBars", "Combat", "Chat", "Keybindings", "Nameplates",
@@ -2556,9 +2566,9 @@
-
+
@@ -2570,16 +2580,16 @@
- BenillaOptionsRow_OnLoad(this, "nameplateShowEnemies",
- "Enemy Name Plates")
+ BenillaOptionsFuncRow_OnLoad(this, BenillaOptionsFrame_GetEnemyPlates,
+ BenillaOptionsFrame_SetEnemyPlates, "0", "Enemy Name Plates")
- BenillaOptionsRow_OnLoad(this, "nameplateShowFriends",
- "Friendly Name Plates")
+ BenillaOptionsFuncRow_OnLoad(this, BenillaOptionsFrame_GetFriendlyPlates,
+ BenillaOptionsFrame_SetFriendlyPlates, "0", "Friendly Name Plates")
diff --git a/crates/benilla-app/src/bindings/commands.rs b/crates/benilla-app/src/bindings/commands.rs
index b8a32735..4c7ab656 100644
--- a/crates/benilla-app/src/bindings/commands.rs
+++ b/crates/benilla-app/src/bindings/commands.rs
@@ -100,9 +100,6 @@ pub(crate) mod cmd {
pub(crate) const TOGGLE_RUN: Cmd = by_name("TOGGLERUN");
pub(crate) const TARGET_NEAREST_ENEMY: Cmd = by_name("TARGETNEARESTENEMY");
pub(crate) const TARGET_PREVIOUS_ENEMY: Cmd = by_name("TARGETPREVIOUSENEMY");
- pub(crate) const NAMEPLATES: Cmd = by_name("NAMEPLATES");
- pub(crate) const FRIEND_NAMEPLATES: Cmd = by_name("FRIENDNAMEPLATES");
- pub(crate) const ALL_NAMEPLATES: Cmd = by_name("ALLNAMEPLATES");
pub(crate) const ATTACK_TARGET: Cmd = by_name("ATTACKTARGET");
pub(crate) const TOGGLE_UI: Cmd = by_name("TOGGLEUI");
pub(crate) const CAMERA_ZOOM_IN: Cmd = by_name("CAMERAZOOMIN");
@@ -793,15 +790,41 @@ const TABLE: &[Spec] = &[
Some("F"),
None
),
- spec!("NAMEPLATES", TARGETING, Kind::Host, Some("V"), None),
+ // Deviation: V and Shift-V toggle their own kind alone, where 1.12's bodies
+ // (`Bindings.xml:516-537`) turn the other kind off.
+ spec!(
+ "NAMEPLATES",
+ TARGETING,
+ Kind::Edge(
+ "if ( NAMEPLATES_ON ) then HideNameplates(); NAMEPLATES_ON = nil; \
+ else ShowNameplates(); NAMEPLATES_ON = 1; end"
+ ),
+ Some("V"),
+ None
+ ),
spec!(
"FRIENDNAMEPLATES",
TARGETING,
- Kind::Host,
+ Kind::Edge(
+ "if ( FRIENDNAMEPLATES_ON ) then HideFriendNameplates(); FRIENDNAMEPLATES_ON = nil; \
+ else ShowFriendNameplates(); FRIENDNAMEPLATES_ON = 1; end"
+ ),
Some("SHIFT-V"),
None
),
- spec!("ALLNAMEPLATES", TARGETING, Kind::Host, Some("CTRL-V"), None),
+ // 1.12's body (`Bindings.xml:538-550`).
+ spec!(
+ "ALLNAMEPLATES",
+ TARGETING,
+ Kind::Edge(
+ "if ( not (NAMEPLATES_ON and FRIENDNAMEPLATES_ON) ) then \
+ ShowNameplates(); NAMEPLATES_ON = 1; ShowFriendNameplates(); FRIENDNAMEPLATES_ON = 1; \
+ else HideNameplates(); NAMEPLATES_ON = nil; HideFriendNameplates(); \
+ FRIENDNAMEPLATES_ON = nil; end"
+ ),
+ Some("CTRL-V"),
+ None
+ ),
spec!("ATTACKTARGET", TARGETING, Kind::Host, Some("T"), None),
spec!(
"PETATTACK",
diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs
index 36f377d1..88563974 100644
--- a/crates/benilla-app/src/capture/fixtures.rs
+++ b/crates/benilla-app/src/capture/fixtures.rs
@@ -41,7 +41,6 @@ pub(super) fn seed_ui_fixture(
mut names: ResMut,
icons: Option>,
mut script: Option>,
- mut vplates: ResMut,
mut selection: ResMut,
mut player: ResMut,
// A nested tuple, one param under Bevy's 16-param cap.
@@ -944,7 +943,8 @@ pub(super) fn seed_ui_fixture(
Visibility::default(),
))
.id();
- vplates.enemies = true;
+ // As the V binding does, so FrameXML's replay at `PLAYER_ENTERING_WORLD` agrees.
+ set_enemy_plates(script.as_deref(), true);
// The wolf is the target, so its plate draws lit with the target ring.
selection.target = Some(wolf);
selection.guid = Some(WOLF_GUID);
@@ -1295,7 +1295,7 @@ pub(super) fn seed_ui_fixture(
Visibility::default(),
));
// A plated unit draws no floating name, so enemy plates go off.
- vplates.enemies = false;
+ set_enemy_plates(script.as_deref(), false);
}
// The lighting matrix: one spawn with a streamed entity's component set, anonymous and
// with plates off, so no glyph rides over the body.
@@ -1339,7 +1339,7 @@ pub(super) fn seed_ui_fixture(
));
}
}
- vplates.enemies = false;
+ set_enemy_plates(script.as_deref(), false);
}
}
}
@@ -1605,3 +1605,16 @@ fn seed_equipped_bags(
}
}
}
+
+/// Turn enemy plates on or off as the V binding does, the verb and FrameXML's global together,
+/// so FrameXML's own replay agrees.
+fn set_enemy_plates(script: Option<&benilla_ui::script::UiScript>, on: bool) {
+ let lua = if on {
+ "NAMEPLATES_ON = 1; ShowNameplates()"
+ } else {
+ "NAMEPLATES_ON = nil; HideNameplates()"
+ };
+ if let Some(Err(e)) = script.map(|s| s.run(lua)) {
+ warn!("nameplates: {e}");
+ }
+}
diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs
index dd0595b0..e0e1a1fd 100644
--- a/crates/benilla-app/src/cvars.rs
+++ b/crates/benilla-app/src/cvars.rs
@@ -319,11 +319,6 @@ pub(crate) const REGISTERED: &[Registered] = &[
// `0x609085`. `UnitNamePlayerPVPTitle` (bit `0x20`, "1") has no row: nothing here draws the
// rank prefix.
same("UnitNamePlayerGuild", "1"),
- // 1.12 has no nameplate CVar: the bitmask `[0xc4da34]` (bits 0 and 3) persists through
- // FrameXML's `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`, so these take the later-era names. Both
- // boot off (`UIOptionsFrame.lua:180-183`, `:769-775`): no plates until V.
- same(crate::vplates::CVAR_ENEMIES, "0"),
- same(crate::vplates::CVAR_FRIENDS, "0"),
// `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`,
@@ -846,6 +841,22 @@ impl Cvars {
self.last_change = Some(Instant::now());
}
+ /// Remove a `config.toml` entry no row claims, a setting retired from the table, and return
+ /// its value; the next save writes the file without it.
+ pub(crate) fn retire_file_entry(&mut self, name: &str) -> Option {
+ if self.index.contains_key(&name.to_ascii_lowercase()) {
+ return None;
+ }
+ let key = self
+ .file
+ .keys()
+ .find(|k| k.eq_ignore_ascii_case(name))?
+ .clone();
+ let value = self.file.remove(&key);
+ self.touch();
+ value
+ }
+
/// A write from either side of the VM boundary. A `from_vm` write is not echoed back, but a
/// refusal is, because the mirror already stored the refused value.
fn write(&mut self, name: &str, value: &str, from_vm: bool) -> SetOutcome {
@@ -1488,7 +1499,6 @@ mod tests {
use crate::ui_loot::LootConfig;
use crate::ui_script::{UiScaleCvar, DEFAULT_UI_SCALE};
use crate::video::VideoConfig;
- use crate::vplates::VPlateMode;
use crate::world_backdrop::{RenderScale, RENDER_SCALE_RANGE};
use benilla_ui::widget::MINIMAP_ZOOM_LEVELS;
use benilla_world::clutter::ClutterConfig;
@@ -1697,13 +1707,6 @@ mod tests {
d["weatherDensity"],
f32::from(benilla_world::weather::WeatherState::default().weather_density)
);
- let plates = VPlateMode::default();
- assert_eq!(d[crate::vplates::CVAR_ENEMIES] != 0.0, plates.enemies);
- assert_eq!(d[crate::vplates::CVAR_FRIENDS] != 0.0, plates.friends);
- assert!(
- !plates.enemies && !plates.friends,
- "a fresh 1.12 client draws no plates until V is pressed"
- );
// `ClutterConfig::default()` reads `$WOW_CLUTTER_DENSITY`; stop 1 is its env-less ×2.
assert_eq!(d["WorldDetail"], 1.0);
assert_eq!(
@@ -1885,10 +1888,6 @@ mod tests {
assert!(!res::(&app).npc);
apply(&mut app, "unitnameown", "1");
assert!(res::(&app).own);
- apply(&mut app, crate::vplates::CVAR_ENEMIES, "0");
- assert!(!res::(&app).enemies);
- apply(&mut app, "nameplateshowfriends", "1");
- assert!(res::(&app).friends);
apply(&mut app, "ChatBubbles", "0");
assert!(!res::(&app).all);
apply(&mut app, "chatbubblesparty", "0");
@@ -2195,7 +2194,6 @@ mod tests {
.init_resource::()
.init_resource::()
.init_resource::()
- .init_resource::()
.init_resource::()
.init_resource::()
.init_resource::()
@@ -2259,9 +2257,6 @@ mod tests {
|app| {
app.add_observer(crate::nameplates::on_cvar);
},
- |app| {
- app.add_observer(crate::vplates::on_cvar);
- },
|app| {
app.add_observer(crate::game_tip::on_cvar);
},
diff --git a/crates/benilla-app/src/ui_saved.rs b/crates/benilla-app/src/ui_saved.rs
index d5388072..a718e605 100644
--- a/crates/benilla-app/src/ui_saved.rs
+++ b/crates/benilla-app/src/ui_saved.rs
@@ -26,8 +26,8 @@ impl Plugin for UiSavedPlugin {
/// A malformed or unreadable file warns and is held ([`UiScript::hold_saved_file`]), so the
/// shutdown write leaves it alone. Read as bytes: the writer keeps a Lua string's raw bytes.
///
-/// `host_settings` seats the `RegisterForSave` globals kept in `config.toml` (the nameplate pair):
-/// after the chunk, so a stale line here cannot outvote `config.toml`, and before the event, where
+/// `host_settings` seats what the host carries into a `RegisterForSave` global (a plate setting
+/// left in `config.toml`): after the chunk, so it outranks the file, and before the event, where
/// the consumers run (`UIParent_OnEvent` → `UpdateNameplates`).
pub(crate) fn load_saved_variables(
script: &mut UiScript,
diff --git a/crates/benilla-app/src/ui_script/layer_tests.rs b/crates/benilla-app/src/ui_script/layer_tests.rs
index f50e2be9..edf6805b 100644
--- a/crates/benilla-app/src/ui_script/layer_tests.rs
+++ b/crates/benilla-app/src/ui_script/layer_tests.rs
@@ -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::(),
+ |_| {},
+ );
+ app.update();
+ if entry {
+ crate::vplates::clear_at_world_entry(app.world_mut());
+ }
+ app.world_mut()
+ .non_send_resource_mut::()
+ .fire_event("PLAYER_ENTERING_WORLD", vec![]);
+ app.update();
+ app
+}
+
+fn plates(app: &bevy::app::App) -> (bool, bool) {
+ let m = app.world().resource::();
+ (m.enemies, m.friends)
+}
+
+fn plate_globals(app: &bevy::app::App) -> (Option, Option) {
+ app.world()
+ .non_send_resource::()
+ .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::());
+ 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::();
+ 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);
+}
diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs
index 58e1841f..086e9a41 100644
--- a/crates/benilla-app/src/ui_script/lifecycle.rs
+++ b/crates/benilla-app/src/ui_script/lifecycle.rs
@@ -216,6 +216,8 @@ pub(crate) fn run_pending_entry_load(world: &mut World) {
world.remove_resource::();
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::();
(z.outdoor, z.inside)
};
- let plates = world
- .get_resource::()
- .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::()
+ .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::().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::default());
+
+ let mut world = world_with(on);
+ world.insert_resource(ReloadUiPending(true));
+ run_pending_reload(&mut world);
+ assert!(!world.resource::().0, "the reload ran");
+ assert_eq!(*world.resource::(), 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() {
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index 29028eee..e52b2211 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -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::("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, Option)>("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::(
+ "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
diff --git a/crates/benilla-app/src/vplates.rs b/crates/benilla-app/src/vplates.rs
index 7bd4cabd..b884f099 100644
--- a/crates/benilla-app/src/vplates.rs
+++ b/crates/benilla-app/src/vplates.rs
@@ -23,7 +23,9 @@ use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use benilla_protocol::EntityKind;
-use benilla_ui::script::{unit_is_grey, PlateGeometry, PlateState};
+use benilla_ui::script::{
+ unit_is_grey, PlateGeometry, PlateState, PLATE_BIT_ENEMY, PLATE_BIT_FRIEND,
+};
use crate::entities::{overhead_anchor, BoneAttach, OverheadFallback};
use crate::names::NameCache;
@@ -34,19 +36,30 @@ use benilla_world::view::WorldCamera;
/// The plate border, sharp-resampled so its 128x32 art stays crisp when magnified.
pub(crate) mod border;
-/// The `[0xc4da34]` master bitmask: bit 0 enemy plates, bit 3 friendly, both clear at boot as in
-/// the reference (`UIOptionsFrame.lua:180-183` sets the saved globals nil). The gate reads this
-/// resource; [`CVAR_ENEMIES`] and [`CVAR_FRIENDS`] persist it. 1.12 registers no nameplate CVar,
-/// so those names are not 1.12's.
-#[derive(Resource, Default, Clone, Copy)]
+/// The two script bits of `[0xc4da34]`, bit `0x1` enemy plates and bit `0x8` friendly: engine
+/// state, written only by the four plate verbs ([`apply_plate_verbs`]) and cleared at every world
+/// entry ([`clear_at_world_entry`]). Nothing persists it: FrameXML replays its own
+/// `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON` saved variables through the verbs
+/// (`UIOptionsFrame.lua:768-780`, `UIParent.lua:234`, `:367`).
+#[derive(Resource, Default, Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct VPlateMode {
pub(crate) enemies: bool,
pub(crate) friends: bool,
}
-/// The persisted names of the two toggles.
-pub(crate) const CVAR_ENEMIES: &str = benilla_ui::script::CVAR_NAMEPLATE_ENEMIES;
-pub(crate) const CVAR_FRIENDS: &str = benilla_ui::script::CVAR_NAMEPLATE_FRIENDS;
+impl VPlateMode {
+ fn bits(self) -> u8 {
+ let bit = |on: bool, b: u8| if on { b } else { 0 };
+ bit(self.enemies, PLATE_BIT_ENEMY) | bit(self.friends, PLATE_BIT_FRIEND)
+ }
+
+ fn from_bits(bits: u8) -> Self {
+ Self {
+ enemies: bits & PLATE_BIT_ENEMY != 0,
+ friends: bits & PLATE_BIT_FRIEND != 0,
+ }
+ }
+}
/// The units carrying a live plate this frame; the overhead-name driver draws no name for them
/// (the reference's ShouldShowName).
@@ -176,78 +189,80 @@ pub(crate) fn text_px(h: f32, basis: f32) -> f32 {
(h * basis).round().min(32.0)
}
-/// Push the mode into FrameXML's `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`. `UpdateNameplates`
-/// (`UIOptionsFrame.lua:768`) replays these globals into the engine on `VARIABLES_LOADED` and
-/// every `PLAYER_ENTERING_WORLD` (`UIParent.lua:234`, `:367`), so they must match the mode or
-/// the replay turns plates off and overwrites the persisted CVar.
-pub(crate) fn push_plate_globals(script: &benilla_ui::script::UiScript, mode: VPlateMode) {
- // The number 1 or nil, never 0: a Lua 0 is truthy.
- let on = |b: bool| b.then_some(1i64);
- let g = script.lua().globals();
- if let Err(e) = g
- .set("NAMEPLATES_ON", on(mode.enemies))
- .and_then(|()| g.set("FRIENDNAMEPLATES_ON", on(mode.friends)))
- {
- warn!("nameplates: FrameXML globals: {e}");
- }
-}
-
-/// Keep the globals in step with the mode, memoised per VM; the world-entry load seeds them
-/// before `VARIABLES_LOADED`, which no `Update` system can reach.
-fn feed_plate_globals(
+/// Every frame: fold the plate verbs' writes into the bits, in the order the VM made them.
+pub(crate) fn apply_plate_verbs(
script: Option>,
- mode: Res,
- mut told: Local>>,
+ mut mode: ResMut,
) {
- let Some(script) = script else {
+ let Some(mut script) = script else {
return;
};
- let now = (mode.enemies, mode.friends);
- let told = told.get(&script);
- if *told != Some(now) {
- *told = Some(now);
- push_plate_globals(&script, *mode);
+ let writes = script.take_nameplate_bit_writes();
+ if writes.is_empty() {
+ return;
+ }
+ let now = VPlateMode::from_bits(writes.apply(mode.bits()));
+ if *mode != now {
+ info!(
+ "nameplates: enemy {}, friendly {}",
+ if now.enemies { "ON" } else { "OFF" },
+ if now.friends { "ON" } else { "OFF" }
+ );
+ *mode = now;
}
}
-/// The NAMEPLATES, FRIENDNAMEPLATES and ALLNAMEPLATES bindings; ALLNAMEPLATES is 1.12's body,
-/// both on unless both already on. A press also writes the CVar, which persists it.
-///
-/// Deviation: V and Shift-V toggle their own bit independently, where 1.12's bodies
-/// (`Bindings.xml:516-537`) turn the other kind off.
-fn toggle_vplates(
- binds: Res,
- mut mode: ResMut,
- mut cvars: ResMut,
+/// The world-entry clear of both script bits (`0x60394b`, `0x603952`, in `0x6033c0`), which the
+/// entry setup `0x401570` runs at `0x401639`, after the UI load at `0x401602` (`InitializeGame`,
+/// whose `UI_Init` fires `VARIABLES_LOADED` at `0x4900b2`): the load's replay is cleared, and
+/// the one at `PLAYER_ENTERING_WORLD` decides. A `ReloadUI()` never reaches it.
+pub(crate) fn clear_at_world_entry(world: &mut World) {
+ if let Some(mut script) = world.get_non_send_resource_mut::() {
+ let _ = script.take_nameplate_bit_writes();
+ }
+ if let Some(mut mode) = world.get_resource_mut::() {
+ *mode = VPlateMode::default();
+ }
+}
+
+/// The `config.toml` names benilla once kept the two bits under, with the FrameXML global each
+/// carries over to. 1.12 has no nameplate CVar; the setting is FrameXML's saved variables.
+const LEGACY_CVARS: [(&str, &str); 2] = [
+ ("nameplateShowEnemies", "NAMEPLATES_ON"),
+ ("nameplateShowFriends", "FRIENDNAMEPLATES_ON"),
+];
+
+/// A plate setting still in `config.toml` from before it moved to FrameXML's saved variables:
+/// each global and whether it was on.
+#[derive(Default)]
+pub(crate) struct LegacyPlateSettings(Vec<(&'static str, bool)>);
+
+/// Take the legacy plate settings out of `config.toml`, once: the next save drops them, and from
+/// then on the saved-variables file carries them.
+pub(crate) fn take_legacy_settings(cvars: &mut crate::cvars::Cvars) -> LegacyPlateSettings {
+ LegacyPlateSettings(
+ LEGACY_CVARS
+ .iter()
+ .filter_map(|&(cvar, global)| {
+ let value = cvars.retire_file_entry(cvar)?;
+ Some((global, value.trim() == "1"))
+ })
+ .collect(),
+ )
+}
+
+/// Seat the legacy settings over the saved-variables chunk's values, before `VARIABLES_LOADED`,
+/// as the number 1 or nil (a Lua 0 is truthy). Until now the setting was the CVar, which
+/// outranked the file.
+pub(crate) fn seat_legacy_settings(
+ script: &benilla_ui::script::UiScript,
+ legacy: &LegacyPlateSettings,
) {
- use crate::bindings::cmd;
- let (was_enemies, was_friends) = (mode.enemies, mode.friends);
- if binds.fired(cmd::NAMEPLATES) {
- mode.enemies = !mode.enemies;
- info!(
- "nameplates: enemy {}",
- if mode.enemies { "ON" } else { "OFF" }
- );
- }
- if binds.fired(cmd::FRIEND_NAMEPLATES) {
- mode.friends = !mode.friends;
- info!(
- "nameplates: friendly {}",
- if mode.friends { "ON" } else { "OFF" }
- );
- }
- if binds.fired(cmd::ALL_NAMEPLATES) {
- let both = mode.enemies && mode.friends;
- mode.enemies = !both;
- mode.friends = !both;
- info!("nameplates: all {}", if !both { "ON" } else { "OFF" });
- }
- let flag = |b: bool| if b { "1" } else { "0" };
- if mode.enemies != was_enemies {
- cvars.set(CVAR_ENEMIES, flag(mode.enemies));
- }
- if mode.friends != was_friends {
- cvars.set(CVAR_FRIENDS, flag(mode.friends));
+ let g = script.lua().globals();
+ for &(global, on) in &legacy.0 {
+ if let Err(e) = g.set(global, on.then_some(1i64)) {
+ warn!("nameplates: carrying {global} over: {e}");
+ }
}
}
@@ -619,18 +634,8 @@ pub(crate) struct VPlateSet;
/// V-key nameplates: the toggles and the per-frame gate and draw.
pub(crate) struct VPlatesPlugin;
-/// Apply a change to either toggle CVar to the bitmask.
-pub(crate) fn on_cvar(ev: On, mut mode: ResMut) {
- if ev.is(CVAR_ENEMIES) {
- mode.enemies = ev.flag();
- } else if ev.is(CVAR_FRIENDS) {
- mode.friends = ev.flag();
- }
-}
-
impl Plugin for VPlatesPlugin {
fn build(&self, app: &mut App) {
- app.add_observer(on_cvar);
app.init_resource::()
.init_resource::()
.init_resource::()
@@ -638,7 +643,8 @@ impl Plugin for VPlatesPlugin {
.add_systems(
Update,
(
- toggle_vplates,
+ // A drain of the verbs the tick ran (a binding body, `UpdateNameplates`).
+ apply_plate_verbs.after(crate::ui_script::UiInput),
// After the targeting chain, itself after `WorldStage::Input`, so this projects
// through this frame's camera; `ui_script::extract::paint_script` runs after
// it, so the plates paint the same frame.
@@ -646,11 +652,7 @@ impl Plugin for VPlatesPlugin {
)
.chain()
.in_set(VPlateSet),
- )
- // Outside `VPlateSet`, which runs after the targeting chain: it only needs to precede
- // the script tick, and a frame's lag is harmless since the globals are read only at
- // world entry.
- .add_systems(Update, feed_plate_globals.in_set(crate::ui_script::UiFeed));
+ );
}
}
@@ -711,103 +713,170 @@ mod tests {
assert_eq!(text_px(NAME_H, 10_000.0), 32.0, "atlas-cell cap, raw law");
}
- /// Without the CVar write, a key-toggled plate would revert at every launch.
- #[test]
- fn the_v_key_mirrors_into_the_cvar_table() {
- use crate::bindings::{cmd, BindingsState};
- use crate::cvars::Cvars;
+ /// An app with the drain and a bare VM: the four verbs are all it needs.
+ fn verb_app() -> App {
let mut app = App::new();
- app.add_systems(Update, toggle_vplates)
- .init_resource::()
- .init_resource::()
- .insert_resource(BindingsState::test_fired(&[cmd::NAMEPLATES]));
- app.update();
- assert!(app.world().resource::().enemies, "V turns on");
- let moved = |app: &mut App| {
- app.world_mut()
- .resource_mut::()
- .take_events()
- .into_iter()
- .map(|e| (e.name, e.new))
- .collect::>()
- };
- assert_eq!(
- moved(&mut app),
- vec![(CVAR_ENEMIES.to_string(), "1".to_string())],
- "the write is an accepted move, so the config dirties and the mirror learns it"
- );
- assert_eq!(
- app.world().resource::().get(CVAR_FRIENDS),
- Some("0"),
- "untouched"
- );
-
- app.world_mut()
- .insert_resource(BindingsState::test_fired(&[cmd::NAMEPLATES]));
- app.update();
- assert!(!app.world().resource::().enemies, "V turns off");
- assert_eq!(
- moved(&mut app),
- vec![(CVAR_ENEMIES.to_string(), "0".to_string())]
- );
-
- // Shift-V moves only the other bit.
- app.world_mut()
- .insert_resource(BindingsState::test_fired(&[cmd::FRIEND_NAMEPLATES]));
- app.update();
- assert!(app.world().resource::().friends);
- assert_eq!(
- moved(&mut app),
- vec![(CVAR_FRIENDS.to_string(), "1".to_string())]
- );
-
- app.world_mut().insert_resource(BindingsState::default());
- app.update();
- assert!(moved(&mut app).is_empty());
- }
-
- #[test]
- fn the_plate_globals_follow_the_mode_and_survive_a_rebuilt_vm() {
- let read = |app: &mut App| {
- let s = app
- .world_mut()
- .non_send_resource_mut::();
- (
- s.lua().globals().get::