295 lines
9.8 KiB
C
295 lines
9.8 KiB
C
|
|
/**
|
||
|
|
* @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 <limits.h>
|
||
|
|
#include <stdbool.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akgl/character.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/heap.h>
|
||
|
|
#include <akgl/registry.h>
|
||
|
|
#include <akgl/renderer.h>
|
||
|
|
#include <akgl/sprite.h>
|
||
|
|
#include <akgl/tilemap.h>
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @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 <kind> <file.json> [<kind> <file.json>]...\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;
|
||
|
|
}
|