Files
libakgl/src/actor.c

446 lines
17 KiB
C
Raw Normal View History

/**
* @file actor.c
* @brief Implements the actor subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <string.h>
#include <akerror.h>
#include <akgl/physics.h>
#include <akgl/game.h>
#include <akgl/sprite.h>
#include <akgl/actor.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/iterator.h>
akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "akgl_actor_initialize received null actor pointer");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "akgl_actor_initialize received null name string pointer");
memset(obj, 0x00, sizeof(akgl_Actor));
strncpy((char *)obj->name, name, AKGL_ACTOR_MAX_NAME_LENGTH);
obj->curSpriteReversing = false;
obj->scale = 1.0;
obj->movement_controls_face = true;
obj->updatefunc = &akgl_actor_update;
obj->renderfunc = &akgl_actor_render;
obj->facefunc = &akgl_actor_automatic_face;
obj->movementlogicfunc = &akgl_actor_logic_movement;
obj->changeframefunc = &akgl_actor_logic_changeframe;
obj->addchild = &akgl_actor_add_child;
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
AKERR_KEY,
"Unable to add actor to registry"
);
obj->refcount += 1;
SDL_Log("Actor %s initialized and added to the registry", (char *)obj->name);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_set_character(akgl_Actor *obj, char *basecharname)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
FAIL_ZERO_RETURN(errctx, basecharname, AKERR_NULLPOINTER, "Null character reference");
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Character not found in the registry");
obj->ax = 0;
obj->ay = 0;
obj->sx = obj->basechar->sx;
obj->sy = obj->basechar->sy;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_automatic_face(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
ATTEMPT {
if ( obj->movement_controls_face == true ) {
// TODO : This doesn't really work properly
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_FACE_ALL);
if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_LEFT);
} else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_RIGHT);
} else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_UP);
} else if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_DOWN);
}
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtime)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Null actor reference");
ATTEMPT {
// are we currently looping in reverse?
if ( curSprite->loop == true && obj->curSpriteReversing == true ) {
// are we at the beginning of the loop?
if ( obj->curSpriteFrameId == 0 ) {
obj->curSpriteReversing = false;
obj->curSpriteFrameId += 1;
} else {
obj->curSpriteFrameId -= 1;
}
// are we at the end of the animation?
} else if ( obj->curSpriteFrameId >= (curSprite->frames-1) ) {
// are we set to loop in reverse?
if ( curSprite->loop == true && curSprite->loopReverse == true ) {
obj->curSpriteReversing = true;
obj->curSpriteFrameId -= 1;
// are we set to loop forward?
} else {
// we are at the end of the animation and we either loop forward or do not loop
obj->curSpriteFrameId = 0;
}
// we are not looping in reverse and we are not at the end of the animation
} else {
obj->curSpriteFrameId += 1;
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
// raises AKGL_ERR_LOGICINTERRUPT if we don't want the physics simulator to process us
akerr_ErrorContext *akgl_actor_logic_movement(akgl_Actor *actor, float32_t dt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
FAIL_ZERO_RETURN(errctx, actor->basechar, AKERR_NULLPOINTER, "actor->basechar");
actor->sx = actor->basechar->sx;
actor->sy = actor->basechar->sy;
actor->sz = actor->basechar->sz;
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ) {
actor->ax = -actor->basechar->ax;
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
actor->ax = actor->basechar->ax;
}
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ) {
actor->ay = -actor->basechar->ay;
} else if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
actor->ay = actor->basechar->ay;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
SDL_Time curtime = 0;
akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
SDL_GetCurrentTime(&curtime);
PASS(errctx, obj->facefunc(obj));
ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
if ( ((curtime) - obj->curSpriteFrameTimer) >= curSprite->speed) {
CATCH(errctx, obj->changeframefunc(obj, curSprite, curtime));
obj->curSpriteFrameTimer = curtime;
}
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
// TODO : Why are we passing this error? It could only come from akgl_character_sprite_get
// or changeframefunc, both of which should never return AKERR_KEY...
SUCCEED_RETURN(errctx);
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
Document what the functions actually do instead of that they can fail The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:02:20 -04:00
* @brief Decide whether an actor is worth drawing this frame.
*
* Two questions at once: is the actor on camera, and does it want to be drawn.
* The camera test allows a sprite's own width and height on the near edges, so
* an actor partly on screen still counts as visible rather than popping in once
* its origin crosses the boundary.
*
* An actor with no sprite for its current state is reported as not visible
* rather than as an error -- there is nothing to draw, which is an answer.
*
* @param obj The actor to test. Required, along with its `basechar`.
* @param camera The visible rectangle in map coordinates. Required in practice;
* not checked, and dereferenced once a sprite has been found.
* @param visible Receives the verdict. Required; not checked. Written on the
* no-sprite path as well as the ordinary ones.
* @return `NULL` on success, otherwise an error context owned by the caller.
Document what the functions actually do instead of that they can fail The Doxygen comments were generated from the declarations, so 217 @throws lines across 21 headers read "When the corresponding validation or operation fails" and told a caller nothing beyond the status name. The @param lines were the same shape: every output was "Output destination populated by the function", every instance "Object to initialize, inspect, or modify". Rewritten against the implementations, following the pattern libakstdlib already uses: - @throws names the condition. akgl_sprite_load_json separated AKERR_KEY (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL and AKGL_ERR_HEAP, which it raises and never declared. - Parameters say whether they are required, what a NULL means, and what is written on a failure path. Where an argument is not checked, the doc says so: akgl_heap_next_actor's dest is a crash on NULL, not an error, and akgl_render_2d_frame_start dereferences self before testing it. - The conventions move up to the file blocks so the per-function docs stay short. json_helpers.h states once that absence is an error here and that json_t * results are borrowed; heap.h explains the pool model and the acquire asymmetry; physics.h carries the thrust/environmental/velocity table. - Struct fields, enum values, macros and exported globals are documented, including the dead ones - sprite_w/sprite_h, movetimer, p_scale and timer_gravity are read by nothing, and say so. Also fixes ten comments in error.h and audio.h that opened with /** rather than /**<, so Doxygen attached them to the following entity and rendered the text as part of the macro's value. Verified against the generated HTML. Comments only - no declaration changed. Doxygen builds clean under WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:02:20 -04:00
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
*/
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
{
PREPARE_ERROR(errctx);
akgl_Sprite *curSprite = NULL;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
// TODO: Actor has no sprite matching the current state. Should we treat this as an error and throw?
*visible = false;
SUCCEED_RETURN(errctx);
} FINISH(errctx, true);
if ( (obj->x < (camera->x - curSprite->width)) ||
(obj->x > (camera->x + camera->w)) ||
(obj->y < (camera->y - curSprite->height)) ||
(obj->y > (camera->y + camera->h)) ) {
*visible = false;
} else {
*visible = obj->visible;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
akgl_Sprite *curSprite = NULL;
bool visible = false;
SDL_FRect src;
SDL_FRect dest;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
ATTEMPT {
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
CATCH(errctx, actor_visible(obj, camera, &visible));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
} HANDLE_GROUP(errctx, AKERR_OUTOFBOUNDS) {
// If an actor doesn't have a sprite for a state, just log it and move on
LOG_ERROR(errctx);
} FINISH(errctx, true);
if ( ! visible ) {
SUCCEED_RETURN(errctx);
}
if ( (obj->curSpriteFrameId > curSprite->frames) ) {
// This isn't necessarily an error - this actor's frame index is outside the range of
// their current sprite. There are a number of reasons this could happen, and it will
// get cleaned up on the next logic update. Just pass on rendering them this frame.
SUCCEED_RETURN(errctx);
}
ATTEMPT {
CATCH(errctx,
akgl_sprite_sheet_coords_for_frame(
curSprite,
&src,
obj->curSpriteFrameId)
);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
if ( obj->parent != NULL ) {
dest.x = (obj->parent->x + obj->x - camera->x);
dest.y = (obj->parent->y + obj->y - camera->y);
} else {
dest.x = (obj->x - camera->x);
dest.y = (obj->y - camera->y);
}
dest.w = curSprite->width * obj->scale;
dest.h = curSprite->width * obj->scale;
PASS(errctx, renderer->draw_texture(renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
{
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");
FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
if ( obj->children[i] == NULL ) {
obj->children[i] = child;
child->parent = obj;
child->refcount += 1;
SUCCEED_RETURN(errctx);
}
}
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
}
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
{
PREPARE_ERROR(errctx);
akgl_Iterator *opflags = (akgl_Iterator *)userdata;
akgl_Actor *obj = NULL;
ATTEMPT {
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "registry_iterate_actor received NULL property name");
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "received NULL iterator flags");
obj = SDL_GetPointerProperty(registry, name, NULL);
FAIL_ZERO_BREAK(errctx, obj, AKERR_KEY, "registry_iterate_actor received property name that was not in the registry");
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) && obj->layer != opflags->layerid) {
break;
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_UPDATE)) {
CATCH(errctx, obj->updatefunc(obj));
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE)) {
CATCH(errctx, akgl_tilemap_scale_actor(gamemap, obj));
} else {
obj->scale = 1.0;
}
if (AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RENDER)) {
CATCH(errctx, obj->renderfunc(obj));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) moves actor left", event->type, event->gbutton.which, event->key.key);
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
obj->ax = -(obj->basechar->ax);
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT));
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) stops moving actor left", event->type, event->gbutton.which, event->key.key);
obj->ax = 0;
obj->ex = 0;
obj->tx = 0;
obj->vx = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) moves actor right", event->type, event->gbutton.which, event->key.key);
obj->ax = obj->basechar->ax;
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT));
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) stops moving actor right", event->type, event->gbutton.which, event->key.key);
obj->ax = 0;
obj->ex = 0;
obj->tx = 0;
obj->vx = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
//SDL_Log("event %d (button %d / key %d) moves actor up", event->type, event->gbutton.which, event->key.key);
obj->ay = -(obj->basechar->ay);
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_UP | AKGL_ACTOR_STATE_MOVING_UP));
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) stops moving actor up", event->type, event->gbutton.which, event->key.key);
obj->ay = 0;
obj->ey = 0;
obj->ty = 0;
obj->vy = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
Add physics, heap, json_helpers, game, and actor test suites Raise line coverage from 39.6 to 61.8 percent with four new suites and an extension to the actor suite, and register every suite through a single CMake list so a new test file cannot be left out of the coverage fixture. Give the test targets a build-tree RPATH and prepend the build tree to LD_LIBRARY_PATH for CTest, so a developer with a previously installed libakgl.so exercises the library that was just compiled. Fix six defects the new tests exposed: - akgl_physics_simulate read self->gravity_time before its NULL check, so a NULL backend crashed instead of reporting AKERR_NULLPOINTER. - akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose inside the PROCESS switch. An ordinary save never flushed or closed its stream and produced an empty file. - akgl_game_save_actors wrote each name table terminator from the address of a single char, emitting stack contents into the save file and a sentinel the loader could not recognize. - akgl_game_load_objectnamemap used CATCH directly inside while(1), where the break leaves the loop rather than propagating, so a truncated name table loaded as a successful game. - akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no NULL check, unlike their left and right counterparts. - akgl_actor_logic_movement checked actor twice instead of checking actor->basechar before dereferencing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "actor->basechar");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) moves actor down", event->type, event->gbutton.which, event->key.key);
obj->ay = obj->basechar->ay;
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN));
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event)
2025-08-08 22:39:48 -04:00
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
2025-08-09 09:28:28 -04:00
//SDL_Log("event %d (button %d / key %d) stops moving actor down", event->type, event->gbutton.which, event->key.key);
obj->ty = 0;
obj->ey = 0;
obj->ay = 0;
obj->vy = 0;
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_DOWN);
2025-08-09 09:28:28 -04:00
//SDL_Log("new target actor state: %b", obj->state);
2025-08-08 22:39:48 -04:00
SUCCEED_RETURN(errctx);
}