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
@@ -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")
|
||||
]]></Script>
|
||||
</Ui>
|
||||
|
||||
@@ -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 @@
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
<!-- The Nameplates page: the V-key plates (`[0xc4da34]` bits 0 and
|
||||
3), then 1.12's UnitName* checkboxes. The plate rows are era
|
||||
rows: 1.12 has only the keybinding. -->
|
||||
<!-- The Nameplates page: the V-key plates, then 1.12's UnitName*
|
||||
checkboxes. The plate rows are era rows: 1.12 has only the
|
||||
keybindings. -->
|
||||
<Frame name="$parentNameplates" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
@@ -2570,16 +2580,16 @@
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-12"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-20" y="-12"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>BenillaOptionsRow_OnLoad(this, "nameplateShowEnemies",
|
||||
"Enemy Name Plates")</OnLoad></Scripts>
|
||||
<Scripts><OnLoad>BenillaOptionsFuncRow_OnLoad(this, BenillaOptionsFrame_GetEnemyPlates,
|
||||
BenillaOptionsFrame_SetEnemyPlates, "0", "Enemy Name Plates")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowFriendlyPlates" inherits="BenillaOptionsCheckboxRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowEnemyPlates" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowEnemyPlates" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>BenillaOptionsRow_OnLoad(this, "nameplateShowFriends",
|
||||
"Friendly Name Plates")</OnLoad></Scripts>
|
||||
<Scripts><OnLoad>BenillaOptionsFuncRow_OnLoad(this, BenillaOptionsFrame_GetFriendlyPlates,
|
||||
BenillaOptionsFrame_SetFriendlyPlates, "0", "Friendly Name Plates")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowPlayerNames" inherits="BenillaOptionsCheckboxRowTemplate">
|
||||
<Anchors>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -41,7 +41,6 @@ pub(super) fn seed_ui_fixture(
|
||||
mut names: ResMut<crate::names::NameCache>,
|
||||
icons: Option<Res<crate::entities::ItemDisplays>>,
|
||||
mut script: Option<NonSendMut<benilla_ui::script::UiScript>>,
|
||||
mut vplates: ResMut<crate::vplates::VPlateMode>,
|
||||
mut selection: ResMut<crate::target::Selection>,
|
||||
mut player: ResMut<crate::player::Player>,
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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::<NameConfig>(&app).npc);
|
||||
apply(&mut app, "unitnameown", "1");
|
||||
assert!(res::<NameConfig>(&app).own);
|
||||
apply(&mut app, crate::vplates::CVAR_ENEMIES, "0");
|
||||
assert!(!res::<VPlateMode>(&app).enemies);
|
||||
apply(&mut app, "nameplateshowfriends", "1");
|
||||
assert!(res::<VPlateMode>(&app).friends);
|
||||
apply(&mut app, "ChatBubbles", "0");
|
||||
assert!(!res::<BubbleConfig>(&app).all);
|
||||
apply(&mut app, "chatbubblesparty", "0");
|
||||
@@ -2195,7 +2194,6 @@ mod tests {
|
||||
.init_resource::<crate::target::AssistAttack>()
|
||||
.init_resource::<LootConfig>()
|
||||
.init_resource::<NameConfig>()
|
||||
.init_resource::<VPlateMode>()
|
||||
.init_resource::<ClutterConfig>()
|
||||
.init_resource::<MinimapZoom>()
|
||||
.init_resource::<BubbleConfig>()
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+242
-173
@@ -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<NonSendMut<benilla_ui::script::UiScript>>,
|
||||
mode: Res<VPlateMode>,
|
||||
mut told: Local<crate::ui_script::VmMemo<Option<(bool, bool)>>>,
|
||||
mut mode: ResMut<VPlateMode>,
|
||||
) {
|
||||
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<crate::bindings::BindingsState>,
|
||||
mut mode: ResMut<VPlateMode>,
|
||||
mut cvars: ResMut<crate::cvars::Cvars>,
|
||||
/// 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::<benilla_ui::script::UiScript>() {
|
||||
let _ = script.take_nameplate_bit_writes();
|
||||
}
|
||||
if let Some(mut mode) = world.get_resource_mut::<VPlateMode>() {
|
||||
*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<crate::cvars::CvarChanged>, mut mode: ResMut<VPlateMode>) {
|
||||
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::<VPlateMode>()
|
||||
.init_resource::<VPlates>()
|
||||
.init_resource::<PlateHover>()
|
||||
@@ -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::<VPlateMode>()
|
||||
.init_resource::<Cvars>()
|
||||
.insert_resource(BindingsState::test_fired(&[cmd::NAMEPLATES]));
|
||||
app.update();
|
||||
assert!(app.world().resource::<VPlateMode>().enemies, "V turns on");
|
||||
let moved = |app: &mut App| {
|
||||
app.world_mut()
|
||||
.resource_mut::<Cvars>()
|
||||
.take_events()
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.new))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
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::<Cvars>().get(CVAR_FRIENDS),
|
||||
Some("0"),
|
||||
"untouched"
|
||||
);
|
||||
|
||||
app.world_mut()
|
||||
.insert_resource(BindingsState::test_fired(&[cmd::NAMEPLATES]));
|
||||
app.update();
|
||||
assert!(!app.world().resource::<VPlateMode>().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::<VPlateMode>().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::<benilla_ui::script::UiScript>();
|
||||
(
|
||||
s.lua().globals().get::<Option<i64>>("NAMEPLATES_ON").ok(),
|
||||
s.lua()
|
||||
.globals()
|
||||
.get::<Option<i64>>("FRIENDNAMEPLATES_ON")
|
||||
.ok(),
|
||||
)
|
||||
};
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, feed_plate_globals)
|
||||
app.add_systems(Update, apply_plate_verbs)
|
||||
.init_resource::<VPlateMode>()
|
||||
.insert_non_send_resource(benilla_ui::script::UiScript::new().unwrap());
|
||||
app
|
||||
}
|
||||
|
||||
fn run(app: &mut App, lua: &str) {
|
||||
app.world()
|
||||
.non_send_resource::<benilla_ui::script::UiScript>()
|
||||
.run(lua)
|
||||
.unwrap();
|
||||
app.update();
|
||||
assert_eq!(read(&mut app), (Some(None), Some(None)), "both off ⇒ nil");
|
||||
}
|
||||
|
||||
app.world_mut().resource_mut::<VPlateMode>().enemies = true;
|
||||
fn mode(app: &App) -> (bool, bool) {
|
||||
let m = app.world().resource::<VPlateMode>();
|
||||
(m.enemies, m.friends)
|
||||
}
|
||||
|
||||
fn global(app: &App, name: &str) -> Option<i64> {
|
||||
app.world()
|
||||
.non_send_resource::<benilla_ui::script::UiScript>()
|
||||
.lua()
|
||||
.globals()
|
||||
.get::<Option<i64>>(name)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Both bits start clear: a fresh 1.12 client draws no plates until something shows them.
|
||||
#[test]
|
||||
fn the_verbs_move_the_bits_in_call_order() {
|
||||
let mut app = verb_app();
|
||||
app.update();
|
||||
assert_eq!(mode(&app), (false, false), "boot: both clear");
|
||||
run(&mut app, "ShowNameplates()");
|
||||
assert_eq!(mode(&app), (true, false));
|
||||
run(&mut app, "ShowFriendNameplates() HideNameplates()");
|
||||
assert_eq!(mode(&app), (false, true), "each verb its own bit");
|
||||
run(
|
||||
&mut app,
|
||||
"ShowNameplates() HideNameplates() HideFriendNameplates() ShowFriendNameplates()",
|
||||
);
|
||||
assert_eq!(mode(&app), (false, true), "the last write per bit stands");
|
||||
}
|
||||
|
||||
/// The entry clears the bits after the UI load (`0x401639` after `0x401602`), so the
|
||||
/// `VARIABLES_LOADED` replay does not survive it and `PLAYER_ENTERING_WORLD`'s decides.
|
||||
#[test]
|
||||
fn world_entry_clears_both_bits_and_the_loads_replay_with_them() {
|
||||
let mut app = verb_app();
|
||||
run(&mut app, "ShowNameplates() ShowFriendNameplates()");
|
||||
assert_eq!(mode(&app), (true, true));
|
||||
// The load's replay, not yet drained when the entry clears.
|
||||
app.world()
|
||||
.non_send_resource::<benilla_ui::script::UiScript>()
|
||||
.run("ShowNameplates()")
|
||||
.unwrap();
|
||||
clear_at_world_entry(app.world_mut());
|
||||
app.update();
|
||||
assert_eq!(mode(&app), (false, false), "both clear after the entry");
|
||||
run(&mut app, "ShowFriendNameplates()");
|
||||
assert_eq!(
|
||||
read(&mut app),
|
||||
(Some(Some(1)), Some(None)),
|
||||
"the reference's own truthiness: the NUMBER 1, never a truthy `0`"
|
||||
mode(&app),
|
||||
(false, true),
|
||||
"and the next verb moves them again"
|
||||
);
|
||||
}
|
||||
|
||||
/// The V, Shift-V and Ctrl-V bodies: each flips the verb and FrameXML's global together, so
|
||||
/// the saved variable carries what the key did, and none writes a CVar.
|
||||
#[test]
|
||||
fn the_v_keys_move_the_bits_and_the_saved_globals_together() {
|
||||
use crate::bindings::commands::{Kind, SPECS};
|
||||
let body = |name: &str| match SPECS.iter().find(|s| s.name == name).map(|s| &s.kind) {
|
||||
Some(Kind::Edge(lua)) => *lua,
|
||||
_ => panic!("{name} runs a Lua body"),
|
||||
};
|
||||
let mut app = verb_app();
|
||||
let state = |app: &App| {
|
||||
(
|
||||
mode(app),
|
||||
global(app, "NAMEPLATES_ON"),
|
||||
global(app, "FRIENDNAMEPLATES_ON"),
|
||||
)
|
||||
};
|
||||
|
||||
run(&mut app, body("NAMEPLATES"));
|
||||
assert_eq!(state(&app), ((true, false), Some(1), None), "V turns on");
|
||||
run(&mut app, body("FRIENDNAMEPLATES"));
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
((true, true), Some(1), Some(1)),
|
||||
"Shift-V moves only the other kind"
|
||||
);
|
||||
run(&mut app, body("NAMEPLATES"));
|
||||
assert_eq!(state(&app), ((false, true), None, Some(1)), "V turns off");
|
||||
run(&mut app, body("ALLNAMEPLATES"));
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
((true, true), Some(1), Some(1)),
|
||||
"Ctrl-V: both on"
|
||||
);
|
||||
run(&mut app, body("ALLNAMEPLATES"));
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
((false, false), None, None),
|
||||
"...then both off"
|
||||
);
|
||||
assert!(
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<benilla_ui::script::UiScript>()
|
||||
.take_cvar_changes()
|
||||
.is_empty(),
|
||||
"no CVar: nothing reaches config.toml"
|
||||
);
|
||||
}
|
||||
|
||||
/// A player's plate setting from `config.toml` is carried into the saved-variables globals
|
||||
/// once, over what the file said, and leaves `config.toml`.
|
||||
#[test]
|
||||
fn a_legacy_config_setting_carries_over_once() {
|
||||
use crate::cvars::Cvars;
|
||||
let mut cvars = Cvars::with_value("nameplateShowFriends", "1");
|
||||
let legacy = take_legacy_settings(&mut cvars);
|
||||
assert_eq!(legacy.0, vec![("FRIENDNAMEPLATES_ON", true)]);
|
||||
assert!(
|
||||
take_legacy_settings(&mut cvars).0.is_empty(),
|
||||
"taken once: the file no longer holds it"
|
||||
);
|
||||
assert!(
|
||||
!cvars
|
||||
.orphans()
|
||||
.iter()
|
||||
.any(|(k, _)| k == "nameplateShowFriends"),
|
||||
"and the next save drops it"
|
||||
);
|
||||
|
||||
// `ReloadUI()`: a fresh VM with the mode unchanged is told again.
|
||||
app.insert_non_send_resource(benilla_ui::script::UiScript::new().unwrap());
|
||||
let s = benilla_ui::script::UiScript::new().unwrap();
|
||||
// The saved-variables chunk said off; the CVar was the store, so it wins.
|
||||
s.run("FRIENDNAMEPLATES_ON = nil NAMEPLATES_ON = 1")
|
||||
.unwrap();
|
||||
seat_legacy_settings(&s, &legacy);
|
||||
assert_eq!(
|
||||
read(&mut app),
|
||||
(Some(None), Some(None)),
|
||||
"a fresh VM knows nothing"
|
||||
s.eval::<Option<i64>>("return FRIENDNAMEPLATES_ON").unwrap(),
|
||||
Some(1),
|
||||
"the number 1, never a truthy `0`"
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
read(&mut app),
|
||||
(Some(Some(1)), Some(None)),
|
||||
"…and is handed the mode again"
|
||||
s.eval::<Option<i64>>("return NAMEPLATES_ON").unwrap(),
|
||||
Some(1),
|
||||
"a global with no legacy entry is left as the file set it"
|
||||
);
|
||||
|
||||
let mut off = Cvars::with_value("NamePlateShowEnemies", "0");
|
||||
let legacy = take_legacy_settings(&mut off);
|
||||
s.run("NAMEPLATES_ON = 1").unwrap();
|
||||
seat_legacy_settings(&s, &legacy);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return NAMEPLATES_ON").unwrap(),
|
||||
None,
|
||||
"an entry of 0 is off, whatever its spelling"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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