#include #include #include #include #include #include #include #include #include #include #include #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); } 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_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); }