// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // tiledCheck.c - the checks of tools/tiled2joey that scripts/check-tiled.sh // runs. // // tiledCheck REPO WORK // // Writes one 4x3 map into WORK as TMX (tileset A embedded, tileset B an // external TSX, one layer csv and one base64 of zlib) and as TMJ (A // embedded, B an external TSJ, csv and base64 of gzip, the second layer in // a group). A's four tiles and B's two (with a margin and spacing) each // hold marker pixels, so every orientation Tiled can ask for (flipped // across, down, and across the diagonal) lands its markers in a known // place. tiled2joey, run from JOEY_BUILD's tools/ (REPO's build/ when it is // not set), must turn both maps into the same sheet and maps, and those // must be what is worked out here by hand: the tiles in the order they // first appear, flips baked, A's colours at their own indices and B's new // one in the first free slot, empty cells 0xFF. assetbake must bake the // sheet as a tile bank, and tiled2joey must refuse a map with 16x16 tiles // and one whose tileset is not indexed (A's made truecolour). // // Prints a "TILED:" line a check. Exit 0 all pass, 1 any fails (or a file // the checks need cannot be read or written, which stops them there), 2 a // usage error. #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include "gateArg.h" #include "toolBuf.h" #include "toolFile.h" #include "toolPng.h" #include "toolZlib.h" #define TOOL_NAME "tiledCheck" #define PASS_EXIT 0 #define FAIL_EXIT 1 #define ARG_COUNT 3 #define SIDE 8u #define BIG_SIDE 16u #define SKIP 0xFFu #define FLIP_H 0x80000000u #define FLIP_V 0x40000000u #define FLIP_D 0x20000000u #define MARKERS_MAX 2u #define OPAQUE 0xFFu #define RGBA_BYTES 4u #define MAP_WIDTH 4u #define MAP_HEIGHT 3u #define A_FIRST 1u #define B_FIRST 5u #define B_MARGIN 1u #define B_SPACING 1u #define TOP_NAME "Top Layer!" #define TOP_FILE "Top_Layer.map" #define GROUND_FILE "ground.map" #define KIND_COUNT 2u #define B_TSX "b.tsx" #define B_TSJ "b.tsj" #define MAP_TMX "map.tmx" #define MAP_TMJ "map.tmj" #define MAP_HEADER "map.h" #define SHEET_PNG "sheet.png" #define SHEET_TBK "sheet.tbk" #define MAPS_DIR "maps" #define BIG_TMX "big.tmx" #define BIG_PNG "big.png" #define BIG_MAPS "bigmaps" #define RGB_PNG "rgb.png" #define RGB_MAPS "rgbmaps" #define TBK_COUNT_OFFSET 6u #define CHECK_NAME_WIDTH 28 #define DETAIL_BYTES 256u // "(255, 255, 255)" and its zero byte. #define RGB_TEXT_BYTES 16u #define RUN_ARGS_MAX 16u #define PATH_BYTES 4096u #define DIR_MODE 0777 // A PNG's bit depth and colour type, in its IHDR (after the signature, the // chunk's length and type, and the width and height). #define PNG_DEPTH_AT 24u #define PNG_COLOUR_AT 25u // A zlib stream is a 2-byte header, raw DEFLATE, and a 4-byte Adler-32. #define ZLIB_HEADER_BYTES 2u #define ZLIB_TRAILER_BYTES 4u // A gzip member's header (RFC 1952): no flags, no time stamp, an unknown OS. #define GZIP_ID1 0x1Fu #define GZIP_ID2 0x8Bu #define GZIP_DEFLATE 8u #define GZIP_OS_UNKNOWN 0xFFu #define BASE64_GROUP 3u #define BASE64_BITS 6u #define BASE64_MASK 0x3Fu #define BYTE_BITS 8u // len bytes at text, not NUL-terminated. typedef struct SpanT { const char *text; size_t len; } SpanT; typedef struct RgbT { uint8_t r; uint8_t g; uint8_t b; } RgbT; // A marker pixel: where it is in its tile, and its palette index. typedef struct MarkerT { uint8_t x; uint8_t y; uint8_t index; } MarkerT; typedef struct TileT { size_t count; MarkerT markers[MARKERS_MAX]; } TileT; // A tileset as written: its tiles in a row (with a margin round them and // spacing between). typedef struct TilesetT { const char *name; const char *image; const RgbT *palette; size_t colours; const TileT *tiles; size_t count; uint32_t margin; uint32_t spacing; } TilesetT; // What a child process printed, and how it ended (its exit status, or // minus the signal that killed it). typedef struct RunT { int status; ToolBufT out; ToolBufT err; } RunT; // A sheet: its kind (gPngModes), and its pixels (palette indices for "P", // else RGBA). typedef struct SheetT { const char *mode; ToolImageT image; } SheetT; // What a map converts to, to set beside the other kind's. typedef struct OutputT { SheetT sheet; uint8_t *ground; size_t groundLen; char *top; size_t topLen; } OutputT; // A PNG's kind by its bit depth and colour type: P palette, L grey (1 when // 1-bit, I when 16-bit), LA grey and alpha, RGB, and RGBA. typedef struct PngModeT { uint8_t depth; uint8_t colourType; const char *mode; } PngModeT; #define BLACK {0x00u, 0x00u, 0x00u} #define RED {0xFFu, 0x00u, 0x00u} #define GREEN {0x00u, 0xFFu, 0x00u} #define BLUE {0x00u, 0x00u, 0xFFu} #define YELLOW {0xFFu, 0xFFu, 0x00u} // Tileset A: four tiles in a row. Tileset B: two, margin 1, spacing 1; its // blue is A's, its yellow new. static const TileT gATiles[] = { {1u, {{1u, 2u, 1u}}}, {1u, {{1u, 2u, 2u}}}, {1u, {{1u, 2u, 3u}}}, {2u, {{1u, 2u, 1u}, {6u, 6u, 2u}}} }; static const RgbT gAPalette[] = {BLACK, RED, GREEN, BLUE}; static const TileT gBTiles[] = { {1u, {{0u, 7u, 2u}}}, {1u, {{7u, 0u, 1u}}} }; static const RgbT gBPalette[] = {BLACK, BLUE, YELLOW}; static const TilesetT gTilesetA = {"a", "a.png", gAPalette, GATE_COUNT_OF(gAPalette), gATiles, GATE_COUNT_OF(gATiles), 0u, 0u}; static const TilesetT gTilesetB = {"b", "b.png", gBPalette, GATE_COUNT_OF(gBPalette), gBTiles, GATE_COUNT_OF(gBTiles), B_MARGIN, B_SPACING}; static const uint32_t gGround[] = { A_FIRST, A_FIRST | FLIP_H, A_FIRST | FLIP_V, A_FIRST | FLIP_D, A_FIRST | FLIP_D | FLIP_H, A_FIRST + 1u, 0u, A_FIRST + 2u, A_FIRST + 3u, (A_FIRST + 1u) | FLIP_H | FLIP_V, 0u, 0u }; static const uint32_t gTop[] = { 0u, 0u, 0u, 0u, B_FIRST, 0u, B_FIRST + 1u, 0u, 0u, (B_FIRST + 1u) | FLIP_H, 0u, 0u }; // What the sheet must hold, worked out by hand: each tile's markers as // sheet palette indices, in first-appearance order. A's colours keep their // indices (red 1, green 2, blue 3); B's yellow takes slot 4. static const TileT gExpectTiles[] = { {1u, {{1u, 2u, 1u}}}, // A0 {1u, {{6u, 2u, 1u}}}, // A0 flipped across {1u, {{1u, 5u, 1u}}}, // A0 flipped down {1u, {{2u, 1u, 1u}}}, // A0 x and y swapped {1u, {{5u, 1u, 1u}}}, // A0 swapped, then across {1u, {{1u, 2u, 2u}}}, // A1 {1u, {{1u, 2u, 3u}}}, // A2 {2u, {{1u, 2u, 1u}, {6u, 6u, 2u}}}, // A3 {1u, {{6u, 5u, 2u}}}, // A1 across and down {1u, {{0u, 7u, 4u}}}, // B0 (yellow) {1u, {{7u, 0u, 3u}}}, // B1 (blue) {1u, {{0u, 0u, 3u}}} // B1 across }; static const RgbT gExpectPalette[] = {BLACK, RED, GREEN, BLUE, YELLOW}; static const uint8_t gExpectGround[] = {0u, 1u, 2u, 3u, 4u, 5u, SKIP, 6u, 7u, 8u, SKIP, SKIP}; static const uint8_t gExpectTop[] = {SKIP, SKIP, SKIP, SKIP, 9u, SKIP, 10u, SKIP, SKIP, 11u, SKIP, SKIP}; static const PngModeT gPngModes[] = { {1u, 0u, "1"}, {2u, 0u, "L"}, {4u, 0u, "L"}, {8u, 0u, "L"}, {16u, 0u, "I"}, {8u, 2u, "RGB"}, {16u, 2u, "RGB"}, {1u, 3u, "P"}, {2u, 3u, "P"}, {4u, 3u, "P"}, {8u, 3u, "P"}, {8u, 4u, "LA"}, {16u, 4u, "RGBA"}, {8u, 6u, "RGBA"}, {16u, 6u, "RGBA"} }; static const char gBase64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static size_t gFailures = 0u; static void base64Print(FILE *f, const ToolBufT *data); static void check(const char *name, bool ok, SpanT detail); static bool contains(const char *data, size_t len, const char *needle); static void fileClose(FILE *f, const char *path); static void fileFail(const char *verb, const char *path, int err) __attribute__((noreturn)); static FILE *fileOpen(const char *path); static char *fileRead(const char *path, size_t *len); static void gidsPrint(FILE *f, const uint32_t *gids, size_t count, const char *separator); static void gzipCompress(const ToolBufT *in, ToolBufT *out); static char *hexText(const char *data, size_t len); static void indexedImage(const char *path, const TilesetT *tileset); static bool outputsAgree(const OutputT *a, const OutputT *b); static void pathMake(char *out, const char *dir, const char *name); static void run(RunT *result, const char *program, ...); static void runFree(RunT *result); static void sheetLoad(const char *path, SheetT *sheet); static bool sheetMatches(const SheetT *sheet, char *detail, size_t detailSize); static SpanT spanOf(const char *text); static SpanT spanTrimmed(const ToolBufT *buf); static void tilesetJson(FILE *f, const TilesetT *tileset, uint32_t firstGid); static void tmxWrite(const char *path, uint32_t mapSide); static void toolPath(const char *repo, const char *name, char *out); static void words(const uint32_t *gids, size_t count, ToolBufT *out); static void writeMaps(const char *work); static void zlibCompress(const ToolBufT *in, ToolBufT *out); // Base64 (with padding) of data. static void base64Print(FILE *f, const ToolBufT *data) { uint32_t group; size_t i; size_t k; size_t take; for (i = 0; i < data->len; i += BASE64_GROUP) { take = data->len - i < BASE64_GROUP ? data->len - i : BASE64_GROUP; group = 0u; for (k = 0; k < BASE64_GROUP; k++) { group = (group << BYTE_BITS) | (k < take ? data->data[i + k] : 0u); } for (k = 0; k <= BASE64_GROUP; k++) { fputc(k <= take ? gBase64[(group >> (BASE64_BITS * (BASE64_GROUP - k))) & BASE64_MASK] : '=', f); } } } // One check's line, "TILED: PASS|FAIL ()". static void check(const char *name, bool ok, SpanT detail) { printf("TILED: %-*s %s", CHECK_NAME_WIDTH, name, ok ? "PASS" : "FAIL"); if (detail.len > 0u) { fputs(" (", stdout); fwrite(detail.text, 1u, detail.len, stdout); fputc(')', stdout); } fputc('\n', stdout); if (!ok) { gFailures++; } } static bool contains(const char *data, size_t len, const char *needle) { size_t needleLen; size_t at; needleLen = strlen(needle); for (at = 0; at + needleLen <= len; at++) { if (memcmp(data + at, needle, needleLen) == 0) { return true; } } return false; } static void fileClose(FILE *f, const char *path) { if (fclose(f) != 0) { fileFail("write", path, errno); } } // A file the checks need cannot be had: say so, and the checks stop there. static void fileFail(const char *verb, const char *path, int err) { fprintf(stderr, "%s: cannot %s %s: %s\n", TOOL_NAME, verb, path, strerror(err)); exit(FAIL_EXIT); } static FILE *fileOpen(const char *path) { FILE *f; f = fopen(path, "w"); if (f == NULL) { fileFail("write", path, errno); } return f; } static char *fileRead(const char *path, size_t *len) { struct stat st; uint8_t *data; if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { fileFail("read", path, EISDIR); } if (!toolReadFile(path, &data, len)) { fileFail("read", path, errno); } return (char *)data; } static void gidsPrint(FILE *f, const uint32_t *gids, size_t count, const char *separator) { size_t i; for (i = 0; i < count; i++) { fprintf(f, "%s%u", i > 0u ? separator : "", (unsigned)gids[i]); } } // A gzip member of in, its DEFLATE data zlibCompress's. static void gzipCompress(const ToolBufT *in, ToolBufT *out) { ToolBufT z; memset(&z, 0, sizeof(z)); zlibCompress(in, &z); toolBufByte(out, GZIP_ID1); toolBufByte(out, GZIP_ID2); toolBufByte(out, GZIP_DEFLATE); toolBufByte(out, 0u); toolBufLe32(out, 0u); toolBufByte(out, 0u); toolBufByte(out, GZIP_OS_UNKNOWN); toolBufAdd(out, z.data + ZLIB_HEADER_BYTES, z.len - ZLIB_HEADER_BYTES - ZLIB_TRAILER_BYTES); toolBufLe32(out, toolCrc32(0u, in->data, in->len)); toolBufLe32(out, (uint32_t)in->len); toolBufFree(&z); } // Bytes as lowercase hex, two digits each. static char *hexText(const char *data, size_t len) { char *text; size_t i; text = (char *)malloc(len * 2u + 1u); if (text == NULL) { toolFail("out of memory"); } for (i = 0; i < len; i++) { sprintf(text + i * 2u, "%02x", (unsigned)(uint8_t)data[i]); } text[len * 2u] = '\0'; return text; } // A tileset's image: an 8-bit palette PNG of 256 entries (black past its // colours), its tiles' markers set. static void indexedImage(const char *path, const TilesetT *tileset) { ToolImageT image; uint32_t width; uint32_t x; size_t i; size_t k; size_t m; width = 2u * tileset->margin + (uint32_t)tileset->count * SIDE + ((uint32_t)tileset->count - 1u) * tileset->spacing; if (!toolImageCreate(&image, width, 2u * tileset->margin + SIDE, true)) { toolFail("out of memory"); } image.paletteCount = TOOL_PNG_PALETTE_MAX; for (i = 0; i < TOOL_PNG_PALETTE_MAX; i++) { image.palette[i][3] = OPAQUE; } for (i = 0; i < tileset->colours; i++) { image.palette[i][0] = tileset->palette[i].r; image.palette[i][1] = tileset->palette[i].g; image.palette[i][2] = tileset->palette[i].b; } for (k = 0; k < tileset->count; k++) { for (m = 0; m < tileset->tiles[k].count; m++) { x = tileset->margin + (uint32_t)k * (SIDE + tileset->spacing) + tileset->tiles[k].markers[m].x; image.pixels[(size_t)(tileset->margin + tileset->tiles[k].markers[m].y) * width + x] = tileset->tiles[k].markers[m].index; } } if (!toolPngSave(path, &image)) { fileFail("write", path, errno); } toolImageFree(&image); } // The sheets (kind, size, and pixels), ground layers, and top layers the // same. static bool outputsAgree(const OutputT *a, const OutputT *b) { size_t bytes; bytes = (size_t)a->sheet.image.width * a->sheet.image.height * (a->sheet.image.indexed ? 1u : RGBA_BYTES); return strcmp(a->sheet.mode, b->sheet.mode) == 0 && a->sheet.image.width == b->sheet.image.width && a->sheet.image.height == b->sheet.image.height && a->sheet.image.indexed == b->sheet.image.indexed && memcmp(a->sheet.image.pixels, b->sheet.image.pixels, bytes) == 0 && a->groundLen == b->groundLen && memcmp(a->ground, b->ground, a->groundLen) == 0 && a->topLen == b->topLen && memcmp(a->top, b->top, a->topLen) == 0; } // dir/name into out (PATH_BYTES). static void pathMake(char *out, const char *dir, const char *name) { if (snprintf(out, PATH_BYTES, "%s/%s", dir, name) >= (int)PATH_BYTES) { toolFail("path too long: %s/%s", dir, name); } } // Run program (a path) with the arguments after it (to a NULL), its stdout // and stderr captured; one that cannot be run stops the checks. static void run(RunT *result, const char *program, ...) { struct pollfd fds[2]; ToolBufT *bufs[2]; char *args[RUN_ARGS_MAX]; char chunk[BUFSIZ]; va_list list; size_t count; ssize_t got; pid_t pid; int outPipe[2]; int errPipe[2]; int execPipe[2]; int execErr; int status; int streams; int i; memset(result, 0, sizeof(*result)); args[0] = (char *)program; va_start(list, program); for (count = 1u; count < RUN_ARGS_MAX - 1u && (args[count] = va_arg(list, char *)) != NULL; count++) { } va_end(list); args[count] = NULL; if (pipe(outPipe) != 0 || pipe(errPipe) != 0 || pipe(execPipe) != 0 || fcntl(execPipe[1], F_SETFD, FD_CLOEXEC) != 0) { toolFail("cannot make a pipe"); } pid = fork(); if (pid < 0) { toolFail("cannot fork"); } if (pid == 0) { dup2(outPipe[1], STDOUT_FILENO); dup2(errPipe[1], STDERR_FILENO); close(outPipe[0]); close(outPipe[1]); close(errPipe[0]); close(errPipe[1]); close(execPipe[0]); execv(args[0], args); execErr = errno; got = write(execPipe[1], &execErr, sizeof(execErr)); _exit(FAIL_EXIT); } close(outPipe[1]); close(errPipe[1]); close(execPipe[1]); if (read(execPipe[0], &execErr, sizeof(execErr)) == (ssize_t)sizeof(execErr)) { waitpid(pid, &status, 0); fileFail("run", program, execErr); } close(execPipe[0]); fds[0].fd = outPipe[0]; fds[1].fd = errPipe[0]; fds[0].events = POLLIN; fds[1].events = POLLIN; bufs[0] = &result->out; bufs[1] = &result->err; streams = 2; while (streams > 0) { if (poll(fds, 2u, -1) < 0) { if (errno == EINTR) { continue; } toolFail("cannot poll"); } for (i = 0; i < 2; i++) { if (fds[i].fd < 0 || fds[i].revents == 0) { continue; } got = read(fds[i].fd, chunk, sizeof(chunk)); if (got > 0) { toolBufAdd(bufs[i], chunk, (size_t)got); } else { close(fds[i].fd); fds[i].fd = -1; streams--; } } } waitpid(pid, &status, 0); result->status = WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status); } static void runFree(RunT *result) { toolBufFree(&result->out); toolBufFree(&result->err); } // A sheet read and decoded; one that cannot be read or decoded stops the // checks. static void sheetLoad(const char *path, SheetT *sheet) { char *data; char err[TOOL_PNG_ERROR_BYTES]; size_t len; size_t i; data = fileRead(path, &len); if (!toolPngDecode((const uint8_t *)data, len, &sheet->image, err)) { toolFail("%s: %s", path, err); } sheet->mode = "?"; for (i = 0; i < GATE_COUNT_OF(gPngModes); i++) { if ((uint8_t)data[PNG_DEPTH_AT] == gPngModes[i].depth && (uint8_t)data[PNG_COLOUR_AT] == gPngModes[i].colourType) { sheet->mode = gPngModes[i].mode; } } free(data); } // The sheet is what was worked out by hand: a palette image of the expected // tiles in a row, its first colours the expected ones, every pixel the // expected marker index or 0. The detail says what differs first. static bool sheetMatches(const SheetT *sheet, char *detail, size_t detailSize) { const ToolImageT *image; const TileT *tile; uint32_t want; uint32_t got; size_t index; size_t k; size_t m; uint32_t x; uint32_t y; char have[RGB_TEXT_BYTES]; image = &sheet->image; if (strcmp(sheet->mode, "P") != 0 || image->width != SIDE * GATE_COUNT_OF(gExpectTiles) || image->height != SIDE) { snprintf(detail, detailSize, "mode %s, size (%u, %u)", sheet->mode, (unsigned)image->width, (unsigned)image->height); return false; } for (index = 0; index < GATE_COUNT_OF(gExpectPalette); index++) { if (index < image->paletteCount && image->palette[index][0] == gExpectPalette[index].r && image->palette[index][1] == gExpectPalette[index].g && image->palette[index][2] == gExpectPalette[index].b) { continue; } snprintf(have, sizeof(have), "()"); if (index < image->paletteCount) { snprintf(have, sizeof(have), "(%u, %u, %u)", image->palette[index][0], image->palette[index][1], image->palette[index][2]); } snprintf(detail, detailSize, "palette index %zu is %s, not (%u, %u, %u)", index, have, gExpectPalette[index].r, gExpectPalette[index].g, gExpectPalette[index].b); return false; } for (k = 0; k < GATE_COUNT_OF(gExpectTiles); k++) { tile = &gExpectTiles[k]; for (y = 0; y < SIDE; y++) { for (x = 0; x < SIDE; x++) { want = 0u; for (m = 0; m < tile->count; m++) { if (tile->markers[m].x == x && tile->markers[m].y == y) { want = tile->markers[m].index; } } got = image->pixels[(size_t)y * image->width + k * SIDE + x]; if (got != want) { snprintf(detail, detailSize, "tile %zu pixel (%u,%u) is %u, not %u", k, (unsigned)x, (unsigned)y, (unsigned)got, (unsigned)want); return false; } } } } snprintf(detail, detailSize, "%zu tiles", GATE_COUNT_OF(gExpectTiles)); return true; } static SpanT spanOf(const char *text) { SpanT span; span.text = text; span.len = strlen(text); return span; } // A child's output with the whitespace round it dropped. static SpanT spanTrimmed(const ToolBufT *buf) { SpanT span; span.text = (const char *)buf->data; span.len = buf->len; while (span.len > 0u && isspace((unsigned char)span.text[0])) { span.text++; span.len--; } while (span.len > 0u && isspace((unsigned char)span.text[span.len - 1u])) { span.len--; } return span; } // A tileset as Tiled's JSON has it (json.dump's spacing), with its // firstgid last when it has one (not 0). static void tilesetJson(FILE *f, const TilesetT *tileset, uint32_t firstGid) { fprintf(f, "{\"name\": \"%s\", \"tilewidth\": %u, \"tileheight\": %u, \"spacing\": %u, \"margin\": %u, \"tilecount\": %zu, \"columns\": %zu, \"image\": \"%s\"", tileset->name, SIDE, SIDE, (unsigned)tileset->spacing, (unsigned)tileset->margin, tileset->count, tileset->count, tileset->image); if (firstGid > 0u) { fprintf(f, ", \"firstgid\": %u", (unsigned)firstGid); } fputc('}', f); } // The map as TMX, its tiles mapSide square (SIDE, or not for the map that // must be refused): A embedded, B external, the ground csv and the top // layer base64 of zlib. static void tmxWrite(const char *path, uint32_t mapSide) { ToolBufT raw; ToolBufT z; FILE *f; memset(&raw, 0, sizeof(raw)); memset(&z, 0, sizeof(z)); words(gTop, GATE_COUNT_OF(gTop), &raw); zlibCompress(&raw, &z); f = fileOpen(path); fputs("\n", f); fprintf(f, "\n", MAP_WIDTH, MAP_HEIGHT, (unsigned)mapSide, (unsigned)mapSide); fprintf(f, " \n", A_FIRST, gTilesetA.name, SIDE, SIDE, gTilesetA.count, gTilesetA.count); fprintf(f, " \n \n", gTilesetA.image); fprintf(f, " \n", B_FIRST, B_TSX); fprintf(f, " \n ", MAP_WIDTH, MAP_HEIGHT); gidsPrint(f, gGround, GATE_COUNT_OF(gGround), ","); fputs("\n \n", f); fprintf(f, " \n ", TOP_NAME, MAP_WIDTH, MAP_HEIGHT); base64Print(f, &z); fputs("\n \n\n", f); fileClose(f, path); toolBufFree(&raw); toolBufFree(&z); } // A tool make/tools.mk builds, into out (PATH_BYTES): in JOEY_BUILD's // tools/, or REPO's build/tools/ when it is not set. static void toolPath(const char *repo, const char *name, char *out) { const char *build; char tools[PATH_BYTES]; build = getenv("JOEY_BUILD"); if (build != NULL) { pathMake(tools, build, "tools"); } else { pathMake(tools, repo, "build/tools"); } pathMake(out, tools, name); } // Gids as little-endian 32-bit words, a layer's base64 data before it is // compressed. static void words(const uint32_t *gids, size_t count, ToolBufT *out) { size_t i; for (i = 0; i < count; i++) { toolBufLe32(out, gids[i]); } } // The tilesets' images, B's TSX and TSJ, and the map as TMX and as TMJ. static void writeMaps(const char *work) { ToolBufT raw; ToolBufT gz; FILE *f; char path[PATH_BYTES]; pathMake(path, work, gTilesetA.image); indexedImage(path, &gTilesetA); pathMake(path, work, gTilesetB.image); indexedImage(path, &gTilesetB); pathMake(path, work, B_TSX); f = fileOpen(path); fputs("\n", f); fprintf(f, "\n", gTilesetB.name, SIDE, SIDE, (unsigned)gTilesetB.spacing, (unsigned)gTilesetB.margin, gTilesetB.count, gTilesetB.count); fprintf(f, " \n\n", gTilesetB.image); fileClose(f, path); pathMake(path, work, B_TSJ); f = fileOpen(path); tilesetJson(f, &gTilesetB, 0u); fileClose(f, path); pathMake(path, work, MAP_TMX); tmxWrite(path, SIDE); memset(&raw, 0, sizeof(raw)); memset(&gz, 0, sizeof(gz)); words(gTop, GATE_COUNT_OF(gTop), &raw); gzipCompress(&raw, &gz); pathMake(path, work, MAP_TMJ); f = fileOpen(path); fprintf(f, "{\"type\": \"map\", \"orientation\": \"orthogonal\", \"infinite\": false, \"width\": %u, \"height\": %u, \"tilewidth\": %u, \"tileheight\": %u, \"tilesets\": [", MAP_WIDTH, MAP_HEIGHT, SIDE, SIDE); tilesetJson(f, &gTilesetA, A_FIRST); fprintf(f, ", {\"firstgid\": %u, \"source\": \"%s\"}], \"layers\": [", B_FIRST, B_TSJ); fprintf(f, "{\"type\": \"tilelayer\", \"name\": \"ground\", \"width\": %u, \"height\": %u, \"data\": [", MAP_WIDTH, MAP_HEIGHT); gidsPrint(f, gGround, GATE_COUNT_OF(gGround), ", "); fprintf(f, "]}, {\"type\": \"group\", \"name\": \"upper\", \"layers\": [{\"type\": \"tilelayer\", \"name\": \"%s\", \"width\": %u, \"height\": %u, \"encoding\": \"base64\", \"compression\": \"gzip\", \"data\": \"", TOP_NAME, MAP_WIDTH, MAP_HEIGHT); base64Print(f, &gz); fputs("\"}]}]}", f); fileClose(f, path); toolBufFree(&raw); toolBufFree(&gz); } // A zlib stream of in (toolZlib's deflate). static void zlibCompress(const ToolBufT *in, ToolBufT *out) { uint8_t *z; size_t zLen; if (!toolDeflateZlib(in->data, in->len, &z, &zLen)) { toolFail("cannot compress a layer"); } toolBufAdd(out, z, zLen); free(z); } int main(int argc, char **argv) { static const char *const kinds[KIND_COUNT] = {"tmx", "tmj"}; OutputT outputs[KIND_COUNT]; bool converted[KIND_COUNT]; RunT result; ToolImageT image; ToolImageT rgba; struct stat st; const char *repo; const char *work; char *text; char *hex; char tiled2joey[PATH_BYTES]; char assetbake[PATH_BYTES]; char out[PATH_BYTES]; char header[PATH_BYTES]; char map[PATH_BYTES]; char sheet[PATH_BYTES]; char maps[PATH_BYTES]; char path[PATH_BYTES]; char name[DETAIL_BYTES]; char detail[DETAIL_BYTES]; char err[TOOL_PNG_ERROR_BYTES]; size_t len; size_t k; long count; bool ok; toolFileName(TOOL_NAME); if (argc != ARG_COUNT) { fprintf(stderr, "usage: %s REPO WORK\n", TOOL_NAME); return GATE_USAGE_EXIT; } repo = argv[1]; work = argv[2]; writeMaps(work); toolPath(repo, "tiled2joey", tiled2joey); toolPath(repo, "assetbake", assetbake); memset(outputs, 0, sizeof(outputs)); for (k = 0; k < KIND_COUNT; k++) { converted[k] = false; pathMake(out, work, kinds[k]); if (mkdir(out, DIR_MODE) != 0 && errno != EEXIST) { fileFail("make", out, errno); } snprintf(name, sizeof(name), "map.%s", kinds[k]); pathMake(map, work, name); pathMake(header, out, MAP_HEADER); pathMake(sheet, out, SHEET_PNG); pathMake(maps, out, MAPS_DIR); run(&result, tiled2joey, "--header", header, map, sheet, maps, (char *)NULL); toolBufAdd(&result.out, result.err.data, result.err.len); snprintf(name, sizeof(name), "%s converts", kinds[k]); check(name, result.status == 0, spanTrimmed(&result.out)); if (result.status == 0) { sheetLoad(sheet, &outputs[k].sheet); snprintf(name, sizeof(name), "%s sheet", kinds[k]); ok = sheetMatches(&outputs[k].sheet, detail, sizeof(detail)); check(name, ok, spanOf(detail)); pathMake(path, maps, GROUND_FILE); outputs[k].ground = (uint8_t *)fileRead(path, &outputs[k].groundLen); hex = hexText((const char *)outputs[k].ground, outputs[k].groundLen); snprintf(name, sizeof(name), "%s ground layer", kinds[k]); check(name, outputs[k].groundLen == sizeof(gExpectGround) && memcmp(outputs[k].ground, gExpectGround, sizeof(gExpectGround)) == 0, spanOf(hex)); free(hex); pathMake(path, maps, TOP_FILE); outputs[k].top = stat(path, &st) == 0 ? fileRead(path, &outputs[k].topLen) : NULL; hex = hexText(outputs[k].top, outputs[k].topLen); snprintf(name, sizeof(name), "%s %s layer", kinds[k], TOP_FILE); check(name, outputs[k].topLen == sizeof(gExpectTop) && memcmp(outputs[k].top, gExpectTop, sizeof(gExpectTop)) == 0, spanOf(hex)); free(hex); text = fileRead(header, &len); snprintf(name, sizeof(name), "%s header", kinds[k]); check(name, contains(text, len, "MAP_WIDTH 4u") && contains(text, len, "MAP_HEIGHT 3u") && contains(text, len, "MAP_TILES 12u") && contains(text, len, TOP_FILE), spanOf("")); free(text); converted[k] = true; } runFree(&result); } if (converted[0] && converted[1]) { check("tmx and tmj agree", outputsAgree(&outputs[0], &outputs[1]), spanOf("")); } // The tmx sheet baked as the DOS port's tile bank: its tile count. pathMake(out, work, kinds[0]); pathMake(sheet, out, SHEET_PNG); pathMake(path, work, SHEET_TBK); run(&result, assetbake, "--type", "tile", "--target", "dos", sheet, path, (char *)NULL); count = -1; if (result.status == 0) { text = fileRead(path, &len); if (len < TBK_COUNT_OFFSET + 2u) { toolFail("%s: %zu bytes, too short to hold a tile bank's tile count", path, len); } count = (long)((uint8_t)text[TBK_COUNT_OFFSET] | ((uint8_t)text[TBK_COUNT_OFFSET + 1u] << BYTE_BITS)); free(text); } snprintf(detail, sizeof(detail), "%ld tiles", count); check("sheet bakes as a tile bank", count == (long)GATE_COUNT_OF(gExpectTiles), spanOf(detail)); runFree(&result); // A map of 16x16 tiles. pathMake(map, work, BIG_TMX); pathMake(sheet, work, BIG_PNG); pathMake(maps, work, BIG_MAPS); tmxWrite(map, BIG_SIDE); run(&result, tiled2joey, map, sheet, maps, (char *)NULL); check("16x16 tiles refused", result.status != 0 && contains((const char *)result.err.data, result.err.len, "8x8"), spanTrimmed(&result.err)); runFree(&result); // Tileset A made truecolour. pathMake(path, work, gTilesetA.image); if (!toolPngLoad(path, &image, err)) { toolFail("%s: %s", path, err); } if (!toolImageToRgba(&image, &rgba)) { toolFail("out of memory"); } if (!toolPngSave(path, &rgba)) { fileFail("write", path, errno); } toolImageFree(&image); toolImageFree(&rgba); pathMake(map, work, MAP_TMX); pathMake(sheet, work, RGB_PNG); pathMake(maps, work, RGB_MAPS); run(&result, tiled2joey, map, sheet, maps, (char *)NULL); check("RGB tileset refused", result.status != 0 && contains((const char *)result.err.data, result.err.len, "indexed"), spanTrimmed(&result.err)); runFree(&result); return gFailures > 0u ? FAIL_EXIT : PASS_EXIT; }