2026-08-02 07:15:25 -04:00
# 16. Input
Add the manual: nineteen chapters and a corrected README
docs/ is a narrative manual, not a second reference. Every header already
carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED
with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was
navigation and worked examples. Chapters teach a task and link to the Doxygen
output; where a declaration or a constant table has to be in front of the
reader it arrives as a `c excerpt=` block, so the text *is* the header and
cannot diverge from it. That also preserves the hand-aligned bit-flag tables
scripts/reindent.el goes out of its way not to destroy.
The manual does not re-document its dependencies. libakerror owns the
ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events,
Tiled owns the map format, jansson owns json_t. A chapter that restated any of
them would be wrong the day upstream changed and nothing here would notice --
the same drift this work exists to fix, arriving from a different direction. So
each chapter says what libakgl adds or constrains and links out for the rest.
Chapter 4 is the exception and the reason for it: libakerror documents the
mechanism, but only libakgl can say which statuses its own functions raise and
what they mean here, and that was written down nowhere. It carries three tables
-- libakgl's five status codes, the libakerror statuses libakgl actually raises
with their meaning in this library, and the exit-status trap where
`exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256.
Every chapter was written against src/ rather than against the header comments,
which is how 27 false claims in those comments came to light. Where a chapter
documents a known defect rather than a design decision it says so and points at
TODO.md.
README.md keeps the development process and hands the reader to docs/. Its
task-oriented FAQ is deleted rather than moved, because one source of truth per
topic is the whole point and that FAQ's examples did not compile.
Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run
through the real loaders, one linked-and-executed program with its output
compared byte for byte, one generated figure. 11 norun blocks, each justified.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
libakgl does not have an input system so much as a * binding * system. SDL3 owns events,
keycodes, keymods, gamepad enumeration and text composition, and it documents all of them
in the [SDL3 wiki ](https://wiki.libsdl.org/SDL3/ ) — start with
[SDL_Event ](https://wiki.libsdl.org/SDL3/SDL_Event ),
[SDL_Keycode ](https://wiki.libsdl.org/SDL3/SDL_Keycode ) and
[CategoryGamepad ](https://wiki.libsdl.org/SDL3/CategoryGamepad ). Nothing here replaces any
of it.
What libakgl adds is two things: a table that says *"when event X arrives from device Y
carrying key or button Z, call this handler on this actor"*, and a small ring buffer of
keystrokes for a caller who wants to ask "is there a key waiting" without owning the event
loop.
## Input is pushed, not polled
**The host pumps every SDL event into `akgl_controller_handle_event` .** There is no
`akgl_input_update()` that reads the keyboard state for you, and there is no internal event
loop. Whatever drains the SDL event queue — your `while ( SDL_PollEvent(&e) )` , or SDL's
`SDL_AppEvent` callback — hands each event over, unconditionally.
Unconditionally is the point: **an event nothing binds is not an error. ** It returns
success having done nothing, which is what makes the blanket call correct and keeps the
host from having to know which events matter.
```c
#include <SDL3/SDL.h>
#include <akgl/actor.h>
#include <akgl/controller.h>
#include <akgl/error.h>
/*
* Bind WASD on top of control map 0, which akgl_controller_default has already
* filled with the four arrow keys and the four D-pad directions.
*
* Bindings are appended and scanned in push order, and the first match stops
* the scan -- so these fire only for keys the eight defaults do not claim.
* There is no way to remove one; zeroing akgl_controlmaps[id] is the only reset.
*/
akerr_ErrorContext AKERR_NOIGNORE *bind_wasd(int mapid, SDL_KeyboardID kbid)
{
akgl_Control control;
PREPARE_ERROR(errctx);
PASS(errctx, akgl_controller_default(mapid, "player", kbid, 0));
/* Copied in by value, so one stack local does for every push. */
SDL_memset(&control, 0x00, sizeof(akgl_Control));
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.key = SDLK_A;
control.handler_on = &akgl_actor_cmhf_left_on;
control.handler_off = &akgl_actor_cmhf_left_off;
PASS(errctx, akgl_controller_pushmap(mapid, &control));
control.key = SDLK_D;
control.handler_on = &akgl_actor_cmhf_right_on;
control.handler_off = &akgl_actor_cmhf_right_off;
PASS(errctx, akgl_controller_pushmap(mapid, &control));
SUCCEED_RETURN(errctx);
}
/* Every event, unconditionally. An event nothing binds is success, not a
* failure, which is what makes the unconditional call correct. */
akerr_ErrorContext AKERR_NOIGNORE *pump(void *appstate, SDL_Event *event)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_controller_handle_event(appstate, event));
SUCCEED_RETURN(errctx);
}
```
## Two independent paths out of one call
`akgl_controller_handle_event` does two unrelated things with the event it is given, and
**neither one suppresses the other**:
```text
akgl_controller_handle_event(appstate, event)
|
+----------------------+----------------------+
| |
(a) the keystroke ring, FIRST (b) the control maps, SECOND
| |
KEY_DOWN -> push {key, mod, ""} for map 0..7 (target != NULL):
TEXT_INPUT -> attach text to the for control 0..31, in push order:
newest press, or push event type == event_on/off ?
a text-only entry device id == kbid / jsid ?
| key/button == this binding ?
| -> call the handler and
akgl_controller_poll_key() STOP the entire scan
akgl_controller_poll_keystroke()
akgl_controller_flush_keys()
```
The ring is filled **first ** , before the maps are consulted, and * whether or not * a map
claims the key. A key that drives an actor is still a key an embedded interpreter polling
with `akgl_controller_poll_key()` wants to see.
### The control-map scan
```c excerpt=include/akgl/controller.h
/** @brief How many control maps exist -- effectively the local player limit. */
#define AKGL_MAX_CONTROL_MAPS 8
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
#define AKGL_MAX_CONTROLS 32
```
Eight maps means up to eight locally controlled players, each on its own keyboard and
gamepad. A map with a `NULL` `target` is skipped entirely — that is how an unused slot is
spelled.
**Maps are scanned in index order, bindings within a map in push order, and the first
match wins and stops the whole scan.** A key bound in two maps only ever fires in the
lower-numbered one. A key bound twice in one map only fires on the earlier push. This is
what makes `akgl_controller_default` followed by your own pushes behave sanely: the
defaults are already in slots 0– 7 and your additions land after them.
A match requires **all three ** of: the event type equals the binding's `event_on` or
`event_off` ; the device id on the event equals the map's `kbid` (keyboard events) or `jsid`
(gamepad events); and the key or button equals the binding's. The device-id half is what
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>
2026-08-01 21:23:02 -04:00
keeps two players on two keyboards apart — `akgl_controller_list_keyboards` logs every
attached keyboard and its id, which is how you find the number to pass.
### Bind device 0 unless you mean a specific device
**A `kbid` or `jsid` of `0` matches every device of that kind**, and it is what a
single-player game wants.
Do not reach for `SDL_GetKeyboards()` to fill it in. That call reports what is * attached * ;
the id a key event actually * carries * is chosen by the video backend, and the two need not
agree. On X11 without XInput2 every key event carries `SDL_GLOBAL_KEYBOARD_ID` , which is
`0` , while SDL registers the attached keyboard as `SDL_DEFAULT_KEYBOARD_ID` , which is `1` .
With XInput2 the events carry the physical slave device's `sourceid` and
`SDL_GetKeyboards()[0]` may be the master. Binding `SDL_GetKeyboards()[0]` therefore gives
you a map that matches nothing at all, and controls that silently do nothing.
2026-08-02 07:15:25 -04:00
That is not hypothetical — the sidescroller in [Chapter 20 ](20-tutorial-sidescroller.md )
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>
2026-08-01 21:23:02 -04:00
shipped exactly that bug, and its smoke test passed anyway because the test called the
handlers instead of dispatching events.
`0` is the right spelling for "any" because SDL documents `which` as `0` when the source is
unknown or virtual, and joystick ids start at 1 — so no real device is `0` and nothing is
given up by spending it.
Add the manual: nineteen chapters and a corrected README
docs/ is a narrative manual, not a second reference. Every header already
carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED
with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was
navigation and worked examples. Chapters teach a task and link to the Doxygen
output; where a declaration or a constant table has to be in front of the
reader it arrives as a `c excerpt=` block, so the text *is* the header and
cannot diverge from it. That also preserves the hand-aligned bit-flag tables
scripts/reindent.el goes out of its way not to destroy.
The manual does not re-document its dependencies. libakerror owns the
ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events,
Tiled owns the map format, jansson owns json_t. A chapter that restated any of
them would be wrong the day upstream changed and nothing here would notice --
the same drift this work exists to fix, arriving from a different direction. So
each chapter says what libakgl adds or constrains and links out for the rest.
Chapter 4 is the exception and the reason for it: libakerror documents the
mechanism, but only libakgl can say which statuses its own functions raise and
what they mean here, and that was written down nowhere. It carries three tables
-- libakgl's five status codes, the libakerror statuses libakgl actually raises
with their meaning in this library, and the exit-status trap where
`exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256.
Every chapter was written against src/ rather than against the header comments,
which is how 27 false claims in those comments came to light. Where a chapter
documents a known defect rather than a design decision it says so and points at
TODO.md.
README.md keeps the development process and hands the reader to docs/. Its
task-oriented FAQ is deleted rather than moved, because one source of truth per
topic is the whole point and that FAQ's examples did not compile.
Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run
through the real loaders, one linked-and-executed program with its output
compared byte for byte, one generated figure. 11 norun blocks, each justified.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
There is no way to remove a binding. `akgl_controller_pushmap` appends and that is the
whole API; the only reset is zeroing `akgl_controlmaps[id]` yourself. **Calling
`akgl_controller_default` twice on one map leaves sixteen bindings**, and the first eight
are the ones that fire.
### The keystroke ring
```c excerpt=src/controller.c
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
{
int tail = 0;
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
keybuffer_awaiting_text = false;
return;
}
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer[tail].key = key;
keybuffer[tail].mod = mod;
keybuffer[tail].text[0] = '\0';
keybuffer_count += 1;
keybuffer_awaiting_text = true;
}
```
Thirty-two entries (`AKGL_CONTROLLER_KEY_BUFFER` ), one fixed array in the library's data
segment, **one ring for the whole process ** — it is a file-scope static, not per map and
not per actor.
**A full buffer drops the newest keystroke, not the oldest.** A caller reading a line of
input wants the characters that were typed first; overwriting the head would hand it the
tail of what the user typed and silently lose the beginning.
Two pollers drain the same ring, and a keystroke taken by one is not waiting for the
other:
| Call | Hands back | Discards |
|---|---|---|
| `akgl_controller_poll_key(&keycode, &available)` | The SDL keycode, as an `int` | Entries carrying only composed text and no keycode — there is no key to report |
| `akgl_controller_poll_keystroke(&dest, &available)` | `key` , `mod` and the composed `text` | Nothing |
| `akgl_controller_flush_keys()` | — | Everything waiting |
An empty ring is success, not a failure: `available` comes back `false` and the caller
polls again next frame. **Check `available`, not the keycode against 0 ** — a text-only
entry legitimately has a keycode of 0.
**The `text` field needs `SDL_StartTextInput()` on your window.** SDL reports one keystroke
as two events — the key going down, and then the character it composed to, if it composed
to anything — and it only sends the second while text input is started. Without that call
`key` and `mod` still arrive and `text` is always the empty string. That is not a libakgl
limitation to work around; **it is the only correct way to get a character out of SDL ** , and
it is what makes a keyboard layout, a compose key, a dead key and an IME commit work. `"` ,
`!` , `(` , `)` , `:` and `;` are all unreachable from a keycode alone, and so is every
lower-case letter. See
[SDL_StartTextInput ](https://wiki.libsdl.org/SDL3/SDL_StartTextInput ).
An entry with composed text and **no ** keycode is not a degenerate case: an input method or
a dead key finishes a character with no key press of its own, and a line editor wants that
character. `akgl_controller_poll_keystroke` reports it; `akgl_controller_poll_key` cannot,
and drops it on the way past.
Text longer than `AKGL_CONTROLLER_KEYSTROKE_TEXT` (8 bytes) — an IME committing a whole
word at once — is truncated **on a code point boundary ** , never through the middle of a
UTF-8 sequence.
```c
#include <SDL3/SDL.h>
#include <akgl/controller.h>
#include <akgl/error.h>
/*
* Drain the keystroke ring into a line buffer, the way an embedded interpreter
* or a name-entry screen wants it.
*
* The `text` field is only populated while SDL text input is started on the
* window, so a caller that wants punctuation, lower case, dead keys or an IME
* commit calls SDL_StartTextInput() first. Without it `key` and `mod` still
* arrive and `text` is always empty.
*/
akerr_ErrorContext AKERR_NOIGNORE *read_line(char *dest, size_t destsize, bool *done)
{
akgl_Keystroke stroke;
bool available = false;
size_t used = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
FAIL_ZERO_RETURN(errctx, done, AKERR_NULLPOINTER, "done");
used = SDL_strlen(dest);
*done = false;
/* The ATTEMPT block goes outside the loop: CATCH reports failure by
* break-ing, which would bind to the loop instead of the block. */
ATTEMPT {
for ( ;; ) {
CATCH(errctx, akgl_controller_poll_keystroke(&stroke, &available));
if ( available == false ) {
break;
}
if ( stroke.key == SDLK_RETURN ) {
*done = true;
break;
}
if ( (stroke.text[0] != '\0') && ((used + 1) < destsize) ) {
dest[used] = stroke.text[0];
used += 1;
dest[used] = '\0';
}
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
```
The ring is filled on whichever thread pumps events and **is not synchronized ** . Poll it
from that thread.
## The default bindings
`akgl_controller_default(controlmapid, actorname, kbid, jsid)` points a map at the actor
registered under `actorname` and pushes **eight ** bindings — the four arrow keys and the
four D-pad directions — each wired to the matching `akgl_actor_cmhf_*_on` /`_off` pair:
| Key | Gamepad button | `handler_on` / `handler_off` |
|---|---|---|
| `SDLK_DOWN` | `SDL_GAMEPAD_BUTTON_DPAD_DOWN` | `akgl_actor_cmhf_down_on` / `_down_off` |
| `SDLK_UP` | `SDL_GAMEPAD_BUTTON_DPAD_UP` | `akgl_actor_cmhf_up_on` / `_up_off` |
| `SDLK_LEFT` | `SDL_GAMEPAD_BUTTON_DPAD_LEFT` | `akgl_actor_cmhf_left_on` / `_left_off` |
| `SDLK_RIGHT` | `SDL_GAMEPAD_BUTTON_DPAD_RIGHT` | `akgl_actor_cmhf_right_on` / `_right_off` |
It is the "just give me something that works" path. A game wanting different keys builds
its own bindings with `akgl_controller_pushmap` , as in the WASD example above.
**The default bindings cannot produce a diagonal.** Every `akgl_actor_cmhf_*_on` handler
clears `AKGL_ACTOR_STATE_MOVING_ALL` before setting its own bit, so pressing Left while Up
is held cancels Up rather than adding to it. That is fine for a four-way JRPG and wrong for
anything that wants eight-way movement — and it means the thrust-vector ellipse cap in
[Chapter 14 ](14-physics.md ) is unreachable through these handlers. Eight-way movement wants
your own handlers that `AKGL_BITMASK_ADD` the movement bit without clearing the others.
`akgl_controller_handle_button_down` / `_up` are a different thing again, and are **not **
control-map bindings: they look the actor up by the literal name `"player"` in
`AKGL_REGISTRY_ACTOR` rather than being told which actor to act on, so they only ever drive
one. The `akgl_actor_cmhf_*` handlers are the general form; see
[Chapter 12 ](12-actors.md ).
## Gamepads
**SDL delivers no button events from a gamepad nobody has opened.** `akgl_game_init` calls
`akgl_controller_open_gamepads()` for the devices present at startup; a device plugged in
mid-session arrives as `SDL_EVENT_GAMEPAD_ADDED` , and `akgl_controller_handle_added` opens
it. Route that event (and `SDL_EVENT_GAMEPAD_REMOVED` , to `akgl_controller_handle_removed` )
or hot-plugged pads stay silent. No gamepads attached at all is success, not an error.
`akgl_controller_handle_added` logs the mapping it found, deliberately: **a controller with
no entry in the mapping database produces no button events at all**, which is otherwise
indistinguishable from a broken binding.
Which is why libakgl ships a database. `include/akgl/SDL_GameControllerDB.h` carries
`AKGL_SDL_GAMECONTROLLER_DB_LEN` (2255) mappings from the community
[SDL_GameControllerDB ](https://github.com/mdqinc/SDL_GameControllerDB ) project, and
`akgl_game_init` feeds every one to `SDL_AddGamepadMapping` before opening anything.
That header is **generated, and tracked on purpose ** . It is the offline fallback that keeps
the library buildable when upstream is renamed, rate-limited or unreachable. Never
hand-edit it; regeneration is `cmake --build build --target controllerdb` and nothing else
runs the script. The full contract, including why a failed fetch used to destroy the very
file it exists to protect, is in `AGENTS.md` under "Generated and Vendored Sources".
## Known defects
**A matched binding's handler pointer is not checked.** `akgl_controller_handle_event`
calls `curcontrol->handler_on(...)` the moment the event type, device id and key all match,
with no `NULL` test. A control pushed with a `NULL` `handler_on` or `handler_off` crashes
when its event arrives — and since `akgl_controller_pushmap` copies whatever it is given,
a `memset` -zeroed `akgl_Control` with only `event_on` filled in is a loaded gun. **Set both
handlers on every binding you push**, even if one of them does nothing. There is no
diagnostic for this at any layer.
**`appstate` is required and never read.** Every entry point in this subsystem —
`akgl_controller_handle_event` , `handle_button_down` , `handle_button_up` , `handle_added` ,
`handle_removed` — raises `AKERR_NULLPOINTER` on a `NULL` `appstate` and then never looks
at it. Pass any non-`NULL` pointer if your program has no app state.
**The analogue-axis, mouse and pen fields are declared and never consulted.**
`akgl_Control::axis` , `axis_range_min` and `axis_range_max` , and `akgl_ControlMap::mouseid`
and `penid` , are in the structs and `akgl_controller_handle_event` does not read any of
them. Setting them has no effect; analogue sticks, mice and pens do not reach the control
maps at all today. Handle those events yourself before calling
`akgl_controller_handle_event` .
**Key auto-repeat is not filtered.** The ring push tests only `event->type ==
SDL_EVENT_KEY_DOWN` and does not consult ` event->key.repeat`, so a held key fills the
32-entry ring at the platform's repeat rate. Check `repeat` in your event pump if you are
polling for discrete presses.
Two defects this chapter used to carry are **fixed ** and are recorded here so nobody
re-derives them from the header prose:
- **A negative `controlmapid` is rejected.** Both `akgl_controller_pushmap` and
`akgl_controller_default` check `controlmapid < 0` as well as the upper bound and raise
`AKERR_OUTOFBOUNDS` . `tests/controller.c` passes `-1` and `-4096` to each. The
`@warning` blocks in `include/akgl/controller.h` saying only the upper bound is checked
are stale; `TODO.md` , "Known and still open" item 11.
- **The `akgl_controller_handle_*` functions exist.** `controller.h` once declared four
names that were defined under different spellings, so they linked nowhere. Fixed in
0.5.0, and `scripts/check_api_surface.sh` runs as the `api_surface` test to stop that
class of drift coming back.
## Statuses
The ones this subsystem raises, with their libakgl meanings in
[Chapter 4 ](04-errors.md ):
| Status | When |
|---|---|
| `AKERR_NULLPOINTER` | A `NULL` `appstate` , `event` , `control` , or poller destination; SDL cannot enumerate or open a device; no actor registered as `"player"` |
| `AKERR_OUTOFBOUNDS` | `controlmapid` outside `0..AKGL_MAX_CONTROL_MAPS - 1` , or the map already holds `AKGL_MAX_CONTROLS` bindings |
| `AKGL_ERR_REGISTRY` | `akgl_controller_default` could not find `actorname` — usually because the actor has not been created yet |
| anything | Whatever the matched binding's handler raises, propagated unchanged |