Files
libakgl/tests/tilemap.c
Andrew Kesterson e4aa6a5084
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / performance (push) Failing after 19s
libakgl CI Build / memory_check (push) Failing after 15s
libakgl CI Build / mutation_test (push) Failing after 17s
Take libakerror 2.0.1 and drop the workaround it makes obsolete
Every libakgl test suite could report success while failing. libakerror's
default unhandled-error handler ended in exit(errctx->status), an exit
status is one byte wide, and libakgl's band starts at 256 -- so
AKGL_ERR_SDL, the most common failure a library built on SDL can have,
exited 0 and CTest recorded a pass. tests/character.c aborted at its
second of four tests on a bad renderer and was green for months.

0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in
tests/testutil.h, and TODO.md ended the entry saying any consumer's
suites have the same problem and it was worth raising upstream. It was.
2.0.1 fixes it at the source: akerr_exit() owns the mapping and the
default handler calls it, so 0 exits 0, 1 through 255 exit themselves,
and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The
trap and its 21 call sites are gone.

Verified by putting the original failure back rather than by reading the
release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main
exits 125 and CTest reports a failure. A standalone consumer raising the
same status unhandled exits 125 where it exited 0 before.

tests/actor.c installs its own handler and called exit(errctx->status)
from it, which is the same defect one layer up. It calls akerr_exit()
now.

2.0.0 also makes the error pool and the status registry thread safe,
which libakgl needs more than it knew: audio_stream_callback raises
error contexts on SDL's audio thread. With an unlocked pool that
callback and the main thread could scan AKERR_ARRAY_ERROR at the same
time and be handed the same slot. The comment there says so.

This is a hard dependency floor, not a preference. 2.0.0 moved
__akerr_last_ignored to thread-local storage and made akerr_next_error()
return a context that already holds its reference, and both expand at
libakgl's call sites -- and at a consumer's, because akerror.h is part
of libakgl's public interface. Mixing headers and libraries across that
line double-counts every reference and never returns a pool slot. The
soname moved to libakerror.so.2; include/akgl/error.h now also feature-
tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe
for 2.0.1 since libakerror publishes no version macro.

0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it
re-exports through its headers is not.

TODO.md records the pkg-config gap this makes sharper -- akgl.pc names
no dependencies at all, so nothing tells a pkg-config consumer which
libakerror it needs.

Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall
-Werror. libakgl.so.0.7 links libakerror.so.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 13:05:43 -04:00

