diff --git a/TODO.md b/TODO.md index 00bc591..0eee623 100644 --- a/TODO.md +++ b/TODO.md @@ -708,6 +708,19 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha synthetic `SDL_EVENT_KEY_DOWN` events through the existing handler and drain them, plus the empty-buffer and overflow cases. + **Resolved.** `akgl_controller_poll_key(int *keycode, bool *available)` and + `akgl_controller_flush_keys(void)` are in `include/akgl/controller.h`, over a + fixed `AKGL_CONTROLLER_KEY_BUFFER` (32) ring in `src/controller.c`. + `akgl_controller_handle_event` records every `SDL_EVENT_KEY_DOWN` *before* it + scans the control maps, so a key bound to an actor still reaches a polling + caller — the scan returns as soon as a binding claims the event, and doing it + afterwards would have lost exactly the keys a game also acts on. An empty + buffer is success with `available` false, not an error. A full buffer drops + the newest key rather than overwriting the oldest, matching the Commodore + keyboard buffer and keeping what was typed first. `tests/controller.c` covers + drain order, the release-is-not-a-keystroke case, a key shared with a control + map, flush, both NULL arguments, and overflow plus reuse afterwards. + ## Carried over 1. **Make character-to-sprite state bindings release their references symmetrically.** diff --git a/include/akgl/controller.h b/include/akgl/controller.h index 8b8fb99..bb6d6c2 100644 --- a/include/akgl/controller.h +++ b/include/akgl/controller.h @@ -13,6 +13,16 @@ #define AKGL_MAX_CONTROL_MAPS 8 #define AKGL_MAX_CONTROLS 32 +/** + * @brief Keystrokes akgl_controller_handle_event() will hold for a poller. + * + * The Commodore keyboard buffer this serves held ten. Thirty-two is enough that + * a program which polls once per frame never loses a key to a fast typist, and + * small enough that the buffer stays a fixed-size object in the library's data + * segment. + */ +#define AKGL_CONTROLLER_KEY_BUFFER 32 + /** @brief Maps one SDL input to pressed and released callbacks. */ typedef struct { uint32_t event_on; @@ -117,4 +127,39 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); + +/** + * @brief Take the oldest waiting keystroke, if there is one. + * + * The rest of this header is built around the host pumping SDL events into + * akgl_controller_handle_event(), which suits a game loop and does not suit an + * embedded interpreter asking "is there a key waiting, yes or no" without + * owning the event loop itself. Every key press that reaches + * akgl_controller_handle_event() is recorded in a fixed ring buffer first, + * whether or not a control map claims it, and this drains that buffer one + * keystroke per call. + * + * When no key is waiting the call still succeeds: @p available is set to + * `false` and @p keycode to 0. The caller polls, it does not block. + * + * A full buffer drops the *newest* keystroke rather than the oldest, so what + * was typed first is what is read first. This runs on whichever thread pumps + * events; it is not synchronized. + * + * @param keycode Output destination populated with the SDL keycode, or 0. + * @param available Output destination set to `true` when a keystroke was taken. + * @return `NULL` on success, otherwise an error context owned by the caller. + * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available); + +/** + * @brief Discard every keystroke waiting in the buffer. + * + * For a caller that has been ignoring input and does not want a backlog acted + * on the moment it starts polling again. + * + * @return `NULL` on success, otherwise an error context owned by the caller. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void); #endif // _CONTROLLER_H_ diff --git a/src/controller.c b/src/controller.c index 2cca3f5..e05ae22 100644 --- a/src/controller.c +++ b/src/controller.c @@ -12,6 +12,39 @@ akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS]; +/* + * Keystrokes waiting for akgl_controller_poll_key(). Filled by + * akgl_controller_handle_event() before it consults the control maps, so a key + * that also drives an actor is still delivered to a polling caller. + * + * head is the next slot to read, count is how many are waiting. Both index a + * fixed array rather than a queue object, which is the whole point: a host that + * never polls cannot make this grow. + */ +static SDL_Keycode keybuffer[AKGL_CONTROLLER_KEY_BUFFER]; +static int keybuffer_head = 0; +static int keybuffer_count = 0; + +/** + * @brief Record one keystroke, discarding it if the buffer is already full. + * + * Dropping the newest rather than overwriting the oldest is deliberate. A + * caller reading a line of input wants the characters that were typed first; + * overwriting would hand it the tail of what the user typed and silently lose + * the head. + */ +static void keybuffer_push(SDL_Keycode key) +{ + int tail = 0; + + if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) { + return; + } + tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER; + keybuffer[tail] = key; + keybuffer_count += 1; +} + akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void) { int count; @@ -65,6 +98,13 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate"); FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event"); + // Before the control maps, not after: a key bound to an actor is still a + // key an interpreter polling with akgl_controller_poll_key() wants to see, + // and the scan below returns as soon as a binding claims the event. + if ( event->type == SDL_EVENT_KEY_DOWN ) { + keybuffer_push(event->key.key); + } + ATTEMPT { for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) { curmap = &GAME_ControlMaps[i]; @@ -386,3 +426,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha } PROCESS(errctx) { } FINISH(errctx, true); } + +akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination"); + FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination"); + + if ( keybuffer_count == 0 ) { + // An empty buffer is the ordinary case, not a failure: the caller is + // asking whether a key is waiting, and the answer is no. + *keycode = 0; + *available = false; + SUCCEED_RETURN(errctx); + } + + *keycode = (int)keybuffer[keybuffer_head]; + *available = true; + keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER; + keybuffer_count -= 1; + SUCCEED_RETURN(errctx); +} + +akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void) +{ + PREPARE_ERROR(errctx); + keybuffer_head = 0; + keybuffer_count = 0; + SUCCEED_RETURN(errctx); +} diff --git a/tests/controller.c b/tests/controller.c index 4a3e0e7..bfd8186 100644 --- a/tests/controller.c +++ b/tests/controller.c @@ -524,6 +524,164 @@ akerr_ErrorContext *test_controller_device_enumeration(void) SUCCEED_RETURN(e); } +akerr_ErrorContext *test_controller_poll_key(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + int keycode = -1; + bool available = true; + + ATTEMPT { + reset_control_maps(); + CATCH(e, make_player()); + CATCH(e, akgl_controller_flush_keys()); + + // An empty buffer answers "no key waiting" and succeeds. + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), + "polling an empty key buffer"); + TEST_ASSERT(e, available == false, + "polling an empty buffer reported a key was available"); + TEST_ASSERT(e, keycode == 0, + "polling an empty buffer left keycode at %d, expected 0", keycode); + + // A key press pumped through the handler is drained by the poller, + // exactly once. + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a key press with no control maps installed"); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key"); + TEST_ASSERT(e, available == true, "the pressed key was not available to the poller"); + TEST_ASSERT(e, keycode == SDLK_A, + "the poller returned keycode %d, expected %d", keycode, (int)SDLK_A); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again"); + TEST_ASSERT(e, available == false, "the same keystroke was delivered twice"); + + // Key releases are not keystrokes. + make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a key release"); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release"); + TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke"); + + // Keys come back in the order they were pressed. + CATCH(e, akgl_controller_flush_keys()); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1); + CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2); + CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3); + CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); + + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key"); + TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d", + keycode, (int)SDLK_1); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key"); + TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d", + keycode, (int)SDLK_2); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key"); + TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d", + keycode, (int)SDLK_3); + + // A key that a control map also acts on still reaches the poller, so a + // game and an embedded interpreter can share one keyboard. + CATCH(e, akgl_controller_flush_keys()); + CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID)); + 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 bound key press"); + TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), + "the bound key stopped driving the actor (state %d)", player->state); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), + "polling for a key a control map claimed"); + TEST_ASSERT(e, available == true, "a bound key never reached the key buffer"); + TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d", + keycode, (int)SDLK_LEFT); + + // Flushing discards the backlog. + CATCH(e, akgl_controller_flush_keys()); + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B); + CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); + TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it"); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush"); + TEST_ASSERT(e, available == false, "a flush left a keystroke behind"); + + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available), + "polling into a NULL keycode"); + TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL), + "polling into a NULL availability flag"); + } CLEANUP { + reset_control_maps(); + IGNORE(akgl_controller_flush_keys()); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext *test_controller_poll_key_overflow(void) +{ + PREPARE_ERROR(e); + SDL_Event event; + int keycode = -1; + bool available = true; + bool ordered = true; + bool pumped = true; + int i = 0; + + ATTEMPT { + reset_control_maps(); + CATCH(e, akgl_controller_flush_keys()); + + // Fill the buffer exactly, then press one more. The overflowing key is + // the one that is dropped -- what was typed first survives. + for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) { + akerr_ErrorContext *pumpresult = NULL; + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i); + pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event); + if ( pumpresult != NULL ) { + pumpresult->handled = true; + pumpresult = akerr_release_error(pumpresult); + pumped = false; + } + } + TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed"); + + for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) { + akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available); + if ( pollresult != NULL ) { + pollresult->handled = true; + pollresult = akerr_release_error(pollresult); + ordered = false; + } + TEST_ASSERT_FLAG(ordered, available == true); + TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i)); + } + TEST_ASSERT(e, ordered == true, + "a full buffer did not return the first %d keys in order", + AKGL_CONTROLLER_KEY_BUFFER); + + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), + "polling after draining a full buffer"); + TEST_ASSERT(e, available == false, + "the key pressed past capacity was buffered anyway (keycode %d)", keycode); + + // The buffer is reusable after an overflow rather than wedged. + make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z); + TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), + "dispatching a key after an overflow"); + TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), + "polling after an overflow"); + TEST_ASSERT(e, available == true, "the buffer stayed full after being drained"); + TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d", + keycode, (int)SDLK_Z); + } CLEANUP { + reset_control_maps(); + IGNORE(akgl_controller_flush_keys()); + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + int main(void) { PREPARE_ERROR(errctx); @@ -551,6 +709,8 @@ int main(void) CATCH(errctx, test_controller_gamepad_button_handlers()); CATCH(errctx, test_controller_device_events()); CATCH(errctx, test_controller_device_enumeration()); + CATCH(errctx, test_controller_poll_key()); + CATCH(errctx, test_controller_poll_key_overflow()); } CLEANUP { SDL_Quit(); } PROCESS(errctx) {