Files
libakgl/src/character.c
Andrew Kesterson 19530f6a97 Fix the type, macro and state-table defects, and the leftover debris
Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180
redundant casts, is deliberately left open with its reasoning in TODO.md: the
benefit only arrives once the build turns on the warnings those casts suppress,
and doing it before that is churn across the two files with the most
outstanding functional defects.

The two that were real bugs are in the actor state table.
AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and
defined [32], so a consumer trusting the declared bound read past the object;
and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h
has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to
either state. tests/registry.c now walks the whole table -- every entry
non-NULL, every entry resolving to its own bit, no two entries sharing a name.

The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the
semicolon inside its body. Writing tests/bitmasks.c for that turned up
something worth knowing: the obvious test does not catch it. For a bit that is
set, the misparse `!(mask & bit) == bit` gives the same answer as the correct
one. It only diverges for an unset bit whose value is not 1, and that is the
shape the suite uses now.

akgl_draw_background was the last public function outside the error protocol.
It takes a backend like everything else in draw.h, restores the draw colour it
found, and is tested -- TODO.md had it filed under "needs the offscreen
renderer harness", which was never true; what it needed was to stop reading the
global.

All eight registry initializers go through one helper, so the seven that leaked
an SDL_PropertiesID on every call after the first no longer do. Fixed in the
same place because it is the same function: akgl_registry_init never called
akgl_registry_init_properties, which made akgl_set_property a silent no-op for
anyone not going through akgl_game_init -- Defects, Known and still open item 3.

Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame
deleted, float32_t/float64_t used consistently, the developer-specific debug
logging removed from the controller inner loop, the abandoned SDL_GetBasePath
comments removed, nine unused locals removed, and dst renamed to dest.

akgl_game_update's default flags no longer OR the same bit twice. That changes
nothing today, and the reason is Performance item 32: the loop never reads
either bit, which is why every actor is updated sixteen times a frame. Still
open.

25/25 pass, memcheck clean, reindent --check clean.

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

261 lines
11 KiB
C

