Files
libakgl/src/controller.c
Andrew Kesterson 9c2f80bcb9 Give back every pooled string and sprite reference the loaders take
Closes Defects items 20, 22, 23 and 29, and the residual of item 38.

The tilemap load leak was five pooled strings per load; the property-lookup
fix in an earlier commit took it to two, and the last two were each a claim
with no matching release -- the string every layer's `type` was read into, and
the dirname the map's relative paths resolve against. tests/tilemap.c asserts
the pool is exactly where it started after one load/release cycle and after 64.

Finding those two was a matter of dumping the contents of every still-claimed
slot after a cycle rather than reading the code again; 'tilelayer' and an
assets directory named themselves immediately.

Same file, same class, fixed with it: akgl_tilemap_load_layer_objects released
its scratch string after reading each object's name and then kept using the
slot, because akgl_get_json_string_value reuses a non-NULL destination without
taking another reference. The slot was free while still live, so any other
claim could have been handed it.

akgl_character_sprite_add wrote over an existing binding without releasing the
sprite it displaced, so a character that rebinds a state while alive leaked a
sprite slot per rebind -- teardown only gives back what the map holds at the
end. The new reference is taken before the write and given back if the write
fails, so there is no window where a sprite is bound with nothing behind it.
The write was unchecked too.

Three failure-path leaks moved into CLEANUP blocks: akgl_render_2d_init's two
pooled strings, akgl_controller_open_gamepads' enumeration array, and
akgl_text_rendertextat's surface and texture -- the last being a leak per frame
on a HUD line.

akgl_text_unloadallfonts() closes every font in the registry and destroys it,
which is what item 38 left open. Deliberately not a whole akgl_game_shutdown:
tearing down the mixer, SDL_ttf and SDL in the right order is a design
question, and this is the part that was simply missing. It is a new public
symbol, which 0.5.0 already covers -- this release has not shipped.

Every fix has a test that fails against the old code.

25/25 pass, memcheck clean, reindent --check, check_api_surface and
check_error_protocol all clean.

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

613 lines
22 KiB
C

