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:
@@ -23,10 +23,20 @@ cmake --build build --parallel --target sidescroller
|
||||
./build/examples/sidescroller/sidescroller
|
||||
```
|
||||
|
||||
Arrow keys to run, space to jump, and holding space longer jumps higher. Three flags exist
|
||||
for the smoke test and are useful by hand too: `--assets DIR` points somewhere other than
|
||||
the compiled-in asset directory, `--frames N` (or `AKGL_SIDESCROLLER_FRAMES`) exits after
|
||||
that many frames, and `--autoplay` drives the player from a script instead of the keyboard.
|
||||
### Controls
|
||||
|
||||
| Key | Gamepad | Does |
|
||||
|---|---|---|
|
||||
| Left arrow | D-pad left | Run left |
|
||||
| Right arrow | D-pad right | Run right |
|
||||
| Space | South button (A) | Jump — hold it longer to jump higher |
|
||||
|
||||
Close the window to quit; there is no key bound to it.
|
||||
|
||||
Three flags exist for the smoke test and are useful by hand too: `--assets DIR` points
|
||||
somewhere other than the compiled-in asset directory, `--frames N` (or
|
||||
`AKGL_SIDESCROLLER_FRAMES`) exits after that many frames, and `--autoplay` drives the
|
||||
player from a script instead of the keyboard.
|
||||
|
||||
## The level
|
||||
|
||||
@@ -745,22 +755,37 @@ set_tests_properties(example_sidescroller PROPERTIES
|
||||
```
|
||||
|
||||
Four seconds of scripted play under the headless drivers. `--autoplay` holds *right* and
|
||||
requests a jump every forty-five frames by calling the handlers a keyboard would have
|
||||
called:
|
||||
taps jump every forty-five frames, and it does it by pushing synthetic events through
|
||||
`akgl_controller_handle_event` rather than by calling the handlers:
|
||||
|
||||
```c excerpt=examples/sidescroller/player.c
|
||||
if ( frame == 1 ) {
|
||||
PASS(errctx, akgl_actor_cmhf_right_on(ss_game.player, &synthetic));
|
||||
}
|
||||
if ( (frame % SS_AUTOPLAY_JUMP_PERIOD) == 0 ) {
|
||||
ss_game.jump_requested = true;
|
||||
synthetic.type = SDL_EVENT_KEY_DOWN;
|
||||
synthetic.key.which = SS_AUTOPLAY_KBID;
|
||||
synthetic.key.key = SDLK_RIGHT;
|
||||
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &synthetic));
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
AKGL_BITMASK_HAS(ss_game.player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
||||
AKGL_ERR_BEHAVIOR,
|
||||
"the right arrow did not reach the player; the control map matched nothing"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
That works because a control-map handler takes the actor and an event, and the keyboard
|
||||
handlers do not read the event beyond requiring one to be there. In four seconds the script
|
||||
walks the level, jumps, collides with terrain, collects a coin, falls in the pit and
|
||||
respawns — so the smoke test exercises the collision and the pickup, not just the startup.
|
||||
**That distinction is the whole value of the smoke test, and the first version got it
|
||||
wrong.** Calling the handlers directly is easier and it works — but it skips the binding
|
||||
table, which is the part with something to get wrong. This game shipped with a control map
|
||||
that matched no keyboard on earth, and a smoke test that called the handlers passed anyway.
|
||||
A test that cannot fail is not a test, so the script drives the real dispatch and then
|
||||
asserts the press actually arrived. Break the binding and the run exits non-zero naming the
|
||||
reason.
|
||||
|
||||
`SS_AUTOPLAY_KBID` is deliberately not `0`: the map binds keyboard `0` meaning *any*
|
||||
keyboard, and driving it from a non-zero device id is what proves that. In four seconds the
|
||||
script walks the level, jumps, collides with terrain, collects a coin, falls in the pit and
|
||||
respawns — so the smoke test exercises the input path, the collision and the pickup, not
|
||||
just the startup.
|
||||
|
||||
A run ends with a line naming what happened, which is worth reading when a change moves the
|
||||
feel:
|
||||
|
||||
Reference in New Issue
Block a user