From 1c5a5681918bbcbe3d0b26f72e00ea0b80adc502 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:30:51 +0700 Subject: [PATCH] Escape on the realm list closes only the list, as in 1.12.1 (#827) Escape on the realm list closed the list and then also quit the game from the login screen, or went back to login from character select. 1.12.1 walks the keyboard frames once per press, strata high to low, and stops at the first handler that takes it (`0x765f10`, leaving at `0x765f64`/`0x765f86`). The realm list is DIALOG strata (`RealmList.xml:246`), so its Escape never reaches the screen beneath, even though it hides itself. benilla's list and screens read the keys in no fixed order. A screen only checked whether the list was still shown, and the list's own Escape had just hidden it. Who owns a frame's keys is now decided once, before any of them runs (`Realms::owns_input`, set in `PreUpdate`). Tests run the real list and screen systems in both orders, and fail before the fix with the reported symptoms. Fixes #817 Co-Authored-By: Claude Opus 5.5 --- crates/benilla-app/src/char_select/input.rs | 77 +++++++++++++++++++- crates/benilla-app/src/login/mod.rs | 72 +++++++++++++++++- crates/benilla-app/src/realm_select/input.rs | 2 +- crates/benilla-app/src/realm_select/mod.rs | 43 +++++++++-- 4 files changed, 184 insertions(+), 10 deletions(-) diff --git a/crates/benilla-app/src/char_select/input.rs b/crates/benilla-app/src/char_select/input.rs index 64170b58..c58f3008 100644 --- a/crates/benilla-app/src/char_select/input.rs +++ b/crates/benilla-app/src/char_select/input.rs @@ -41,7 +41,7 @@ pub(super) fn select_input( ) { // A modal owns the input while it is up: the delete confirm, the AddOns list, the realm list // over this screen, or the glue dialog (whose Okay must not double as Enter World or Escape). - if dialog.open || panel.open || realms.shown || glue_dialog.is_open() { + if dialog.open || panel.open || realms.owns_input() || glue_dialog.is_open() { return; } let now = time.elapsed_secs(); @@ -169,3 +169,78 @@ pub(super) fn rotate_model( } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::{RealmChoice, RealmRequest}; + use crate::realm_select::{list_keys, take_input, Realms}; + + /// Escape on the realm list raised by Change Realm closes the list and nothing else: the + /// press is the list's (`Realms::owns_input`), so this screen does not also go back to + /// login, in either order the two run. + #[test] + fn escape_on_the_realm_list_does_not_also_leave_for_login() { + for list_first in [true, false] { + let (pick_tx, pick_rx) = crossbeam_channel::unbounded(); + let (realm_tx, realm_rx) = crossbeam_channel::unbounded(); + let mut app = App::new(); + app.add_plugins((MinimalPlugins, bevy::state::app::StatesPlugin)) + .insert_state(ClientState::CharSelect) + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .insert_resource(CharPick(pick_tx)) + .insert_resource(RealmChoice(realm_tx)) + .add_message::() + .add_message::() + .add_systems(PreUpdate, take_input); + if list_first { + app.add_systems(Update, (list_keys, select_input).chain()); + } else { + app.add_systems(Update, (select_input, list_keys).chain()); + } + app.world_mut() + .resource_scope(|world, mut realms: Mut| { + crate::realm_select::open_over_char_select( + &mut realms, + world.resource::(), + ); + }); + app.world_mut() + .resource_mut::>() + .press(KeyCode::Escape); + app.update(); + app.world_mut() + .resource_mut::>() + .clear(); + app.update(); // `StateTransition` applies a pending state at the next frame + + let order = if list_first { + "list first" + } else { + "screen first" + }; + assert!( + realm_rx + .try_iter() + .any(|r| matches!(r, RealmRequest::Abandon)), + "{order}: the list's Cancel ran" + ); + assert_eq!( + *app.world().resource::>().get(), + ClientState::CharSelect, + "{order}: the press was the list's, so the screen under it stays" + ); + assert!( + pick_rx.try_recv().is_err(), + "{order}: and the session is not dropped" + ); + } + } +} diff --git a/crates/benilla-app/src/login/mod.rs b/crates/benilla-app/src/login/mod.rs index 58cfefd4..40308fea 100644 --- a/crates/benilla-app/src/login/mod.rs +++ b/crates/benilla-app/src/login/mod.rs @@ -664,8 +664,9 @@ fn login_input( time: Res