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>
234 lines
8.8 KiB
C
234 lines
8.8 KiB
C
/**
|
|
* @file sprite.c
|
|
* @brief Implements the sprite subsystem.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <string.h>
|
|
#include <jansson.h>
|
|
#include <akerror.h>
|
|
|
|
#include <libgen.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_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid)
|
|
{
|
|
PREPARE_ERROR(e);
|
|
|
|
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "NULL sprite");
|
|
FAIL_ZERO_RETURN(e, srccoords, AKERR_NULLPOINTER, "NULL SDL_Rect");
|
|
FAIL_ZERO_RETURN(e, self->sheet, AKERR_NULLPOINTER, "NULL spritesheet");
|
|
|
|
srccoords->x = self->width * self->frameids[frameid];
|
|
if ( srccoords->x >= self->sheet->texture->w ) {
|
|
srccoords->y = ((int)srccoords->x / self->sheet->texture->w) * self->height;
|
|
srccoords->x = ((int)srccoords->x % self->sheet->texture->w);
|
|
} else {
|
|
srccoords->y = 0;
|
|
}
|
|
srccoords->w = self->width;
|
|
srccoords->h = self->height;
|
|
|
|
SUCCEED_RETURN(e);
|
|
}
|
|
|
|
/**
|
|
* @brief Find or load the spritesheet a sprite definition names.
|
|
*
|
|
* Resolves the `spritesheet.filename` path relative to the sprite definition's
|
|
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
|
|
* A hit is reused as is -- which is what keeps ten sprites cut from one image
|
|
* down to one texture -- and a miss claims a sheet from the pool and loads it,
|
|
* reading `frame_width` and `frame_height` only in that case.
|
|
*
|
|
* @param json The parsed sprite document, whose `spritesheet` object
|
|
* this reads. Required in practice; not checked.
|
|
* @param sheet Receives the sheet, whether found or freshly loaded.
|
|
* Required; not checked. On the reuse path the sheet's
|
|
* reference count is *not* incremented, so the sprite
|
|
* borrows it.
|
|
* @param relative_path Directory to resolve the image path against -- the
|
|
* directory holding the sprite JSON. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
|
|
* has no `filename` -- or, on the load path, no `frame_width` or
|
|
* `frame_height`.
|
|
* @throws AKERR_TYPE If one of those has the wrong JSON type.
|
|
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
|
|
* @throws ENOENT If the image path does not exist.
|
|
* @throws AKGL_ERR_SDL If the image cannot be decoded.
|
|
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
|
|
*/
|
|
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
json_t *spritesheet_json = NULL;
|
|
int ss_frame_width = 0;
|
|
int ss_frame_height = 0;
|
|
akgl_String *ss_filename = NULL;
|
|
akgl_String *tmpstr = NULL;
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
|
CATCH(errctx, akgl_get_json_object_value((json_t *)json, "spritesheet", &spritesheet_json));
|
|
CATCH(errctx, akgl_get_json_string_value((json_t *)spritesheet_json, "filename", &ss_filename));
|
|
CATCH(errctx, akgl_path_relative(relative_path, ss_filename->data, tmpstr));
|
|
*sheet = SDL_GetPointerProperty(
|
|
AKGL_REGISTRY_SPRITESHEET,
|
|
(char *)&tmpstr->data,
|
|
NULL
|
|
);
|
|
if ( *sheet == NULL ) {
|
|
CATCH(errctx, akgl_heap_next_spritesheet(sheet));
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_width", &ss_frame_width));
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)spritesheet_json, "frame_height", &ss_frame_height));
|
|
CATCH(errctx,
|
|
akgl_spritesheet_initialize(
|
|
(akgl_SpriteSheet *)*sheet,
|
|
ss_frame_width,
|
|
ss_frame_height,
|
|
(char *)&tmpstr->data)
|
|
);
|
|
}
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(ss_filename));
|
|
IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
json_t *json = NULL;
|
|
json_t *frames = NULL;
|
|
json_error_t error;
|
|
akgl_Sprite *obj = NULL;
|
|
akgl_SpriteSheet *sheet = NULL;
|
|
akgl_String *spritename = NULL;
|
|
akgl_String *filename_copy = NULL;
|
|
int i = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
|
CATCH(errctx, akgl_heap_next_string(&filename_copy));
|
|
FAIL_NONZERO_BREAK(
|
|
errctx,
|
|
strlen(filename) >= AKGL_MAX_STRING_LENGTH,
|
|
AKERR_OUTOFBOUNDS,
|
|
"Sprite filename exceeds temporary string capacity"
|
|
);
|
|
SDL_strlcpy(filename_copy->data, filename, AKGL_MAX_STRING_LENGTH);
|
|
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
|
CATCH(errctx, akgl_heap_next_string(&spritename));
|
|
CATCH(errctx, akgl_string_initialize(spritename, 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 sprite from %s on line %d: %s", filename, error.line, error.text
|
|
);
|
|
|
|
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
|
|
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
|
|
CATCH(errctx,
|
|
akgl_sprite_initialize(
|
|
(akgl_Sprite *)obj,
|
|
spritename->data,
|
|
(akgl_SpriteSheet *)sheet)
|
|
);
|
|
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
|
|
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
|
|
obj->speed = obj->speed * AKGL_TIME_ONESEC_MS;
|
|
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
|
|
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
|
|
|
|
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
|
|
obj->frames = json_array_size((json_t *)frames);
|
|
for ( i = 0 ; i < obj->frames; i++ ) {
|
|
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
|
|
}
|
|
} CLEANUP {
|
|
if ( errctx != NULL && errctx->status != 0 ) {
|
|
IGNORE(akgl_heap_release_sprite(obj));
|
|
IGNORE(akgl_heap_release_spritesheet(sheet));
|
|
}
|
|
IGNORE(akgl_heap_release_string(spritename));
|
|
IGNORE(akgl_heap_release_string(filename_copy));
|
|
//IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
|
|
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
|
|
|
|
memset(spr, 0x00, sizeof(akgl_Sprite));
|
|
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
|
|
spr->sheet = sheet;
|
|
FAIL_ZERO_RETURN(
|
|
errctx,
|
|
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITE, (char *)&spr->name, (void *)spr),
|
|
AKERR_KEY,
|
|
"Unable to add sprite to registry");
|
|
spr->refcount += 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
//akgl_String *tmpstr = NULL;
|
|
|
|
ATTEMPT {
|
|
FAIL_ZERO_BREAK(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet pointer");
|
|
FAIL_ZERO_BREAK(errctx, filename, AKERR_NULLPOINTER, "Null filename pointer");
|
|
|
|
memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
|
|
|
|
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
|
|
|
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
|
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
|
|
|
|
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
|
sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
|
|
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
|
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
|
|
AKERR_KEY,
|
|
"Unable to add spritesheet to registry: %s",
|
|
SDL_GetError());
|
|
sheet->refcount += 1;
|
|
} CLEANUP {
|
|
//IGNORE(akgl_heap_release_string(tmpstr));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|