Files
libakgl/src/tilemap.c
Tachikoma da4cdb546d Report the failures libakstdlib's wrappers were already catching
Updates deps/libakstdlib to 669b2b3. That commit is a TODO entry rather
than new code, so the interesting part is what libakgl was not yet
calling: it links libakstdlib and uses ten of its wrappers while calling
the raw libc function at a good many more sites. Four of those were
carrying a real defect.

akgl_game_save checked every write and threw away the close. Every write
goes through aksl_fwrite, and for a record this size all of them land in
stdio's buffer -- nothing reaches the device until the close flushes it,
so a full disk, an exceeded quota or an NFS server going away is
reported only there. The function returned success over a savegame that
was never written. aksl_fclose surfaces it. tests/game.c reaches the
path through /dev/full, which accepts writes and fails at the flush;
against the unfixed code the test reports "expected a failure, got
success" with ENOSPC.

The close has to drop the FILE * before the status is examined, because
fclose(3) disassociates the stream either way and CLEANUP would close it
again. Written the other way round it aborted in glibc with "double free
detected in tcache" the first time a close actually failed, which is how
that ordering was found.

akgl_game_load's end-of-file check used fgetc(3), which spells "end of
file" and "the read failed" with the same EOF -- a disk error there read
as a clean end and passed the check it was meant to fail. aksl_fgetc
tells them apart. Extracted to require_at_eof, because inverting the
sense of a call inline needs a nested ATTEMPT whose break binds to the
wrong switch.

Two snprintf sites were truncating under a silenced
-Wformat-truncation. The compiler was right about both. The tileset one
handed the shortened path to IMG_LoadTexture, so a length error was
reported as a missing file, with SDL naming a path the caller never
wrote. Both use aksl_snprintf now and the suppression is gone; nothing
in the tree uses those macros any more. tests/tilemap.c covers the join,
and against the unfixed code it gets AKGL_ERR_SDL where it wants
AKERR_OUTOFBOUNDS.

The two src/game.c strncpy sites the fixed-width copy sweep missed --
the savegame name field and the libversion stamp -- use aksl_strncpy and
sizeof rather than a repeated 32.

Also makes tests/physics_sim.c deterministic. It placed gravity_time at
"now - dt" and let the real clock supply the step, so a machine busy
enough to deschedule the process between that store and the
SDL_GetTicksNS() inside simulate got a longer step. It went red once,
under a parallel ctest. It now sets max_timestep to the step it wants
and gravity_time to zero, so the bound supplies the step and the
scheduler cannot reach it.

TODO.md records what is deliberately not adopted: the pure-arithmetic
wrappers, which can only fail on NULL and which the tree already spells
two ways, and the collections, which the registries do not want because
SDL owns the property-set lifetime. AGENTS.md has the rule and the
fclose ordering trap.

26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 12:36:36 -04:00

898 lines
34 KiB
C

