Closes Defects items 20, 22, 23 and 29, and the residual of item 38. The tilemap load leak was five pooled strings per load; the property-lookup fix in an earlier commit took it to two, and the last two were each a claim with no matching release -- the string every layer's `type` was read into, and the dirname the map's relative paths resolve against. tests/tilemap.c asserts the pool is exactly where it started after one load/release cycle and after 64. Finding those two was a matter of dumping the contents of every still-claimed slot after a cycle rather than reading the code again; 'tilelayer' and an assets directory named themselves immediately. Same file, same class, fixed with it: akgl_tilemap_load_layer_objects released its scratch string after reading each object's name and then kept using the slot, because akgl_get_json_string_value reuses a non-NULL destination without taking another reference. The slot was free while still live, so any other claim could have been handed it. akgl_character_sprite_add wrote over an existing binding without releasing the sprite it displaced, so a character that rebinds a state while alive leaked a sprite slot per rebind -- teardown only gives back what the map holds at the end. The new reference is taken before the write and given back if the write fails, so there is no window where a sprite is bound with nothing behind it. The write was unchecked too. Three failure-path leaks moved into CLEANUP blocks: akgl_render_2d_init's two pooled strings, akgl_controller_open_gamepads' enumeration array, and akgl_text_rendertextat's surface and texture -- the last being a leak per frame on a HUD line. akgl_text_unloadallfonts() closes every font in the registry and destroys it, which is what item 38 left open. Deliberately not a whole akgl_game_shutdown: tearing down the mixer, SDL_ttf and SDL in the right order is a design question, and this is the part that was simply missing. It is a new public symbol, which 0.5.0 already covers -- this release has not shipped. Every fix has a test that fails against the old code. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol all clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
878 lines
33 KiB
C
878 lines
33 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 {
|
|
strncpy((char *)&dest->tilesets[tsidx].name,
|
|
(char *)&tmpstr->data,
|
|
AKGL_TILEMAP_MAX_TILESET_NAME_SIZE
|
|
);
|
|
|
|
CATCH(errctx, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
|
|
CATCH(errctx, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
|
|
strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH);
|
|
} 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));
|
|
strncpy((char *)curobj->name, tmpstr->data, AKGL_ACTOR_MAX_NAME_LENGTH);
|
|
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;
|
|
|
|
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));
|
|
|
|
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
|
|
snprintf((char *)&fpath->data,
|
|
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
|
|
"%s/%s",
|
|
dirname->data,
|
|
tmpstr->data
|
|
);
|
|
RESTORE_GCC_WARNINGS
|
|
|
|
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);
|
|
}
|