Files
libakgl/tests/actor.c
Andrew Kesterson 913834a3af Draw actors at their sprite's height, and update each one once a frame
Closes Defects item 26 and Performance item 32.

akgl_actor_render set dest.h from curSprite->width, so every actor was drawn
square and a non-square sprite was stretched or squashed. Invisible in the
fixtures because they are all square, so tests/actor.c gets a 48x24 sprite and
a render backend whose draw_texture records the rectangle it is handed instead
of drawing it. That recording backend is the first coverage akgl_actor_render
has had at all -- every other test in the file stubs renderfunc out.

akgl_game_update looped over AKGL_TILEMAP_MAX_LAYERS with the actor sweep
nested inside it and never compared an actor's layer to the layer it was on, so
every live actor's updatefunc ran sixteen times a frame. The sweep is hoisted
out: updating an actor is not a per-layer operation, and
akgl_render_2d_draw_world already walks the layers for the half that is.
AKGL_ITERATOR_OP_LAYERMASK is honoured rather than ignored now, so a caller who
wants one layer can still ask, and gets each of those actors once.

Counting is the assertion, deliberately. The defect was invisible in the frame
total because the tilemap blits are three orders of magnitude larger, so a
timing test would have measured the rasterizer. tests/game.c counts calls into
a stub updatefunc and reports 16 against the old code.

PERFORMANCE.md records the timing side as what it honestly is: the gap between
the akgl_game_update and draw_world rows of the same run, 92 us before and
noise in both directions after. The absolute table is not re-taken -- a later
run on this machine read every row about 15% high, including rows nothing has
touched.

25/25 pass, reindent --check clean.

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

1068 lines
41 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 ) {
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 zeroes that axis outright: acceleration, thrust,
// environmental force, and velocity all go to zero together.
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.ex, 0.0f, "left off left ex at %f", actor.ex);
TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
TEST_ASSERT_FEQ(e, actor.vx, 0.0f, "left off left vx at %f", actor.vx);
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.vy, 5.0f, "left off disturbed vy (%f)", actor.vy);
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.vx, 0.0f, "right off left vx at %f", actor.vx);
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.vy, 0.0f, "up off left vy at %f", actor.vy);
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.vy, 0.0f, "down off left vy at %f", actor.vy);
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());
TEST_TRAP_UNHANDLED_ERRORS();
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;
}