783 lines
27 KiB
C

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <jansson.h>
#include <akerror.h>
#include <akgl/util.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/tilemap.h>
#include <akgl/actor.h>
#include <akgl/game.h>
#include <akstdlib.h>
#include <akgl/json_helpers.h>
#include "testutil.h"
/**
* @brief akgl_get_json_tilemap_property must give back its scratch strings on every path.
*
* It claims two pool strings per call -- one for the property name it is
* comparing, one for the declared type -- and used to leave the block through a
* `SUCCEED_RETURN` from inside `ATTEMPT` on the path where it *found* what it
* was asked for. That returns past `CLEANUP`, so the ordinary, successful
* lookup was the leaking one, at two of the pool's 256 entries a time. A map
* load does this several times per layer.
*
* Three paths, run more times than the pool has entries: found, absent, and
* present-but-wrong-type. Against the old code the found loop exhausts the pool
* and comes back AKGL_ERR_HEAP long before it finishes.
*/
akerr_ErrorContext *test_tilemap_property_lookup_releases_strings(void)
{
PREPARE_ERROR(errctx);
json_t *jsondoc = NULL;
json_error_t jsonerr;
akgl_String *pathstr = NULL;
int baseline = 0;
int propnum = 0;
int i = 0;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf(
(char *)&pathstr->data,
AKGL_MAX_STRING_LENGTH,
"%s%s",
SDL_GetBasePath(),
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
);
jsondoc = json_load_file((char *)&pathstr->data, 0, (json_error_t *)&jsonerr);
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
baseline = test_string_pool_used();
// The success path. Twice the pool size, so a leak of even one entry
// per call cannot finish.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
CATCH(errctx, akgl_get_json_properties_integer(jsondoc, "state", &propnum));
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"%d successful property lookups left %d pool strings claimed, expected %d",
(AKGL_MAX_HEAP_STRING * 2),
test_string_pool_used(),
baseline);
// The absent path, which reports AKERR_KEY from after FINISH.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_STATUS(
errctx,
AKERR_KEY,
akgl_get_json_properties_integer(jsondoc, "no_such_property", &propnum),
"looking up a property the object does not have");
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"absent-property lookups left %d pool strings claimed, expected %d",
test_string_pool_used(),
baseline);
// The wrong-type path, which fails from inside the ATTEMPT block with
// both scratch strings already claimed.
for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
TEST_EXPECT_STATUS(
errctx,
AKERR_TYPE,
akgl_get_json_properties_integer(jsondoc, "character", &propnum),
"reading a string property as an int");
}
TEST_ASSERT(
errctx,
test_string_pool_used() == baseline,
"wrong-type lookups left %d pool strings claimed, expected %d",
test_string_pool_used(),
baseline);
} CLEANUP {
if ( jsondoc != NULL ) {
json_decref(jsondoc);
jsondoc = NULL;
}
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
{
PREPARE_ERROR(errctx);
json_t *jsondoc = NULL;
json_error_t jsonerr;
akgl_String *tmpstr = NULL;
int propnum;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&tmpstr));
snprintf(
(char *)&tmpstr->data,
AKGL_MAX_STRING_LENGTH,
"%s%s",
SDL_GetBasePath(),
"assets/snippets/test_tilemap_get_json_tilemap_property.json"
);
jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr);
FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
CATCH(
errctx,
akgl_get_json_properties_string(
jsondoc,
"character",
&tmpstr
)
);
FAIL_NONZERO_BREAK(
errctx,
strcmp((char *)&tmpstr->data, "testcharacter"),
AKERR_VALUE,
"Incorrect value loaded from property `character` (`testcharacter` vs `%s`)",
(char *)&tmpstr->data
);
CATCH(
errctx,
akgl_get_json_properties_integer(
jsondoc,
"state",
&propnum
)
);
FAIL_NONZERO_BREAK(
errctx,
(propnum != 6),
AKERR_VALUE,
"Incorrect value loaded from property `state` (6 vs %d)",
propnum
);
} CLEANUP {
if ( tmpstr != NULL ) {
IGNORE(akgl_heap_release_string(tmpstr));
}
if ( jsondoc != NULL ) {
json_decref(jsondoc);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
{
int comparison_values[8] = {
0, // Tile 0 X
0, // Tile 0 Y
10, // Tile 1 X
0, // Tile 1 Y
0, // Tile 2 X
10, // Tile 2 Y
10, // Tile 2 X
10 // Tile 2 Y
};
int *comparison_ptrs[8] = {
&akgl_gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
&akgl_gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
&akgl_gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
&akgl_gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
&akgl_gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
&akgl_gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
&akgl_gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
&akgl_gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
};
int i = 0;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
akgl_gamemap->tilesets[0].tilecount = 4;
akgl_gamemap->tilesets[0].columns = 2;
akgl_gamemap->tilesets[0].firstgid = 1;
akgl_gamemap->tilesets[0].imageheight = 20;
akgl_gamemap->tilesets[0].imagewidth = 20;
akgl_gamemap->tilesets[0].tilecount = 4;
akgl_gamemap->tilesets[0].tileheight = 10;
akgl_gamemap->tilesets[0].tilewidth = 10;
akgl_gamemap->tilesets[0].spacing = 0;
akgl_gamemap->tilesets[0].margin = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_tilemap_compute_tileset_offsets(akgl_gamemap, 0));
// Tile 0 X offset
for ( i = 0; i < 8; i++ ) {
FAIL_NONZERO_BREAK(
errctx,
(*comparison_ptrs[i] != comparison_values[i]),
AKERR_VALUE,
"Tile offset incorrectly calculated for index %d (%d vs %d)",
i,
*comparison_ptrs[i],
comparison_values[i]
);
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief The loader must refuse a map that would overrun its fixed tables.
*
* Two loops indexed straight from the document. `curlayer->objects[j]` had no
* check against AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, and `dest->tilesets[i]`
* none against AKGL_TILEMAP_MAX_TILESETS. The object one is the reachable one:
* 128 objects is not a large object layer, and akgl_TilemapObject is big, so
* the 129th wrote well past the end of the layer.
*
* akgl_tilemap_load_layers already bounded its own loop and raised
* AKERR_OUTOFBOUNDS, so the shape to copy was in the same file.
*/
/**
* @brief Releasing a map twice must not free a texture twice.
*
* The layers loop tested `dest->layers[i].texture` and then destroyed
* `dest->tilesets[i].texture` -- already destroyed by the loop above it. So
* every tileset texture was freed twice on a single release, and no image
* layer's texture was freed at all. Neither pointer was cleared either, so a
* second release was a use-after-free on top of that.
*/
/**
* @brief A load/release cycle must give back every pooled string it took.
*
* Measured before the fix by counting non-zero HEAP_STRING refcounts across
* cycles: five per cycle, exactly, and akgl_tilemap_release gave none of them
* back. The pool is 256 entries, so the 52nd map load in a process found it
* empty -- and until the accessors were fixed, "empty" arrived as a segfault
* rather than as AKGL_ERR_HEAP.
*
* Blast radius is every level transition, and a game that reloads a level on
* death hits it sooner.
*/
akerr_ErrorContext *test_akgl_tilemap_load_releases_strings(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
int baseline = 0;
int afterfirst = 0;
int i = 0;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/testmap.tmj");
baseline = test_string_pool_used();
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
CATCH(errctx, akgl_tilemap_release(akgl_gamemap));
afterfirst = test_string_pool_used();
TEST_ASSERT(errctx, afterfirst == baseline,
"one load/release cycle left %d pool strings claimed, expected %d",
afterfirst, baseline);
// Then enough cycles that a leak of even one string per load would run
// the pool dry, so this cannot pass by rounding.
for ( i = 0; i < 64; i++ ) {
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
CATCH(errctx, akgl_tilemap_release(akgl_gamemap));
}
TEST_ASSERT(errctx, test_string_pool_used() == baseline,
"64 load/release cycles left %d pool strings claimed, expected %d",
test_string_pool_used(), baseline);
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_release_is_idempotent(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
int i = 0;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/testmap.tmj");
CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
TEST_ASSERT(errctx, akgl_gamemap->numtilesets > 0,
"the fixture map loaded no tilesets, so this test proves nothing");
TEST_ASSERT(errctx, akgl_gamemap->tilesets[0].texture != NULL,
"the fixture map's first tileset has no texture");
TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing a loaded map");
for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
TEST_ASSERT(errctx, akgl_gamemap->tilesets[i].texture == NULL,
"tileset %d's texture pointer survived release", i);
}
for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
TEST_ASSERT(errctx, akgl_gamemap->layers[i].texture == NULL,
"layer %d's texture pointer survived release", i);
}
// The one that used to be a use-after-free.
TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing the same map again");
TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing it a third time");
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_tilemap_release(NULL),
"releasing a NULL map");
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_bounds_fixed_tables(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
json_t *doc = NULL;
json_error_t errdata;
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
// Exactly the maximum must still load.
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/snippets/test_tilemap_max_objects.json");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "max-objects fixture: %s", (char *)&errdata.text);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
TEST_EXPECT_OK(errctx,
akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr),
"loading an object layer with exactly the maximum objects");
json_decref(doc);
doc = NULL;
// One more must be refused rather than written.
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_objects.json");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-objects fixture: %s", (char *)&errdata.text);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr),
"loading an object layer with one object too many");
json_decref(doc);
doc = NULL;
// And the tileset table.
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_tilesets.json");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-tilesets fixture: %s", (char *)&errdata.text);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr),
"loading a map with one tileset too many");
TEST_ASSERT(errctx, akgl_gamemap->numtilesets <= AKGL_TILEMAP_MAX_TILESETS,
"numtilesets reached %d, past the %d the table holds",
akgl_gamemap->numtilesets, AKGL_TILEMAP_MAX_TILESETS);
} CLEANUP {
if ( doc != NULL ) {
json_decref(doc);
}
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
{
akgl_String *pathstr;
PREPARE_ERROR(errctx);
json_t *doc = NULL;
json_t *layers = NULL;
json_t *objectlayer = NULL;
json_error_t errdata;
akgl_Actor *testactor;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
CATCH(errctx, akgl_tilemap_load_layer_objects(akgl_gamemap, objectlayer, 1, pathstr));
testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
FAIL_ZERO_BREAK(
errctx,
testactor,
AKERR_NULLPOINTER,
"Test Actor was not loaded from the test map"
);
if ( (testactor->basechar != SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL)) ||
(testactor->layer != 1) ||
(testactor->state != 6) ||
(testactor->visible != true) ||
(testactor->x != 16) ||
(testactor->y != 16) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Test actor was loaded with incorrect values (check gdb)");
}
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
if ( doc != NULL ) {
json_decref(doc);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
{
akgl_String *pathstr;
PREPARE_ERROR(errctx);
json_t *doc = NULL;
json_t *layers = NULL;
json_t *tilelayer = NULL;
json_t *tiledata = NULL;
json_error_t errdata;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
CATCH(errctx, akgl_tilemap_load_layer_tile(akgl_gamemap, tilelayer, 0, pathstr));
if ( (akgl_gamemap->layers[0].data[0] != 1) ||
(akgl_gamemap->layers[0].data[1] != 2) ||
(akgl_gamemap->layers[0].data[2] != 3) ||
(akgl_gamemap->layers[0].data[3] != 4) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
}
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
if ( doc != NULL ) {
json_decref(doc);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
/**
* @brief A layer image path too long for the field must say so, not load the wrong file.
*
* The join is `"%s/%s"` of a directory and an image name into a
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE buffer, and both inputs are
* #AKGL_MAX_STRING_LENGTH wide, so it can overflow. It used to be a raw
* `snprintf` under a silenced `-Wformat-truncation`: the path was quietly cut
* short and handed to `IMG_LoadTexture`, which failed with SDL's "couldn't
* open" against a filename the caller never wrote -- the length problem
* reported as a missing-file problem, pointing at the wrong thing.
*
* `aksl_snprintf` treats truncation as the error it is. The directory here is
* filled to capacity so the join cannot fit whatever the image name is.
*/
akerr_ErrorContext *test_akgl_tilemap_layer_image_path_too_long(void)
{
PREPARE_ERROR(errctx);
akgl_String *dirname = NULL;
json_t *layer = NULL;
json_error_t errdata;
ATTEMPT {
CATCH(errctx, akgl_heap_next_string(&dirname));
CATCH(errctx, aksl_memset((void *)&dirname->data, 'd', sizeof(dirname->data)));
dirname->data[sizeof(dirname->data) - 1] = '\0';
layer = json_loads("{\"image\": \"tiles.png\"}", 0, &errdata);
FAIL_ZERO_BREAK(errctx, layer, AKERR_VALUE, "Failed to build the test layer: %s", errdata.text);
TEST_EXPECT_STATUS(
errctx,
AKERR_OUTOFBOUNDS,
akgl_tilemap_load_layer_image(akgl_gamemap, layer, 0, dirname),
"joining an image name onto a directory that fills the field");
} CLEANUP {
if ( dirname != NULL ) {
IGNORE(akgl_heap_release_string(dirname));
}
if ( layer != NULL ) {
json_decref(layer);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
{
akgl_String *pathstr;
PREPARE_ERROR(errctx);
json_t *doc = NULL;
json_error_t errdata;
int i = 0;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_layers(akgl_gamemap, doc, pathstr));
FAIL_NONZERO_BREAK(
errctx,
(akgl_gamemap->numlayers != 3),
AKERR_VALUE,
"Map layer count incorrect"
);
for ( i = 0; i < akgl_gamemap->numlayers; i++ ) {
if ( (akgl_gamemap->layers[i].opacity != 1) ||
(akgl_gamemap->layers[i].visible != true) ||
(akgl_gamemap->layers[i].id != (i + 1)) ||
(akgl_gamemap->layers[i].x != 0) ||
(akgl_gamemap->layers[i].y != 0) ) {
FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
// Layer 2 should have 1 object loaded
if ( (akgl_gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
(akgl_gamemap->layers[1].objects[1].name[0] != '\0' ) ||
(akgl_gamemap->layers[1].objects[1].id != 0) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
}
// Layer 1 and 3 should have no objects
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( akgl_gamemap->layers[0].objects[i].id != 0 ) {
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
if ( akgl_gamemap->layers[2].objects[i].id != 0 ) {
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
goto _test_akgl_tilemap_load_layers_cleanup;
}
}
// Layers 1 and 3 should have tile data
if ( (akgl_gamemap->layers[0].data[0] != 1) ||
(akgl_gamemap->layers[0].data[1] != 2) ||
(akgl_gamemap->layers[0].data[2] != 3) ||
(akgl_gamemap->layers[0].data[3] != 4) ||
(akgl_gamemap->layers[2].data[0] != 0) ||
(akgl_gamemap->layers[2].data[1] != 5) ||
(akgl_gamemap->layers[2].data[2] != 0) ||
(akgl_gamemap->layers[2].data[3] != 6)
) {
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
}
_test_akgl_tilemap_load_layers_cleanup:
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
if ( doc != NULL ) {
json_decref(doc);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
{
akgl_String *pathstr = NULL;
PREPARE_ERROR(errctx);
json_t *doc = NULL;
json_error_t errdata;
SDL_Texture *image = NULL;
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_heap_next_string(&pathstr));
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
doc = json_load_file((char *)&pathstr->data, 0, &errdata);
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
CATCH(errctx, akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr));
FAIL_NONZERO_BREAK(errctx, (akgl_gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
if ( (akgl_gamemap->tilesets[0].columns != 48 ) ||
(akgl_gamemap->tilesets[0].firstgid != 1) ||
(akgl_gamemap->tilesets[0].imageheight != 576) ||
(akgl_gamemap->tilesets[0].imagewidth != 768) ||
(akgl_gamemap->tilesets[0].margin != 0) ||
(akgl_gamemap->tilesets[0].spacing != 0) ||
(akgl_gamemap->tilesets[0].tilecount != 1728) ||
(akgl_gamemap->tilesets[0].tileheight != 16) ||
(akgl_gamemap->tilesets[0].tilewidth != 16) ) {
FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
}
FAIL_NONZERO_BREAK(
errctx,
strcmp((char *)&akgl_gamemap->tilesets[0].name, "World_A1"),
AKERR_VALUE,
"Tileset loaded with incorrect name");
snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&pathstr->data);
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
CATCH(
errctx,
akgl_render_and_compare(
akgl_gamemap->tilesets[0].texture,
image,
0, 0, 768, 576,
"test_akgl_tilemap_loaded_tileset.png")
);
} CLEANUP {
if ( pathstr != NULL ) {
IGNORE(akgl_heap_release_string(pathstr));
}
if ( doc != NULL ) {
json_decref(doc);
}
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_load(void)
{
PREPARE_ERROR(errctx);
memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", akgl_gamemap));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_draw(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
akgl_gamemap = &akgl_default_gamemap;
akgl_renderer = &akgl_default_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
}
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &akgl_window, &akgl_renderer->sdl_renderer)) {
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
}
akgl_renderer->draw_texture = &akgl_render_2d_draw_texture;
CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
CATCH(errctx, test_tilemap_akgl_get_json_tilemap_property());
CATCH(errctx, test_tilemap_property_lookup_releases_strings());
CATCH(errctx, test_akgl_tilemap_compute_tileset_offsets());
CATCH(errctx, test_akgl_tilemap_load_layer_objects());
CATCH(errctx, test_akgl_tilemap_load_layer_tile());
CATCH(errctx, test_akgl_tilemap_layer_image_path_too_long());
CATCH(errctx, test_akgl_tilemap_load_layers());
CATCH(errctx, test_akgl_tilemap_load_tilesets());
CATCH(errctx, test_akgl_tilemap_load_bounds_fixed_tables());
CATCH(errctx, test_akgl_tilemap_release_is_idempotent());
CATCH(errctx, test_akgl_tilemap_load_releases_strings());
//CATCH(errctx, test_akgl_tilemap_load());
//CATCH(errctx, test_akgl_tilemap_draw_tileset());
//CATCH(errctx, test_akgl_tilemap_draw());
} CLEANUP {
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}