zone channels survive a border crossing, and nameplates survive a login
- crossing a zone border prints one Changed Channel line instead of a leave and a join, and General and LocalDefense keep printing in the new zone; walking out of a capital keeps Trade's number and its registration instead of dropping it (2130) - outside a capital the channel numbers are the reference's: a fresh character holds 1 General, 2 Trade (suspended until a city) and 3 LocalDefense, so /2 reaches the same channel it does on the real client (2137) - logging into a character reads that character's zone, not the previous one's, so a login in the Eastern Plaguelands no longer joins and leaves Stormwind's channels first; a chat cache with General saved as a custom name gets its id back (2130) - the Show Tutorials option survives a /reload: it loaded unchecked and clicking it reset every dismissed tutorial account-wide; a world entry no longer switches off nameplates you had turned on (2131, 2132) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -444,9 +444,23 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
// any `CHAT_MSG_*` (ref `ChatFrame.lua` l.1261-1273) — as an `Update` system it landed after
|
||||
// the session's first chat had already been routed, and the login MOTD went to a window
|
||||
// registered for nothing.
|
||||
finish_ui_load_with(&mut script, |script| {
|
||||
crate::ui_chat::restore_chat_looks(world, script);
|
||||
});
|
||||
// The plate pair is read out of the world FIRST: the second closure borrows `world` for the
|
||||
// chat-cache restore, and a `Copy` of two bools costs nothing next to fighting that borrow.
|
||||
// Absent in a bare test world, where "both off" is also the resource's own default.
|
||||
let plates = world
|
||||
.get_resource::<crate::vplates::VPlateMode>()
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
finish_ui_load_with(
|
||||
&mut script,
|
||||
// `NAMEPLATES_ON` / `FRIENDNAMEPLATES_ON` (2132). This seat, and not the `Update` feed
|
||||
// beside it, is what fixes the bug: `UIParent_OnEvent`'s first `UpdateNameplates()` runs
|
||||
// inside the `VARIABLES_LOADED` fired at the end of this very call.
|
||||
|script| crate::vplates::push_plate_globals(script, plates),
|
||||
|script| {
|
||||
crate::ui_chat::restore_chat_looks(world, script);
|
||||
},
|
||||
);
|
||||
// **Say it out loud when an addon didn't load** (decision 1495). Every failure the walk found
|
||||
// is retained now, but a log nobody knows to open does not fix silence — and silence is the
|
||||
// actual defect B293 reports: *"there are a lot of addons that still doesn't work"*, with
|
||||
@@ -758,7 +772,7 @@ pub(crate) fn shutdown_on_exit(
|
||||
/// restore has to sit inside it. A test that only wants the tail keeps the plain shape.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn finish_ui_load(script: &mut UiScript) {
|
||||
finish_ui_load_with(script, |_| {});
|
||||
finish_ui_load_with(script, |_| {}, |_| {});
|
||||
}
|
||||
|
||||
/// [`finish_ui_load`] with the one step that has to land **between** `VARIABLES_LOADED` and
|
||||
@@ -773,14 +787,23 @@ pub(crate) fn finish_ui_load(script: &mut UiScript) {
|
||||
/// an addon reading its chat colours out of a `VARIABLES_LOADED` handler would see the file's
|
||||
/// values where the reference shows it the boot ones.
|
||||
///
|
||||
/// A callback rather than a split pair because the budget re-arm and the two fires are one edge,
|
||||
/// and a caller that forgets the middle step should not be able to compile.
|
||||
pub(crate) fn finish_ui_load_with(script: &mut UiScript, between: impl FnOnce(&mut UiScript)) {
|
||||
/// `host_settings` is the earlier of the two seams — **between the saved-variables chunk and
|
||||
/// `VARIABLES_LOADED`** — where the settings benilla keeps in `config.toml` rather than in that
|
||||
/// file are pushed into the VM (decision 2132; the rationale for the exact seat is on
|
||||
/// [`crate::ui_saved::load_saved_variables`]).
|
||||
///
|
||||
/// Callbacks rather than a split trio because the budget re-arm and the two fires are one edge,
|
||||
/// and a caller that forgets a middle step should not be able to compile.
|
||||
pub(crate) fn finish_ui_load_with(
|
||||
script: &mut UiScript,
|
||||
host_settings: impl FnOnce(&mut UiScript),
|
||||
between: impl FnOnce(&mut UiScript),
|
||||
) {
|
||||
// Still the load edge, so still bounded (1306) — re-armed because the walk's last addon left
|
||||
// an arbitrary amount on the counter, and the saved-variables chunk plus every PLAYER_LOGIN
|
||||
// handler deserve the full allowance. The entry edge disarms after this returns.
|
||||
script.set_instruction_budget(addons::LOAD_INSTRUCTION_BUDGET);
|
||||
crate::ui_saved::load_saved_variables(script);
|
||||
crate::ui_saved::load_saved_variables(script, host_settings);
|
||||
between(script);
|
||||
script.fire_event("PLAYER_LOGIN", vec![]);
|
||||
}
|
||||
|
||||
@@ -1363,6 +1363,98 @@ fn the_world_detail_slider_writes_the_cvar_and_the_readout_names_its_stop() {
|
||||
/// The Nameplates page (0992 — live at last): the three 1.12 UnitName* checkbox rows read the
|
||||
/// table on select and write their flags on the interface panel's own click kit (checked →
|
||||
/// CheckBoxOn; these rows carry no soundQuirk).
|
||||
/// **A world entry must leave the saved nameplate setting alone** (decision 2132) — the report,
|
||||
/// on the real manifest with a real CVar table, which is the one combination no test had.
|
||||
///
|
||||
/// `UIParent_OnEvent` calls `UpdateNameplates()` twice per entry — `UIParent.lua` l.234
|
||||
/// (VARIABLES_LOADED) and l.367 (PLAYER_ENTERING_WORLD) — and it acts on
|
||||
/// `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`. On benilla the four verbs it calls write the CVar pair,
|
||||
/// and the CVar pair IS the store, so with the globals left nil every entry wrote `"0"` over the
|
||||
/// player's `"1"` and `config.toml` then dropped the line as "at default".
|
||||
///
|
||||
/// Two mechanisms are needed and this asserts both: the host seeding the globals ahead of
|
||||
/// `VARIABLES_LOADED` (the enemy half), and our re-declared `UpdateNameplates` without the stock
|
||||
/// `ShowFriendNameplates(); HideFriendNameplates();` typo (the friendly half). Drop either and the
|
||||
/// matching row here goes back to `"0"`.
|
||||
#[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);
|
||||
// A manifest load with no player is a state the client never reaches (1848).
|
||||
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();
|
||||
|
||||
// The player has both halves on, restored from `config.toml` — a host write, so nothing is
|
||||
// queued and the file is not re-dirtied by having been read.
|
||||
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 bug, kept as the control: with the globals nil the entry erases both halves ────────
|
||||
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();
|
||||
|
||||
// ── The fix: the host seeds the globals, and the entry is a no-op end to end ───────────────
|
||||
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());
|
||||
|
||||
// The other direction still works: an addon that moves a global and calls the verb.
|
||||
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() {
|
||||
let mut s = harness_on(audio_harness());
|
||||
|
||||
@@ -227,8 +227,9 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
|
||||
RaidOptionsFrame_UpdatePartyFrames = RaidOptionsFrame_UpdatePartyFrames or function() end
|
||||
LocalizeFrames = LocalizeFrames or function() end
|
||||
updateContainerFrameAnchors = updateContainerFrameAnchors or function() end
|
||||
-- Ours, not the reference's file: 1.12 keeps UpdateNameplates in UIOptionsFrame.lua and
|
||||
-- benilla's own options window carries it (the options family stays ours).
|
||||
-- 1.12 keeps UpdateNameplates in UIOptionsFrame.lua, which a kit reaches only at manifest
|
||||
-- l.21; our own OptionsFrame.xml re-declares it below that (decision 2132). Both are plain
|
||||
-- `function X()` writes, so a full kit ends on ours and a short one keeps this no-op.
|
||||
UpdateNameplates = UpdateNameplates or function() end
|
||||
CloseAllBags = CloseAllBags or function() end
|
||||
OpenBackpack = OpenBackpack or function() end
|
||||
|
||||
Reference in New Issue
Block a user