joeylib2/tests/gate/tiledCheck.c
2026-10-19 15:46:47 -05:00

928 lines
33 KiB
C

// 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 colors 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 truecolor).
//
// 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 <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#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 color 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_COLOR_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 colors;
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 color type: P palette, L gray (1 when
// 1-bit, I when 16-bit), LA gray and alpha, RGB, and RGBA.
typedef struct PngModeT {
uint8_t depth;
uint8_t colorType;
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 colors 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: <name> PASS|FAIL (<detail>)".
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
// colors), 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->colors; 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_COLOR_AT] == gPngModes[i].colorType) {
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 colors 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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f);
fprintf(f, "<map version=\"1.10\" orientation=\"orthogonal\" renderorder=\"right-down\" width=\"%u\" height=\"%u\" tilewidth=\"%u\" tileheight=\"%u\" infinite=\"0\">\n", MAP_WIDTH, MAP_HEIGHT, (unsigned)mapSide, (unsigned)mapSide);
fprintf(f, " <tileset firstgid=\"%u\" name=\"%s\" tilewidth=\"%u\" tileheight=\"%u\" tilecount=\"%zu\" columns=\"%zu\">\n", A_FIRST, gTilesetA.name, SIDE, SIDE, gTilesetA.count, gTilesetA.count);
fprintf(f, " <image source=\"%s\"/>\n </tileset>\n", gTilesetA.image);
fprintf(f, " <tileset firstgid=\"%u\" source=\"%s\"/>\n", B_FIRST, B_TSX);
fprintf(f, " <layer id=\"1\" name=\"ground\" width=\"%u\" height=\"%u\">\n <data encoding=\"csv\">", MAP_WIDTH, MAP_HEIGHT);
gidsPrint(f, gGround, GATE_COUNT_OF(gGround), ",");
fputs("</data>\n </layer>\n", f);
fprintf(f, " <layer id=\"2\" name=\"%s\" width=\"%u\" height=\"%u\">\n <data encoding=\"base64\" compression=\"zlib\">", TOP_NAME, MAP_WIDTH, MAP_HEIGHT);
base64Print(f, &z);
fputs("</data>\n </layer>\n</map>\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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f);
fprintf(f, "<tileset version=\"1.10\" name=\"%s\" tilewidth=\"%u\" tileheight=\"%u\" spacing=\"%u\" margin=\"%u\" tilecount=\"%zu\" columns=\"%zu\">\n", gTilesetB.name, SIDE, SIDE, (unsigned)gTilesetB.spacing, (unsigned)gTilesetB.margin, gTilesetB.count, gTilesetB.count);
fprintf(f, " <image source=\"%s\"/>\n</tileset>\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 truecolor.
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;
}