Six findings, all of them libakgl's, all closed. The memcheck run is clean. Not one of the four json_load_file calls in src/ was ever matched by a json_decref, so every asset load abandoned its parsed document: 1.5 KB per sprite, 2.1 KB per character, 9 KB per load of the 2x2 fixture map, and on the order of a megabyte for a real one. Each loader now releases it in the CLEANUP block it already had, on the success path as well as the failure one. akgl_registry_load_properties needed its loop moved inside the ATTEMPT block first: props is a borrowed reference into the document and is read after the block ends, so every exit from that loop leaked the whole tree. akgl_get_property copied a fixed AKGL_MAX_STRING_LENGTH bytes out of what SDL handed back, which is SDL's strdup of the value -- four bytes for "0.0". That read up to 4 KiB past the end of somebody else's allocation on every property read, returned whatever was there past the terminator, and would have faulted on a value that landed at the end of a page. It copies the value and its terminator now, and refuses a value too long for an akgl_String rather than truncating it into an unterminated buffer. The header note that described the overread as a quirk describes correct behaviour instead. The four savegame name tables wrote a fixed-width field starting at the registry key, and SDL sizes that allocation to the name. They read past it on every entry and put what they found into the save file: up to half a kilobyte of this process's heap per registered object, in a file a player might send to somebody. They stage through a zeroed buffer now, and a negative-array-size typedef fails the build if a table's width ever outgrows it. akgl_controller_list_keyboards never freed the array SDL_GetKeyboards allocated for it. A font could be opened and published and never handed back -- there was no way to close one, so a game that changed fonts between scenes leaked ten kilobytes each time, and loading over a live name leaked the font it displaced. akgl_text_unloadfont is that missing half, and akgl_text_loadfont calls it when it replaces a name, after the new font has opened so a failed reload leaves the caller with the font they had. A new public symbol takes the version to 0.4.0 and the soname with it: an 0.3 consumer cannot be handed this library and told it is the same ABI. tests/registry.c fills a destination with a sentinel and asserts the bytes past the terminator survive a read, which fails against the old copy. tests/text.c covers unload, double unload, unloading a name that was never registered, and replacement closing the displaced font. The JSON releases have no test of their own and cannot sensibly have one -- nothing in the public API can observe a jansson refcount -- so the memcheck run is their test, which is an argument for gating it rather than against. The two remaining findings are in deps/semver's own unit test, which is vendored. They are suppressed by function name, so a rewrite of those cases comes back as a finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
677 lines
25 KiB
C
677 lines
25 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 GAME_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 AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
|
{
|
|
int count;
|
|
SDL_KeyboardID *keyboards = SDL_GetKeyboards(&count);
|
|
PREPARE_ERROR(e);
|
|
|
|
FAIL_ZERO_RETURN(e, 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(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void)
|
|
{
|
|
int count = 0;
|
|
int i = 0;
|
|
SDL_JoystickID *gamepads = NULL;
|
|
SDL_Gamepad *gamepad = NULL;
|
|
|
|
PREPARE_ERROR(e);
|
|
if ( SDL_HasGamepad() ) {
|
|
gamepads = SDL_GetGamepads(&count);
|
|
if ( count > 0 ) {
|
|
FAIL_ZERO_RETURN(e, gamepads, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
|
for ( i = 0; i < count ; i++ ) {
|
|
gamepad = SDL_OpenGamepad(gamepads[i]);
|
|
FAIL_ZERO_RETURN(e, gamepad, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
|
SDL_Log("Gamepad %d is %s", i, SDL_GetGamepadNameForID(gamepads[i]));
|
|
}
|
|
SDL_free(gamepads);
|
|
} else {
|
|
SDL_Log("No gamepads enumerated");
|
|
}
|
|
} else {
|
|
SDL_Log("No gamepads connected");
|
|
}
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
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 = &GAME_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 == 768 && event->key.which == 11 && event->key.key == 13 ) {
|
|
SDL_Log("Event type=%d, keyboard=%d, key=%d", event->type, event->key.which, event->key.key);
|
|
SDL_Log("ControlMap[%d].Controls[%d] keyboard=%d, key=%d", i, j, curmap->kbid, curcontrol->key);
|
|
SDL_Log("event %d -> control on %d off %d", event->type, curcontrol->event_on, curcontrol->event_off);
|
|
SDL_Log("eventButtonComboMatch for controlmap %d id %d = %d",
|
|
i, j, eventButtonComboMatch);
|
|
}
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* Declared nowhere -- `controller.h` advertises this as
|
|
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
|
|
* and still open" item 10.
|
|
*
|
|
* @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 *gamepad_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);
|
|
}
|
|
|
|
/**
|
|
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
|
|
*
|
|
* The counterpart to gamepad_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.
|
|
*
|
|
* Declared nowhere; see gamepad_handle_button_down.
|
|
*
|
|
* @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 *gamepad_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);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* Declared nowhere; see gamepad_handle_button_down.
|
|
*
|
|
* @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 *gamepad_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);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* Declared nowhere; see gamepad_handle_button_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 *gamepad_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 AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control)
|
|
{
|
|
int newmapid = 0;
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
FAIL_ZERO_RETURN(errctx, control, AKERR_NULLPOINTER, "NULL Control");
|
|
FAIL_NONZERO_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
|
|
newmapid = GAME_ControlMaps[controlmapid].nextMap;
|
|
FAIL_ZERO_RETURN(errctx, (AKGL_MAX_CONTROLS - newmapid), AKERR_OUTOFBOUNDS, "Control map ID %d is full", controlmapid);
|
|
memcpy((void *)&GAME_ControlMaps[controlmapid].controls[newmapid], control, sizeof(akgl_Control));
|
|
GAME_ControlMaps[controlmapid].nextMap = newmapid + 1;
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *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_RETURN(errctx, (controlmapid >= AKGL_MAX_CONTROL_MAPS), AKERR_OUTOFBOUNDS, "ID %d exceeds maximum %d", controlmapid, AKGL_MAX_CONTROL_MAPS);
|
|
memset((void *)&control, 0x00, sizeof(akgl_Control));
|
|
controlmap = &GAME_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));
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
}
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *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 AKERR_NOIGNORE *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 AKERR_NOIGNORE *akgl_controller_flush_keys(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
keybuffer_head = 0;
|
|
keybuffer_count = 0;
|
|
keybuffer_awaiting_text = false;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|