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:
2026-07-31 06:57:50 -04:00
parent 17e6e04c79
commit 1ddc64010a
4 changed files with 287 additions and 0 deletions

View File

@@ -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);
}