/**
* @file controller.c
* @brief Implements the controller subsystem.
*/
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/game.h>
#include <akgl/controller.h>
akgl_ControlMap akgl_controlmaps[AKGL_MAX_CONTROL_MAPS];
/*
* Keystrokes waiting for akgl_controller_poll_key() and
* akgl_controller_poll_keystroke(). 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 akgl_Keystroke keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
static int keybuffer_head = 0;
static int keybuffer_count = 0;
/*
* Whether the newest entry is a key press that has not been given its composed
* text yet. SDL reports one keystroke as two events -- the key going down, and
* then the text it composed to, if it composed to anything -- so the text that
* arrives next belongs to the press that arrived last. Without this flag a
* press dropped by a full buffer would hand its text to whatever older press
* happened to still be sitting at the end of the ring.
*/
static bool keybuffer_awaiting_text = false;
/**
* @brief Length in bytes of the UTF-8 sequence @p src begins with, or 0.
*
* 0 both for a byte that cannot start a sequence and for a sequence the string
* ends in the middle of. SDL should hand over neither, and reading past a
* terminator to find out otherwise is not a risk worth taking for input that
* arrives from a keyboard driver.
*
* @param src UTF-8 text, NUL terminated. Required.
*/
static size_t utf8_sequence_length(const char *src)
{
unsigned char lead = (unsigned char)src[0];
size_t len = 0;
size_t i = 0;
if ( lead < 0x80 ) {
return 1;
} else if ( (lead & 0xe0) == 0xc0 ) {
len = 2;
} else if ( (lead & 0xf0) == 0xe0 ) {
len = 3;
} else if ( (lead & 0xf8) == 0xf0 ) {
len = 4;
} else {
return 0;
}
for ( i = 1; i < len; i++ ) {
// A NUL fails this too, which is what stops the scan at the end of a
// truncated string instead of past it.
if ( ((unsigned char)src[i] & 0xc0) != 0x80 ) {
return 0;
}
}
return len;
}
/**
* @brief Copy as much of @p src as fits in @p dest without splitting a character.
*
* SDL hands over whatever the platform composed, which for an input method can
* be several characters at once, and the ring entry holds one. Truncating on a
* byte boundary would leave a partial UTF-8 sequence that nothing downstream
* can render, so this stops at the last code point that fits whole.
*
* @param dest Receives the prefix, always NUL terminated. Required.
* @param destsize Bytes available in @p dest, terminator included.
* @param src UTF-8 text to copy. Required.
*/
static void copy_utf8_prefix(char *dest, size_t destsize, const char *src)
{
size_t used = 0;
size_t len = 0;
dest[0] = '\0';
while ( src[used] != '\0' ) {
len = utf8_sequence_length(&src[used]);
if ( len == 0 ) {
// Not valid UTF-8 from here on. Keep what is already known good.
break;
}
if ( (used + len) >= destsize ) {
break;
}
memcpy(&dest[used], &src[used], len);
used += len;
}
dest[used] = '\0';
}
/**
* @brief Record one key press, 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.
*
* The entry starts with no text. Whatever the press composes to arrives as a
* separate event and is attached by keybuffer_attach_text().
*/
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;
}
/**
* @brief Give composed text to the press it belongs to, or buffer it alone.
*
* An entry with no keycode is not a degenerate case to be avoided: 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(), which has no keycode to give, discards it.
*/
static void keybuffer_attach_text(const char *text)
{
int newest = 0;
if ( (keybuffer_awaiting_text == true) && (keybuffer_count > 0) ) {
newest = (keybuffer_head + keybuffer_count - 1) % AKGL_CONTROLLER_KEY_BUFFER;
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
keybuffer_awaiting_text = false;
return;
}
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
return;
}
newest = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer[newest].key = 0;
keybuffer[newest].mod = 0;
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
keybuffer_count += 1;
}
/**
* @brief Take the oldest entry, whatever it carries.
*
* @param dest Receives the entry. Required.
* @return `true` when an entry was taken, `false` when the buffer was empty.
*/
static bool keybuffer_take(akgl_Keystroke *dest)
{
if ( keybuffer_count == 0 ) {
return false;
}
memcpy(dest, &keybuffer[keybuffer_head], sizeof(akgl_Keystroke));
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
keybuffer_count -= 1;
if ( keybuffer_count == 0 ) {
// Nothing is left for a text event to attach itself to.
keybuffer_awaiting_text = false;
}
return true;
}
akerr_ErrorContext *akgl_controller_list_keyboards(void)
{
int count;
SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
// The array is SDL's to allocate and ours to free -- that is the contract on
// every SDL_Get*s() enumeration. Released in CLEANUP so the loop can fail
// without taking the array with it.
ATTEMPT {
for (int i = 0; i < count; i++) {
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
}
} CLEANUP {
SDL_free(keyboards);
keyboards = NULL;
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_open_gamepads(void)
{
int count = 0;
int i = 0;
SDL_JoystickID *gamepads = NULL;
SDL_Gamepad *gamepad = NULL;
bool openfailed = false;
PREPARE_ERROR(errctx);
if ( SDL_HasGamepad() ) {
gamepads = SDL_GetGamepads(&count);
if ( count > 0 ) {
// SDL_free in CLEANUP, not after the loop. A gamepad that failed to
// open used to return straight out of here and take the enumeration
// array with it.
ATTEMPT {
FAIL_ZERO_BREAK(errctx, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
for ( i = 0; i < count ; i++ ) {
gamepad = SDL_OpenGamepad(gamepads[i]);
if ( gamepad == NULL ) {
openfailed = true;
break;
}
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
}
// Reported after the loop rather than inside it: a
// FAIL_ZERO_BREAK in there would break the loop, not the block.
FAIL_NONZERO_BREAK(errctx, openfailed, AKERR_NULLPOINTER, "%s", SDL_GetError());
} CLEANUP {
if ( gamepads != NULL ) {
SDL_free(gamepads);
gamepads = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
} else {
// SDL_GetGamepads allocates even when it reports none.
if ( gamepads != NULL ) {
SDL_free(gamepads);
gamepads = NULL;
}
SDL_Log("No gamepads enumerated");
}
} else {
SDL_Log("No gamepads connected");
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *event)
{
int i = 0;
int j = 0;
int eventButtonComboMatch = 0;
akgl_ControlMap *curmap = NULL;
akgl_Control *curcontrol = NULL;
PREPARE_ERROR(errctx);
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_key(event->key.key, event->key.mod);
} else if ( event->type == SDL_EVENT_TEXT_INPUT ) {
// The character SDL composed for the press recorded a moment ago. A
// keycode cannot express it: the layout, the shift state, a compose key
// and a dead key all have their say between the two events.
keybuffer_attach_text(event->text.text);
}
ATTEMPT {
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
curmap = &akgl_controlmaps[i];
if ( curmap->target == NULL ) {
continue;
}
//SDL_Log("Control map %d maps to actor %s", i, curmap->target->name);
//SDL_Log("event from keyboard %d", event->key.which);
for ( j = 0; j < AKGL_MAX_CONTROLS; j++ ) {
curcontrol = &curmap->controls[j];
//SDL_Log("button/key is processed by controlmap %d control %d", i, j);
//SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
// This controlmap processes this control
eventButtonComboMatch = (
((event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
event->gbutton.which == curmap->jsid &&
event->gbutton.button == curcontrol->button) ||
((event->type == SDL_EVENT_KEY_DOWN ||
event->type == SDL_EVENT_KEY_UP) &&
event->key.which == curmap->kbid &&
event->key.key == curcontrol->key)
);
if ( event->type == curcontrol->event_on && eventButtonComboMatch) {
CATCH(errctx, curcontrol->handler_on(curmap->target, event));
goto _akgl_controller_handle_event_success;
} else if ( event->type == curcontrol->event_off && eventButtonComboMatch ) {
CATCH(errctx, curcontrol->handler_off(curmap->target, event));
goto _akgl_controller_handle_event_success;
}
}
}
_akgl_controller_handle_event_success:
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_handle_button_down(void *appstate, SDL_Event *event)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) {
SDL_Log("Processing dpad down : state %d", player->state);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
}
SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
event->key.key == SDLK_UP ) {
SDL_Log("Processing dpad up");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
}
SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
event->key.key == SDLK_LEFT ) {
SDL_Log("Processing dpad left");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
}
SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
event->key.key == SDLK_RIGHT ) {
SDL_Log("Processing dpad right");
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
if ( !player->movement_controls_face ) {
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
}
SDL_Log("New state : %d", player->state);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_handle_button_up(void *appstate, SDL_Event *event)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
event->key.key == SDLK_DOWN ) {
SDL_Log("processing down release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
event->key.key == SDLK_UP ) {
SDL_Log("processing up release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
event->key.key == SDLK_RIGHT) {
SDL_Log("processing right release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
event->key.key == SDLK_LEFT ) {
SDL_Log("processing left release");
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
player->curSpriteFrameId = 0;
SDL_Log("New state : %d", player->state);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_handle_added(void *appstate, SDL_Event *event)
{
SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL;
char *mapping = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which);
if (!gamepad) {
SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError());
gamepad = SDL_OpenGamepad(which);
SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError());
mapping = SDL_GetGamepadMapping(gamepad);
if ( mapping == NULL ) {
SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
} else if ( mapping != NULL ) {
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
SDL_free(mapping);
}
} else {
SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_handle_removed(void *appstate, SDL_Event *event)
{
SDL_JoystickID which;
SDL_Gamepad *gamepad = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
which = event->gbutton.which;
gamepad = SDL_GetGamepadFromID(which);
if (gamepad) {
SDL_CloseGamepad(gamepad); /* the joystick was unplugged. */
}
SDL_Log("Gamepad #%u removed", (unsigned int) which);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
{
int newmapid = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
FAIL_ZERO_BREAK(errctx, control, AKERR_NULLPOINTER, "NULL Control");
FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
newmapid = akgl_controlmaps[controlmapid].nextMap;
FAIL_ZERO_BREAK(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
memcpy((void *)&akgl_controlmaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
akgl_controlmaps[controlmapid].nextMap = newmapid + 1;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
{
akgl_ControlMap *controlmap;
akgl_Control control;
PREPARE_ERROR(errctx);
ATTEMPT {
// set up the control map
FAIL_NONZERO_BREAK(errctx, ((controlmapid < 0) || (controlmapid >= AKGL_MAX_CONTROL_MAPS)), AKERR_OUTOFBOUNDS, "Control map id %d is outside 0..%d", controlmapid, (AKGL_MAX_CONTROL_MAPS - 1));
memset((void *)&control, 0x00, sizeof(akgl_Control));
controlmap = &akgl_controlmaps[controlmapid];
controlmap->kbid = kbid;
controlmap->jsid = jsid;
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
// ---- KEYBOARD CONTROLS ----
// Move down
control.key = SDLK_DOWN;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_actor_cmhf_down_on;
control.handler_off = &akgl_actor_cmhf_down_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move up
control.key = SDLK_UP;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_actor_cmhf_up_on;
control.handler_off = &akgl_actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left
control.key = SDLK_LEFT;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_actor_cmhf_left_on;
control.handler_off = &akgl_actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right
control.key = SDLK_RIGHT;
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.handler_on = &akgl_actor_cmhf_right_on;
control.handler_off = &akgl_actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
control.key = 0;
// ----- GAMEPAD CONTROLS
// Move down
control.button = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_actor_cmhf_down_on;
control.handler_off = &akgl_actor_cmhf_down_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move up
control.button = SDL_GAMEPAD_BUTTON_DPAD_UP;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_actor_cmhf_up_on;
control.handler_off = &akgl_actor_cmhf_up_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move left
control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_actor_cmhf_left_on;
control.handler_off = &akgl_actor_cmhf_left_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
// Move right
control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
control.handler_on = &akgl_actor_cmhf_right_on;
control.handler_off = &akgl_actor_cmhf_right_off;
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
// The SUCCEED_RETURN used to sit at the end of the ATTEMPT block, which
// returned past CLEANUP and left this function with no return statement at
// all on the path that falls out of FINISH.
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_poll_key(int *keycode, bool *available)
{
akgl_Keystroke keystroke;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
// 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;
while ( keybuffer_take(&keystroke) == true ) {
if ( keystroke.key == 0 ) {
// Composed text with no key behind it. This form has no way to
// report it, so it is dropped rather than handed back as key 0.
continue;
}
*keycode = (int)keystroke.key;
*available = true;
break;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
memset(dest, 0x00, sizeof(akgl_Keystroke));
*available = keybuffer_take(dest);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_controller_flush_keys(void)
{
PREPARE_ERROR(errctx);
keybuffer_head = 0;
keybuffer_count = 0;
keybuffer_awaiting_text = false;
SUCCEED_RETURN(errctx);
}