Files
libakgl/tests/actor.c
Andrew Kesterson e4aa6a5084
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / performance (push) Failing after 19s
libakgl CI Build / memory_check (push) Failing after 15s
libakgl CI Build / mutation_test (push) Failing after 17s
Take libakerror 2.0.1 and drop the workaround it makes obsolete
Every libakgl test suite could report success while failing. libakerror's
default unhandled-error handler ended in exit(errctx->status), an exit
status is one byte wide, and libakgl's band starts at 256 -- so
AKGL_ERR_SDL, the most common failure a library built on SDL can have,
exited 0 and CTest recorded a pass. tests/character.c aborted at its
second of four tests on a bad renderer and was green for months.

0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in
tests/testutil.h, and TODO.md ended the entry saying any consumer's
suites have the same problem and it was worth raising upstream. It was.
2.0.1 fixes it at the source: akerr_exit() owns the mapping and the
default handler calls it, so 0 exits 0, 1 through 255 exit themselves,
and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The
trap and its 21 call sites are gone.

Verified by putting the original failure back rather than by reading the
release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main
exits 125 and CTest reports a failure. A standalone consumer raising the
same status unhandled exits 125 where it exited 0 before.

tests/actor.c installs its own handler and called exit(errctx->status)
from it, which is the same defect one layer up. It calls akerr_exit()
now.

2.0.0 also makes the error pool and the status registry thread safe,
which libakgl needs more than it knew: audio_stream_callback raises
error contexts on SDL's audio thread. With an unlocked pool that
callback and the main thread could scan AKERR_ARRAY_ERROR at the same
time and be handed the same slot. The comment there says so.

This is a hard dependency floor, not a preference. 2.0.0 moved
__akerr_last_ignored to thread-local storage and made akerr_next_error()
return a context that already holds its reference, and both expand at
libakgl's call sites -- and at a consumer's, because akerror.h is part
of libakgl's public interface. Mixing headers and libraries across that
line double-counts every reference and never returns a pool slot. The
soname moved to libakerror.so.2; include/akgl/error.h now also feature-
tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe
for 2.0.1 since libakerror publishes no version macro.

0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it
re-exports through its headers is not.

TODO.md records the pkg-config gap this makes sharper -- akgl.pc names
no dependencies at all, so nothing tells a pkg-config consumer which
libakerror it needs.

Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall
-Werror. libakgl.so.0.7 links libakerror.so.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 13:05:43 -04:00

1085 lines
42 KiB
C

