libakerror 1.0.0 replaced the consumer-sized status-name array with a private registry and made status-code ownership explicit and enforced. AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, and the registry entry points raise akerr_ErrorContext * instead of returning int. See deps/libakerror/UPGRADING.md. The break was not only source-level. libakgl's codes sat at AKERR_LAST_ERRNO_VALUE + 18 through + 22, and 1.0.0 claimed exactly those five offsets for its own AKERR_STATUS_* registry codes, so every AKGL_ERR_* was aliasing a libakerror status. HANDLE(e, AKGL_ERR_LOGICINTERRUPT) at physics.c:222 would have swallowed a foreign-name refusal. - Move the band to AKERR_FIRST_CONSUMER_STATUS (256) as fixed offsets, so a libc that grows an errno cannot move the codes, and add AKGL_ERR_OWNER, AKGL_ERR_LIMIT and AKGL_ERR_COUNT to describe it. - Reserve the range and register the names through the owned entry points, PASS-ing each: these are AKERR_NOIGNORE, and the old akerr_name_for_status calls discarded failure silently. - Drop the AKERR_MAX_ERR_VALUE=256 compile definition. - Guard on AKERR_FIRST_CONSUMER_STATUS in include/akgl/error.h, which now includes <akerror.h> so the guard is reliable. The embedded build is fine, but the find_package path can pick up a stale installed header, and 1.0.0 has an soname, so that pairing is an ABI mismatch rather than a compile problem. Same guard libakstdlib already carries. Registration also moves out of akgl_heap_init into a new akgl_error_init in src/error.c. It was in the heap pool's initializer only because that was the first thing akgl_game_init called, and the upgrade turned five fire-and-forget name calls into a library-wide ownership claim that can fail. That placement was hiding a defect: game.c raises AKGL_ERR_SDL when SDL_CreateMutex fails, five lines before akgl_heap_init ran, so the earliest error path in the library was guaranteed to print "Unknown Error". akgl_error_init is now the first statement in akgl_game_init. Callers that drive subsystems directly must call akgl_error_init first; it is idempotent, so ordering it precisely is not required. The eleven test suites that relied on akgl_heap_init to name their statuses now call it explicitly, or their failure messages would have degraded to "Unknown Error". Add tests/error.c: assert every code reads back its registered name, that the name table and AKGL_ERR_COUNT agree, that a foreign owner is refused with AKERR_STATUS_NAME_FOREIGN and AKERR_STATUS_RANGE_OVERLAP, and that repeating the init is a no-op. That last one is a live constraint, not a triviality -- libakerror treats only an identical reservation as a repeat, so a subset or superset raises. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
559 lines
23 KiB
C
559 lines
23 KiB
C
/**
|
|
* @file controller.c
|
|
* @brief Unit tests for control maps and SDL input dispatch.
|
|
*
|
|
* SDL events are synthesized directly rather than pumped through the event
|
|
* queue, so none of this needs a physical keyboard or gamepad. The device
|
|
* enumeration helpers are exercised against the dummy drivers, where the
|
|
* expected outcome is "no devices, no crash".
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <string.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/error.h>
|
|
// akgl/controller.h uses akgl_Actor without declaring it, so actor.h has to
|
|
// come first, the same way src/controller.c reaches it through akgl/game.h.
|
|
#include <akgl/actor.h>
|
|
#include <akgl/character.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/controller.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/*
|
|
* akgl/controller.h declares these as akgl_controller_handle_*, but src has
|
|
* always defined them as gamepad_handle_*. Declared here under the names that
|
|
* actually link, so the tests can reach them without renaming shipped symbols.
|
|
*/
|
|
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event);
|
|
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event);
|
|
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event);
|
|
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event);
|
|
|
|
/** @brief Keyboard id the tests bind their control maps to. */
|
|
#define TEST_KBID 11
|
|
/** @brief Gamepad id the tests bind their control maps to. */
|
|
#define TEST_JSID 22
|
|
|
|
/** @brief Stand-in for the application state pointer SDL hands to callbacks. */
|
|
static int appstate_placeholder = 0;
|
|
|
|
/** @brief The actor every control map in this file targets. */
|
|
static akgl_Actor *player = NULL;
|
|
/** @brief Base character supplying the player's acceleration constants. */
|
|
static akgl_Character *playerchar = NULL;
|
|
|
|
/** @brief Clear every control map so each test starts from an empty binding set. */
|
|
static void reset_control_maps(void)
|
|
{
|
|
int i = 0;
|
|
for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
|
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
|
|
}
|
|
}
|
|
|
|
/** @brief Register the "player" actor the gamepad handlers look up by name. */
|
|
static akerr_ErrorContext *make_player(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
ATTEMPT {
|
|
CATCH(e, akgl_registry_init_actor());
|
|
CATCH(e, akgl_registry_init_character());
|
|
CATCH(e, akgl_heap_init());
|
|
|
|
CATCH(e, akgl_heap_next_character(&playerchar));
|
|
CATCH(e, akgl_character_initialize(playerchar, "playerchar"));
|
|
playerchar->ax = 6.0f;
|
|
playerchar->ay = 8.0f;
|
|
|
|
CATCH(e, akgl_heap_next_actor(&player));
|
|
CATCH(e, akgl_actor_initialize(player, "player"));
|
|
player->basechar = playerchar;
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/** @brief Build a synthetic keyboard event. */
|
|
static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key)
|
|
{
|
|
memset(event, 0x00, sizeof(SDL_Event));
|
|
event->type = type;
|
|
event->key.which = which;
|
|
event->key.key = key;
|
|
}
|
|
|
|
/** @brief Build a synthetic gamepad button event. */
|
|
static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button)
|
|
{
|
|
memset(event, 0x00, sizeof(SDL_Event));
|
|
event->type = type;
|
|
event->gbutton.which = which;
|
|
event->gbutton.button = button;
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_pushmap(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
akgl_Control control;
|
|
int i = 0;
|
|
|
|
ATTEMPT {
|
|
reset_control_maps();
|
|
memset(&control, 0x00, sizeof(akgl_Control));
|
|
control.key = SDLK_SPACE;
|
|
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;
|
|
|
|
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 1,
|
|
"pushing one control left nextMap at %d, expected 1",
|
|
GAME_ControlMaps[0].nextMap);
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_SPACE,
|
|
"the pushed control did not land in slot 0");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_left_on,
|
|
"the pushed control lost its press handler");
|
|
|
|
// Pushes accumulate rather than overwrite.
|
|
TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
|
|
"pushing a second control left nextMap at %d, expected 2",
|
|
GAME_ControlMaps[0].nextMap);
|
|
|
|
// Maps are independent of each other.
|
|
TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map");
|
|
TEST_ASSERT(e, GAME_ControlMaps[3].nextMap == 1,
|
|
"map 3 nextMap is %d, expected 1", GAME_ControlMaps[3].nextMap);
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 2,
|
|
"pushing into map 3 disturbed map 0 (nextMap %d)", GAME_ControlMaps[0].nextMap);
|
|
|
|
// Filling a map exactly to capacity still succeeds.
|
|
reset_control_maps();
|
|
for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) {
|
|
akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control);
|
|
if ( pushresult != NULL ) {
|
|
pushresult->handled = true;
|
|
pushresult = akerr_release_error(pushresult);
|
|
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS);
|
|
}
|
|
}
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == AKGL_MAX_CONTROLS,
|
|
"a full map reports nextMap %d, expected %d",
|
|
GAME_ControlMaps[0].nextMap, AKGL_MAX_CONTROLS);
|
|
|
|
// One past capacity is refused.
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control),
|
|
"pushing into a full control map");
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL),
|
|
"pushing a NULL control");
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
|
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control),
|
|
"pushing into a control map id at the limit");
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
|
akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control),
|
|
"pushing into a control map id past the limit");
|
|
} CLEANUP {
|
|
reset_control_maps();
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_default_bindings(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
reset_control_maps();
|
|
CATCH(e, make_player());
|
|
|
|
TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID),
|
|
"installing the default control map");
|
|
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].target == player,
|
|
"the default map did not target the player actor");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].kbid == TEST_KBID,
|
|
"the default map recorded keyboard %d, expected %d",
|
|
GAME_ControlMaps[0].kbid, TEST_KBID);
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].jsid == TEST_JSID,
|
|
"the default map recorded gamepad %d, expected %d",
|
|
GAME_ControlMaps[0].jsid, TEST_JSID);
|
|
|
|
// Four keyboard bindings then four gamepad bindings.
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].nextMap == 8,
|
|
"the default map installed %d controls, expected 8",
|
|
GAME_ControlMaps[0].nextMap);
|
|
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].key == SDLK_DOWN,
|
|
"the first default binding is not the down arrow");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[0].handler_on == &akgl_Actor_cmhf_down_on,
|
|
"the down arrow is not bound to the down handler");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[1].key == SDLK_UP,
|
|
"the second default binding is not the up arrow");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[2].key == SDLK_LEFT,
|
|
"the third default binding is not the left arrow");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[3].key == SDLK_RIGHT,
|
|
"the fourth default binding is not the right arrow");
|
|
|
|
// The gamepad half binds buttons and leaves the keycode clear, so a
|
|
// keyboard event cannot accidentally match a dpad binding.
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
|
"the fifth default binding is not the dpad down button");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[4].key == 0,
|
|
"a gamepad binding also carries a keycode");
|
|
TEST_ASSERT(e, GAME_ControlMaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
|
|
"the eighth default binding is not the dpad right button");
|
|
|
|
// An unknown actor name is a registry error, not a crash.
|
|
reset_control_maps();
|
|
TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY,
|
|
akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID),
|
|
"installing a default map for an unregistered actor");
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
|
|
akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID),
|
|
"installing a default map at a control map id past the limit");
|
|
} CLEANUP {
|
|
reset_control_maps();
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_handle_keyboard_events(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
SDL_Event event;
|
|
|
|
ATTEMPT {
|
|
reset_control_maps();
|
|
CATCH(e, make_player());
|
|
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
|
|
|
// Pressing left routes to the left press handler.
|
|
player->state = 0;
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a left key press");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
|
"a left key press did not start the actor moving left (state %d)", player->state);
|
|
TEST_ASSERT_FEQ(e, player->ax, -6.0f,
|
|
"a left key press set ax to %f, expected -6", player->ax);
|
|
|
|
// Releasing it routes to the release handler.
|
|
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a left key release");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
|
"a left key release did not stop the actor (state %d)", player->state);
|
|
|
|
// The other three directions route the same way.
|
|
player->state = 0;
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a right key press");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
|
"a right key press did not start the actor moving right (state %d)", player->state);
|
|
|
|
player->state = 0;
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching an up key press");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
|
"an up key press did not start the actor moving up (state %d)", player->state);
|
|
|
|
player->state = 0;
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a down key press");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
|
"a down key press did not start the actor moving down (state %d)", player->state);
|
|
|
|
// An unbound key is ignored.
|
|
player->state = 0;
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching an unbound key");
|
|
TEST_ASSERT(e, player->state == 0,
|
|
"an unbound key changed the actor state to %d", player->state);
|
|
|
|
// A bound key from a different keyboard is ignored, so split-keyboard
|
|
// local multiplayer does not cross-talk.
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a bound key from an unbound keyboard");
|
|
TEST_ASSERT(e, player->state == 0,
|
|
"a key from another keyboard changed the actor state to %d", player->state);
|
|
} CLEANUP {
|
|
reset_control_maps();
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_handle_gamepad_events(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
SDL_Event event;
|
|
|
|
ATTEMPT {
|
|
reset_control_maps();
|
|
CATCH(e, make_player());
|
|
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
|
|
|
player->state = 0;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a dpad left press");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
|
"a dpad left press did not start the actor moving left (state %d)", player->state);
|
|
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a dpad left release");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
|
"a dpad left release did not stop the actor (state %d)", player->state);
|
|
|
|
player->state = 0;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a dpad up press");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
|
"a dpad up press did not start the actor moving up (state %d)", player->state);
|
|
|
|
// A press from an unbound gamepad is ignored.
|
|
player->state = 0;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching a dpad press from an unbound gamepad");
|
|
TEST_ASSERT(e, player->state == 0,
|
|
"a press from another gamepad changed the actor state to %d", player->state);
|
|
|
|
// An unbound button on the bound gamepad is ignored.
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching an unbound gamepad button");
|
|
TEST_ASSERT(e, player->state == 0,
|
|
"an unbound gamepad button changed the actor state to %d", player->state);
|
|
} CLEANUP {
|
|
reset_control_maps();
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_handle_event_edge_cases(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
SDL_Event event;
|
|
|
|
ATTEMPT {
|
|
reset_control_maps();
|
|
CATCH(e, make_player());
|
|
|
|
// With no control maps installed at all, every event is a no-op.
|
|
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching into an empty control map table");
|
|
|
|
// A map in a later slot is still reached, so the scan does not stop at
|
|
// the first unpopulated entry.
|
|
CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID));
|
|
player->state = 0;
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching to a control map in a later slot");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
|
"a control map in slot 5 was not consulted (state %d)", player->state);
|
|
|
|
// An event type that no binding uses falls through harmlessly.
|
|
memset(&event, 0x00, sizeof(SDL_Event));
|
|
event.type = SDL_EVENT_MOUSE_MOTION;
|
|
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
|
"dispatching an event type with no bindings");
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event),
|
|
"dispatching with a NULL appstate");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
akgl_controller_handle_event(&appstate_placeholder, NULL),
|
|
"dispatching a NULL event");
|
|
} CLEANUP {
|
|
reset_control_maps();
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
SDL_Event event;
|
|
|
|
ATTEMPT {
|
|
CATCH(e, make_player());
|
|
|
|
// These handlers find their actor by looking up "player" in the registry
|
|
// rather than taking it as an argument.
|
|
player->state = 0;
|
|
player->movement_controls_face = false;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
|
|
"dpad down press handler");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
|
"the dpad down handler did not set MOVING_DOWN (state %d)", player->state);
|
|
// With automatic facing off, the handler sets the facing itself.
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
|
|
"the dpad down handler did not set FACE_DOWN (state %d)", player->state);
|
|
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event),
|
|
"dpad down release handler");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
|
|
"the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state);
|
|
TEST_ASSERT(e, player->curSpriteFrameId == 0,
|
|
"the release handler did not reset the animation frame");
|
|
|
|
player->state = 0;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
|
|
"the dpad up handler did not set MOVING_UP (state %d)", player->state);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler");
|
|
|
|
player->state = 0;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
|
"the dpad left handler did not set MOVING_LEFT (state %d)", player->state);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler");
|
|
|
|
player->state = 0;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
|
|
"the dpad right handler did not set MOVING_RIGHT (state %d)", player->state);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler");
|
|
|
|
// With automatic facing on, the handler leaves facing to the actor's own
|
|
// face function.
|
|
player->state = 0;
|
|
player->movement_controls_face = true;
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
|
TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event),
|
|
"dpad down press with automatic facing on");
|
|
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
|
|
"the handler set facing even though the actor faces automatically (state %d)",
|
|
player->state);
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event),
|
|
"dpad press handler with a NULL appstate");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
gamepad_handle_button_down(&appstate_placeholder, NULL),
|
|
"dpad press handler with a NULL event");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event),
|
|
"dpad release handler with a NULL appstate");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
gamepad_handle_button_up(&appstate_placeholder, NULL),
|
|
"dpad release handler with a NULL event");
|
|
|
|
// With no actor named "player" registered there is nothing to drive.
|
|
CATCH(e, akgl_registry_init_actor());
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
gamepad_handle_button_down(&appstate_placeholder, &event),
|
|
"dpad press handler with no player actor registered");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
gamepad_handle_button_up(&appstate_placeholder, &event),
|
|
"dpad release handler with no player actor registered");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_device_events(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
SDL_Event event;
|
|
|
|
ATTEMPT {
|
|
// The dummy drivers present no gamepads, so the add and remove handlers
|
|
// take their "unknown device" paths. Neither may crash.
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0);
|
|
TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event),
|
|
"handling an add for a device that cannot be opened");
|
|
|
|
make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0);
|
|
TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event),
|
|
"handling a remove for a device that was never open");
|
|
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event),
|
|
"add handler with a NULL appstate");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
gamepad_handle_added(&appstate_placeholder, NULL),
|
|
"add handler with a NULL event");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event),
|
|
"remove handler with a NULL appstate");
|
|
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
|
|
gamepad_handle_removed(&appstate_placeholder, NULL),
|
|
"remove handler with a NULL event");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
akerr_ErrorContext *test_controller_device_enumeration(void)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
ATTEMPT {
|
|
// Enumeration has to succeed on a machine with no input devices, which
|
|
// is the normal state under the dummy drivers and in CI.
|
|
TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards");
|
|
TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads");
|
|
} CLEANUP {
|
|
} PROCESS(e) {
|
|
} FINISH(e, true);
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),
|
|
AKGL_ERR_SDL,
|
|
"Couldn't initialize SDL: %s",
|
|
SDL_GetError());
|
|
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init());
|
|
|
|
CATCH(errctx, test_controller_pushmap());
|
|
CATCH(errctx, test_controller_default_bindings());
|
|
CATCH(errctx, test_controller_handle_keyboard_events());
|
|
CATCH(errctx, test_controller_handle_gamepad_events());
|
|
CATCH(errctx, test_controller_handle_event_edge_cases());
|
|
CATCH(errctx, test_controller_gamepad_button_handlers());
|
|
CATCH(errctx, test_controller_device_events());
|
|
CATCH(errctx, test_controller_device_enumeration());
|
|
} CLEANUP {
|
|
SDL_Quit();
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
}
|