Let a caller take keystrokes without owning the event loop
akbasic's GET and GETKEY ask "is there a keystroke waiting, yes or no" and must not require the interpreter to pump SDL events itself. akgl_controller_poll_key drains one key per call from a fixed 32-entry ring that akgl_controller_handle_event fills, so the host keeps pumping and the embedded interpreter reads at its own pace. akgl_controller_flush_keys discards a backlog. The recording happens before the control-map scan, not after: that scan returns as soon as a binding claims the event, so recording afterwards would have dropped exactly the keys a game also acts on. A full buffer refuses the newest keystroke rather than overwriting the oldest, so a caller reading a line gets what was typed first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
TODO.md
13
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
|
synthetic `SDL_EVENT_KEY_DOWN` events through the existing handler and drain them, plus the
|
||||||
empty-buffer and overflow cases.
|
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
|
## Carried over
|
||||||
|
|
||||||
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
||||||
|
|||||||
@@ -13,6 +13,16 @@
|
|||||||
#define AKGL_MAX_CONTROL_MAPS 8
|
#define AKGL_MAX_CONTROL_MAPS 8
|
||||||
#define AKGL_MAX_CONTROLS 32
|
#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. */
|
/** @brief Maps one SDL input to pressed and released callbacks. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t event_on;
|
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.
|
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
|
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_
|
#endif // _CONTROLLER_H_
|
||||||
|
|||||||
@@ -12,6 +12,39 @@
|
|||||||
|
|
||||||
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
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)
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
||||||
{
|
{
|
||||||
int count;
|
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, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
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 {
|
ATTEMPT {
|
||||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||||
curmap = &GAME_ControlMaps[i];
|
curmap = &GAME_ControlMaps[i];
|
||||||
@@ -386,3 +426,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
|||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -524,6 +524,164 @@ akerr_ErrorContext *test_controller_device_enumeration(void)
|
|||||||
SUCCEED_RETURN(e);
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -551,6 +709,8 @@ int main(void)
|
|||||||
CATCH(errctx, test_controller_gamepad_button_handlers());
|
CATCH(errctx, test_controller_gamepad_button_handlers());
|
||||||
CATCH(errctx, test_controller_device_events());
|
CATCH(errctx, test_controller_device_events());
|
||||||
CATCH(errctx, test_controller_device_enumeration());
|
CATCH(errctx, test_controller_device_enumeration());
|
||||||
|
CATCH(errctx, test_controller_poll_key());
|
||||||
|
CATCH(errctx, test_controller_poll_key_overflow());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user