/** * @file docs_checkjson.c * @brief Load one documented asset file through the loader that really reads it. * * libakgl's four asset formats -- sprite, character, tilemap and the property * configuration -- are described in prose in the manual and parsed by * `src/sprite.c`, `src/character.c`, `src/tilemap.c` and `src/registry.c`, with * nothing tying the two together. That gap is not hypothetical: * `util/assets/littleguy.json` ships beside the loader it is invalid against, * using pre-prefix state names and a `velocity_x` the current loader does not * accept. * * So a `json kind=sprite` block in a chapter is written to a file and handed * here, and the documented format is by construction the format the loader * accepts. `tests/docs_examples.sh` is the caller. * * **It takes a list of kind/file pairs and loads them in order**, because three * of the four formats are not self-contained: a character names sprites that * have to already be in the registry, and a tilemap names characters. One * process, several files, in the order the library requires them -- which is * itself a fact about the format that the chapter has to state, and now states * by writing it down as `preload=`. * * **It is a host**, like `tools/docs_screenshot.c` and the test suites: it * brings SDL and libakgl up headless itself rather than calling * `akgl_game_init()`, because the loaders need a renderer and the registries and * nothing else. * * The exit status is 0 when every file loaded and 1 when one did not; 2 is a * usage error. Whatever the loader said is on stderr, which is what the harness * shows. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @brief The map a `kind=tilemap` block is loaded into. * * File scope because an akgl_Tilemap carries every layer and tileset it owns and * does not belong on a stack -- the same reason src/game.c keeps * akgl_default_gamemap where it does. */ static akgl_Tilemap MAP; /** @brief Set in HANDLE_DEFAULT and read after FINISH; see the note in main(). */ static int failed = 0; /** * @brief akgl_tilemap_load() with the signature the dispatch table wants. * * The other three loaders take a filename and nothing else. Rather than give the * table two shapes and a branch to pick between them, the odd one out gets a * wrapper. */ static akerr_ErrorContext AKERR_NOIGNORE *load_tilemap(char *fname) { PREPARE_ERROR(errctx); PASS(errctx, akgl_tilemap_load(fname, &MAP)); SUCCEED_RETURN(errctx); } /** * @brief Load every `*.json` in @p dir through @p load, in whatever order the * filesystem hands them back. * * An absent directory is not a failure. A `kind=character` block with no * `setup=` should fail on the sprite it actually names -- which is the sentence * a chapter author needs to read -- rather than on a fixture directory they * never asked for. */ static akerr_ErrorContext AKERR_NOIGNORE *load_directory( const char *dir, akerr_ErrorContext *(*load)(char *fname)) { PREPARE_ERROR(errctx); char **found = NULL; char path[PATH_MAX]; int count = 0; int i = 0; FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "Received null directory"); FAIL_ZERO_RETURN(errctx, load, AKERR_NULLPOINTER, "Received null loader"); found = SDL_GlobDirectory(dir, "*.json", 0, &count); if ( found == NULL ) { SUCCEED_RETURN(errctx); } for ( i = 0; i < count; i++ ) { snprintf(path, sizeof(path), "%s/%s", dir, found[i]); ATTEMPT { CATCH(errctx, load(path)); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); } SDL_free(found); SUCCEED_RETURN(errctx); } /** * @brief Put the sprites a `kind=character` block names into the registry. * * `akgl_character_load_json` resolves every `sprite` field against * #AKGL_REGISTRY_SPRITE and fails if one is missing, so a chapter showing a * character file on its own still needs sprites behind it. * `tests/docs_setups/character.sh` is what stages them. */ static akerr_ErrorContext AKERR_NOIGNORE *prepare_character(void) { static bool done = false; PREPARE_ERROR(errctx); /* * Once per process, whether it is reached through the `character` row or * through prepare_tilemap(). Loading the same sprite file twice registers * the name twice and leaks the first sprite's pool slot. */ if ( done == true ) { SUCCEED_RETURN(errctx); } done = true; PASS(errctx, load_directory("./sprites", akgl_sprite_load_json)); SUCCEED_RETURN(errctx); } /** * @brief The same, one level further out: a tilemap's actor objects name characters. * * `akgl_tilemap_load_layer_object_actor` calls `akgl_actor_set_character`, which * is a registry lookup and nothing more, so the character has to be loaded -- * and a character needs its sprites, which is why this runs both. */ static akerr_ErrorContext AKERR_NOIGNORE *prepare_tilemap(void) { static bool done = false; PREPARE_ERROR(errctx); if ( done == true ) { SUCCEED_RETURN(errctx); } done = true; PASS(errctx, prepare_character()); PASS(errctx, load_directory("./characters", akgl_character_load_json)); SUCCEED_RETURN(errctx); } /** @brief One documented asset format, what has to exist first, and what reads it. */ typedef struct { char *kind; /**< The `kind=` value in the fence. */ akerr_ErrorContext *(*prepare)(void); /**< Registry state the format needs, or `NULL`. */ akerr_ErrorContext *(*load)(char *fname); /**< What the library does with it. */ } docs_JsonKind; /* * A table rather than an if/else ladder, for the reason AGENTS.md gives about * backends: a fifth format is a row, not a branch. The names are the vocabulary * docs/MAINTENANCE.md publishes, so they are also the error message when a * chapter invents one. * * The `prepare` column is where "three of these four formats are not * self-contained" is written down. Each one guards itself so it runs at most * once per process, however it is reached. */ static docs_JsonKind KINDS[] = { { "sprite", NULL, akgl_sprite_load_json }, { "character", prepare_character, akgl_character_load_json }, { "tilemap", prepare_tilemap, load_tilemap }, { "properties", NULL, akgl_registry_load_properties }, { NULL, NULL, NULL } }; static void usage(void) { int i = 0; fprintf(stderr, "usage: akgl_docs_checkjson [ ]...\n" "\n kind is one of:"); for ( i = 0; KINDS[i].kind != NULL; i++ ) { fprintf(stderr, " %s", KINDS[i].kind); } fprintf(stderr, "\n\nLoads each file through libakgl's own loader for that format, in the\n" "order given, in one process. A character's sprites and a tilemap's\n" "characters have to be in the registry before the file that names them.\n"); } /** @brief Load one file through the loader that owns its format. */ static akerr_ErrorContext AKERR_NOIGNORE *load_one(char *kind, char *fname) { PREPARE_ERROR(errctx); int i = 0; FAIL_ZERO_RETURN(errctx, kind, AKERR_NULLPOINTER, "Received null kind"); FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "Received null filename"); for ( i = 0; KINDS[i].kind != NULL; i++ ) { if ( strcmp(KINDS[i].kind, kind) == 0 ) { if ( KINDS[i].prepare != NULL ) { PASS(errctx, KINDS[i].prepare()); } PASS(errctx, KINDS[i].load(fname)); SUCCEED_RETURN(errctx); } } FAIL_RETURN(errctx, AKERR_KEY, "There is no documented asset format called \"%s\"", kind); } /** @brief Everything between SDL being up and the loaders having answered. */ static akerr_ErrorContext AKERR_NOIGNORE *check(int pairs, char **argv) { PREPARE_ERROR(errctx); int i = 0; FAIL_ZERO_RETURN(errctx, argv, AKERR_NULLPOINTER, "Received null argument vector"); PASS(errctx, akgl_error_init()); akgl_renderer = &akgl_default_renderer; FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_ZERO_RETURN( errctx, SDL_CreateWindowAndRenderer( "net/aklabs/libakgl/docs_checkjson", 320, 240, 0, &akgl_window, &akgl_renderer->sdl_renderer), AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); PASS(errctx, akgl_render_2d_bind(akgl_renderer)); PASS(errctx, akgl_heap_init()); PASS(errctx, akgl_registry_init()); for ( i = 0; i < pairs; i++ ) { PASS(errctx, load_one(argv[i * 2], argv[(i * 2) + 1])); } SUCCEED_RETURN(errctx); } int main(int argc, char **argv) { PREPARE_ERROR(errctx); if ( argc < 3 || ((argc - 1) % 2) != 0 ) { usage(); return 2; } SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { CATCH(errctx, check((argc - 1) / 2, &argv[1])); } CLEANUP { if ( akgl_window != NULL ) { SDL_DestroyWindow(akgl_window); akgl_window = NULL; } SDL_Quit(); } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "the documented JSON did not load"); /* * A flag rather than a `return` here: FINISH ends with RELEASE_ERROR, * and leaving a HANDLE block early means the context never goes back to * AKERR_ARRAY_ERROR. AGENTS.md documents that as a process-killing leak, * and scripts/check_error_protocol.py fails the build over it. */ failed = 1; /* * FINISH_NORETURN rather than FINISH: FINISH expands a * `return __err_context` that an int-returning function cannot compile, * even where the branch is unreachable. */ } FINISH_NORETURN(errctx); return failed; }