Login notices show after the loading screen, as in 1.12.1 (#1809)

The red "GM mode is ON" line did not show on a session's first login, only on a relog.

In 1.12.1 the interface and the world load before `CMSG_PLAYER_LOGIN` goes out (`0x46c272`), and the loading screen drops when the player is created (`0x5debbd`). vmangos sends that create ahead of the login notices (`Handlers/CharacterHandler.cpp:675`), so the line is added after the reveal and gets its full time on screen. benilla's cover stays up through its own warm-up, about 6 s on a cold login, and the line faded underneath it. A relog is warm, so the line survived.

Server notice lines (`SMSG_NOTIFICATION`, area-trigger messages) now wait while the cover is up and fire on the frame it drops.

Test: a notice under the cover fires once, at the reveal. A live cold login shows the line the frame after the cover clears.

Fixes #1252

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 86c71919284d51473602a5ee668b5f818d71d980)
This commit is contained in:
Sam Sung
2026-10-25 23:18:11 -06:00
committed by grom
parent 440d20fd60
commit 0fde3c281f
3 changed files with 78 additions and 10 deletions
+21 -10
View File
@@ -71,8 +71,13 @@ pub(super) fn feed_actions(
mut cast_errors: ResMut<CastErrors>,
mut mount_errors: ResMut<MountErrors>,
mut pet_tame_failures: ResMut<PetTameFailures>,
// The by-key and resolved-text queues as one parameter, at Bevy's 16-parameter limit.
ui_errors: (ResMut<UiErrorKeys>, ResMut<UiErrorTexts>),
// The by-key and resolved-text queues and the cover that holds the latter, as one parameter,
// at Bevy's 16-parameter limit.
ui_errors: (
ResMut<UiErrorKeys>,
ResMut<UiErrorTexts>,
Option<Res<crate::loading_screen::LoadingScreen>>,
),
spells: Option<Res<Spells>>,
self_q: Query<&ObjectStore, With<SelfPlayer>>,
objects: crate::net::Objects,
@@ -87,7 +92,7 @@ pub(super) fn feed_actions(
let Some(mut script) = script else {
return;
};
let (mut ui_error_keys, mut ui_error_texts) = ui_errors;
let (mut ui_error_keys, mut ui_error_texts, cover) = ui_errors;
let memory = memory.get(&script);
// Cast failures become error lines through [`cast_fail`], all resolved before any event
@@ -296,13 +301,19 @@ pub(super) fn feed_actions(
.collect();
show_messages(&mut script, &mut sink, "ui_action", key_lines);
// Already-resolved lines ([`UiErrorTexts`]); the queued kind is the `0x4945b0` flag.
let resolved: Vec<Shown> = ui_error_texts
.0
.drain(..)
.map(|(text, kind)| Shown::unkeyed(kind, text))
.collect();
show_messages(&mut script, &mut sink, "ui_action", resolved);
// Already-resolved lines ([`UiErrorTexts`]); the queued kind is the `0x4945b0` flag. Held
// while the loading cover is up, so a login notice's hold starts at the reveal: the reference
// sends `CMSG_PLAYER_LOGIN` only after its world load (`0x46c272`), and its cover drops at the
// player's create (`0x5debbd`), which vmangos sends ahead of the notices (`Maps/Map.cpp:432`,
// `Handlers/CharacterHandler.cpp:675`).
if !cover.is_some_and(|c| c.covering()) {
let resolved: Vec<Shown> = ui_error_texts
.0
.drain(..)
.map(|(text, kind)| Shown::unkeyed(kind, text))
.collect();
show_messages(&mut script, &mut sink, "ui_action", resolved);
}
// The engine's own by-key refusals (`ERR_PASSIVE_ABILITY`): `benilla_ui` cannot reach
// [`UiErrorKeys`], so it queues keys that show here, one frame late.
@@ -434,6 +434,53 @@ fn pre_resolved_lines_land_on_the_errors_frame_in_the_arms_colour() {
);
}
/// A login notice queued while the loading cover is up waits for the reveal: the reference's
/// cover drops at the player's create (`0x5debbd`), ahead of the notice, so its `UIErrorsFrame`
/// hold is never spent under the cover.
#[test]
fn a_notice_under_the_loading_cover_fires_at_the_reveal() {
let (mut app, _rx) = app_with_food_on_the_bar();
app.insert_resource(crate::loading_screen::LoadingScreen::test_covering());
app.world_mut()
.non_send_resource_mut::<UiScript>()
.run(
r#"
BENILLA_TEST_ERRORS = {}
local f = CreateFrame("Frame")
f:RegisterEvent("UI_ERROR_MESSAGE")
f:SetScript("OnEvent", function() table.insert(BENILLA_TEST_ERRORS, arg1) end)
"#,
)
.unwrap();
let fired = |app: &mut App| {
app.world_mut()
.non_send_resource::<UiScript>()
.eval::<Vec<String>>("return BENILLA_TEST_ERRORS")
.unwrap()
};
app.world_mut()
.resource_mut::<UiErrorTexts>()
.error("GM mode is ON".to_string());
app.update();
app.update();
assert!(fired(&mut app).is_empty(), "nothing fires under the cover");
assert_eq!(
app.world().resource::<UiErrorTexts>().0.len(),
1,
"the notice stays queued"
);
app.insert_resource(crate::loading_screen::LoadingScreen::default());
app.update();
assert_eq!(
fired(&mut app),
["GM mode is ON"],
"the reveal fires it once"
);
assert!(app.world().resource::<UiErrorTexts>().0.is_empty());
}
/// `HandleCastFailed 0x6e1a00` logs through `0x62c360`; `HandlePetCastFailed 0x6e8eb0` calls only
/// the packet readers and `0x496720`, with no log line and no error sound. Both entries name the
/// same spell, so only the caster differs.
+10
View File
@@ -250,10 +250,20 @@ impl Plugin for UiActionPlugin {
.in_set(UnitFeed)
.after(feed::feed_actions),
),
)
.add_systems(
OnExit(crate::client_state::ClientState::InWorld),
drop_held_lines,
);
}
}
/// `OnExit(InWorld)`: a line still held under the cover dies with the world's interface, as the
/// reference's, drawn at once by `0x4945b0`, dies in the UI teardown (`0x490bd0`).
fn drop_held_lines(mut texts: ResMut<UiErrorTexts>) {
texts.0.clear();
}
fn load_spells(mut commands: Commands, assets: Option<Res<WorldAssets>>) {
let Some(assets) = assets else { return };
let loaded = {