TODO.md item 37 said the cast sweep buys nothing until the build turns on the warnings those casts suppress. This is that precondition, and the argument turned out to be right with evidence: -Wall found three genuine signedness mismatches in sprite.c, where uint32_t width, height and speed were passed straight to akgl_get_json_integer_value(..., int *). A cast would have silenced all three. Fixing them properly also turned up that speed is scaled by a million into a 32-bit field, so anything past 4294 ms overflowed rather than being held. The other real finding was ten -Wstringop-truncation warnings, every one a fixed-width strncpy. Those leave a name field unterminated when the input fills it -- and those fields are registry keys, handed to strcmp and SDL property calls that do not stop at the field. Same overread class fixed twice already this release. All ten now use aksl_strncpy from akstdlib, which always terminates and never NUL-pads, bounded to size - 1 so an over-long name still truncates rather than being refused. staticstring.h documented the old strncpy semantics explicitly and has been rewritten to match. Two sites in game.c are deliberately left on strncpy: both stage into a pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding is part of the format. Neither is flagged. sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated string, which reads past the end of any shorter name. Not flagged by anything; found while converting its neighbours. -Werror is an option, default OFF, on only in the cmake_build and performance CI jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that -- so a target-level -Werror turns every diagnostic a newer compiler invents into a broken build for somebody else. The warning set is not even stable across build types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not, because they need the middle end. The two jobs that set it are one of each. -Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must match a signature, and 4 -Wimplicit-fallthrough from libakerror's own PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as deps/libakerror TODO item 8; the submodule bump carries it. deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE options reach it. Exempted with -w so a future semver update cannot fail this build -- verified by forcing -Wextra -Werror and watching our files fail while semver compiled clean. Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror; -Werror actually fails on a planted unused variable; an embedded consumer with AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
288 lines
12 KiB
C
288 lines
12 KiB
C
/**
|
|
* @file character.c
|
|
* @brief Implements the character subsystem.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <akerror.h>
|
|
#include <akstdlib.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));
|
|
// Always terminated: this is a registry key, and strncpy at exactly the
|
|
// field width leaves an over-long name unterminated.
|
|
PASS(errctx, aksl_strncpy(obj->name, sizeof(obj->name), name, sizeof(obj->name) - 1));
|
|
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];
|
|
akgl_Sprite *displaced = NULL;
|
|
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);
|
|
|
|
// Whatever this state was bound to is losing its binding, and the reference
|
|
// taken for that binding with it. Without this a character that rebinds a
|
|
// state while alive leaks one sprite slot per rebind -- releasing at
|
|
// teardown only gives back whatever the map happens to hold at the end.
|
|
displaced = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL);
|
|
|
|
// Take the new reference before anything can fail, so a failed write cannot
|
|
// leave the sprite bound with no reference behind it.
|
|
ref->refcount += 1;
|
|
if ( SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref) == false ) {
|
|
ref->refcount -= 1;
|
|
FAIL_RETURN(
|
|
errctx,
|
|
AKERR_KEY,
|
|
"Unable to bind sprite %s to character %s for state %d: %s",
|
|
(char *)&ref->name,
|
|
(char *)&basechar->name,
|
|
state,
|
|
SDL_GetError());
|
|
}
|
|
|
|
if ( (displaced != NULL) && (displaced != ref) ) {
|
|
PASS(errctx, akgl_heap_release_sprite(displaced));
|
|
}
|
|
SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state);
|
|
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, dest, 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);
|
|
}
|