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
784 lines
28 KiB
C
784 lines
28 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());
|
|
TEST_TRAP_UNHANDLED_ERRORS();
|
|
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);
|
|
|
|
}
|