/**
* @file character.c
* @brief Implements the character subsystem.
*/
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
#include <string.h>
#include <jansson.h>
#include <akgl/game.h>
#include <akgl/sprite.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/iterator.h>
#include <akgl/util.h>
akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference");
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer");
memset(obj, 0x00, sizeof(akgl_Character));
strncpy(obj->name, name, AKGL_CHARACTER_MAX_NAME_LENGTH);
obj->state_sprites = SDL_CreateProperties();
FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
obj->sprite_add = &akgl_character_sprite_add;
obj->sprite_get = &akgl_character_sprite_get;
FAIL_ZERO_RETURN(
errctx,
SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
AKERR_KEY,
"Unable to add character to registry");
obj->refcount += 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state)
{
PREPARE_ERROR(errctx);
char stateval[32];
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference");
memset(&stateval, 0x00, 32);
SDL_itoa(state, (char *)&stateval, 10);
SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref);
SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
ref->refcount += 1;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest)
{
PREPARE_ERROR(errctx);
char stateval[32];
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)");
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
memset(&stateval, 0x00, 32);
SDL_itoa(state, (char *)&stateval, 10);
*dest = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state);
SUCCEED_RETURN(errctx);
}
// SDL iterator so we can't return error information here, void only
// this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name)
{
PREPARE_ERROR(errctx);
akgl_Sprite *spriteptr;
akgl_Iterator *opflags = (akgl_Iterator *)userdata;
ATTEMPT {
FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer");
FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name");
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(props, name, NULL);
FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name);
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) {
CATCH(errctx, akgl_heap_release_sprite(spriteptr));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
/**
* @brief OR a JSON array of actor-state names together into one bitmask.
*
* This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather
* than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS
* and OR-ed in. A name that is not a known state is an error rather than being
* skipped -- a typo in a state name would otherwise bind a sprite to the wrong
* combination and show up as art that never appears.
*
* @param states JSON array of state-name strings. Required. An empty array is
* legal and leaves @p dest as it was.
* @param dest Receives the OR of every named bit. Required in practice, and
* **not** checked -- the guard that should test it tests @p states
* a second time instead, so a `NULL` here is a crash. It is OR-ed
* into rather than assigned, so the caller must zero it first.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p states is `NULL`.
* @throws AKERR_KEY If a name is not in the actor-state registry. The message
* quotes it. Note that bits 11 and 12 are unreachable by name; see
* akgl_registry_init_actor_state_strings.
* @throws AKERR_TYPE If an array element is not a string.
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest)
{
int i = 0;
long newstate = 0;
akgl_String *tmpstring = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstring));
for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
CATCH(errctx, akgl_get_json_array_index_string(states, i, &tmpstring));
newstate = (long)SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0);
FAIL_ZERO_BREAK(errctx, newstate, AKERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data);
*dest = (*dest | (int)(newstate));
}
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstring));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief Name a character and build its state-to-sprite map from a JSON document.
*
* The half of akgl_character_load_json that deals with identity and sprites; the
* caller handles the numeric fields afterwards. Reads `name`, initializes the
* character with it, then walks `sprite_mappings` binding each named sprite to
* the bitmask its `state` array adds up to.
*
* Every sprite named must already be in #AKGL_REGISTRY_SPRITE.
*
* @param json The parsed character document. Required in practice; not checked,
* and passed straight to accessors that report it.
* @param obj The pooled character to fill in. Required, unchecked -- it is
* handed to akgl_character_initialize, which does check it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered.
* The message names the character, the state, and the sprite.
* @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or
* `state` is absent, or if a state name is unknown.
* @throws AKERR_TYPE If one of those keys has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *character_load_json_inner(json_t *json, akgl_Character *obj)
{
PREPARE_ERROR(errctx);
json_t *mappings = NULL;
json_t *curmapping = NULL;
json_t *statearray = NULL;
akgl_Sprite *spriteptr = NULL;
int i = 0;
akgl_String *tmpstr = NULL;
akgl_String *tmpstr2 = NULL;
int stateval = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "sprite_mappings", &mappings));
for ( i = 0; i < json_array_size((json_t *)mappings) ; i++ ) {
stateval = 0;
CATCH(errctx, akgl_get_json_array_index_object((json_t *)mappings, i, &curmapping));
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(
AKGL_REGISTRY_SPRITE,
tmpstr->data,
NULL
);
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval));
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
FAIL_ZERO_BREAK(
errctx,
spriteptr,
AKERR_NULLPOINTER,
"Character %s for state %b references sprite %s but not found in the registry",
tmpstr2->data,
stateval,
tmpstr->data
);
CATCH(errctx, akgl_character_sprite_add((akgl_Character *)obj, (akgl_Sprite *)spriteptr, stateval));
}
} CLEANUP {
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
if ( tmpstr2 != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr2));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_character_load_json(char *filename)
{
PREPARE_ERROR(errctx);
json_t *json = NULL;
json_error_t error;
akgl_Character *obj = NULL;
//akgl_String *tmpstr = NULL;
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&obj));
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
json = (json_t *)json_load_file(filename, 0, &error);
FAIL_ZERO_BREAK(
errctx,
json,
AKERR_NULLPOINTER,
"Error while loading character from %s on line %d: %s", filename, error.line, error.text
);
CATCH(errctx, character_load_json_inner(json, obj));
CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime));
obj->speedtime = obj->speedtime * AKGL_TIME_ONEMS_NS;
CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx));
CATCH(errctx, akgl_get_json_number_value(json, "speed_y", &obj->sy));
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_x", &obj->ax));
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay));
} CLEANUP {
//IGNORE(akgl_heap_release_string(tmpstr));
// The character keeps nothing that points into the document -- every
// field above is copied out of it -- so the whole parsed tree goes back
// here, on the success path as well as the failure one.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
if ( errctx != NULL ) {
if ( errctx->status != 0 ) {
IGNORE(akgl_heap_release_character(obj));
}
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SDL_Log("Character %s loaded from %s", obj->name, filename);
SUCCEED_RETURN(errctx);
}