diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index 6bd1e9dd..d227f553 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -458,6 +458,14 @@ fn latch_and_dispatch( // affordances: a player build keeps the reference's `CTRL-SHIFT-P` fallback to `SHIFT-P`. let dev_plane = ctrl && shift && !alt && crate::run_mode::dev_affordances(); + // **This system's one read of the keyboard stream.** A `MessageReader` advances its own + // cursor, so a second `keyboard.read()` in the same run answers nothing — and this body has + // three consumers (the capture seam, nampower's `KEY_DOWN`/`KEY_UP`, and the binding walk + // itself). Reading once into a buffer is what keeps them independent; draining it in the first + // of them is how the whole keyboard goes dead, with no error anywhere, for every player whose + // addons happen to register one of those events. + let key_msgs: Vec = keyboard.read().cloned().collect(); + let mut script = script; let armed = script.as_ref().is_some_and(|s| s.bind_capture_armed()); let run_lua = |script: &mut Option>, lua: &str, tag: &str| { @@ -474,7 +482,7 @@ fn latch_and_dispatch( // Super press is ignored outright. if armed { let mut captured: Option = None; - for ev in keyboard.read() { + for ev in &key_msgs { if ev.state != ButtonState::Pressed || ev.repeat || sup { continue; } @@ -538,7 +546,7 @@ fn latch_and_dispatch( .is_some_and(|s| s.has_listener("KEY_DOWN") || s.has_listener("KEY_UP")); if key_events { let mut pending: Vec<(bool, i32, bool)> = Vec::new(); - for ev in keyboard.read() { + for ev in &key_msgs { let code = key_event_code(ev.key_code); let repeat = ev.state == ButtonState::Pressed && state.down.contains(&chord::normalize_key(ev.key_code)); @@ -562,7 +570,7 @@ fn latch_and_dispatch( } } } - for ev in keyboard.read() { + for ev in &key_msgs { let key = chord::normalize_key(ev.key_code); match ev.state { ButtonState::Pressed => { @@ -969,6 +977,87 @@ mod tests { .expect("eval") } + /// **A frame registered for `KEY_DOWN` must not cost the player their keyboard.** + /// + /// nampower's key events and the binding walk are two consumers of one `MessageReader`, and a + /// reader advances its own cursor: the first `keyboard.read()` in a system run takes the + /// messages and every later one answers nothing. So firing the events in their own pass ate + /// every key before it could reach a binding — no error, no log line, just a dead keyboard + /// for anyone whose addons register either event. `SuperCleveRoidMacros/Core.lua:4911-4912` + /// registers both, so that was everyone with it installed. + /// + /// Both halves are asserted on purpose: the events must fire *and* the binding must still + /// run. Either one alone passes with the other broken. + #[test] + fn key_events_do_not_eat_the_keys_the_bindings_need() { + let mut script = UiScript::new().expect("VM"); + script.register_bindings(®istry_commands()); + script + .run( + "KEYS = 0 EDGE = 0 \ + function ToggleGameMenuProbe() end \ + local f = CreateFrame(\"Frame\") \ + f:RegisterEvent(\"KEY_DOWN\") \ + f:RegisterEvent(\"KEY_UP\") \ + f:SetScript(\"OnEvent\", function() KEYS = KEYS + 1 KEY_ARGS = arg1 end) \ + function MultiActionButtonDown() EDGE = EDGE + 1 end \ + SetBinding(\"J\", \"MULTIACTIONBAR1BUTTON1\")", + ) + .expect("bind"); + let mut app = vm_harness(script); + assert_eq!(lua_count(&app, "KEYS"), 0); + + press_key(&mut app, KeyCode::KeyJ); + app.update(); + assert_eq!(lua_count(&app, "KEYS"), 1, "KEY_DOWN fired"); + assert_eq!( + lua_count(&app, "KEY_ARGS"), + 74, + "arg1 is the client's code for J" + ); + assert_eq!( + lua_count(&app, "EDGE"), + 1, + "and the binding still ran — the event pass must not drain the stream" + ); + + release_key(&mut app, KeyCode::KeyJ); + app.update(); + assert_eq!(lua_count(&app, "KEYS"), 2, "KEY_UP fired too"); + } + + /// The modifier keys report as nampower numbers them (0 Shift, 1 Ctrl, 2 Alt), outside every + /// bank — which is how its one consumer skips them (`CleveRoids.Frame:KEY_DOWN`). + #[test] + fn the_modifier_keys_report_their_own_codes() { + let mut script = UiScript::new().expect("VM"); + script.register_bindings(®istry_commands()); + script + .run( + "CODES = \"\" \ + local f = CreateFrame(\"Frame\") \ + f:RegisterEvent(\"KEY_DOWN\") \ + f:SetScript(\"OnEvent\", function() \ + CODES = CODES .. tostring(arg1) .. \":\" .. tostring(arg2) .. \" \" end)", + ) + .expect("probe"); + let mut app = vm_harness(script); + + press_key(&mut app, KeyCode::ShiftLeft); + app.update(); + press_key(&mut app, KeyCode::KeyA); + app.update(); + // Shift reports code 0 with its own bit **already set** — the button plane is written + // before `Update`, so the modifier a press is reporting is held by the time it reports. + // `A` then reports 65 with the same bit, which is the pair a `[mod:shift]` conditional + // is written against. + assert_eq!( + lua_str(&app, "CODES"), + "0:1 65:1 ", + "code:metaKeyState pairs" + ); + } + /// A **chorded** registry `EdgeUpDown` runs both halves, which is what an action button on a /// modified key is. ///