Let a control map listen to any keyboard, and fix the dead arrow keys
The sidescroller's controls did nothing. `akgl_controller_handle_event` matched `event->key.which == curmap->kbid` exactly, with no way to say "whatever keyboard the player is typing on", so the game did the obvious thing and bound `SDL_GetKeyboards()[0]`. That cannot work. The id a key event *carries* is chosen by the video backend and is not required to be an id `SDL_GetKeyboards()` reports. On X11 without XInput2 every key event carries `SDL_GLOBAL_KEYBOARD_ID`, which is 0, while `SDL_AddKeyboard` registers the attached keyboard as `SDL_DEFAULT_KEYBOARD_ID`, which is 1; with XInput2 the events carry the physical slave device's `sourceid` while `SDL_GetKeyboards()[0]` may be the master. Either way the map matched nothing and every key press was dropped. A `kbid` or `jsid` of 0 now matches any device of that kind. 0 is safe to spend: SDL documents `which` as 0 when the source is unknown or virtual, and joystick ids start at 1, so no real device is 0. A non-zero id still matches only that device, which is what keeps two local players on two keyboards apart, and there is a test for that half too. `util/charviewer.c` already passed 0 and depended on the old behaviour by accident; it works under XInput2 now as well. Two reasons this shipped, both closed: - Every test in tests/controller.c dispatched an event whose id equalled the id the map was bound with, so none of them could see it. - The sidescroller's smoke test called the control handlers directly instead of dispatching events, so it passed against a control map that matched nothing. It now pushes synthetic events through akgl_controller_handle_event from a deliberately non-zero device id and fails if the press does not arrive. Verified by breaking the binding and watching the run exit non-zero. `test_controller_wildcard_device_ids` was written first and failed against the unfixed library with "a map bound to keyboard 0 ignored a key press from device 11", which is the whole reason to trust it now that it passes. The controls were documented, in one sentence. Chapter 19 has a proper table of them, and chapter 15 explains why not to reach for SDL_GetKeyboards(). The header said the match was exact and now says what it does. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -372,6 +372,74 @@ akerr_ErrorContext *test_controller_handle_gamepad_events(void)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* A control map bound to keyboard 0 hears every keyboard.
|
||||
*
|
||||
* There is no way to enumerate "the keyboard the player will actually type on".
|
||||
* SDL_GetKeyboards() reports what is attached, but the id a key event carries is
|
||||
* decided by the video backend: X11 without XInput2 sends
|
||||
* SDL_GLOBAL_KEYBOARD_ID, which is 0, while X11 *with* XInput2 sends the
|
||||
* physical slave device's sourceid -- and neither is the SDL_DEFAULT_KEYBOARD_ID
|
||||
* of 1 that SDL_AddKeyboard registers when XInput2 is missing. A game that binds
|
||||
* SDL_GetKeyboards()[0] therefore gets a map that matches nothing at all.
|
||||
*
|
||||
* 0 is the right spelling for "any", because SDL documents `which` as 0 when the
|
||||
* source is unknown or virtual, so no real device can be 0 and nothing is given
|
||||
* up by spending it. The same argument applies to a joystick id of 0.
|
||||
*
|
||||
* Every other test in this suite dispatches an event whose id equals the id the
|
||||
* map was bound with, so none of them can see this.
|
||||
*/
|
||||
akerr_ErrorContext *test_controller_wildcard_device_ids(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
// 0 for both: "whatever keyboard and whatever gamepad".
|
||||
CATCH(e, akgl_controller_default(0, "player", 0, 0));
|
||||
|
||||
// A key event from a real XInput2 device id must still be heard.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a left key press from a real device id");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"a map bound to keyboard 0 ignored a key press from device %d (state %d)",
|
||||
TEST_KBID, player->state);
|
||||
|
||||
// And so must one carrying SDL_GLOBAL_KEYBOARD_ID, which is 0.
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, 0, SDLK_RIGHT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a right key press from the global keyboard");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
"a map bound to keyboard 0 ignored a key press from keyboard 0 (state %d)",
|
||||
player->state);
|
||||
|
||||
/*
|
||||
* A map bound to a *specific* keyboard still hears only that one. This is
|
||||
* the half that keeps two local players on two keyboards apart, and the
|
||||
* wildcard must not cost it.
|
||||
*/
|
||||
reset_control_maps();
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a left key press from the wrong device");
|
||||
TEST_ASSERT(e, (player->state == 0),
|
||||
"a map bound to keyboard %d acted on a press from %d (state %d)",
|
||||
TEST_KBID, (TEST_KBID + 1), player->state);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_handle_event_edge_cases(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
@@ -872,6 +940,7 @@ int main(void)
|
||||
CATCH(errctx, test_controller_default_bindings());
|
||||
CATCH(errctx, test_controller_handle_keyboard_events());
|
||||
CATCH(errctx, test_controller_handle_gamepad_events());
|
||||
CATCH(errctx, test_controller_wildcard_device_ids());
|
||||
CATCH(errctx, test_controller_handle_event_edge_cases());
|
||||
CATCH(errctx, test_controller_gamepad_button_handlers());
|
||||
CATCH(errctx, test_controller_device_events());
|
||||
|
||||
Reference in New Issue
Block a user