#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
handle_unhandled_error(errctx);
#include <akerror.h>
#include <akgl/error.h>
#define UNHANDLED_ERROR_EXIT 0
#define UNHANDLED_ERROR_SET 1
#include <SDL3/SDL.h>
#include <stdlib.h>
#include <akgl/iterator.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/renderer.h>
#include "testutil.h"
int UNHANDLED_ERROR_BEHAVIOR;
akerr_ErrorContext *unhandled_error_context;
void handle_unhandled_error_noexit(akerr_ErrorContext *errctx)
{
if ( errctx == NULL ) {
return;
}
if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_EXIT ) {
// akerr_exit rather than exit(3): a status past 255 does not survive a
// wait status, and this suite's are AKGL_* codes, which all are.
akerr_exit(errctx->status);
}
if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_SET ) {
unhandled_error_context = errctx;
errctx->refcount += 1;
return;
}
}
int akgl_actor_updated;
akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
akgl_actor_updated = 1;
SUCCEED_RETURN(errctx);
}
// Currently the renderer assumes there is a global variable named `renderer`
int akgl_actor_rendered;
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
akgl_actor_rendered = 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
ATTEMPT {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
DETECT(unhandled_error_context, akgl_registry_iterate_actor(NULL, AKGL_REGISTRY_ACTOR, ""));
} CLEANUP {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
} PROCESS(unhandled_error_context) {
} HANDLE(unhandled_error_context, AKERR_NULLPOINTER) {
printf("Handled\n");
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_registry_actor_iterator_missingactor(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
akgl_Iterator iter = {
.layerid = 0,
.flags = 0
};
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
ATTEMPT {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
DETECT(
unhandled_error_context,
akgl_registry_iterate_actor(
&iter,
AKGL_REGISTRY_ACTOR,
"Actor who doesn't exist")
);
} CLEANUP {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
} PROCESS(unhandled_error_context) {
} HANDLE(unhandled_error_context, AKERR_KEY) {
printf("Handled\n");
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
{
akgl_Actor *testactor;
akgl_Iterator iter = {
.layerid = 0,
.flags = AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_RENDER
};
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
PREPARE_ERROR(errctx);
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
ATTEMPT {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
CATCH(unhandled_error_context, akgl_heap_next_actor(&testactor));
CATCH(unhandled_error_context, akgl_actor_initialize(testactor, "test"));
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
DETECT(
unhandled_error_context,
akgl_registry_iterate_actor(
&iter,
AKGL_REGISTRY_ACTOR,
"test")
);
FAIL_ZERO_BREAK(
unhandled_error_context,
akgl_actor_updated,
AKGL_ERR_BEHAVIOR,
"actor->updatefunc not called by the iterator"
);
FAIL_ZERO_BREAK(
unhandled_error_context,
akgl_actor_rendered,
AKGL_ERR_BEHAVIOR,
"actor->renderfunc not called by the iterator"
);
} CLEANUP {
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
IGNORE(akgl_heap_release_actor(testactor));
} PROCESS(unhandled_error_context) {
} FINISH(unhandled_error_context, true);
akerr_handler_unhandled_error = defaulthandler;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_actor_set_character(void)
{
akgl_Actor *testactor = NULL;
akgl_Character *testchar = NULL;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_actor_set_character(NULL, "test"));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&testactor));
CATCH(errctx, akgl_actor_initialize(testactor, "test"));
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP {
IGNORE(akgl_heap_release_actor(testactor));
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
printf("Handled\n");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&testactor));
CATCH(errctx, akgl_heap_next_character(&testchar));
CATCH(errctx, akgl_actor_initialize(testactor, "test"));
testactor->layer = 0;
testactor->updatefunc = &akgl_actor_update_noop;
testactor->renderfunc = &akgl_actor_render_noop;
CATCH(errctx, akgl_character_initialize(testchar, "test"));
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
} CLEANUP {
IGNORE(akgl_heap_release_actor(testactor));
IGNORE(akgl_heap_release_character(testchar));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_actor_manage_children(void)
{
akgl_Actor *parent = NULL;
akgl_Actor *child = NULL;
akgl_String *tmpstring = NULL;
int i = 0;
int j = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_heap_next_string(&tmpstring));
CATCH(errctx, akgl_heap_next_actor(&parent));
CATCH(errctx, akgl_actor_initialize(parent, "parent"));
for ( i = 0 ; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
sprintf((char *)&tmpstring->data, "child %d", i);
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, akgl_actor_initialize(child, (char *)&tmpstring->data));
CATCH(errctx, parent->addchild(parent, child));
// Release our claim on the actor so the parent can own the child and kill it
CATCH(errctx, akgl_heap_release_actor(child));
}
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstring));
} PROCESS(errctx) {
} FINISH(errctx, true);
ATTEMPT {
// Doesn't matter which child we use for this test
child = parent->children[1];
CATCH(errctx, parent->addchild(parent, child));
} CLEANUP {
if ( errctx == NULL ) {
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_RELATIONSHIP) {
// Expected behavior
SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, parent->addchild(parent, child));
} CLEANUP {
if ( errctx == NULL ) {
FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
}
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
// Expected behavior
SDL_Log("addchild throws AKERR_OUTOFBOUNDS when all children already set");
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_release_actor(parent));
// All actor objects on the heap should be empty now
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
FAIL_NONZERO_BREAK(errctx, akgl_heap_actors[i].refcount, AKERR_VALUE, "Actor not properly cleared");
FAIL_NONZERO_BREAK(errctx, akgl_heap_actors[i].parent, AKERR_VALUE, "Actor not properly cleared");
for ( j = 0 ; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
if ( akgl_heap_actors[i].children[j] != NULL ) {
FAIL(errctx, AKERR_VALUE, "Actor not properly cleared");
goto _test_actor_addchild_heaprelease_cleanup;
}
}
}
for ( j = 0; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
sprintf((char *)&tmpstring->data, "child %d", i);
FAIL_NONZERO_BREAK(
errctx,
SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&tmpstring->data, NULL),
AKERR_KEY,
"Child %s was not removed from the registry",
(char *)&tmpstring->data);
}
_test_actor_addchild_heaprelease_cleanup:
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&parent));
CATCH(errctx, akgl_actor_initialize(parent, "parent"));
CATCH(errctx, akgl_heap_next_actor(&child));
CATCH(errctx, akgl_actor_initialize(child, "child"));
// Don't release our claim on the child. The child should not be reclaimed.
CATCH(errctx, akgl_heap_release_actor(parent));
FAIL_NONZERO_BREAK(errctx, child->parent, AKERR_VALUE, "Child still references released parent");
FAIL_ZERO_BREAK(errctx, child->refcount, AKERR_VALUE, "Child prematurely released");
FAIL_NONZERO_BREAK(errctx, strcmp(child->name, "child"), AKERR_VALUE, "Child had identity erased");
FAIL_ZERO_BREAK(
errctx,
(child == SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, child->name, NULL)),
AKERR_KEY,
"Child prematurely removed from the registry");
// Now we can release the child
CATCH(errctx, akgl_heap_release_actor(child));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Build an actor bound to a character, without touching the renderer.
*
* The sprite and spritesheet are taken straight off the heap and populated by
* hand, because akgl_sprite_load_json() would need a live renderer to build a
* texture and none of the logic under test reads one.
*/
static akerr_ErrorContext *make_bound_actor(
akgl_Actor **actor,
akgl_Character **basechar,
akgl_Sprite **sprite,
char *name,
int state)
{
PREPARE_ERROR(e);
akgl_SpriteSheet *sheet = NULL;
char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
ATTEMPT {
CATCH(e, akgl_heap_next_character(basechar));
CATCH(e, akgl_character_initialize(*basechar, name));
CATCH(e, akgl_heap_next_spritesheet(&sheet));
sheet->refcount += 1;
snprintf((char *)&spritename, AKGL_SPRITE_MAX_NAME_LENGTH, "%s_sprite", name);
CATCH(e, akgl_heap_next_sprite(sprite));
CATCH(e, akgl_sprite_initialize(*sprite, (char *)&spritename, sheet));
(*sprite)->width = 32;
(*sprite)->height = 32;
(*sprite)->frames = 4;
(*sprite)->speed = 100;
CATCH(e, akgl_character_sprite_add(*basechar, *sprite, state));
CATCH(e, akgl_heap_next_actor(actor));
CATCH(e, akgl_actor_initialize(*actor, name));
(*actor)->basechar = *basechar;
(*actor)->state = state;
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_on(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 7.0f;
basechar.ay = 11.0f;
// Left: clears every facing and movement bit, then sets its own pair and
// takes acceleration from the base character with the sign reversed.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.state = (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_ALIVE);
TEST_EXPECT_OK(e, akgl_actor_cmhf_left_on(&actor, &event), "left on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left on did not set MOVING_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"left on did not set FACE_LEFT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"left on left MOVING_RIGHT set (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"left on left FACE_RIGHT set (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
"left on cleared an unrelated state bit (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ax, -7.0f, "left on set ax to %f, expected -7", actor.ax);
// Right: same, with the sign preserved.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_cmhf_right_on(&actor, &event), "right on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right on did not set MOVING_RIGHT (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"right on did not set FACE_RIGHT (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ax, 7.0f, "right on set ax to %f, expected 7", actor.ax);
// Up reverses the Y acceleration, because Y grows downward.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_cmhf_up_on(&actor, &event), "up on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up on did not set MOVING_UP (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"up on did not set FACE_UP (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ay, -11.0f, "up on set ay to %f, expected -11", actor.ay);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_cmhf_down_on(&actor, &event), "down on");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down on did not set MOVING_DOWN (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
"down on did not set FACE_DOWN (state %d)", actor.state);
TEST_ASSERT_FEQ(e, actor.ay, 11.0f, "down on set ay to %f, expected 11", actor.ay);
// Each direction is exclusive: turning right after left leaves only right.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_cmhf_left_on(&actor, &event), "left on before turning");
TEST_EXPECT_OK(e, akgl_actor_cmhf_right_on(&actor, &event), "right on after left");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"turning right left MOVING_LEFT set (state %d)", actor.state);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_off(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 7.0f;
basechar.ay = 11.0f;
// Releasing a direction stops the actor pushing: acceleration and thrust
// go to zero. It does *not* touch the environmental force on that axis.
//
// It used to zero `e` and `v` as well, and that is a real bug rather
// than a style difference: `ey` is where the arcade backend accumulates
// gravity, so tapping down while falling zeroed the fall and the
// character stopped dead in mid-air. `tests/physics_sim.c` measures it
// (`vertical release keeps gravity`). Velocity is not the actor's to
// clear either -- akgl_physics_simulate recomputes `vx` as `ex + tx`
// every step, so whatever a handler writes there is overwritten before
// anything can read it.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT);
TEST_EXPECT_OK(e, akgl_actor_cmhf_left_off(&actor, &event), "left off");
TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "left off left ax at %f", actor.ax);
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
TEST_ASSERT_FEQ(e, actor.ex, 5.0f, "left off cleared ex (%f), which is the world's, not the actor's", actor.ex);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
"left off did not clear MOVING_LEFT (state %d)", actor.state);
// Facing is deliberately sticky, so an idle actor keeps looking where it was.
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"left off cleared FACE_LEFT, which should persist (state %d)", actor.state);
// The Y axis is untouched by a horizontal release.
TEST_ASSERT_FEQ(e, actor.ay, 5.0f, "left off disturbed ay (%f)", actor.ay);
TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "left off disturbed ey (%f)", actor.ey);
TEST_ASSERT_FEQ(e, actor.ty, 5.0f, "left off disturbed ty (%f)", actor.ty);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_cmhf_right_off(&actor, &event), "right off");
TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "right off left ax at %f", actor.ax);
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "right off left tx at %f", actor.tx);
TEST_ASSERT_FEQ(e, actor.ex, 5.0f, "right off cleared ex (%f)", actor.ex);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
"right off did not clear MOVING_RIGHT (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_cmhf_up_off(&actor, &event), "up off");
TEST_ASSERT_FEQ(e, actor.ay, 0.0f, "up off left ay at %f", actor.ay);
TEST_ASSERT_FEQ(e, actor.ty, 0.0f, "up off left ty at %f", actor.ty);
TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "up off cleared ey (%f), which is where gravity accumulates", actor.ey);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
"up off did not clear MOVING_UP (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_cmhf_down_off(&actor, &event), "down off");
TEST_ASSERT_FEQ(e, actor.ay, 0.0f, "down off left ay at %f", actor.ay);
TEST_ASSERT_FEQ(e, actor.ty, 0.0f, "down off left ty at %f", actor.ty);
TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "down off cleared ey (%f), which is where gravity accumulates", actor.ey);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
"down off did not clear MOVING_DOWN (state %d)", actor.state);
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_control_handlers_nullpointers(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
SDL_Event event;
ATTEMPT {
memset(&event, 0x00, sizeof(SDL_Event));
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_off(NULL, &event), "down off, NULL actor");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_off(&actor, NULL), "down off, NULL event");
// A movement handler reads acceleration off the base character, so an
// actor without one has to be reported rather than dereferenced.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(&actor, &event),
"left on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(&actor, &event),
"right on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(&actor, &event),
"up on, actor with no base character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(&actor, &event),
"down on, actor with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_automatic_face(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
ATTEMPT {
// With the flag off, facing is left entirely to the caller.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = false;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face with the flag off");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face changed facing while disabled (state %d)", actor.state);
// With the flag on, facing follows movement.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving left");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"automatic face did not turn the actor left (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face left the stale facing set (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving right");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
"automatic face did not turn the actor right (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving up");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"automatic face did not turn the actor up (state %d)", actor.state);
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving down");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
"automatic face did not turn the actor down (state %d)", actor.state);
// Left wins over up when both are set, per the order of the checks.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_UP);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving diagonally");
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
"diagonal movement did not resolve to the left facing (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_UP),
"diagonal movement set two facings at once (state %d)", actor.state);
// A stationary actor ends up facing nowhere.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.movement_controls_face = true;
actor.state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP);
TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while stationary");
TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_ALL),
"a stationary actor kept a facing bit (state %d)", actor.state);
TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
"automatic face cleared an unrelated state bit (state %d)", actor.state);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_automatic_face(NULL),
"automatic face with a NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_logic_movement(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Character basechar;
ATTEMPT {
memset(&basechar, 0x00, sizeof(akgl_Character));
basechar.ax = 3.0f; basechar.ay = 4.0f;
basechar.sx = 30.0f; basechar.sy = 40.0f; basechar.sz = 50.0f;
// Max speed is always refreshed from the base character.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.basechar = &basechar;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while idle");
TEST_ASSERT_FEQ(e, actor.sx, 30.0f, "sx copied as %f, expected 30", actor.sx);
TEST_ASSERT_FEQ(e, actor.sy, 40.0f, "sy copied as %f, expected 40", actor.sy);
TEST_ASSERT_FEQ(e, actor.sz, 50.0f, "sz copied as %f, expected 50", actor.sz);
actor.state = AKGL_ACTOR_STATE_MOVING_LEFT;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving left");
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "moving left set ax to %f, expected -3", actor.ax);
actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving right");
TEST_ASSERT_FEQ(e, actor.ax, 3.0f, "moving right set ax to %f, expected 3", actor.ax);
actor.state = AKGL_ACTOR_STATE_MOVING_UP;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving up");
TEST_ASSERT_FEQ(e, actor.ay, -4.0f, "moving up set ay to %f, expected -4", actor.ay);
actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving down");
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "moving down set ay to %f, expected 4", actor.ay);
// Both axes at once.
actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_DOWN);
TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving diagonally");
TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "diagonal movement set ax to %f, expected -3", actor.ax);
TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "diagonal movement set ay to %f, expected 4", actor.ay);
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(NULL, 1.0f),
"movement logic with a NULL actor");
// Every value the movement logic writes is read off the base character,
// so an actor without one has to be reported rather than dereferenced.
memset(&actor, 0x00, sizeof(akgl_Actor));
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(&actor, 1.0f),
"movement logic with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_logic_changeframe(void)
{
PREPARE_ERROR(e);
akgl_Actor actor;
akgl_Sprite sprite;
ATTEMPT {
memset(&sprite, 0x00, sizeof(akgl_Sprite));
sprite.frames = 4;
// Mid-animation, the frame simply advances.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 1;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing mid-animation");
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
"frame advanced to %d, expected 2", actor.curSpriteFrameId);
// At the last frame without looping, it wraps to the start.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = false;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a non-looping sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"the last frame wrapped to %d, expected 0", actor.curSpriteFrameId);
// A forward-looping sprite behaves the same way at the end.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = true;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a forward-looping sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"the forward loop wrapped to %d, expected 0", actor.curSpriteFrameId);
// A ping-pong sprite turns around at the end instead of wrapping.
memset(&actor, 0x00, sizeof(akgl_Actor));
actor.curSpriteFrameId = 3;
sprite.loop = true;
sprite.loopReverse = true;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "reversing at the end");
TEST_ASSERT(e, actor.curSpriteReversing == true,
"the sprite did not enter its reverse phase at the last frame");
TEST_ASSERT(e, actor.curSpriteFrameId == 2,
"reversing stepped to frame %d, expected 2", actor.curSpriteFrameId);
// While reversing, the frame counts back down.
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "stepping back while reversing");
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
"reversing stepped to frame %d, expected 1", actor.curSpriteFrameId);
// At frame zero it turns around again and resumes going forward.
actor.curSpriteFrameId = 0;
actor.curSpriteReversing = true;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "turning around at frame zero");
TEST_ASSERT(e, actor.curSpriteReversing == false,
"the sprite stayed in its reverse phase at frame zero");
TEST_ASSERT(e, actor.curSpriteFrameId == 1,
"turning around stepped to frame %d, expected 1", actor.curSpriteFrameId);
// A single-frame sprite has nowhere to advance to.
memset(&actor, 0x00, sizeof(akgl_Actor));
sprite.frames = 1;
sprite.loop = false;
sprite.loopReverse = false;
TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing a single-frame sprite");
TEST_ASSERT(e, actor.curSpriteFrameId == 0,
"a single-frame sprite moved to frame %d, expected 0", actor.curSpriteFrameId);
sprite.frames = 4;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_changeframe(NULL, &sprite, 0),
"changeframe with a NULL actor");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/** @brief Records calls made to the changeframe stub. */
static int changeframe_calls = 0;
/** @brief Changeframe stub that records its invocation without advancing. */
static akerr_ErrorContext *stub_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems)
{
PREPARE_ERROR(e);
changeframe_calls += 1;
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_update(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
SDL_Time now = 0;
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_sprite());
CATCH(e, akgl_registry_init_spritesheet());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "updatable", AKGL_ACTOR_STATE_ALIVE));
actor->changeframefunc = &stub_changeframe;
// Long enough since the last frame change, so the sprite advances.
SDL_GetCurrentTime(&now);
sprite->speed = 1;
actor->curSpriteFrameTimer = 0;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is due");
TEST_ASSERT(e, changeframe_calls == 1,
"a due frame change fired %d times, expected 1", changeframe_calls);
TEST_ASSERT(e, actor->curSpriteFrameTimer != 0,
"the frame timer was not restamped after a frame change");
// Too soon since the last change, so nothing happens.
SDL_GetCurrentTime(&now);
sprite->speed = 1000000000;
actor->curSpriteFrameTimer = now;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is not due");
TEST_ASSERT(e, changeframe_calls == 0,
"an early frame change fired %d times, expected 0", changeframe_calls);
// An actor in a state with no sprite bound is skipped, not failed: the
// missing binding is reported as AKERR_KEY and swallowed by update.
actor->state = AKGL_ACTOR_STATE_DEAD;
changeframe_calls = 0;
TEST_EXPECT_OK(e, akgl_actor_update(actor),
"updating an actor whose state has no sprite");
TEST_ASSERT(e, changeframe_calls == 0,
"an actor with no sprite for its state still changed frames");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(NULL),
"updating a NULL actor");
// An actor with no base character cannot resolve a sprite at all.
actor->basechar = NULL;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(actor),
"updating an actor with no base character");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_character_sprite_binding(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
akgl_Sprite *found = NULL;
ATTEMPT {
CATCH(e, akgl_registry_init_actor());
CATCH(e, akgl_registry_init_sprite());
CATCH(e, akgl_registry_init_spritesheet());
CATCH(e, akgl_registry_init_character());
CATCH(e, akgl_heap_init());
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "bound", AKGL_ACTOR_STATE_ALIVE));
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, &found),
"reading back a bound sprite");
TEST_ASSERT(e, found == sprite, "the bound sprite came back as a different object");
// A composite state is a distinct key from either of its components.
TEST_EXPECT_STATUS(e, AKERR_KEY,
akgl_character_sprite_get(basechar,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
&found),
"reading a composite state that was never bound");
TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_character_sprite_get(basechar, 0, &found),
"reading state zero, which was never bound");
// Binding a composite state makes it resolvable.
TEST_EXPECT_OK(e, akgl_character_sprite_add(basechar, sprite,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP)),
"binding a sprite to a composite state");
TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar,
(AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
&found),
"reading back the composite binding");
TEST_ASSERT(e, found == sprite, "the composite binding resolved to a different sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_get(NULL, 0, &found),
"sprite_get with a NULL character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, NULL),
"sprite_get with a NULL destination");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(NULL, sprite, 0),
"sprite_add with a NULL character");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(basechar, NULL, 0),
"sprite_add with a NULL sprite");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
{
PREPARE_ERROR(e);
akgl_Sprite sprite;
SDL_FRect coords;
ATTEMPT {
memset(&sprite, 0x00, sizeof(akgl_Sprite));
sprite.width = 32;
sprite.height = 32;
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(NULL, &coords, 0),
"sheet coords with a NULL sprite");
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(&sprite, NULL, 0),
"sheet coords with a NULL rectangle");
// The sprite has no sheet attached, so there is no texture to measure against.
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(&sprite, &coords, 0),
"sheet coords with a NULL spritesheet");
} CLEANUP {
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
/*
* A render backend whose draw_texture records what it was asked to draw instead
* of drawing it. akgl_actor_render's whole output is the rectangle it hands to
* draw_texture, so that rectangle is the only thing there is to assert on.
*/
static akgl_RenderBackend testbackend;
static SDL_FRect lastdrawdest;
static SDL_FRect lastdrawsrc;
static int drawcalls = 0;
static akerr_ErrorContext *record_draw_texture(
akgl_RenderBackend *self,
SDL_Texture *texture,
SDL_FRect *src,
SDL_FRect *dest,
double angle,
SDL_FPoint *center,
SDL_FlipMode flip)
{
PREPARE_ERROR(errctx);
if ( src != NULL ) {
lastdrawsrc = *src;
}
if ( dest != NULL ) {
lastdrawdest = *dest;
}
drawcalls += 1;
SUCCEED_RETURN(errctx);
}
/**
* @brief akgl_actor_render must take the drawn height from the sprite's height.
*
* It read `curSprite->width` for both, so every actor was drawn square and a
* non-square sprite was stretched or squashed. Invisible in the fixtures
* because they are square, which is exactly why this test uses a sprite that
* is not: 48 wide by 24 high, so a regression puts 48 in both.
*
* This is also the first test of akgl_actor_render at all -- everything else in
* this file stubs renderfunc out.
*/
akerr_ErrorContext *test_actor_render_uses_sprite_height(void)
{
PREPARE_ERROR(e);
akgl_Actor *actor = NULL;
akgl_Character *basechar = NULL;
akgl_Sprite *sprite = NULL;
SDL_Texture *sheettexture = NULL;
ATTEMPT {
CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "renderactor", AKGL_ACTOR_STATE_ALIVE));
// akgl_spritesheet_coords_for_frame reads sheet->texture->w, so the
// sheet needs a real one even though nothing is rasterized.
sheettexture = SDL_CreateTexture(
testbackend.sdl_renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
96, 48);
FAIL_ZERO_BREAK(e, sheettexture, AKGL_ERR_SDL, "%s", SDL_GetError());
sprite->sheet->texture = sheettexture;
sprite->width = 48;
sprite->height = 24;
sprite->frames = 2;
actor->curSpriteFrameId = 0;
actor->visible = true;
actor->scale = 1.0f;
actor->x = 0;
actor->y = 0;
akgl_camera->x = 0;
akgl_camera->y = 0;
akgl_camera->w = 640;
akgl_camera->h = 480;
drawcalls = 0;
TEST_EXPECT_OK(e, akgl_actor_render(actor), "rendering a non-square actor");
TEST_ASSERT(e, drawcalls == 1, "actor render made %d draw calls, expected 1", drawcalls);
TEST_ASSERT_FEQ(e, lastdrawdest.w, 48.0f,
"drawn width is %f, expected the sprite's 48", lastdrawdest.w);
TEST_ASSERT_FEQ(e, lastdrawdest.h, 24.0f,
"drawn height is %f, expected the sprite's 24 -- it used to take the width",
lastdrawdest.h);
// The source rectangle has always come from the sprite properly; assert
// it too, so a regression cannot move the bug one line up.
TEST_ASSERT_FEQ(e, lastdrawsrc.w, 48.0f, "source width is %f, expected 48", lastdrawsrc.w);
TEST_ASSERT_FEQ(e, lastdrawsrc.h, 24.0f, "source height is %f, expected 24", lastdrawsrc.h);
// Scale multiplies both, and must not collapse them back together.
actor->scale = 2.0f;
drawcalls = 0;
TEST_EXPECT_OK(e, akgl_actor_render(actor), "rendering a scaled non-square actor");
TEST_ASSERT_FEQ(e, lastdrawdest.w, 96.0f, "scaled width is %f, expected 96", lastdrawdest.w);
TEST_ASSERT_FEQ(e, lastdrawdest.h, 48.0f, "scaled height is %f, expected 48", lastdrawdest.h);
} CLEANUP {
if ( sprite != NULL && sprite->sheet != NULL ) {
sprite->sheet->texture = NULL;
}
if ( sheettexture != NULL ) {
SDL_DestroyTexture(sheettexture);
}
if ( actor != NULL ) {
IGNORE(akgl_heap_release_actor(actor));
}
if ( basechar != NULL ) {
IGNORE(akgl_heap_release_character(basechar));
}
} PROCESS(e) {
} FINISH(e, true);
SUCCEED_RETURN(e);
}
int main(void)
{
akgl_actor_updated = 0;
akgl_actor_rendered = 0;
UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet());
CATCH(errctx, akgl_registry_init_character());
// A renderer, so akgl_actor_render can be driven at all. The backend is
// bound and then has its draw_texture replaced, which is the point:
// what the function computes is only visible in what it hands over.
if ( !SDL_Init(SDL_INIT_VIDEO) ) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
if ( !SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_actor", 640, 480,
SDL_WINDOW_HIDDEN, &akgl_window, &testbackend.sdl_renderer) ) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
CATCH(errctx, akgl_render_2d_bind(&testbackend));
testbackend.draw_texture = &record_draw_texture;
akgl_renderer = &testbackend;
akgl_camera = &akgl_default_camera;
CATCH(errctx, test_registry_actor_iterator_nullpointers());
CATCH(errctx, test_registry_actor_iterator_missingactor());
CATCH(errctx, test_registry_actor_iterator_updaterender());
CATCH(errctx, test_akgl_actor_set_character());
CATCH(errctx, test_actor_manage_children());
CATCH(errctx, test_actor_control_handlers_on());
CATCH(errctx, test_actor_control_handlers_off());
CATCH(errctx, test_actor_control_handlers_nullpointers());
CATCH(errctx, test_actor_automatic_face());
CATCH(errctx, test_actor_logic_movement());
CATCH(errctx, test_actor_logic_changeframe());
CATCH(errctx, test_actor_update());
CATCH(errctx, test_actor_character_sprite_binding());
CATCH(errctx, test_actor_sprite_sheet_coords());
CATCH(errctx, test_actor_render_uses_sprite_height());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
return 0;
}