SetCVar and RegisterCVar read their arguments as 1.12.1 does: a number as %.14g text, "0" for a value with no string form, the reference usage text, realmList/realmName/scriptMemory read-only in the world, and CVAR_UPDATE fired inside the call for every token (#557)
`SetCVar` and `RegisterCVar` now read their arguments as the reference's bindings do (`0x488c10`, `0x488b00`): - a number is stored as Lua's `%.14g` text (`luaV_tostring` `0x6f7c80`), so the stock Sound slider's 0.4 stores `"0.40000000596046"`; - a value with no string form (nil, absent, boolean, table) stores `"0"` (`0x488c98`, `0x488b6f`), so unticking a stock Sound options box no longer raises and music can be switched off there; - the reference's usage texts (`0x84235c`, `0x8422e8`); - `realmList`, `realmName` and `scriptMemory` are read-only to Lua `SetCVar` in the world (`0x48f566`-`0x48f584`), and host writes still land; - `CVAR_UPDATE` fires inside the call for every string token, changed or not (`0x488cad`-`0x488cd5`, `SignalEvent2` `0x703f50`). Verified: 7 new VM tests, a stock Sound window test off the chain and a world-entry read-only test, each failing with the change undone; `scripts/gates.sh` green. Built on macOS; no platform seam touched. Fixes #167 Fixes #139 Fixes #547 🤖 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
db94ea038e
commit
44fa1813ac
@@ -306,6 +306,13 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
.and_then(|r| r.realm.as_ref().map(|r| r.name.clone()))
|
||||
.unwrap_or_default();
|
||||
script.set_realm_name(&realm);
|
||||
// `CGGameUI::InitializeGame` flags these read-only before `UI_Init` loads anything
|
||||
// (`0x48f566`-`0x48f584`), so an in-game `SetCVar` of one raises, logout handlers included;
|
||||
// `ShutdownGame` clears them (`0x491240`), and here the flags die with this VM. The host's
|
||||
// own realm writes are engine writes, which the flag does not stop.
|
||||
for name in benilla_ui::script::IN_WORLD_READ_ONLY_CVARS {
|
||||
script.set_cvar_read_only(name, true);
|
||||
}
|
||||
// The player too, before the addon walk. The record first: the reference's
|
||||
// `UnitName`/`UnitRace`/`UnitClass`/`UnitSex` read only a copy of the char-enum row made at the
|
||||
// Enter World commit (`0x5abd9e`) and never cleared, and each rebuilt VM is told once. The
|
||||
|
||||
@@ -3567,8 +3567,7 @@ fn the_status_bar_text_row_pins_the_numerals_the_moment_it_is_clicked() {
|
||||
s.take_cvar_changes(),
|
||||
vec![("statusBarText".to_string(), "1".to_string())]
|
||||
);
|
||||
// No repaint and no XP tick: only the queued `CVAR_UPDATE`.
|
||||
s.tick(0.0);
|
||||
// No repaint, no XP tick and no frame: only the `CVAR_UPDATE` the click fired.
|
||||
assert!(
|
||||
s.eval::<bool>("return MainMenuBarExpText:IsShown()")
|
||||
.unwrap(),
|
||||
@@ -3577,7 +3576,6 @@ fn the_status_bar_text_row_pins_the_numerals_the_moment_it_is_clicked() {
|
||||
|
||||
s.run("BenillaOptionsFrameContainerBodyInterfaceRowStatusTextCheck:Click()")
|
||||
.unwrap();
|
||||
s.tick(0.0);
|
||||
assert!(!s
|
||||
.eval::<bool>("return MainMenuBarExpText:IsShown()")
|
||||
.unwrap());
|
||||
|
||||
@@ -1453,6 +1453,54 @@ fn the_stock_sound_options_window_loads_hidden_and_the_alias_is_gone() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The stock Sound window's boxes write `this:GetChecked()` as is (`SoundOptionsFrame.lua:82`),
|
||||
/// which is nil once unticked, and `SetCVar` stores that as "0" (`0x488c98`); its sliders write
|
||||
/// `GetValue()`, a single-precision number stored as Lua's `%.14g` text (`0x6f7c80`).
|
||||
#[test]
|
||||
fn the_stock_sound_window_turns_a_box_off_and_stores_a_slider_as_lua_text() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = benilla_ui::script::UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
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(), "manifest load errors: {failures:#?}");
|
||||
s.set_cvar_host("EnableMusic", "1");
|
||||
s.run("this = SoundOptionsFrameOkay SoundOptionsFrame_Load()")
|
||||
.unwrap();
|
||||
let _ = s.take_cvar_changes();
|
||||
let checked = "return SoundOptionsFrameCheckButton5:GetChecked() and true or false";
|
||||
assert!(s.eval::<bool>(checked).unwrap(), "Enable Music starts on");
|
||||
|
||||
s.run("SoundOptionsFrameCheckButton5:Click()").unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert!(!s.eval::<bool>(checked).unwrap());
|
||||
assert_eq!(
|
||||
s.take_cvar_changes(),
|
||||
vec![("EnableMusic".to_string(), "0".to_string())],
|
||||
"the unticked box turns music off"
|
||||
);
|
||||
s.run("SoundOptionsFrameCheckButton5:Click()").unwrap();
|
||||
assert_eq!(
|
||||
s.take_cvar_changes(),
|
||||
vec![("EnableMusic".to_string(), "1".to_string())]
|
||||
);
|
||||
|
||||
s.run("SoundOptionsFrameSlider3:SetValue(0.4)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetCVar(\"MusicVolume\")").unwrap(),
|
||||
"0.40000000596046"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The stock Video Options window loads hidden (`OptionsFrame.xml:5`) and owns the `OptionsFrame`
|
||||
/// name; ours is `BenillaOptionsFrame`, which the ESC menu opens and the stock "options window
|
||||
/// open?" consumers see only through the `GameMenuFrame.xml` wrappers.
|
||||
|
||||
@@ -270,6 +270,50 @@ fn the_login_arms_the_world_latch_and_the_logout_spends_it() {
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// `CGGameUI::InitializeGame` flags `realmList`, `realmName` and `scriptMemory` read-only for the
|
||||
/// world session (`0x48f566`-`0x48f584`) and `ShutdownGame` clears them (`0x491240`): an
|
||||
/// in-game `SetCVar` of one raises, through a reload too, and the character screen's does not.
|
||||
#[test]
|
||||
fn the_realm_cvars_are_read_only_to_lua_only_in_the_world() {
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (tmp, _c, _h) = hermetic_probe("readonly");
|
||||
let mut world = booted_world();
|
||||
world.init_resource::<super::LeavingWorldArmed>();
|
||||
let set = |world: &World, chunk: &str| {
|
||||
let s = world.non_send_resource::<benilla_ui::script::UiScript>();
|
||||
// A bare test world has no registry to seed the VM from.
|
||||
s.register_cvars(crate::cvars::registered_pairs());
|
||||
s.run(chunk).map_err(|e| e.to_string())
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
set(&world, "SetCVar(\"realmList\", \"glue.example.org\")"),
|
||||
Ok(()),
|
||||
"the character screen's VM may write it"
|
||||
);
|
||||
|
||||
log_in_as(&mut world, "Onehunter", 1);
|
||||
for name in ["realmList", "realmName"] {
|
||||
let err = set(&world, &format!("SetCVar({name:?}, \"x\")")).expect_err(name);
|
||||
assert!(err.contains(&format!("\"{name}\" is read-only")), "{err}");
|
||||
}
|
||||
assert_eq!(set(&world, "SetCVar(\"MusicVolume\", 0.5)"), Ok(()));
|
||||
|
||||
reload(&mut world, crate::char_select::ClientState::InWorld);
|
||||
assert!(
|
||||
set(&world, "SetCVar(\"realmList\", \"x\")").is_err(),
|
||||
"a reload keeps the flag, as the reference's rows keep theirs"
|
||||
);
|
||||
|
||||
super::end_ui_session(&mut world);
|
||||
assert_eq!(set(&world, "SetCVar(\"realmList\", \"x\")"), Ok(()));
|
||||
|
||||
drop(world);
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// The reference's one guard in the shutdown tail: `0x490bd0` tests the active player's GUID pair
|
||||
/// (`0x490bee call 0x468550`, `0x490bf3 or eax,edx`) and with none jumps (`0x490bf5 je 0x490c25`)
|
||||
/// past only the `PLAYER_LEAVING_WORLD` fire, `0x490c20 call 0x490a80`; in-world roots fire both.
|
||||
|
||||
Reference in New Issue
Block a user