Files
libakgl/include/akgl/controller.h
Andrew Kesterson 3a262bee54 Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.

The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.

The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.

scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.

The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.

Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.

24/24 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00

366 lines
19 KiB
C

/**
* @file controller.h
* @brief Input binding: SDL events in, actor state changes out.
*
* A control map ties one actor to one keyboard and one gamepad, and holds up to
* #AKGL_MAX_CONTROLS bindings. A binding says "when event X arrives from device
* Y carrying button or key Z, call this handler". Eight maps means up to eight
* locally controlled players, each on its own device.
*
* The host pumps SDL events into akgl_controller_handle_event(), which scans the
* maps in order and stops at the first binding that claims the event -- so a key
* bound in two maps only fires once, in the lower-numbered one.
*
* Alongside that, and independent of it, every key press is pushed into a small
* ring buffer that akgl_controller_poll_key() and akgl_controller_poll_keystroke()
* drain. That serves a caller that wants "is there a key waiting" without owning
* an event loop, and it sees keys whether or not a control map also claimed them.
*
* The two pollers read the same buffer and differ in what they hand back. A game
* asking "was the up arrow pressed" wants a keycode and nothing else, which is
* akgl_controller_poll_key(). A line editor asking "what did the user type" needs
* the modifier state and the character SDL composed -- there is no `"` keycode,
* and no keycode at all for a dead key or an IME commit -- which is
* akgl_controller_poll_keystroke().
*/
#ifndef _AKGL_CONTROLLER_H_
#define _AKGL_CONTROLLER_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/types.h>
// The binding handlers below take an akgl_Actor *, so this header cannot be
// included before akgl/actor.h without one. akgl_Actor is a typedef of a named
// struct, and repeating a typedef is not C99, so it is included rather than
// forward-declared. actor.h reaches only types.h and character.h, so this does
// not close a cycle.
#include <akgl/actor.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
/**
* @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 Bytes of composed text one buffered keystroke can carry, NUL included.
*
* A UTF-8 code point is at most four bytes, so this holds one character and its
* terminator with room to spare. An input event carrying more than fits -- an
* IME committing a whole word at once -- is truncated on a code point boundary
* rather than split through the middle of one.
*/
#define AKGL_CONTROLLER_KEYSTROKE_TEXT 8
/** @brief One buffered keystroke: which key, which modifiers, and what it typed. */
typedef struct {
SDL_Keycode key; /**< The keycode, or 0 for an entry that carries only composed text -- an IME commit, or a character finished by a dead key. */
SDL_Keymod mod; /**< Modifier state when the key went down. 0 on a text-only entry, whose text already reflects them. */
char text[AKGL_CONTROLLER_KEYSTROKE_TEXT]; /**< What the keystroke composed to, UTF-8. The empty string for a key that produces no character, such as an arrow or a bare modifier. */
} akgl_Keystroke;
/** @brief Maps one SDL input to pressed and released callbacks. */
typedef struct {
uint32_t event_on; /**< SDL event type that fires `handler_on`, e.g. `SDL_EVENT_KEY_DOWN`. */
uint32_t event_off; /**< SDL event type that fires `handler_off`, e.g. `SDL_EVENT_KEY_UP`. */
uint8_t button; /**< Gamepad button (`SDL_GamepadButton`) this binding is for. Only consulted for gamepad events. */
SDL_Keycode key; /**< Keycode this binding is for. Only consulted for keyboard events. */
uint8_t axis; /**< Analogue axis. Declared but not yet consulted by akgl_controller_handle_event. */
uint8_t axis_range_min; /**< Low end of the axis range that counts as "on". Not yet consulted. */
uint8_t axis_range_max; /**< High end of that range. Not yet consulted. */
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_on`. Required if `event_on` can fire. */
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_off`. */
} akgl_Control;
/** @brief Groups input bindings for one actor and its input devices. */
typedef struct {
akgl_Actor *target; /**< The actor these bindings drive. A `NULL` target makes the whole map inert, which is how an unused slot is spelled. */
uint16_t nextMap; /**< Number of bindings in use; the index akgl_controller_pushmap writes to next. */
akgl_Control controls[AKGL_MAX_CONTROLS]; /**< The bindings, scanned in order. */
SDL_KeyboardID kbid; /**< Keyboard this map listens to. A keyboard event from any other id is ignored, which is what keeps two players on two keyboards apart. */
SDL_JoystickID jsid; /**< Gamepad this map listens to, matched the same way. */
SDL_MouseID mouseid; /**< Mouse this map listens to. Declared but not yet consulted. */
SDL_PenID penid; /**< Pen this map listens to. Declared but not yet consulted. */
} akgl_ControlMap;
/** @brief Every control map. Zeroed by akgl_game_init; index it by the same id the functions below take. */
extern akgl_ControlMap akgl_controlmaps[AKGL_MAX_CONTROL_MAPS];
/**
* @brief Log every attached keyboard and its SDL id.
*
* A diagnostic, not a query: it writes to the SDL log rather than returning
* anything. Its use is finding the `kbid` to hand akgl_controller_default when
* more than one keyboard is attached.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL cannot enumerate keyboards. The message
* carries `SDL_GetError()`; note that "no keyboards attached" is
* reported by SDL as an empty list, not as a failure.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
/**
* @brief Dispatch one SDL event to whichever control map binds it.
*
* The entry point for the whole subsystem -- call it for every event the host
* pumps. A key press is recorded in the poll buffer first, whether or not any
* map wants it, and an `SDL_EVENT_TEXT_INPUT` is attached to the press it
* belongs to; then the maps are scanned in index order and, within a map,
* bindings in the order they were pushed. The **first** binding whose event type
* and device id and button/key all match wins, and the scan stops there.
*
* An event nothing binds is not an error: it returns success having done
* nothing, which is what lets a host pass every event through unconditionally.
*
* @param appstate Passed through from SDL's callback. Required -- but only as a
* non-`NULL` token: nothing here reads it. Pass any non-`NULL`
* pointer if your program has no app state.
* @param event The event to dispatch. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
* @throws AKERR_* Whatever the matched binding's handler raises.
*
* @warning A matched binding's handler pointer is not checked, so a control
* pushed with a `NULL` `handler_on` or `handler_off` crashes when its
* event arrives.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
/**
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
*
* A whole-application shortcut rather than a control-map binding: it looks the
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* being told which actor to act on, so it only ever drives one. The
* akgl_actor_cmhf_* handlers are the general form.
*
* Facing follows movement unless the actor sets `movement_controls_face`, which
* is how an actor that aims independently of the direction it walks opts out.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
/**
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
*
* The counterpart to akgl_controller_handle_button_down. It clears only the movement
* bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The button or key release event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
/**
* @brief Open a gamepad that has just been plugged in, and log its mapping.
*
* SDL delivers no button events from an unopened gamepad, so a device that
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
*
* The mapping is logged because a controller with no entry in the database
* produces no button events at all, and that is otherwise indistinguishable
* from a broken binding.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
/**
* @brief Close a gamepad that has been unplugged.
*
* An unplugged device SDL still has open is a leaked handle, so this closes it.
* A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
/**
* @brief Append a binding to a control map.
*
* The binding is copied, so the caller's `akgl_Control` can be a stack local
* reused across pushes -- which is exactly what akgl_controller_default does.
* Bindings can only be appended; there is no remove, and no way to reset a map
* short of zeroing it in ::akgl_controlmaps directly.
*
* @param controlmapid Which map to append to, 0 through
* #AKGL_MAX_CONTROL_MAPS - 1.
* @param control The binding to copy in. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p control is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
* #AKGL_MAX_CONTROL_MAPS, or if the map already holds
* #AKGL_MAX_CONTROLS bindings.
*
* @warning A **negative** @p controlmapid is not rejected -- only the upper
* bound is checked -- and indexes before the start of
* ::akgl_controlmaps. TODO.md, "Known and still open" item 11.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
/**
* @brief Bind an actor to the arrow keys and the D-pad, in one call.
*
* Points the map at the named actor 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. It is the "just give me something that
* works" path -- a game wanting different keys builds its own bindings with
* akgl_controller_pushmap.
*
* @param controlmapid Which map to configure, 0 through
* #AKGL_MAX_CONTROL_MAPS - 1.
* @param actorname Registry name of the actor to drive. Required in practice,
* though a `NULL` is reported as "not found" rather than as
* a null pointer.
* @param kbid SDL keyboard id to listen to. Only events from this
* keyboard match; see akgl_controller_list_keyboards for
* how to find it.
* @param jsid SDL gamepad id to listen to, matched the same way.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
* #AKGL_MAX_CONTROL_MAPS, or if the map cannot hold eight more bindings.
* @throws AKGL_ERR_REGISTRY If @p actorname is not in #AKGL_REGISTRY_ACTOR --
* usually because the actor has not been created yet.
*
* @warning A negative @p controlmapid is not rejected. See
* akgl_controller_pushmap.
* @note It appends rather than replaces, so calling it twice on the same map
* leaves sixteen bindings and the first eight are the ones that fire.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
/**
* @brief Open every gamepad currently attached.
*
* SDL will not deliver button events from a gamepad nobody has opened, so this
* runs once at startup -- akgl_game_init calls it. Devices plugged in later are
* SDL_EVENT_GAMEPAD_ADDED events, handled elsewhere.
*
* No gamepads attached is success, not an error.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If SDL reports gamepads present but cannot
* enumerate them, or if one of them cannot be opened. The message
* carries `SDL_GetError()`.
*
* @note The enumeration array is only freed on the success path, so a failure
* part-way through leaks it.
*/
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.
*
* Entries that carry only composed text and no keycode are discarded on the way
* past rather than reported as keycode 0: this form is for a caller that acts on
* keys, and there is no key to report. A caller that wants those characters uses
* akgl_controller_poll_keystroke() instead.
*
* @param keycode Receives the SDL keycode, or 0 when nothing was waiting.
* Required -- the return value is the error context.
* @param available Receives `true` when a keystroke was taken, `false` when the
* buffer was empty. Required. Check this rather than testing
* @p keycode against 0.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p keycode or @p available is `NULL`. Both are
* required; there is no "I only want to know whether one is waiting"
* form.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
/**
* @brief Take the oldest waiting keystroke whole: key, modifiers and text.
*
* akgl_controller_poll_key() with nothing thrown away. A line editor needs all
* three: the keycode to recognise Backspace and Return, the modifier state to
* tell Ctrl-C from a `c`, and the composed text because a keycode cannot express
* what a shifted key produces on the user's own layout. `"`, `!`, `(`, `)`, `:`
* and `;` are all unreachable from a keycode alone, and so is every lower-case
* letter.
*
* The text is what SDL composed, taken from the `SDL_EVENT_TEXT_INPUT` that
* follows the key press -- the only correct way to get a character out of SDL,
* and what makes a keyboard layout, a compose key and a dead key work. **SDL
* only sends those events while text input is started**, so a host that wants
* the `text` field populated calls `SDL_StartTextInput()` on its window first.
* Without it, `key` and `mod` still arrive and `text` is always empty.
*
* The two pollers drain the same buffer, so a keystroke taken by one is not
* waiting for the other.
*
* @param dest Receives the keystroke. Required. Written only when a
* keystroke was waiting; zeroed otherwise, so `key` is 0 and
* `text` is the empty string.
* @param available Receives `true` when a keystroke was taken, `false` when the
* buffer was empty. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p available is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, 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.
*
* This empties only the polling buffer. Keys already dispatched to control maps
* have had their effect and cannot be taken back.
*
* @return `NULL`. There is no failure path -- it resets two counters.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
#endif // _AKGL_CONTROLLER_H_