Key input follows the client's walks; stock login takes typing (#1027)
Key input follows the client's walks: key-down (`0x765f10`), char (`0x765df0`), key-up to the recorded consumer (`0x765fd0`) and repeat (`0x766070`). - EditBoxes are in the walk from creation (`0x779ced`), so the stock login's boxes take typing; only a focused box consumes. A box declines PRINTSCREEN and unaltered arrows on an `ignoreArrows` box, so stack-split steps under a focused chat box. A binding's press records the world frame. - Repeats follow the pressed-key list, so a held key resumes after alt-tab. Chords reach a box only through the walk. Keys the client can't name walk nowhere. - The stock login reads the agreements before its first screen. Cinematics returns to login (no movie player; Deviation). The Realmlist box has its middle border. - Alt+F4 closes the window on Windows and Linux (Deviation: the reference relies on the OS). Fixes #179 Fixes #1003 Fixes #1004 Fixes #1005 Fixes #1019 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7547081a58
commit
781bf36ab3
@@ -982,7 +982,7 @@ fn equipped_bag_window_snug_fits_its_row_count() {
|
||||
}
|
||||
|
||||
/// Open a backpack holding a five-stack in slot 1; returns that slot button's centre.
|
||||
fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
|
||||
pub(super) fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
|
||||
for file in crate::ui_script::test_ui::production_order(&[
|
||||
BAG_UI,
|
||||
&[
|
||||
|
||||
@@ -182,15 +182,6 @@ mod tests {
|
||||
/// `0x87a95c`, and the walk goes on).
|
||||
const INSTALL_WARNINGS: &[&str] = &["Frame TOSText: Unknown element type: H4"];
|
||||
|
||||
/// The movie frame's three handlers, which benilla's `MovieFrame` does not take: it plays no
|
||||
/// movie. Each is a loader warning, `<frame>: SetScript("<handler>") unsupported`; the list only
|
||||
/// shrinks.
|
||||
const UNBUILT_HANDLERS: &[(&str, &str)] = &[
|
||||
("MovieFrame", "OnMovieFinished"),
|
||||
("MovieFrame", "OnMovieHideSubtitle"),
|
||||
("MovieFrame", "OnMovieShowSubtitle"),
|
||||
];
|
||||
|
||||
/// The frames each stock glue screen and dialog is, which the load must build.
|
||||
const SCREENS: [&str; 8] = [
|
||||
"GlueParent",
|
||||
@@ -269,7 +260,6 @@ mod tests {
|
||||
|
||||
let surface = glue_surface();
|
||||
let mut called = BTreeSet::new();
|
||||
let mut handlers = BTreeSet::new();
|
||||
let other: Vec<&String> = errors
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
@@ -280,19 +270,13 @@ mod tests {
|
||||
if INSTALL_WARNINGS.contains(&e.as_str()) {
|
||||
return false;
|
||||
}
|
||||
if let Some(h) = UNBUILT_HANDLERS.iter().find(|(frame, handler)| {
|
||||
e.contains(&format!("{frame}: SetScript(\"{handler}\") unsupported"))
|
||||
}) {
|
||||
handlers.insert(*h);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
other.is_empty(),
|
||||
"the glue load raised {} error(s) or warning(s) that are not a call of an unbuilt \
|
||||
glue function on UNBUILT_CALLED or a handler on UNBUILT_HANDLERS:\n{}",
|
||||
glue function on UNBUILT_CALLED:\n{}",
|
||||
other.len(),
|
||||
other
|
||||
.iter()
|
||||
@@ -306,14 +290,6 @@ mod tests {
|
||||
stale.is_empty(),
|
||||
"UNBUILT_CALLED names no longer called unbuilt; take them off: {stale:?}"
|
||||
);
|
||||
let stale: Vec<&(&str, &str)> = UNBUILT_HANDLERS
|
||||
.iter()
|
||||
.filter(|h| !handlers.contains(*h))
|
||||
.collect();
|
||||
assert!(
|
||||
stale.is_empty(),
|
||||
"UNBUILT_HANDLERS entries the load no longer warns about; take them off: {stale:?}"
|
||||
);
|
||||
for name in UNBUILT_CALLED {
|
||||
assert!(
|
||||
surface.contains(*name),
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
//! Keys on the stock glue screens, end to end: the host's key and char events into the stock
|
||||
//! GlueXML in the glue VM.
|
||||
|
||||
use benilla_ui::script::{GlueNotice, NoticeState, UiScript};
|
||||
|
||||
use super::glue::{load_glue, set_glue_screen, GlueScreen};
|
||||
|
||||
/// The stock glue VM on `screen` with both agreements accepted, as a returning player's is.
|
||||
fn glue_on(screen: GlueScreen) -> UiScript {
|
||||
let mut s = UiScript::new_glue().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let accepted = NoticeState {
|
||||
accepted: true,
|
||||
show_notice: false,
|
||||
};
|
||||
for notice in [GlueNotice::Eula, GlueNotice::Tos] {
|
||||
s.set_glue_notice(notice, accepted);
|
||||
}
|
||||
let _ = load_glue(&mut s);
|
||||
set_glue_screen(&mut s, screen);
|
||||
s.resolve();
|
||||
let _ = s.take_errors();
|
||||
s
|
||||
}
|
||||
|
||||
/// One typed key as the host feeds it (`ui_script::input`): the key-down by name, then its char.
|
||||
fn type_key(s: &mut UiScript, name: &str, text: &str) {
|
||||
s.frame_key_input(name);
|
||||
s.char_input(text);
|
||||
s.key_up(name);
|
||||
}
|
||||
|
||||
fn text(s: &UiScript, frame: &str) -> String {
|
||||
s.eval::<String>(&format!("return {frame}:GetText() or ''"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The login boxes take the keys typed into them. `AccountLogin` is keyboard-enabled with an
|
||||
/// `OnChar` and an `OnKeyDown`, and walks ahead of any box that is not itself in the walk; the
|
||||
/// EditBox ctor enables key input (`0x779ced`, `0x779cf8`) so the focused box answers at its own
|
||||
/// level above its parent.
|
||||
#[test]
|
||||
fn the_stock_login_boxes_take_typed_keys() {
|
||||
let _ = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = glue_on(GlueScreen::Login);
|
||||
assert!(
|
||||
s.eval::<bool>("return AccountLoginUI:IsShown() == 1")
|
||||
.unwrap(),
|
||||
"accepted agreements put the login boxes up"
|
||||
);
|
||||
s.run("AccountLoginAccountEdit:SetFocus()").unwrap();
|
||||
for (name, c) in [("P", "p"), ("R", "r"), ("O", "o"), ("B", "b"), ("E", "e")] {
|
||||
type_key(&mut s, name, c);
|
||||
}
|
||||
assert_eq!(text(&s, "AccountLoginAccountEdit"), "probe");
|
||||
|
||||
// Tab moves to the password box (`AccountLoginAccountEdit`'s `OnTabPressed`), which then types.
|
||||
assert!(s.key_input("TAB"));
|
||||
assert_eq!(
|
||||
s.focused_editbox_name().as_deref(),
|
||||
Some("AccountLoginPasswordEdit")
|
||||
);
|
||||
for (name, c) in [("S", "s"), ("E", "e"), ("C", "c")] {
|
||||
type_key(&mut s, name, c);
|
||||
}
|
||||
assert_eq!(text(&s, "AccountLoginPasswordEdit"), "sec");
|
||||
assert_eq!(text(&s, "AccountLoginAccountEdit"), "probe");
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// The login screen's Cinematics button shows `MovieFrame`; no movie plays here, so
|
||||
/// `StartMovie` answers nil and the stock `MovieFrame_PlayMovie` hides the frame, whose `OnHide`
|
||||
/// puts the login screen back and the pointer with it (`MovieFrame.lua:18-23,40-44`).
|
||||
#[test]
|
||||
fn the_cinematics_button_comes_back_to_the_login_screen() {
|
||||
let _ = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = glue_on(GlueScreen::Login);
|
||||
s.register_cvars([("movieSubtitle", "1"), ("gxResolution", "1024x768")]);
|
||||
s.run("AccountLoginCinematicsButton:Click()").unwrap();
|
||||
s.resolve();
|
||||
let (movie, login, cursor_hidden) = (
|
||||
s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap(),
|
||||
s.eval::<bool>("return AccountLoginUI:IsVisible() == 1")
|
||||
.unwrap(),
|
||||
s.glue_cursor_hidden(),
|
||||
);
|
||||
assert_eq!(
|
||||
(movie, login, cursor_hidden),
|
||||
(false, true, false),
|
||||
"the movie frame is gone, the login boxes are back and the pointer shows"
|
||||
);
|
||||
assert_eq!(s.take_errors(), Vec::<String>::new());
|
||||
}
|
||||
|
||||
/// A frame whose only key script is `OnKeyUp` takes the key-down and runs its `OnKeyUp` on the
|
||||
/// release (`0x76b7d0`, `0x765fd0`, `0x76bba0`): Escape on a playing movie hides the frame
|
||||
/// (`MovieFrame_OnKeyUp`), which returns to login.
|
||||
#[test]
|
||||
fn escape_on_a_playing_movie_returns_to_login_on_the_key_up() {
|
||||
let _ = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = glue_on(GlueScreen::Login);
|
||||
s.register_cvars([("movieSubtitle", "0"), ("gxResolution", "1024x768")]);
|
||||
// A movie that starts, as the reference's player would.
|
||||
s.run("MovieFrame.StartMovie = function() return 1 end SetGlueScreen('movie')")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert!(s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap());
|
||||
assert!(s.glue_cursor_hidden(), "a playing movie hides the pointer");
|
||||
|
||||
assert!(
|
||||
s.frame_key_input("SPACE"),
|
||||
"the movie swallows the key-down"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap(),
|
||||
"…and nothing happens until the key is up"
|
||||
);
|
||||
assert!(s.key_input("ESCAPE"));
|
||||
assert!(s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap());
|
||||
s.key_up("ESCAPE");
|
||||
s.resolve();
|
||||
assert!(!s.eval::<bool>("return MovieFrame:IsShown() == 1").unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return AccountLoginUI:IsVisible() == 1")
|
||||
.unwrap());
|
||||
assert!(!s.glue_cursor_hidden());
|
||||
assert_eq!(s.take_errors(), Vec::<String>::new());
|
||||
}
|
||||
|
||||
/// The Realmlist dialog widens the stock edit box, whose border is two 75-pixel caps that meet at
|
||||
/// its own 130 pixels: while it is wide the stretched middle fills the gap between the caps, as the
|
||||
/// chat box's does (`ChatFrame.xml:41-55`), and the stock box comes back untouched.
|
||||
#[test]
|
||||
fn the_widened_realmlist_box_has_a_border_between_its_caps() {
|
||||
let _ = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = glue_on(GlueScreen::Login);
|
||||
s.run("BENILLA_REALMLIST = 'logon.example.org' BenillaRealmlistButton:Click()")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let span = |s: &UiScript| {
|
||||
s.eval::<(f64, f64, f64, f64)>(
|
||||
"return GlueDialogEditBox:GetLeft(), GlueDialogEditBox:GetRight(), \
|
||||
BenillaRealmlist_Middle:GetLeft(), BenillaRealmlist_Middle:GetRight()",
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let (box_left, box_right, mid_left, mid_right) = span(&s);
|
||||
assert_eq!(box_right - box_left, 300.0);
|
||||
assert_eq!(
|
||||
(mid_left - box_left, box_right - mid_right),
|
||||
(65.0, 65.0),
|
||||
"the middle starts where the left cap ends and ends where the right one starts"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaRealmlist_Middle:IsShown() == 1")
|
||||
.unwrap());
|
||||
|
||||
s.run("GlueDialogButton2:Click()").unwrap();
|
||||
s.resolve();
|
||||
assert!(!s
|
||||
.eval::<bool>("return BenillaRealmlist_Middle:IsShown() == 1")
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return GlueDialogEditBox:GetWidth()")
|
||||
.unwrap(),
|
||||
130.0
|
||||
);
|
||||
assert_eq!(s.take_errors(), Vec::<String>::new());
|
||||
}
|
||||
@@ -485,6 +485,11 @@ pub(crate) struct GlueMgr {
|
||||
}
|
||||
|
||||
impl GlueMgr {
|
||||
/// The agreements as read at the startup (`0x46a400`).
|
||||
pub(super) fn set_notices(&mut self, notices: [NoticeState; 4]) {
|
||||
self.notices = Some(notices);
|
||||
}
|
||||
|
||||
fn notice(&self, notice: GlueNotice) -> NoticeState {
|
||||
self.notices.map(|n| n[notice as usize]).unwrap_or_default()
|
||||
}
|
||||
@@ -1096,6 +1101,19 @@ fn notice_cvar(notice: GlueNotice) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The four agreements' states off the CVar registry: the read `0x46a400` makes at `0x402b36`,
|
||||
/// before the first glue screen is set (`0x402b65`), so a returning player's login screen never
|
||||
/// sees them unaccepted.
|
||||
pub(crate) fn read_agreements(cvars: &crate::cvars::Cvars) -> [NoticeState; 4] {
|
||||
let mut n = [NoticeState::default(); 4];
|
||||
for notice in GlueNotice::ALL {
|
||||
if let Some(value) = cvars.get(notice_cvar(notice)) {
|
||||
n[notice as usize] = NoticeState::from_cvar(value);
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// The glue manager's frame: the VM's requests, the native login's news, the per-frame arms, then
|
||||
/// the events into the glue VM and its slots seeded. Runs under `WOW_GLUE_STOCK=1` only.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -7,7 +7,7 @@ use bevy::input::ButtonState;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
use benilla_ui::script::{KeyWalk, UiScript};
|
||||
|
||||
use super::{
|
||||
CursorPayloadHeld, PlayerUiClickConsumed, PlayerUiHover, PlayerUiPressConsumed,
|
||||
@@ -96,10 +96,19 @@ pub(super) fn feed_ui_input(
|
||||
ResMut<UiKeyboardCapture>,
|
||||
NonSendMut<HostClipboard>,
|
||||
),
|
||||
// The keys this feed believes down, a fact about the keyboard and not the VM.
|
||||
mut pressed: Local<crate::bindings::KeyDownList>,
|
||||
// Who sends the key-ups: the bindings pass in the world, this feed on the glue screens.
|
||||
state: Option<Res<State<crate::char_select::ClientState>>>,
|
||||
// The characters the active layout makes, which name a key for a keyboard frame.
|
||||
layout: Res<crate::bindings::LayoutNames>,
|
||||
) {
|
||||
let (keyboard, keys, capture, clipboard) = (&mut kbd.0, &kbd.1, &mut kbd.2, &mut kbd.3);
|
||||
// In the world the bindings pass sends the key-ups, in event order with the presses it
|
||||
// follows; on the glue screens, which have none, this feed does.
|
||||
let releases = !state
|
||||
.as_ref()
|
||||
.is_some_and(|s| *s.get() == crate::char_select::ClientState::InWorld);
|
||||
let world_pick = pointer.world_pick();
|
||||
// The OS pointer is not ours while a probe drives a gesture through the real pointer path or a
|
||||
// capture pins it: skip the mouse half whole, else-arm included, whose `pointer_left_window`
|
||||
@@ -210,15 +219,20 @@ pub(super) fn feed_ui_input(
|
||||
// and before the keys (an Escape that clears focus still counts as captured this frame).
|
||||
capture.typing = script.has_keyboard_focus();
|
||||
capture.consumed.clear();
|
||||
script.set_alt_down(alt);
|
||||
// ── The alt-arrow exemption ────────────────────────────────────────────────────────────────
|
||||
// An EditBox in alt-arrow mode (`ignoreArrows`, set on the chat box at `ChatFrame.xml:21`)
|
||||
// passes the arrows on unless ALT is held: its handler returns 0 at `0x77b1c4` and the
|
||||
// binding runs at `CGWorldFrame`; a focused box swallows any other key (`0x77b35e` returns 1).
|
||||
// binding runs at `CGWorldFrame`; a focused box swallows any other key (`0x77b35e` returns 1),
|
||||
// PRINTSCREEN excepted (`0x77b17f`, see `crate::bindings`).
|
||||
capture.arrows_fall_through = !alt && script.editbox_alt_arrow_mode();
|
||||
|
||||
// ── Keyboard → VM ── box-event keys go to `key_input` by name, editing keys through the
|
||||
// per-OS chord table as an `EditAction` or a clipboard operation (the macOS NSPasteboard
|
||||
// needs this main-thread system), the rest to `char_input`. Repeats arrive as `Pressed`.
|
||||
// ── Keyboard → VM ── every press is one key-down walk by name (`0x765f10`), in which a box is
|
||||
// a frame like the rest; a box the walk reached then takes the key's editing action through
|
||||
// the per-OS chord table as an `EditAction` or a clipboard operation (the macOS NSPasteboard
|
||||
// needs this main-thread system), and the characters go to `char_input`, a walk of its own.
|
||||
// A repeat goes to the frame that took the press instead (`0x766070`), a release likewise
|
||||
// (`0x765fd0`).
|
||||
let sup = keys.pressed(KeyCode::SuperLeft) || keys.pressed(KeyCode::SuperRight);
|
||||
let mods = keymap::Mods {
|
||||
shift,
|
||||
@@ -226,84 +240,121 @@ pub(super) fn feed_ui_input(
|
||||
alt,
|
||||
sup,
|
||||
};
|
||||
feed_keys(
|
||||
&mut script,
|
||||
capture,
|
||||
keyboard.read(),
|
||||
keys,
|
||||
&mut pressed,
|
||||
releases,
|
||||
mods,
|
||||
&layout,
|
||||
clipboard,
|
||||
wl_display,
|
||||
);
|
||||
|
||||
// The payload-held mirror, written last so a same-frame pickup or ESC clear shows: the
|
||||
// Send-side view the world-click deselect gates on.
|
||||
payload_held.0 = script.cursor_payload().is_some();
|
||||
|
||||
for err in script.take_errors() {
|
||||
warn!("ui_script(input): {err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The keyboard half of the frame: each event as the key-down or repeat walk and what a box the
|
||||
/// walk reached does with the key. `down` is this feed's pressed-key list, which decides repeat
|
||||
/// as the bindings' does (`0x4248b3`), never from the OS flag: after an alt-tab wipes it
|
||||
/// (`0x424790`, the reconcile against `keys`) the first repeat is a fresh press, and a platform
|
||||
/// that never sets the flag still repeats right. `releases` says whether the key-up dispatch is
|
||||
/// this feed's: in the world the bindings pass sends it, in order with the press it follows
|
||||
/// (`crate::bindings`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn feed_keys<'a>(
|
||||
script: &mut UiScript,
|
||||
capture: &mut UiKeyboardCapture,
|
||||
events: impl Iterator<Item = &'a KeyboardInput>,
|
||||
keys: &ButtonInput<KeyCode>,
|
||||
down: &mut crate::bindings::KeyDownList,
|
||||
releases: bool,
|
||||
mods: keymap::Mods,
|
||||
layout: &crate::bindings::LayoutNames,
|
||||
clipboard: &mut HostClipboard,
|
||||
wl_display: Option<*mut std::ffi::c_void>,
|
||||
) {
|
||||
down.reconcile(keys);
|
||||
let keymap::Mods {
|
||||
shift: _,
|
||||
ctrl,
|
||||
alt,
|
||||
sup,
|
||||
} = mods;
|
||||
let mac = cfg!(target_os = "macos");
|
||||
for ev in keyboard.read() {
|
||||
for ev in events {
|
||||
// What the layout names the key: the clipboard chords' letters, and a keyboard frame's
|
||||
// `arg1`, which the Key Bindings window also stores, so a key's press dispatches by the
|
||||
// name it was bound under.
|
||||
let token = crate::bindings::chord::key_token(ev.key_code, layout);
|
||||
// `None` is a key the client drops before any walk (a Windows or Command key, a media
|
||||
// key): no down, no up, and no focus granted to an `autoFocus` box.
|
||||
let name = crate::bindings::chord::walk_name(ev.key_code, layout);
|
||||
if ev.state != ButtonState::Pressed {
|
||||
down.release(ev.key_code);
|
||||
if let Some(name) = name.as_deref().filter(|_| releases) {
|
||||
script.key_up(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let named = match ev.key_code {
|
||||
KeyCode::Enter | KeyCode::NumpadEnter => Some("ENTER"),
|
||||
KeyCode::Escape => Some("ESCAPE"),
|
||||
KeyCode::Tab => Some("TAB"),
|
||||
_ => None,
|
||||
let repeat = down.press(ev.key_code);
|
||||
let named = matches!(
|
||||
ev.key_code,
|
||||
KeyCode::Enter | KeyCode::NumpadEnter | KeyCode::Escape | KeyCode::Tab
|
||||
);
|
||||
let walk = match name.as_deref() {
|
||||
Some(name) if repeat => script.key_repeat(name),
|
||||
Some(name) => script.key_down(name),
|
||||
None => KeyWalk::Declined,
|
||||
};
|
||||
// What the layout names the key: the clipboard chords' letters, and a keyboard frame's
|
||||
// `arg1` below.
|
||||
let token = crate::bindings::chord::key_token(ev.key_code, &layout);
|
||||
// Dispatched unconditionally: unfocused, they fall through to the camera and turn keys.
|
||||
// The gate is existence, not handling: a shown keyboard frame with an `OnKeyDown` swallows
|
||||
// the key whatever its script does (`0x76b7d0`, `0x76ba25`). Consumption suppresses only
|
||||
// the key's binding: `OnChar` is a separate dispatcher (`0x765df0`), so the stack-split
|
||||
// spinner still gets a digit its `OnKeyDown` ate, and it is not a focus change, so it
|
||||
// releases nothing held. A box that took the key counts too: it returned 1 (`0x77b1db`
|
||||
// -> `0x77b35e`) and the world frame, last in the walk, never saw it, so an F-key that
|
||||
// gave an `autoFocus` box the focus runs no binding. An unconsumed press is acted on in
|
||||
// the binding dispatch (`crate::bindings`), which runs after this pass and reads the
|
||||
// capture gate.
|
||||
if walk != KeyWalk::Declined {
|
||||
capture.consumed.push(ev.key_code);
|
||||
}
|
||||
let editing_key = matches!(
|
||||
ev.key_code,
|
||||
KeyCode::Backspace
|
||||
| KeyCode::Delete
|
||||
| KeyCode::ArrowLeft
|
||||
| KeyCode::ArrowRight
|
||||
| KeyCode::ArrowUp
|
||||
| KeyCode::ArrowDown
|
||||
| KeyCode::Home
|
||||
| KeyCode::End
|
||||
);
|
||||
if walk == KeyWalk::Frame && editing_key {
|
||||
// Not `typing`: a frame eating one key is not a box taking focus, and treating it as
|
||||
// one would stop a held movement key.
|
||||
continue;
|
||||
}
|
||||
if named {
|
||||
continue;
|
||||
}
|
||||
// The box's own key-down switch (`0x77b160`) does the editing, copy, cut, paste and
|
||||
// select-all, so only a box the walk reached acts; a frame that took the key, or a gated
|
||||
// arrow the box declined, leaves it alone.
|
||||
let chord = keymap::chord(ev.key_code, token, mods, mac);
|
||||
// A keyboard frame gets these by name before their chord runs (a dialog needs BACKSPACE);
|
||||
// `frame_key_input` declines at a focused box, so no frame steals its editing keys. `true`
|
||||
// suppresses the chord and the key's binding (the reference's existence gate, `0x76b7d0`).
|
||||
let frame_named = match ev.key_code {
|
||||
KeyCode::Backspace => Some("BACKSPACE"),
|
||||
KeyCode::Delete => Some("DELETE"),
|
||||
KeyCode::ArrowLeft => Some("LEFT"),
|
||||
KeyCode::ArrowRight => Some("RIGHT"),
|
||||
KeyCode::ArrowUp => Some("UP"),
|
||||
KeyCode::ArrowDown => Some("DOWN"),
|
||||
KeyCode::Home => Some("HOME"),
|
||||
KeyCode::End => Some("END"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(name) = frame_named {
|
||||
if script.frame_key_input(name) {
|
||||
// Not `typing`: a frame eating one key is not a box taking focus, and treating it
|
||||
// as one would stop a held movement key.
|
||||
capture.consumed.push(ev.key_code);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Every other key reaches a keyboard frame by name too: the reference's key-down walk
|
||||
// takes its `arg1` from the table the binding chord uses (`0x4b66b0`), `chord::key_token`
|
||||
// under the active layout here, so the Key Bindings window stores the name the key's
|
||||
// press dispatches by. The gate is existence, not handling: a shown keyboard frame with
|
||||
// an `OnKeyDown` swallows the key whatever its script does (`0x76b7d0`, `0x76ba25`).
|
||||
// Consumption suppresses only the key's binding: `OnChar` is a separate dispatcher
|
||||
// (`0x765df0`), so the stack-split spinner still gets a digit its `OnKeyDown` ate, and it
|
||||
// is not a focus change, so it releases nothing held.
|
||||
else if named.is_none() {
|
||||
if let Some(token) = token {
|
||||
if script.frame_key_input(&token.to_string()) {
|
||||
capture.consumed.push(ev.key_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(name) = named {
|
||||
// An unconsumed press is not acted on here: ESCAPE's close ladder, TAB's targeting and
|
||||
// ENTER's chat open live in the binding dispatch (`crate::bindings`), which runs after
|
||||
// this pass and reads the capture gate.
|
||||
if script.key_input(name) {
|
||||
capture.consumed.push(ev.key_code);
|
||||
}
|
||||
} else if let Some(chord) = chord {
|
||||
// The gate is on the key, not the action: a gated arrow never reaches the box (the
|
||||
// reference's `return 0`), while HOME/END, which also make a `Move`, do.
|
||||
let gated_arrow = capture.arrows_fall_through
|
||||
&& matches!(
|
||||
ev.key_code,
|
||||
KeyCode::ArrowLeft
|
||||
| KeyCode::ArrowRight
|
||||
| KeyCode::ArrowUp
|
||||
| KeyCode::ArrowDown
|
||||
);
|
||||
if let Some(chord) = chord.filter(|_| walk == KeyWalk::Box) {
|
||||
match chord {
|
||||
keymap::Chord::Edit(action) if !gated_arrow => {
|
||||
keymap::Chord::Edit(action) => {
|
||||
script.editbox_action(action);
|
||||
}
|
||||
// Declined: the key falls through to its binding, which `bindings.rs` allows
|
||||
// because `arrows_fall_through` exempts exactly these four from the typing gate.
|
||||
keymap::Chord::Edit(_) => {}
|
||||
// Copy and cut need a selection (`0x77e1d0`), and a password box yields its mask
|
||||
// run, never the real text; paste sanitizes and inserts.
|
||||
keymap::Chord::Copy => {
|
||||
@@ -322,26 +373,16 @@ pub(super) fn feed_ui_input(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !(sup || (ctrl && !alt)) {
|
||||
} else if chord.is_none() && !(sup || (ctrl && !alt)) {
|
||||
// Command-modified chars never insert (Cmd+L must not type "l"), but Ctrl+Alt passes:
|
||||
// that is AltGr, and macOS Option+letter arrives with only `alt`.
|
||||
// The char walk gates no binding (`0x765df0` reaches no keybinding), so whether a
|
||||
// frame took the character is not recorded: the key-down already decided that.
|
||||
if let Some(text) = &ev.text {
|
||||
// Same suppression as the named keys: the number row is a binding, so a digit typed
|
||||
// into a keyboard frame (the stack-split spinner) must not also fire an action.
|
||||
if script.char_input(text) {
|
||||
capture.consumed.push(ev.key_code);
|
||||
}
|
||||
script.char_input(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The payload-held mirror, written last so a same-frame pickup or ESC clear shows: the
|
||||
// Send-side view the world-click deselect gates on.
|
||||
payload_held.0 = script.cursor_payload().is_some();
|
||||
|
||||
for err in script.take_errors() {
|
||||
warn!("ui_script(input): {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
//! The host's keyboard feed ([`super::input::feed_keys`]) against the reference's four dispatchers:
|
||||
//! key-down (`0x765f10`), char (`0x765df0`), key-up (`0x765fd0`) and auto-repeat (`0x766070`).
|
||||
|
||||
use bevy::input::keyboard::{Key, KeyboardInput};
|
||||
use bevy::input::ButtonState;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
use super::bag_tests::open_backpack_with_a_five_stack;
|
||||
use super::input::feed_keys;
|
||||
use super::UiKeyboardCapture;
|
||||
use crate::bindings::{KeyDownList, LayoutNames};
|
||||
use crate::textinput::{keymap::Mods, HostClipboard};
|
||||
|
||||
/// One keyboard event as winit hands it over.
|
||||
fn ev(code: KeyCode, state: ButtonState, repeat: bool, text: Option<&str>) -> KeyboardInput {
|
||||
KeyboardInput {
|
||||
key_code: code,
|
||||
logical_key: Key::Character(text.unwrap_or("?").into()),
|
||||
state,
|
||||
text: text.map(Into::into),
|
||||
repeat,
|
||||
window: Entity::PLACEHOLDER,
|
||||
}
|
||||
}
|
||||
|
||||
fn down(code: KeyCode, text: Option<&str>) -> KeyboardInput {
|
||||
ev(code, ButtonState::Pressed, false, text)
|
||||
}
|
||||
|
||||
fn repeat(code: KeyCode) -> KeyboardInput {
|
||||
ev(code, ButtonState::Pressed, true, None)
|
||||
}
|
||||
|
||||
fn up(code: KeyCode) -> KeyboardInput {
|
||||
ev(code, ButtonState::Released, false, None)
|
||||
}
|
||||
|
||||
/// A harness for one script: the capture the bindings read, a clipboard holding `paste`.
|
||||
struct Feed {
|
||||
capture: UiKeyboardCapture,
|
||||
clipboard: HostClipboard,
|
||||
mods: Mods,
|
||||
keys: ButtonInput<KeyCode>,
|
||||
down: KeyDownList,
|
||||
}
|
||||
|
||||
impl Feed {
|
||||
fn new(paste: &str) -> Self {
|
||||
Feed {
|
||||
capture: UiKeyboardCapture::default(),
|
||||
clipboard: HostClipboard::holding(paste),
|
||||
mods: Mods {
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
sup: false,
|
||||
},
|
||||
keys: ButtonInput::default(),
|
||||
down: KeyDownList::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The events as the frame brings them, `ButtonInput` following them as bevy's does.
|
||||
fn send(&mut self, script: &mut UiScript, events: &[KeyboardInput]) {
|
||||
script.set_alt_down(self.mods.alt);
|
||||
self.keys.clear();
|
||||
for ev in events {
|
||||
match ev.state {
|
||||
ButtonState::Pressed => self.keys.press(ev.key_code),
|
||||
ButtonState::Released => self.keys.release(ev.key_code),
|
||||
}
|
||||
}
|
||||
feed_keys(
|
||||
script,
|
||||
&mut self.capture,
|
||||
events.iter(),
|
||||
&self.keys,
|
||||
&mut self.down,
|
||||
true,
|
||||
self.mods,
|
||||
&LayoutNames::default(),
|
||||
&mut self.clipboard,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A chat-like box in alt-arrow mode at DIALOG, focused, over the real split dialog: the arrows
|
||||
/// are the box's to decline (`0x77b18e`-`0x77b1c4`), so the dialog steps with them, and holding
|
||||
/// RIGHT steps it once (`0x766070` hands the repeat to the dialog's stub).
|
||||
#[test]
|
||||
fn the_split_dialog_steps_with_the_arrows_under_a_focused_chat_box() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let (x, y) = open_backpack_with_a_five_stack(&mut s);
|
||||
s.set_modifiers(true, false, false);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
s.set_modifiers(false, false, false);
|
||||
s.run(
|
||||
r#"
|
||||
ChatLike = CreateFrame("EditBox", "ChatLike")
|
||||
ChatLike:SetAutoFocus(false)
|
||||
ChatLike:SetAltArrowKeyMode(1)
|
||||
ChatLike:SetFrameStrata("DIALOG")
|
||||
ChatLike:SetFrameLevel(StackSplitFrame:GetFrameLevel() + 1)
|
||||
ChatLike:SetFocus()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 1);
|
||||
|
||||
let mut feed = Feed::new("");
|
||||
feed.send(&mut s, &[down(KeyCode::ArrowRight, None)]);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StackSplitFrame.split").unwrap(),
|
||||
2,
|
||||
"the dialog took the arrow the box declined"
|
||||
);
|
||||
assert!(feed.capture.consumed.contains(&KeyCode::ArrowRight));
|
||||
feed.send(
|
||||
&mut s,
|
||||
&[repeat(KeyCode::ArrowRight), repeat(KeyCode::ArrowRight)],
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StackSplitFrame.split").unwrap(),
|
||||
2,
|
||||
"holding RIGHT steps once"
|
||||
);
|
||||
feed.send(&mut s, &[up(KeyCode::ArrowRight)]);
|
||||
feed.send(&mut s, &[down(KeyCode::ArrowRight, None)]);
|
||||
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 3);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ChatLike:GetText()").unwrap(),
|
||||
"",
|
||||
"and the box saw nothing"
|
||||
);
|
||||
|
||||
// With ALT the box keeps its arrows.
|
||||
feed.mods.alt = true;
|
||||
feed.send(&mut s, &[down(KeyCode::ArrowRight, None)]);
|
||||
assert_eq!(s.eval::<i64>("return StackSplitFrame.split").unwrap(), 3);
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// The paste chord with a keyboard frame above a focused box: the frame took the key-down, and the paste
|
||||
/// is the box's own key-down arm (`0x77b160`), which the walk never reached.
|
||||
#[test]
|
||||
fn a_frame_that_took_the_key_keeps_the_paste_from_the_box() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
got = {}
|
||||
box = CreateFrame("EditBox", "PasteBox")
|
||||
box:SetAutoFocus(false)
|
||||
box:SetFrameStrata("DIALOG")
|
||||
box:SetFocus()
|
||||
map = CreateFrame("Frame", "PasteMap")
|
||||
map:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
map:EnableKeyboard(true)
|
||||
map:SetScript("OnKeyDown", function() table.insert(got, arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut feed = Feed::new("pasted");
|
||||
// The paste chord of the platform: Cmd+V on a Mac, Ctrl+V elsewhere.
|
||||
feed.mods.ctrl = !cfg!(target_os = "macos");
|
||||
feed.mods.sup = cfg!(target_os = "macos");
|
||||
feed.send(&mut s, &[down(KeyCode::KeyV, Some("\u{16}"))]);
|
||||
assert_eq!(s.eval::<String>("return box:GetText()").unwrap(), "");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(got, ',')").unwrap(),
|
||||
"V"
|
||||
);
|
||||
|
||||
// The map closes: the walk reaches the box, which pastes.
|
||||
s.run("map:Hide()").unwrap();
|
||||
feed.send(&mut s, &[up(KeyCode::KeyV)]);
|
||||
feed.send(&mut s, &[down(KeyCode::KeyV, Some("\u{16}"))]);
|
||||
assert_eq!(s.eval::<String>("return box:GetText()").unwrap(), "pasted");
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// The modifiers go through the walk as key-downs named `SHIFT`, `CTRL` and `ALT` (`0x76b7d0`'s
|
||||
/// table), the codes it cannot name (ScrollLock) as `UNKNOWN`, each with its key-up; a key the
|
||||
/// client drops (Windows, Menu, fn, Command) walks as nothing.
|
||||
#[test]
|
||||
fn modifiers_and_unnamed_keys_walk_as_keys_with_their_ups() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
log = {}
|
||||
f = CreateFrame("Frame", "ModWatch")
|
||||
f:EnableKeyboard(true)
|
||||
f:SetScript("OnKeyDown", function() table.insert(log, "v" .. arg1) end)
|
||||
f:SetScript("OnKeyUp", function() table.insert(log, "^" .. arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut feed = Feed::new("");
|
||||
for code in [
|
||||
KeyCode::ShiftLeft,
|
||||
KeyCode::ControlRight,
|
||||
KeyCode::AltLeft,
|
||||
KeyCode::ScrollLock,
|
||||
// The client drops these before any walk (`0x42da39`; the Mac table's -1): no down, no up.
|
||||
KeyCode::Fn,
|
||||
KeyCode::SuperLeft,
|
||||
KeyCode::ContextMenu,
|
||||
] {
|
||||
feed.send(&mut s, &[down(code, None), up(code)]);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(log, ',')").unwrap(),
|
||||
"vSHIFT,^SHIFT,vCTRL,^CTRL,vALT,^ALT,vUNKNOWN,^UNKNOWN"
|
||||
);
|
||||
assert!(feed.capture.consumed.contains(&KeyCode::ShiftLeft));
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// A box the walk reached ate the key (`0x77b1db` -> `0x77b35e`; the world frame never sees it), so
|
||||
/// an F-key that gave an `autoFocus` box the focus runs no binding; the character walk gates
|
||||
/// nothing, so a frame that took a character leaves its key's binding alone.
|
||||
#[test]
|
||||
fn a_box_that_took_the_key_eats_its_binding_and_a_char_gates_none() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
box = CreateFrame("EditBox", "EatBox")
|
||||
box:SetAutoFocus(true)
|
||||
box:Hide() box:Show()
|
||||
box:ClearFocus()
|
||||
chars = CreateFrame("Frame", "CharsOnly")
|
||||
chars:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
chars:EnableKeyboard(true)
|
||||
chars:SetScript("OnChar", function() end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut feed = Feed::new("");
|
||||
feed.send(&mut s, &[down(KeyCode::F5, None)]);
|
||||
assert_eq!(s.focused_editbox_name().as_deref(), Some("EatBox"));
|
||||
assert!(
|
||||
feed.capture.consumed.contains(&KeyCode::F5),
|
||||
"the box ate F5, so its binding does not run"
|
||||
);
|
||||
|
||||
s.run("box:ClearFocus() box:Hide()").unwrap();
|
||||
feed.send(&mut s, &[up(KeyCode::F5)]);
|
||||
feed.capture.consumed.clear();
|
||||
feed.send(&mut s, &[down(KeyCode::KeyQ, Some("q"))]);
|
||||
assert!(
|
||||
feed.capture.consumed.is_empty(),
|
||||
"OnChar took the character, which is no reason to eat Q's binding"
|
||||
);
|
||||
}
|
||||
|
||||
/// A key the client drops before any walk (Windows key, Command, fn) gives an `autoFocus` box no
|
||||
/// focus and no key-up; the Windows key is `SuperLeft` here.
|
||||
#[test]
|
||||
fn a_dropped_key_walks_nowhere_and_grants_no_focus() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
box = CreateFrame("EditBox", "DropBox")
|
||||
box:SetAutoFocus(true)
|
||||
box:Hide() box:Show()
|
||||
box:ClearFocus()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut feed = Feed::new("");
|
||||
feed.send(
|
||||
&mut s,
|
||||
&[down(KeyCode::SuperLeft, None), up(KeyCode::SuperLeft)],
|
||||
);
|
||||
assert_eq!(
|
||||
s.focused_editbox_name(),
|
||||
None,
|
||||
"Cmd+Tab gives no box the focus"
|
||||
);
|
||||
feed.send(&mut s, &[down(KeyCode::KeyA, Some("a"))]);
|
||||
assert_eq!(s.focused_editbox_name().as_deref(), Some("DropBox"));
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_assets::LockRecover;
|
||||
use benilla_ui::script::UiScript;
|
||||
use benilla_ui::script::{GlueNotice, UiScript};
|
||||
|
||||
use super::manifest::silenced_ui_load;
|
||||
use super::{load_ingame_ui, CursorPayloadHeld, PlayerUiHover, UiClock, UiKeyboardCapture};
|
||||
@@ -73,6 +73,7 @@ fn install_glue_vm(world: &mut World, screen: super::glue::GlueScreen) -> bool {
|
||||
return false;
|
||||
};
|
||||
seat_raster_seam_for_load(world, &mut script);
|
||||
seed_agreements(world, &mut script);
|
||||
let start = std::time::Instant::now();
|
||||
let failures = super::glue::load_glue(&mut script);
|
||||
super::glue::finish_glue_load_log(&mut script);
|
||||
@@ -94,6 +95,22 @@ fn install_glue_vm(world: &mut World, screen: super::glue::GlueScreen) -> bool {
|
||||
charselect
|
||||
}
|
||||
|
||||
/// Read the four agreement CVars into the glue VM and the manager before any screen is set, as
|
||||
/// the reference does (`0x46a400` at `0x402b36`, then `SET_GLUE_SCREEN` at `0x402b65`): the login
|
||||
/// screen's `OnShow` asks them at once, and nothing asks again.
|
||||
fn seed_agreements(world: &mut World, script: &mut UiScript) {
|
||||
let Some(cvars) = world.get_resource::<crate::cvars::Cvars>() else {
|
||||
return;
|
||||
};
|
||||
let notices = super::glue_mgr::read_agreements(cvars);
|
||||
for notice in GlueNotice::ALL {
|
||||
script.set_glue_notice(notice, notices[notice as usize]);
|
||||
}
|
||||
if let Some(mut mgr) = world.get_resource_mut::<super::glue_mgr::GlueMgr>() {
|
||||
mgr.set_notices(notices);
|
||||
}
|
||||
}
|
||||
|
||||
/// A fresh VM from `make` on the process clock with the addon asset probes wired, or `None` (the
|
||||
/// world's VM removed) when the VM cannot start.
|
||||
fn new_vm<E: std::fmt::Display>(
|
||||
@@ -1089,6 +1106,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The agreements are read from their CVars before the first screen is set (`0x46a400` at
|
||||
/// `0x402b36`, `SET_GLUE_SCREEN` at `0x402b65`): a returning player's login shows the boxes,
|
||||
/// not the EULA, and a first launch still shows the EULA.
|
||||
#[test]
|
||||
fn the_login_screen_comes_up_on_the_accepted_agreements() {
|
||||
use crate::char_select::ClientState;
|
||||
let _ = benilla_formats::wow_data_or_skip!();
|
||||
let screens = |cvars: crate::cvars::Cvars| {
|
||||
let mut world = World::new();
|
||||
world.insert_resource(super::super::glue::GlueStock(true));
|
||||
world.insert_resource(State::new(ClientState::Login));
|
||||
world.insert_resource(cvars);
|
||||
world.init_resource::<super::super::glue_mgr::GlueMgr>();
|
||||
setup_script(&mut world);
|
||||
world
|
||||
.non_send_resource::<UiScript>()
|
||||
.eval::<(bool, bool, bool)>(
|
||||
"return TOSFrame:IsShown() == 1, AccountLoginUI:IsShown() == 1, \
|
||||
AccountLoginAccountEdit:IsVisible() == 1",
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let mut accepted = crate::cvars::Cvars::with_value("readEULA", "1");
|
||||
accepted.set("readTOS", "1");
|
||||
assert_eq!(
|
||||
screens(accepted),
|
||||
(false, true, true),
|
||||
"accepted agreements go straight to the login boxes"
|
||||
);
|
||||
assert_eq!(
|
||||
screens(crate::cvars::Cvars::default()),
|
||||
(true, false, false),
|
||||
"a first launch is asked for the EULA"
|
||||
);
|
||||
}
|
||||
|
||||
/// Under `WOW_GLUE_STOCK=1` the world holds the glue VM where it holds a boot VM, at startup
|
||||
/// on the login screen (`0x402b65`) and back from the world on the screen the exit enters
|
||||
/// (`0x402090`); a `ReloadUI()` never builds the glue.
|
||||
|
||||
@@ -22,6 +22,8 @@ pub(crate) mod glue;
|
||||
pub(crate) mod glue_mgr;
|
||||
mod input;
|
||||
#[cfg(test)]
|
||||
pub(crate) use input::feed_keys;
|
||||
#[cfg(test)]
|
||||
mod layer_tests;
|
||||
mod load_log;
|
||||
mod manifest;
|
||||
@@ -315,7 +317,13 @@ impl Plugin for UiScriptPlugin {
|
||||
.init_resource::<UiClock>()
|
||||
.init_resource::<AddOnIdentity>()
|
||||
// After `AssetSet::Open`: the VM's first load is the chain's `GlobalStrings.lua`.
|
||||
.add_systems(Startup, setup_script.after(benilla_assets::AssetSet::Open))
|
||||
.add_systems(
|
||||
Startup,
|
||||
// After the CVars load: the glue screens read the agreements as they come up.
|
||||
setup_script
|
||||
.after(benilla_assets::AssetSet::Open)
|
||||
.after(crate::cvars::CvarLoad),
|
||||
)
|
||||
// The in-game UI loads on world entry, as the reference's does, once the loading cover
|
||||
// has presented, so its ~0.5 s never stalls the cover's first frame.
|
||||
.add_systems(
|
||||
@@ -721,6 +729,10 @@ pub(crate) fn load_ui_for_test(script: &benilla_ui::script::UiScript, entry: &st
|
||||
|
||||
#[cfg(test)]
|
||||
mod cinematic_tests;
|
||||
#[cfg(test)]
|
||||
mod glue_input_tests;
|
||||
#[cfg(test)]
|
||||
mod key_walk_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod cast_tests;
|
||||
|
||||
@@ -178,6 +178,9 @@ mod tests {
|
||||
("cursor.rs", "last_ptr"),
|
||||
// The wheel's fraction toward its next notch, a fact about the device, not the VM.
|
||||
("bindings.rs", "notches"),
|
||||
// The keys believed down, a fact about the keyboard; an alt-tab wipes it by reconcile.
|
||||
("bindings.rs", "down"),
|
||||
("ui_script/input.rs", "pressed"),
|
||||
];
|
||||
|
||||
use crate::test_support::{fn_parameter_lists, rust_files};
|
||||
|
||||
Reference in New Issue
Block a user