Firing KEY_DOWN in its own pass ate every key before it reached a binding

A `MessageReader` advances its own cursor, so the first `keyboard.read()` in a
system run takes the messages and every later one answers nothing. The
`KEY_DOWN`/`KEY_UP` producer added in the previous commit read the stream in its
own loop ahead of the binding walk, so the walk saw an empty stream and no key
ever latched, resolved or ran a binding. No error, no log line: the keyboard
simply stops.

It is gated on a listener, so it only bites a player whose addons register one
of those events — which is everyone with SuperCleveRoidMacros installed
(`Core.lua:4911-4912` registers both). grom: *"Keyboard is not working in game
right now for some reason, launched thrice to no avail."*

The whole system now reads the stream exactly once into a buffer and its three
consumers iterate that: the bind-capture seam, the key events, and the binding
walk. That also fixes a latent version of the same thing — the capture seam was
already draining the stream, so the release half its own comment promises
("releases still unlatch below so a key held across the arm cannot stick") could
not run.

The regression test asserts both halves, because either one alone passes with
the other broken: the events fire AND the bound key still runs its binding. It
fails on exactly the second assertion with the bug restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-06 09:53:11 -07:00
co-authored by Claude Opus 5
parent 4974326381
commit 15e1ba1fcd
+92 -3
View File
@@ -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<KeyboardInput> = 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<NonSendMut<UiScript>>, lua: &str, tag: &str| {
@@ -474,7 +482,7 @@ fn latch_and_dispatch(
// Super press is ignored outright.
if armed {
let mut captured: Option<String> = 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(&registry_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(&registry_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.
///