Closes internal-consistency items 7 through 15. Nineteen non-static functions were in the ABI with no declaration anywhere, so no consumer could call them and any consumer could collide with them. The four gamepad_handle_* functions are the ones that mattered: controller.h declared akgl_controller_handle_button_down and three siblings that did not exist, so anything compiled against the header alone failed to link. The definitions carry the declared names now, which also closes Defects -> Known and still open item 10, and their documentation moved to the header. The rest are either declared under a "part of the internal API" block -- akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to declare for itself, plus six tilemap loader helpers the untested-loader work wants to reach -- or static, which is what the four save iterators and load_objectnamemap should always have been. akgl_path_relative_from is deleted: declared nowhere, called from nowhere, never wrote its output, and leaked a pooled string on every call, so it closes Known and still open item 4 and item 40 by ceasing to exist. scripts/check_api_surface.sh keeps it closed. It reads the built library's dynamic symbol table and every public header with comments stripped, and fails on an exported akgl_* symbol that is declared nowhere. Stripping comments is the whole point -- four of these were mentioned in controller.h prose, which is how they went unnoticed. The pool-size ceilings are defined once, in heap.h, so the #ifndef override hook fires for the first time; actor.h, sprite.h and character.h were defining the same four unconditionally from headers heap.h includes above its own guard. tests/header_pool_override.c fails the compile if that regresses. Also here: (void) rather than () on the twelve no-argument entry points, AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix dropped, and the six parameter-name mismatches. akgl_get_json_with_default had its two contexts swapped rather than merely misspelled -- the incoming one was `err` and its own was `e`, which is the name reserved for an incoming one. 24/24 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
265 lines
11 KiB
C
265 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)
|
|
{
|
|
akgl_Sprite *target = NULL;
|
|
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);
|
|
target = *dest;
|
|
//SDL_Log("Sprite state %d (%s) has character %s", state, (char *)&stateval, target->name);
|
|
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));
|
|
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
|
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);
|
|
}
|