/**
* @file tilemap.c
* @brief Implements the tilemap subsystem.
*/
#include <string.h>
#include <libgen.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <jansson.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/tilemap.h>
#include <akgl/actor.h>
#include <akgl/json_helpers.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>
#include <akgl/game.h>
#include <akgl/util.h>
akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest)
{
PREPARE_ERROR(errctx);
json_t *properties = NULL;
json_t *property = NULL;
akgl_String *tmpstr = NULL;
akgl_String *typestr = NULL;
bool found = false;
int i = 0;
// This is not a generic JSON helper. It assumes we are receiving an object with a 'properties' key
// inside of it. That key is an array of objects, and each object has a name, type, and value.
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL json obj reference");
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL key string");
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(obj, "properties", &properties));
for (i = 0; i < json_array_size(properties); i++) {
CATCH(errctx, akgl_get_json_array_index_object(properties, i, &property));
// One scratch string for the whole scan. akgl_get_json_string_value
// reuses a non-NULL *dest without taking another reference, so
// releasing it per iteration -- which this used to do -- dropped the
// refcount to zero while the slot was still in use, and the next
// claim anywhere could have been handed the same one.
CATCH(errctx, akgl_get_json_string_value(property, "name", &tmpstr));
if ( strcmp(tmpstr->data, key) != 0 ) {
continue;
}
CATCH(errctx, akgl_get_json_string_value(property, "type", &typestr));
if ( strcmp(typestr->data, type) != 0 ) {
FAIL_BREAK(errctx, AKERR_TYPE, "Property %s is present but is incorrect type(expected %s got %s)", key, type, (char *)typestr->data);
}
*dest = property;
found = true;
break;
}
} CLEANUP {
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
if ( typestr != NULL ) {
IGNORE(akgl_heap_release_string(typestr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
// The success path used to be a SUCCEED_RETURN from inside the ATTEMPT
// block, which returned past CLEANUP and leaked both scratch strings on
// every successful lookup. The pool is 256 entries and a map load does this
// many times per layer.
if ( found == true ) {
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
}
akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest)
{
PREPARE_ERROR(errctx);
json_t *property;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
PASS(errctx, akgl_heap_next_string(dest));
PASS(errctx, akgl_string_initialize(*dest, NULL));
PASS(errctx, akgl_get_json_string_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
PASS(errctx, akgl_get_json_integer_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float32_t *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float32_t *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, float64_t *dest)
{
PREPARE_ERROR(errctx);
json_t *property = NULL;
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
PASS(errctx, akgl_get_json_double_value(property, "value", dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
akgl_String *tmpstr = NULL;
akgl_String *tmppath = NULL;
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "imagewidth", &dest->tilesets[tsidx].imagewidth));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "margin", &dest->tilesets[tsidx].margin));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "spacing", &dest->tilesets[tsidx].spacing));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tilecount", &dest->tilesets[tsidx].tilecount));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tileheight", &dest->tilesets[tsidx].tileheight));
PASS(errctx, akgl_get_json_integer_value((json_t *)tileset, "tilewidth", &dest->tilesets[tsidx].tilewidth));
PASS(errctx, akgl_get_json_string_value((json_t *)tileset, "name", &tmpstr));
PASS(errctx, akgl_heap_next_string(&tmppath));
ATTEMPT {
CATCH(errctx, aksl_strncpy(
(char *)&dest->tilesets[tsidx].name,
sizeof(dest->tilesets[tsidx].name),
(char *)&tmpstr->data,
sizeof(dest->tilesets[tsidx].name) - 1
));
CATCH(errctx, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
CATCH(errctx, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
CATCH(errctx, aksl_strncpy(
(char *)&dest->tilesets[tsidx].imagefilename,
sizeof(dest->tilesets[tsidx].imagefilename),
tmppath->data,
sizeof(dest->tilesets[tsidx].imagefilename) - 1
));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(akgl_heap_release_string(tmppath));
} PROCESS(errctx) {
} FINISH(errctx, true);
dest->tilesets[tsidx].texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
FAIL_ZERO_RETURN(errctx, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx)
{
int x_offset = 0;
int y_offset = 0;
int x_col = 0;
int y_col = 0;
int j = 0;
PREPARE_ERROR(errctx);
/* FIXME: THIS DOES NOT PROPERLY ACCOUNT FOR MARGINS
* It should be possible to make it work easily I just didn't feel like accounting for them in the
* initial math.
*/
/*SDL_Log("Tileset %s has %d rows %d columns",
dest->tilesets[tilesetidx].name,
(dest->tilesets[tilesetidx].tilecount / dest->tilesets[tilesetidx].columns),
dest->tilesets[tilesetidx].columns);*/
for (j = 0; j <= (dest->tilesets[tilesetidx].tilecount); j++) {
/*
* For a given 8x2 tilemap like this with 10x10 tiles and 0 spacing and 0 margin
*
* 01234567
* 89ABCDEF
*
* tile 0 would be offset (0,0)
* tile 4 would be offset (40,1)
* tile 7 would be offset (70,1)
* tile 8 would be offset (1,8)
* tile C would be offset (40,8)
* tile F would be offset (70,8)
*/
if ( j >= dest->tilesets[tilesetidx].columns ) {
x_col = (j % dest->tilesets[tilesetidx].columns);
y_col = (j / dest->tilesets[tilesetidx].columns);
x_offset = x_col * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing);
y_offset = y_col * (dest->tilesets[tilesetidx].tileheight + dest->tilesets[tilesetidx].spacing);
} else {
x_col = j;
y_col = 0;
x_offset = (j * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing));
y_offset = dest->tilesets[tilesetidx].spacing;
}
dest->tilesets[tilesetidx].tile_offsets[j][0] = x_offset;
dest->tilesets[tilesetidx].tile_offsets[j][1] = y_offset;
/* SDL_Log("Tileset %s index (%d, %d) is offset (%d, %d)",
dest->tilesets[tilesetidx].name,
x_col,
y_col,
x_offset,
y_offset);*/
// SDL_Log("Processed %d total tiles for tileset", j);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "Received NULL tilemap pointer");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "Received NULL json object pointer");
json_t *tilesets = NULL;
json_t *jstileset = NULL;
int i;
dest->numtilesets = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
for (i = 0; i < json_array_size((json_t *)tilesets); i++) {
// The bound goes at the top of the body, matching
// akgl_tilemap_load_layers. Without it a map with seventeen
// tilesets wrote past the fixed table -- and unlike the layer
// count, nothing anywhere else was checking this one.
FAIL_NONZERO_BREAK(
errctx,
(i >= AKGL_TILEMAP_MAX_TILESETS),
AKERR_OUTOFBOUNDS,
"Map declares more than %d tilesets",
AKGL_TILEMAP_MAX_TILESETS
);
CATCH(errctx, akgl_get_json_array_index_object((json_t *)tilesets, i, &jstileset));
CATCH(errctx, akgl_tilemap_load_tilesets_each(jstileset, dest, i, dirname));
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(dest, i));
dest->numtilesets += 1;
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
akgl_String *tmpstr = NULL;
akgl_Actor *actorobj = NULL;
curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR;
if ( strlen((char *)&curobj->name) == 0 ) {
FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name");
}
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr));
actorobj = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&curobj->name, NULL);
if ( actorobj == NULL ) {
CATCH(errctx, akgl_heap_next_actor(&actorobj));
CATCH(errctx, akgl_actor_initialize((akgl_Actor *)actorobj, (char *)&curobj->name));
CATCH(errctx, akgl_get_json_properties_string((json_t *)layerdatavalue, "character", &tmpstr));
CATCH(errctx,
akgl_actor_set_character(
(akgl_Actor *)actorobj,
(char *)&tmpstr->data
)
);
} else {
actorobj->refcount += 1;
}
CATCH(errctx, akgl_get_json_properties_integer((json_t *)layerdatavalue, "state", &actorobj->state));
} CLEANUP {
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
actorobj->layer = layerid;
actorobj->x = curobj->x;
actorobj->y = curobj->y;
actorobj->visible = curobj->visible;
curobj->actorptr = (akgl_Actor *)actorobj;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
json_t *layerdata = NULL;
json_t *layerdatavalue = NULL;
int j;
int len;
akgl_TilemapLayer *curlayer = NULL;
akgl_TilemapObject *curobj = NULL;
akgl_String *tmpstr = NULL;
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
PASS(errctx, akgl_get_json_array_value(root, "objects", &layerdata));
len = json_array_size((json_t *)layerdata);
curlayer = &dest->layers[layerid];
// The loop is the whole ATTEMPT body on purpose. A CATCH inside a loop
// breaks the loop rather than the block, which is only safe because there
// is nothing after it -- CLEANUP, PROCESS and FINISH still run and still
// propagate. Anything added after this loop has to account for that.
ATTEMPT {
for ( j = 0; j < len; j++ ) {
FAIL_NONZERO_BREAK(
errctx,
(j >= AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER),
AKERR_OUTOFBOUNDS,
"Object layer %d has more than %d objects",
layerid,
AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER
);
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layerdata, j, &layerdatavalue));
curobj = &curlayer->objects[j];
// One scratch string for the whole walk, released once in CLEANUP.
// Releasing it after the name read -- which is what this used to do
// -- dropped its refcount to zero while the very next line went on
// using the same slot, because akgl_get_json_string_value reuses a
// non-NULL destination without taking another reference. Any other
// claim in between would have been handed the same string.
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "name", &tmpstr));
CATCH(errctx, aksl_strncpy(
(char *)curobj->name,
sizeof(curobj->name),
tmpstr->data,
sizeof(curobj->name) - 1
));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
CATCH(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
CATCH(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
if ( strcmp(tmpstr->data, "actor") == 0 ) {
CATCH(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
} else if ( strcmp(tmpstr->data, "perspective") == 0 ) {
curobj->visible = false;
if ( strcmp((char *)curobj->name, "p_foreground") == 0 ) {
dest->p_foreground_y = curobj->y;
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_foreground_h));
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_foreground_scale));
} else if ( strcmp((char *)curobj->name, "p_vanishing") == 0 ) {
dest->p_vanishing_y = curobj->y;
CATCH(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
CATCH(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
}
}
layerdatavalue = NULL;
}
} CLEANUP {
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
json_t *layerdata = NULL;
int j;
int layerdatalen;
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
ATTEMPT {
CATCH(errctx, akgl_get_json_integer_value(root, "height", &dest->layers[layerid].height));
CATCH(errctx, akgl_get_json_integer_value(root, "width", &dest->layers[layerid].width));
CATCH(errctx, akgl_get_json_array_value(root, "data", &layerdata));
layerdatalen = (dest->layers[layerid].width * dest->layers[layerid].height);
if ( layerdatalen >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map layer exceeds the maximum size");
}
for ( j = 0; j < layerdatalen; j++ ) {
CATCH(errctx, akgl_get_json_array_index_integer(layerdata, j, &dest->layers[layerid].data[j]));
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
akgl_String *tmpstr;
akgl_String *fpath;
int fpathlen = 0;
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination tilemap reference");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL tilemap root reference");
FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpstr));
CATCH(errctx, akgl_heap_next_string(&fpath));
CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr));
// aksl_snprintf rather than snprintf(3) under a silenced
// -Wformat-truncation: the compiler was right that "%s/%s" of two
// AKGL_MAX_STRING_LENGTH strings does not fit, and silencing it left
// the truncation happening and unreported. What followed was
// IMG_LoadTexture on a path that was almost the right one, failing with
// SDL's "couldn't open" against a filename the caller never wrote.
// aksl_snprintf raises AKERR_OUTOFBOUNDS naming both lengths instead.
CATCH(errctx, aksl_snprintf(
&fpathlen,
(char *)&fpath->data,
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
"%s/%s",
dirname->data,
tmpstr->data
));
dest->layers[layerid].texture = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)fpath->data);
FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
dest->layers[layerid].width = dest->layers[layerid].texture->w;
dest->layers[layerid].height = dest->layers[layerid].texture->h;
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpstr));
IGNORE(akgl_heap_release_string(fpath));
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "akgl_tilemap_load_layers received NULL tilemap pointer");
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "akgl_tilemap_load_layers received NULL json object pointer");
FAIL_ZERO_RETURN(errctx, dirname, AKERR_NULLPOINTER, "dirname");
json_t *layers = NULL;
json_t *layer = NULL;
akgl_String *tmpstr = NULL;
int i;
int layerid = 0;
int tmpint = 0;
ATTEMPT {
CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers));
dest->numlayers = json_array_size((json_t *)layers);
for ( i = 0; i < dest->numlayers; i++) {
if ( i >= AKGL_TILEMAP_MAX_LAYERS ) {
FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum number of layers");
}
CATCH(errctx, akgl_get_json_array_index_object((json_t *)layers, i, &layer));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "id", &tmpint));
CATCH(errctx, akgl_get_json_number_value((json_t *)layer, "opacity", &dest->layers[layerid].opacity));
CATCH(errctx, akgl_get_json_boolean_value((json_t *)layer, "visible", &dest->layers[layerid].visible));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "id", &dest->layers[layerid].id));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "x", &dest->layers[layerid].x));
CATCH(errctx, akgl_get_json_integer_value((json_t *)layer, "y", &dest->layers[layerid].y));
CATCH(errctx, akgl_get_json_string_value((json_t *)layer, "type", &tmpstr));
SDL_Log("Layer %d has type %s", layerid, tmpstr->data);
if ( strncmp((char *)tmpstr->data, "objectgroup", strlen((char *)tmpstr->data)) == 0 ) {
dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_OBJECTS;
CATCH(errctx, akgl_tilemap_load_layer_objects((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
} else if ( strncmp((char *)tmpstr->data, "tilelayer", strlen((char *)tmpstr->data)) == 0 ) {
dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
CATCH(errctx, akgl_tilemap_load_layer_tile((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
} else if ( strncmp((char *)tmpstr->data, "imagelayer", strlen((char *)tmpstr->data)) == 0 ) {
dest->layers[layerid].type = AKGL_TILEMAP_LAYER_TYPE_IMAGE;
CATCH(errctx, akgl_tilemap_load_layer_image((akgl_Tilemap *)dest, (json_t *)layer, layerid, dirname));
}
layer = NULL;
layerid += 1;
}
} CLEANUP {
// One scratch string for every layer's type, given back once. It was
// claimed by the first accessor call and never released, which is one
// of the two pool strings a map load used to keep for good.
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{
PREPARE_ERROR(errctx);
json_t *props = NULL;
akgl_String *tmpval = NULL;
double defzero = 0.0;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpval));
CATCH(errctx, akgl_get_json_array_value((json_t *)root, "properties", &props));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
// Map has no properties, do nothing
SDL_Log("Map has no properties");
SUCCEED_RETURN(errctx);
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&tmpval));
CATCH(errctx, akgl_get_json_properties_string(
root,
"physics.model",
&tmpval
)
);
PASS(errctx, akgl_physics_factory(&dest->physics, tmpval));
dest->use_own_physics = true;
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.x", &dest->physics.gravity_x
),
(void *)&defzero,
(void *)&dest->physics.gravity_x,
sizeof(double)));
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.y", &dest->physics.gravity_y
),
(void *)&defzero,
(void *)&dest->physics.gravity_y,
sizeof(double)));
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.gravity.z", &dest->physics.gravity_z
),
(void *)&defzero,
(void *)&dest->physics.gravity_z,
sizeof(double)));
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.x", &dest->physics.drag_x
),
(void *)&defzero,
(void *)&dest->physics.drag_x,
sizeof(double)));
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.y", &dest->physics.drag_y
),
(void *)&defzero,
(void *)&dest->physics.drag_y,
sizeof(double)));
CATCH(errctx, akgl_get_json_with_default(
akgl_get_json_properties_double(
root, "physics.drag.z", &dest->physics.drag_z
),
(void *)&defzero,
(void *)&dest->physics.drag_z,
sizeof(double)));
} CLEANUP {
IGNORE(akgl_heap_release_string(tmpval));
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
SDL_Log("Map uses game physics");
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
{
PREPARE_ERROR(errctx);
json_t *json = NULL;
//akgl_String *tmpstr = NULL;
json_error_t error;
akgl_String *dirnamestr = NULL;
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap");
memset(dest, 0x00, sizeof(akgl_Tilemap));
dest->p_foreground_scale = 1.0;
dest->p_vanishing_scale = 1.0;
PASS(errctx, akgl_heap_next_string(&dirnamestr));
ATTEMPT {
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
dirname((char *)&dirnamestr->data);
json = json_load_file(fname, 0, &error);
FAIL_ZERO_BREAK(
errctx,
json,
AKERR_NULLPOINTER,
"Error while loading tilemap from %s on line %d: %s-",
fname,
error.line,
error.text
);
CATCH(errctx, akgl_tilemap_load_physics(dest, json));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tileheight", &dest->tileheight));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "tilewidth", &dest->tilewidth));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &dest->height));
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &dest->width));
dest->orientation = 0;
if ( (dest->width * dest->height) >= (AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT) ) {
FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Map exceeds the maximum size");
}
CATCH(errctx, akgl_tilemap_load_layers((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));
CATCH(errctx, akgl_tilemap_load_tilesets((akgl_Tilemap *)dest, (json_t *)json, dirnamestr));
if ( dest->p_foreground_y && dest->p_vanishing_y ) {
// How much bigger is the foreground vs the vanishing point?
// if vanishing is height 16, and foreground is height 32, that is a 2x scale difference
/* dest->p_scale = ((float)dest->p_foreground_h / (float)dest->p_vanishing_h); */
/* SDL_Log("Map perspective scale is (%d/%d) = %f", dest->p_foreground_h, dest->p_vanishing_h, dest->p_scale); */
// Sprites are scale N (default 1.0) at the foreground, so how much do we need to
// scale them for every pixel above foreground_y before they reach vanishing_y?
// If vanishing is at 320 and foreground is at 640, that is a 320 line difference
// If our scaling rate is 2x, then our rate is (((N=1.0) / (640 - 320)) / (dest->p_scale = 2)), or
// 0.0066% scale per pixel.
dest->p_rate = ((dest->p_foreground_scale - dest->p_vanishing_scale) / (dest->p_foreground_y - dest->p_vanishing_y));
SDL_Log("Map perspective rate is %f", dest->p_rate);
}
} CLEANUP {
// The map is built entirely out of copies -- layer data, tileset
// geometry, object names -- so the document is dead the moment the
// loaders above return, whether they succeeded or not. It is also the
// largest of the four: a map's JSON is the size of its layer data.
if ( json != NULL ) {
json_decref(json);
json = NULL;
}
// And the directory the map's relative paths resolve against, which
// was claimed above and never released -- the other of the two pool
// strings a map load kept.
if ( dirnamestr != NULL ) {
IGNORE(akgl_heap_release_string(dirnamestr));
dirnamestr = NULL;
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, int layeridx)
{
PREPARE_ERROR(errctx);
SDL_FRect dest = {.x = 0, .y = 0, .w = 0, .h = 0};;
SDL_FRect src = {.x = 0, .y = 0, .w = 0, .h = 0};
int start_x = 0;
int start_y = 0;
int end_x = 0;
int end_y = 0;
int yidx = 0;
int xidx = 0;
int tilesetidx = 0;
int tilenum = 0;
int offset = 0;
/*
* Render every tile in the map that partially intersects the viewport
*
* For an 8x2 tilemap with 16 pixel square tiles like this
*
* 01234567
* 89ABCDEF
*
* With a viewport of (x=20, y=8, w=90, y=20), we would render:
*
* 123456
* 9ABCDE
*
* 0 and 8 would not be rendered. 1, 9, 6, and E would be partially rendered at their corner.
* 2,3,4,5 and A,B,C,D would be partially rendered with a slice from their center.
*/
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw received NULL pointer to tilemap");
FAIL_ZERO_RETURN(errctx, viewport, AKERR_NULLPOINTER, "akgl_tilemap_draw received NULL pointer to viewport");
/* Only try to render the stuff that is partially within the viewport */
start_x = viewport->x / map->tilewidth;
start_y = viewport->y / map->tileheight;
end_x = (viewport->x + viewport->w) / map->tilewidth;
end_y = (viewport->y + viewport->h) / map->tileheight;
if ( end_x > map->width ) {
end_x = map->width;
}
if ( end_y > map->height ) {
end_y = map->height;
}
/*SDL_Log("Rendering map into viewport from (%d, %d) to (%d, %d)",
start_x, start_y, end_x, end_y);*/
if ( map->layers[layeridx].type == AKGL_TILEMAP_LAYER_TYPE_IMAGE ) {
dest.x = 0;
dest.y = 0;
src.w = map->layers[layeridx].width;
src.h = map->layers[layeridx].height;
dest.w = map->layers[layeridx].width;
dest.h = map->layers[layeridx].height;
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);
}
dest.x = 0;
dest.y = 0;
dest.w = map->tilewidth;
dest.h = map->tileheight;
for ( yidx = start_y; yidx < end_y; yidx++ ) {
dest.x = 0;
for ( xidx = start_x; xidx < end_x; xidx++ ) {
if ( yidx == 0 ) {
offset = xidx;
} else {
offset = xidx + (yidx * (map->width));
}
tilenum = map->layers[layeridx].data[offset];
// FIXME: This is probably not very efficient. Need a better way to look up
// tile offsets within the tilesets by their tile ID.
for ( tilesetidx = 0; tilesetidx < map->numtilesets ; tilesetidx++ ) {
if ( map->tilesets[tilesetidx].firstgid <= tilenum &&
(map->tilesets[tilesetidx].firstgid + map->tilesets[tilesetidx].tilecount) >= tilenum ) {
// Render this tile to the correct screen position
// FIXME: These conditionals are probably not very efficient. Need a better way of getting
// the intersection of this tile with the viewport and rendering only that portion.
if ( xidx == 0 ) {
src.x += (int)viewport->x % map->tilewidth;
src.w = map->tilewidth - ((int)viewport->x % map->tilewidth);
} else {
src.x = map->tilesets[tilesetidx].tile_offsets[tilenum - map->tilesets[tilesetidx].firstgid][0];
src.w = map->tilewidth;
}
if ( yidx == 0 ) {
src.y += (int)viewport->y % map->tileheight;
src.h = map->tileheight - ((int)viewport->y % map->tileheight);
} else {
src.y = map->tilesets[tilesetidx].tile_offsets[tilenum - map->tilesets[tilesetidx].firstgid][1];
src.h = map->tileheight;
}
/*SDL_Log("Blitting tile #%d (local tileset id %d from offset %d) from map layer %d map (x=%d,y=%d) tileset %d (x=%f,y=%f,w=%f,h=%f) to (x=%f,y=%f,w=%f,h=%f)",
tilenum,
(tilenum - map->tilesets[tilesetidx].firstgid),
offset,
layeridx,
xidx,
yidx,
tilesetidx,
src.x,
src.y,
src.w,
src.h,
dest.x,
dest.y,
dest.w,
dest.h);*/
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
}
}
dest.x += map->tilewidth;
}
dest.y += map->tileheight;
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
{
PREPARE_ERROR(errctx);
SDL_FRect dest;
SDL_FRect src;
int tilenum = 0;
/*
* Render every tile in a tileset to the default renderer
* (this is a debugging tool that shows that the recorded tile offsets are correct,
* by proving that we can reconstruct the original tileset image)
*/
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw_tileset received NULL pointer to tilemap");
FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), AKERR_OUTOFBOUNDS, "akgl_tilemap_draw_tileset received a tileset index out of bounds");
for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) {
// Render this tile to the correct screen position
// FIXME: These conditionals are probably not very efficient. Need a better way of getting
// the intersection of this tile with the viewport and rendering only that portion.
src.x = map->tilesets[tilesetidx].tile_offsets[tilenum][0];
src.y = map->tilesets[tilesetidx].tile_offsets[tilenum][1];
src.w = map->tilewidth;
src.h = map->tileheight;
dest.x = tilenum * map->tilewidth;
if ( tilenum >= map->tilesets[tilesetidx].columns ) {
dest.x = (tilenum % (map->tilesets[tilesetidx].columns)) * map->tilewidth;
}
if ( tilenum >= (map->tilesets[tilesetidx].columns) ) {
dest.y = (tilenum / (map->tilesets[tilesetidx].columns)) * map->tileheight;
} else {
dest.y = 0;
}
dest.w = src.w;
dest.h = src.h;
/*SDL_Log("Blitting tile #%d from map tileset %d (x=%f,y=%f,w=%f,h=%f) to (x=%f,y=%f,w=%f,h=%f)",
tilenum,
tilesetidx,
src.x,
src.y,
src.w,
src.h,
dest.x,
dest.y,
dest.w,
dest.h);*/
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, map->tilesets[tilesetidx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "NULL map");
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "NULL actor");
if ( actor->y <= map->p_vanishing_y ) {
actor->scale = map->p_vanishing_scale;
} else if ( actor->y >= map->p_foreground_y ) {
actor->scale = map->p_foreground_scale;
} else {
actor->scale = map->p_foreground_scale - (map->p_rate * (map->p_foreground_y - actor->y));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_tilemap_release(akgl_Tilemap *dest)
{
// Release all tileset textures
// Release all image layer textures
// Memset to zero
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL map");
int i = 0;
// Each pointer is cleared as it goes. Without that a second release
// destroys textures SDL has already freed, and the second loop used to
// destroy `tilesets[i]` while testing `layers[i]` -- so every tileset
// texture was freed twice and no image layer's texture was freed at all.
for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
if ( dest->tilesets[i].texture != NULL ) {
SDL_DestroyTexture(dest->tilesets[i].texture);
dest->tilesets[i].texture = NULL;
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( dest->layers[i].texture != NULL ) {
SDL_DestroyTexture(dest->layers[i].texture);
dest->layers[i].texture = NULL;
}
}
SUCCEED_RETURN(errctx);
}