joeylib2/tools/tiled2joey/tiled2joey.c

1206 lines
44 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// tiled2joey.c - a Tiled map as JoeyLib tiles and tile maps.
//
// tiled2joey [--header OUT.h] MAP SHEET.png MAPDIR
//
// MAP is a Tiled map, TMX (XML) or TMJ (JSON): orthogonal, finite, with 8x8
// tiles. Its tilesets, embedded or external (TSX or TSJ), are single images,
// indexed PNGs whose colours make one palette of at most 16 between them:
// the first tileset's colours keep their indices (so its index 0 stays the
// background), and the others' are matched by colour or given a slot the
// first does not use.
//
// SHEET.png gets the tiles the map's tile layers use, in a row, in the
// order they first appear: what assetbake --type tile bakes into a tile bank
// for each port. A tile Tiled draws flipped or rotated is baked as a tile of
// its own (JoeyLib draws no tile flipped), so the map can use up to 255
// distinct tiles and orientations between all its layers.
//
// MAPDIR gets one LAYER.map for each tile layer (its name, letters, digits,
// - and _ kept): width x height bytes, row after row, each a tile's index in
// the bank or 0xFF (JL_TILE_MAP_SKIP) where the layer is empty, as
// jlTileMapPaste takes them. The files are the same on every port.
//
// --header writes a C header naming the map's size, its tile count, and
// each layer's file. Object and image layers, tile animations, and tile
// properties are left out.
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "toolBase64.h"
#include "toolBuf.h"
#include "toolFile.h"
#include "toolJson.h"
#include "toolPng.h"
#include "toolXml.h"
#include "toolZlib.h"
#define TOOL_NAME "tiled2joey"
#define USAGE_EXIT 2
#define TILE_SIDE 8
#define TILE_PIXELS (TILE_SIDE * TILE_SIDE)
#define MAX_TILES 255u
#define SKIP 0xFFu
#define MAX_COLOURS 16u
#define FLIP_H 0x80000000u
#define FLIP_V 0x40000000u
#define FLIP_D 0x20000000u
#define ROTATE_HEX 0x10000000u
#define GID_MASK 0x0FFFFFFFu
#define GID_BYTES 4u
#define MAP_EXTENSION ".map"
// A map is XML when its first bytes, after any space, start with '<'.
#define XML_SNIFF_BYTES 64u
// Room for one field's text, and for a name as Python's repr() shows it.
#define FIELD_TEXT_MAX 64u
#define REPR_MAX 512u
#define PATH_MAX_BYTES 4096u
#define DIR_MODE 0777
#define PRINTABLE_FIRST 0x20u
#define PRINTABLE_END 0x7Fu
#define ASCII_END 0x80u
#define UTF8_CONT_MASK 0xC0u
#define UTF8_CONT 0x80u
// Python's float repr: the shortest of up to this many digits that reads
// back the same.
#define FLOAT_DIGITS_MAX 17
// A header or tileset field as Python held it: missing (None), and if not,
// its str() and, when int() of it works, that.
typedef struct FieldT {
bool present;
char text[FIELD_TEXT_MAX];
bool hasInt;
long value;
} FieldT;
// A tileset: what the map says of it, and once its image is read, its
// tiles as 8x8 grids of sheet palette indices.
typedef struct TilesetT {
char *name;
long firstGid;
long tileWidth;
long tileHeight;
long margin;
long spacing;
long columns;
char *image;
bool animated;
uint8_t *tiles;
size_t tileCount;
} TilesetT;
typedef struct LayerT {
char *name;
uint32_t *gids;
size_t gidCount;
} LayerT;
typedef struct MapT {
FieldT orientation;
FieldT infinite;
FieldT tileWidth;
FieldT tileHeight;
FieldT width;
FieldT height;
TilesetT *tilesets;
size_t tilesetCount;
LayerT *layers;
size_t layerCount;
} MapT;
// A distinct tile the layers place: its tileset, its tile there, and how
// Tiled flips it.
typedef struct TileKeyT {
long firstGid;
size_t local;
uint32_t flags;
} TileKeyT;
static void decodeData(const char *layerName, const char *text, const ToolJsonT *list, const char *encoding, const char *compression, LayerT *layer);
static char *dirName(const char *path);
static void fieldFromJson(FieldT *field, const ToolJsonT *value);
static void fieldFromText(FieldT *field, const char *text);
static void gidAdd(LayerT *layer, uint32_t gid);
static bool isXml(const uint8_t *data, size_t len);
static void jsonLayers(const ToolJsonT *items, MapT *map);
static long jsonLong(const ToolJsonT *value, const char *what, const char *name);
static void layerAdd(MapT *map, const char *name, LayerT *layer);
static void layerFileName(const char *name, char **taken, size_t takenCount, char *out, size_t outSize);
static void loadMap(const char *path, MapT *map);
static void loadTileset(const char *path, TilesetT *tileset);
static void loadTilesetJson(const ToolJsonT *data, const char *base, TilesetT *tileset);
static void loadTilesetXml(const ToolXmlT *element, const char *base, TilesetT *tileset);
static void makeDirs(const char *path);
static char *pathJoin(const char *base, const char *name);
static const char *pyRepr(const char *s);
static void pyUpperIdent(const char *s, size_t len, FILE *f);
static uint8_t *readAll(const char *path, size_t *len);
static char *stringCopy(const char *s);
static void tilesetAdd(MapT *map, long firstGid, TilesetT *tileset);
static void tilesetPixels(TilesetT *tileset, uint8_t palette[MAX_COLOURS][3], bool used[MAX_COLOURS], bool first);
static void usage(void);
static void writeHeader(const char *path, const char *mapPath, long width, long height, size_t count, const MapT *map, char **files);
static void xmlLayers(const ToolXmlT *element, MapT *map);
static long xmlLong(const ToolXmlT *element, const char *attr, long fallback, bool required);
// A layer's gids from its data: csv (text, or a JSON list), or base64 of
// little-endian 32-bit words, raw, zlib or gzip.
static void decodeData(const char *layerName, const char *text, const ToolJsonT *list, const char *encoding, const char *compression, LayerT *layer) {
const char *p;
const char *start;
uint8_t *raw;
uint8_t *inflated;
size_t rawLen;
size_t inflatedLen;
size_t i;
long v;
double d;
if (encoding == NULL || encoding[0] == '\0' || strcmp(encoding, "csv") == 0) {
if (list != NULL) {
for (i = 0; i < list->count; i++) {
if (list->items[i]->type == TOOL_JSON_NUMBER) {
d = trunc(list->items[i]->number);
gidAdd(layer, (uint32_t)(int64_t)d);
} else if (list->items[i]->type == TOOL_JSON_BOOL) {
gidAdd(layer, list->items[i]->boolean ? 1u : 0u);
} else if (list->items[i]->type == TOOL_JSON_STRING && toolParseInt(list->items[i]->string, strlen(list->items[i]->string), &v)) {
gidAdd(layer, (uint32_t)v);
} else {
toolFail("layer %s holds a cell that is not a number", pyRepr(layerName));
}
}
return;
}
// Numbers apart by any run of commas and space.
for (p = text; *p != '\0';) {
while (*p == ',' || isspace((unsigned char)*p)) {
p++;
}
if (*p == '\0') {
break;
}
start = p;
while (*p != '\0' && *p != ',' && !isspace((unsigned char)*p)) {
p++;
}
if (!toolParseInt(start, (size_t)(p - start), &v)) {
toolFail("layer %s holds '%.*s', which is not a number", pyRepr(layerName), (int)(p - start), start);
}
gidAdd(layer, (uint32_t)v);
}
return;
}
if (strcmp(encoding, "base64") != 0) {
toolFail("layer encoding %s is not supported", pyRepr(encoding));
}
if (text == NULL || !toolBase64Decode(text, strlen(text), &raw, &rawLen)) {
toolFail("layer %s's base64 data is broken", pyRepr(layerName));
}
if (compression != NULL && compression[0] != '\0') {
if (strcmp(compression, "zlib") != 0 && strcmp(compression, "gzip") != 0) {
toolFail("layer compression %s is not supported (use zlib, gzip, or none)", pyRepr(compression));
}
if (!toolInflate(raw, rawLen, strcmp(compression, "zlib") == 0 ? TOOL_ZLIB_ZLIB : TOOL_ZLIB_GZIP, &inflated, &inflatedLen)) {
toolFail("layer %s's %s data is broken", pyRepr(layerName), compression);
}
free(raw);
raw = inflated;
rawLen = inflatedLen;
}
if (rawLen % GID_BYTES != 0u) {
toolFail("layer %s's data is not whole 32-bit cells", pyRepr(layerName));
}
for (i = 0; i < rawLen; i += GID_BYTES) {
gidAdd(layer, (uint32_t)raw[i] | ((uint32_t)raw[i + 1u] << 8) | ((uint32_t)raw[i + 2u] << 16) | ((uint32_t)raw[i + 3u] << 24));
}
free(raw);
}
// The folder a path is in ("" for a bare name), as Python's os.path.dirname.
static char *dirName(const char *path) {
const char *slash;
char *out;
size_t n;
slash = strrchr(path, '/');
n = slash == NULL ? 0u : (size_t)(slash - path);
// A path's leading slashes stay (dirname("/a") is "/").
while (slash != NULL && n == 0u && path[n] == '/') {
n++;
}
while (n > 1u && path[n - 1u] == '/') {
n--;
}
if (slash != NULL && n == 0u) {
n = 1u;
}
out = (char *)malloc(n + 1u);
if (out == NULL) {
toolFail("out of memory");
}
memcpy(out, path, n);
out[n] = '\0';
return out;
}
// A JSON member as Python held it: None for null or missing; str() and
// int() as Python makes them of a string, a number, or a bool.
static void fieldFromJson(FieldT *field, const ToolJsonT *value) {
int digits;
double back;
memset(field, 0, sizeof(*field));
if (value == NULL || value->type == TOOL_JSON_NULL) {
snprintf(field->text, FIELD_TEXT_MAX, "None");
return;
}
field->present = true;
if (value->type == TOOL_JSON_STRING) {
snprintf(field->text, FIELD_TEXT_MAX, "%s", value->string);
field->hasInt = toolParseInt(value->string, strlen(value->string), &field->value);
} else if (value->type == TOOL_JSON_BOOL) {
snprintf(field->text, FIELD_TEXT_MAX, "%s", value->boolean ? "True" : "False");
field->hasInt = true;
field->value = value->boolean ? 1 : 0;
} else if (value->type == TOOL_JSON_NUMBER) {
field->hasInt = isfinite(value->number) != 0;
field->value = field->hasInt ? (long)trunc(value->number) : 0;
if (value->integer) {
snprintf(field->text, FIELD_TEXT_MAX, "%.0f", value->number);
} else {
// Python's repr of a float: the fewest digits that read back.
for (digits = 1; digits <= FLOAT_DIGITS_MAX; digits++) {
snprintf(field->text, FIELD_TEXT_MAX, "%.*g", digits, value->number);
back = strtod(field->text, NULL);
if (back == value->number) {
break;
}
}
if (strpbrk(field->text, ".einf") == NULL) {
strncat(field->text, ".0", FIELD_TEXT_MAX - strlen(field->text) - 1u);
}
}
} else {
snprintf(field->text, FIELD_TEXT_MAX, "%s", value->type == TOOL_JSON_ARRAY ? "[...]" : "{...}");
}
}
// An XML attribute as Python held it: None when missing, else its text, and
// int() of it when that works.
static void fieldFromText(FieldT *field, const char *text) {
memset(field, 0, sizeof(*field));
if (text == NULL) {
snprintf(field->text, FIELD_TEXT_MAX, "None");
return;
}
field->present = true;
snprintf(field->text, FIELD_TEXT_MAX, "%s", text);
field->hasInt = toolParseInt(text, strlen(text), &field->value);
}
// A gid added to a layer (the list grows to each next power of two).
static void gidAdd(LayerT *layer, uint32_t gid) {
uint32_t *gids;
if ((layer->gidCount & (layer->gidCount - 1u)) == 0u) {
gids = (uint32_t *)realloc(layer->gids, (layer->gidCount == 0u ? 1u : layer->gidCount * 2u) * sizeof(uint32_t));
if (gids == NULL) {
toolFail("out of memory");
}
layer->gids = gids;
}
layer->gids[layer->gidCount++] = gid;
}
// XML when the first bytes, after any space, start with '<'.
static bool isXml(const uint8_t *data, size_t len) {
size_t i;
size_t n;
n = len < XML_SNIFF_BYTES ? len : XML_SNIFF_BYTES;
for (i = 0; i < n && (data[i] == ' ' || data[i] == '\t' || data[i] == '\n' || data[i] == '\r' || data[i] == '\v' || data[i] == '\f'); i++) {
}
return i < n && data[i] == '<';
}
// A TMJ layer list's tile layers, groups' too, in order.
static void jsonLayers(const ToolJsonT *items, MapT *map) {
const ToolJsonT *layer;
const ToolJsonT *type;
const ToolJsonT *data;
const ToolJsonT *nameValue;
const ToolJsonT *encoding;
const ToolJsonT *compression;
const char *name;
LayerT out;
size_t i;
if (items == NULL || items->type != TOOL_JSON_ARRAY) {
return;
}
for (i = 0; i < items->count; i++) {
layer = items->items[i];
type = toolJsonGet(layer, "type");
if (type == NULL || type->type != TOOL_JSON_STRING) {
continue;
}
nameValue = toolJsonGet(layer, "name");
name = (nameValue != NULL && nameValue->type == TOOL_JSON_STRING) ? nameValue->string : NULL;
if (strcmp(type->string, "group") == 0) {
jsonLayers(toolJsonGet(layer, "layers"), map);
} else if (strcmp(type->string, "tilelayer") == 0) {
if (toolJsonGet(layer, "chunks") != NULL) {
toolFail("layer %s is in chunks (an infinite map); make the map finite", pyRepr(name));
}
data = toolJsonGet(layer, "data");
if (data == NULL) {
toolFail("layer %s has no data", pyRepr(name));
}
encoding = toolJsonGet(layer, "encoding");
compression = toolJsonGet(layer, "compression");
memset(&out, 0, sizeof(out));
decodeData(name, data->type == TOOL_JSON_STRING ? data->string : NULL, data->type == TOOL_JSON_ARRAY ? data : NULL, (encoding != NULL && encoding->type == TOOL_JSON_STRING) ? encoding->string : NULL, (compression != NULL && compression->type == TOOL_JSON_STRING) ? compression->string : NULL, &out);
layerAdd(map, name, &out);
}
}
}
// A JSON tileset's number field; a fatal error when it is not one.
static long jsonLong(const ToolJsonT *value, const char *what, const char *name) {
if (value == NULL || value->type != TOOL_JSON_NUMBER) {
toolFail("tileset %s has no %s", pyRepr(name), what);
}
return (long)trunc(value->number);
}
static void layerAdd(MapT *map, const char *name, LayerT *layer) {
LayerT *layers;
layers = (LayerT *)realloc(map->layers, (map->layerCount + 1u) * sizeof(LayerT));
if (layers == NULL) {
toolFail("out of memory");
}
map->layers = layers;
layer->name = name != NULL ? stringCopy(name) : NULL;
map->layers[map->layerCount++] = *layer;
}
// A layer's file name: its name, every run of characters but letters,
// digits, - and _ made one _, with no _ at either end ("layer" if nothing
// is left), and _2, _3, ... after it while that is taken.
static void layerFileName(const char *name, char **taken, size_t takenCount, char *out, size_t outSize) {
ToolBufT base;
size_t i;
size_t start;
size_t end;
size_t t;
long n;
bool clash;
bool inRun;
char c;
memset(&base, 0, sizeof(base));
inRun = false;
for (i = 0; name != NULL && name[i] != '\0'; i++) {
c = name[i];
if (isalnum((unsigned char)c) && (unsigned char)c < ASCII_END) {
toolBufByte(&base, (uint8_t)c);
inRun = false;
} else if (c == '_' || c == '-') {
toolBufByte(&base, (uint8_t)c);
inRun = false;
} else if (!inRun) {
toolBufByte(&base, '_');
inRun = true;
}
}
start = 0u;
end = base.len;
while (start < end && base.data[start] == '_') {
start++;
}
while (end > start && base.data[end - 1u] == '_') {
end--;
}
if (start == end) {
snprintf(out, outSize, "layer");
} else {
snprintf(out, outSize, "%.*s", (int)(end - start), (const char *)base.data + start);
}
toolBufFree(&base);
start = strlen(out);
for (n = 2;; n++) {
clash = false;
for (t = 0; t < takenCount; t++) {
if (strcmp(taken[t], out) == 0) {
clash = true;
}
}
if (!clash) {
break;
}
snprintf(out + start, outSize - start, "_%ld", n);
}
}
// The map at path: its header, tilesets (sorted by firstgid, a stable
// sort), and tile layers.
static void loadMap(const char *path, MapT *map) {
uint8_t *data;
size_t len;
size_t i;
size_t j;
char *base;
char *joined;
char err[TOOL_XML_ERROR_BYTES];
ToolXmlT *root;
ToolJsonT *json;
const ToolJsonT *tilesets;
const ToolJsonT *entry;
const ToolJsonT *firstGid;
const ToolJsonT *source;
const char *sourceText;
TilesetT tileset;
TilesetT held;
long width;
long height;
memset(map, 0, sizeof(*map));
data = readAll(path, &len);
base = dirName(path);
if (isXml(data, len)) {
root = toolXmlParse((const char *)data, len, err);
if (root == NULL) {
toolFail("%s: %s", path, err);
}
fieldFromText(&map->orientation, toolXmlAttr(root, "orientation"));
fieldFromText(&map->infinite, toolXmlAttr(root, "infinite"));
fieldFromText(&map->tileWidth, toolXmlAttr(root, "tilewidth"));
fieldFromText(&map->tileHeight, toolXmlAttr(root, "tileheight"));
fieldFromText(&map->width, toolXmlAttr(root, "width"));
fieldFromText(&map->height, toolXmlAttr(root, "height"));
for (i = 0; i < root->childCount; i++) {
if (strcmp(root->children[i]->name, "tileset") != 0) {
continue;
}
memset(&tileset, 0, sizeof(tileset));
sourceText = toolXmlAttr(root->children[i], "source");
if (sourceText != NULL && sourceText[0] != '\0') {
joined = pathJoin(base, sourceText);
loadTileset(joined, &tileset);
free(joined);
} else {
loadTilesetXml(root->children[i], base, &tileset);
}
tilesetAdd(map, xmlLong(root->children[i], "firstgid", 0, true), &tileset);
}
xmlLayers(root, map);
toolXmlFree(root);
} else {
json = toolJsonParse((const char *)data, len, err);
if (json == NULL) {
toolFail("%s: %s", path, err);
}
if (json->type != TOOL_JSON_OBJECT) {
toolFail("%s is not a Tiled map", path);
}
fieldFromJson(&map->orientation, toolJsonGet(json, "orientation"));
fieldFromJson(&map->infinite, toolJsonGet(json, "infinite"));
fieldFromJson(&map->tileWidth, toolJsonGet(json, "tilewidth"));
fieldFromJson(&map->tileHeight, toolJsonGet(json, "tileheight"));
fieldFromJson(&map->width, toolJsonGet(json, "width"));
fieldFromJson(&map->height, toolJsonGet(json, "height"));
tilesets = toolJsonGet(json, "tilesets");
for (i = 0; tilesets != NULL && tilesets->type == TOOL_JSON_ARRAY && i < tilesets->count; i++) {
entry = tilesets->items[i];
memset(&tileset, 0, sizeof(tileset));
source = toolJsonGet(entry, "source");
if (source != NULL) {
if (source->type != TOOL_JSON_STRING) {
toolFail("a tileset's source is not a file name");
}
joined = pathJoin(base, source->string);
loadTileset(joined, &tileset);
free(joined);
} else {
loadTilesetJson(entry, base, &tileset);
}
firstGid = toolJsonGet(entry, "firstgid");
if (firstGid == NULL || firstGid->type != TOOL_JSON_NUMBER) {
toolFail("a tileset has no firstgid");
}
tilesetAdd(map, (long)trunc(firstGid->number), &tileset);
}
jsonLayers(toolJsonGet(json, "layers"), map);
toolJsonFree(json);
}
free(base);
free(data);
if (!map->orientation.present || strcmp(map->orientation.text, "orthogonal") != 0) {
toolFail("the map is %s; only orthogonal maps are supported", map->orientation.text);
}
for (i = 0; map->infinite.text[i] != '\0'; i++) {
map->infinite.text[i] = (char)tolower((unsigned char)map->infinite.text[i]);
}
if (map->infinite.present && (strcmp(map->infinite.text, "1") == 0 || strcmp(map->infinite.text, "true") == 0)) {
toolFail("the map is infinite; make it finite");
}
if (!map->tileWidth.hasInt || !map->tileHeight.hasInt) {
toolFail("the map's tile size is %sx%s, not numbers", map->tileWidth.text, map->tileHeight.text);
}
if (map->tileWidth.value != TILE_SIDE || map->tileHeight.value != TILE_SIDE) {
toolFail("the map's tiles are %sx%s; JoeyLib's are 8x8", map->tileWidth.text, map->tileHeight.text);
}
if (!map->width.hasInt || !map->height.hasInt) {
toolFail("the map's size is %sx%s, not numbers", map->width.text, map->height.text);
}
width = map->width.value;
height = map->height.value;
if (map->layerCount == 0u) {
toolFail("the map has no tile layers");
}
for (i = 0; i < map->layerCount; i++) {
if ((long)map->layers[i].gidCount != width * height) {
toolFail("layer %s holds %zu cells, not %ld", pyRepr(map->layers[i].name), map->layers[i].gidCount, width * height);
}
}
// A stable insertion sort by firstgid.
for (i = 1; i < map->tilesetCount; i++) {
held = map->tilesets[i];
for (j = i; j > 0u && map->tilesets[j - 1u].firstGid > held.firstGid; j--) {
map->tilesets[j] = map->tilesets[j - 1u];
}
map->tilesets[j] = held;
}
}
// An external tileset file, TSX or TSJ; its image is relative to it.
static void loadTileset(const char *path, TilesetT *tileset) {
uint8_t *data;
size_t len;
char *base;
char err[TOOL_XML_ERROR_BYTES];
ToolXmlT *root;
ToolJsonT *json;
data = readAll(path, &len);
base = dirName(path);
if (isXml(data, len)) {
root = toolXmlParse((const char *)data, len, err);
if (root == NULL) {
toolFail("%s: %s", path, err);
}
loadTilesetXml(root, base, tileset);
toolXmlFree(root);
} else {
json = toolJsonParse((const char *)data, len, err);
if (json == NULL) {
toolFail("%s: %s", path, err);
}
loadTilesetJson(json, base, tileset);
toolJsonFree(json);
}
free(base);
free(data);
}
static void loadTilesetJson(const ToolJsonT *data, const char *base, TilesetT *tileset) {
const ToolJsonT *nameValue;
const ToolJsonT *image;
const ToolJsonT *value;
const ToolJsonT *tiles;
size_t i;
nameValue = toolJsonGet(data, "name");
tileset->name = (nameValue != NULL && nameValue->type == TOOL_JSON_STRING) ? stringCopy(nameValue->string) : NULL;
image = toolJsonGet(data, "image");
if (image == NULL) {
toolFail("tileset %s is a collection of images; use one image", pyRepr(tileset->name));
}
tileset->tileWidth = jsonLong(toolJsonGet(data, "tilewidth"), "tilewidth", tileset->name);
tileset->tileHeight = jsonLong(toolJsonGet(data, "tileheight"), "tileheight", tileset->name);
value = toolJsonGet(data, "margin");
tileset->margin = value != NULL ? jsonLong(value, "margin", tileset->name) : 0;
value = toolJsonGet(data, "spacing");
tileset->spacing = value != NULL ? jsonLong(value, "spacing", tileset->name) : 0;
value = toolJsonGet(data, "columns");
tileset->columns = value != NULL ? jsonLong(value, "columns", tileset->name) : 0;
if (image->type != TOOL_JSON_STRING) {
toolFail("tileset %s's image is not a file name", pyRepr(tileset->name));
}
tileset->image = pathJoin(base, image->string);
tiles = toolJsonGet(data, "tiles");
for (i = 0; tiles != NULL && tiles->type == TOOL_JSON_ARRAY && i < tiles->count; i++) {
if (toolJsonGet(tiles->items[i], "animation") != NULL) {
tileset->animated = true;
}
}
}
static void loadTilesetXml(const ToolXmlT *element, const char *base, TilesetT *tileset) {
const ToolXmlT *image;
const char *nameText;
const char *source;
size_t i;
nameText = toolXmlAttr(element, "name");
tileset->name = nameText != NULL ? stringCopy(nameText) : NULL;
image = toolXmlChild(element, "image");
if (image == NULL) {
toolFail("tileset %s is a collection of images; use one image", pyRepr(tileset->name));
}
tileset->tileWidth = xmlLong(element, "tilewidth", 0, true);
tileset->tileHeight = xmlLong(element, "tileheight", 0, true);
tileset->margin = xmlLong(element, "margin", 0, false);
tileset->spacing = xmlLong(element, "spacing", 0, false);
tileset->columns = xmlLong(element, "columns", 0, false);
source = toolXmlAttr(image, "source");
if (source == NULL) {
toolFail("tileset %s's image has no source", pyRepr(tileset->name));
}
tileset->image = pathJoin(base, source);
for (i = 0; i < element->childCount; i++) {
if (strcmp(element->children[i]->name, "tile") == 0 && toolXmlChild(element->children[i], "animation") != NULL) {
tileset->animated = true;
}
}
}
// A folder and every folder above it that is not there yet.
static void makeDirs(const char *path) {
char buf[PATH_MAX_BYTES];
size_t i;
size_t n;
n = strlen(path);
if (n == 0u || n >= sizeof(buf)) {
toolFail("cannot make the folder %s", path);
}
memcpy(buf, path, n + 1u);
for (i = 1; i <= n; i++) {
if (buf[i] == '/' || buf[i] == '\0') {
buf[i] = '\0';
if (mkdir(buf, DIR_MODE) != 0 && errno != EEXIST) {
toolFail("cannot make the folder %s: %s", buf, strerror(errno));
}
buf[i] = path[i];
}
}
}
// name in base's folder, as Python's os.path.join: an absolute name as it
// is, a name in an empty base as it is.
static char *pathJoin(const char *base, const char *name) {
char *out;
size_t n;
if (name[0] == '/' || base[0] == '\0') {
return stringCopy(name);
}
n = strlen(base) + strlen(name) + 2u;
out = (char *)malloc(n);
if (out == NULL) {
toolFail("out of memory");
}
snprintf(out, n, "%s%s%s", base, base[strlen(base) - 1u] == '/' ? "" : "/", name);
return out;
}
// A string as Python's repr() shows it (None for NULL): single quotes, or
// double when it holds a single quote and no double; backslashes, the
// quote, and control characters escaped. In a static buffer.
static const char *pyRepr(const char *s) {
static char out[REPR_MAX];
size_t n;
char quote;
char c;
if (s == NULL) {
return "None";
}
quote = (strchr(s, '\'') != NULL && strchr(s, '"') == NULL) ? '"' : '\'';
n = 0u;
out[n++] = quote;
for (; *s != '\0' && n + 5u < sizeof(out); s++) {
c = *s;
if (c == '\\' || c == quote) {
out[n++] = '\\';
out[n++] = c;
} else if (c == '\t') {
n += (size_t)snprintf(out + n, sizeof(out) - n, "\\t");
} else if (c == '\n') {
n += (size_t)snprintf(out + n, sizeof(out) - n, "\\n");
} else if (c == '\r') {
n += (size_t)snprintf(out + n, sizeof(out) - n, "\\r");
} else if ((unsigned char)c < PRINTABLE_FIRST || (unsigned char)c == PRINTABLE_END) {
n += (size_t)snprintf(out + n, sizeof(out) - n, "\\x%02x", (unsigned char)c);
} else {
out[n++] = c;
}
}
out[n++] = quote;
out[n] = '\0';
return out;
}
// len bytes of s as an upper-case C identifier, each character but ASCII
// letters and digits made one _ (a multi-byte UTF-8 character one too).
static void pyUpperIdent(const char *s, size_t len, FILE *f) {
size_t i;
for (i = 0; i < len; i++) {
if ((unsigned char)s[i] < ASCII_END && isalnum((unsigned char)s[i])) {
fputc(toupper((unsigned char)s[i]), f);
} else if (((unsigned char)s[i] & UTF8_CONT_MASK) != UTF8_CONT) {
fputc('_', f);
}
}
}
static uint8_t *readAll(const char *path, size_t *len) {
uint8_t *data;
if (!toolReadFile(path, &data, len)) {
toolFail("cannot read %s", path);
}
return data;
}
static char *stringCopy(const char *s) {
char *out;
size_t n;
n = strlen(s) + 1u;
out = (char *)malloc(n);
if (out == NULL) {
toolFail("out of memory");
}
memcpy(out, s, n);
return out;
}
static void tilesetAdd(MapT *map, long firstGid, TilesetT *tileset) {
TilesetT *tilesets;
tilesets = (TilesetT *)realloc(map->tilesets, (map->tilesetCount + 1u) * sizeof(TilesetT));
if (tilesets == NULL) {
toolFail("out of memory");
}
tileset->firstGid = firstGid;
map->tilesets = tilesets;
map->tilesets[map->tilesetCount++] = *tileset;
}
// The tileset's tiles as 8x8 grids of indices into palette (16 colours,
// used marks the slots taken). The first tileset's colours take their own
// indices (a colour it holds twice reads as its first); another's are
// matched by colour, or take the first free slot.
static void tilesetPixels(TilesetT *tileset, uint8_t palette[MAX_COLOURS][3], bool used[MAX_COLOURS], bool first) {
ToolImageT image;
char err[TOOL_PNG_ERROR_BYTES];
bool inImage[TOOL_PNG_PALETTE_MAX];
uint8_t remap[TOOL_PNG_PALETTE_MAX];
uint32_t index;
uint32_t slot;
long columns;
long rows;
long row;
long column;
long x0;
long y0;
long x;
long y;
size_t i;
size_t t;
if (tileset->tileWidth != TILE_SIDE || tileset->tileHeight != TILE_SIDE) {
toolFail("tileset %s has %ldx%ld tiles; JoeyLib's are 8x8", pyRepr(tileset->name), tileset->tileWidth, tileset->tileHeight);
}
if (tileset->animated) {
fprintf(stderr, "%s: tileset %s animates tiles; only the tiles the map places are kept\n", TOOL_NAME, pyRepr(tileset->name));
}
if (!toolPngLoad(tileset->image, &image, err)) {
toolFail("%s: %s", tileset->image, err);
}
if (!image.indexed) {
toolFail("%s is not indexed; tilesets must be indexed (palette) PNGs", tileset->image);
}
memset(inImage, 0, sizeof(inImage));
for (i = 0; i < (size_t)image.width * image.height; i++) {
inImage[image.pixels[i]] = true;
}
for (index = 0; index < TOOL_PNG_PALETTE_MAX; index++) {
if (!inImage[index]) {
continue;
}
if (first) {
if (index >= MAX_COLOURS) {
toolFail("%s uses palette index %u; tiles have %u colours", tileset->image, index, MAX_COLOURS);
}
memcpy(palette[index], image.palette[index], 3u);
used[index] = true;
} else {
for (slot = 0; slot < MAX_COLOURS && !(used[slot] && memcmp(palette[slot], image.palette[index], 3u) == 0); slot++) {
}
if (slot == MAX_COLOURS) {
for (slot = 0; slot < MAX_COLOURS && used[slot]; slot++) {
}
if (slot == MAX_COLOURS) {
toolFail("the tilesets' colours make more than %u", MAX_COLOURS);
}
memcpy(palette[slot], image.palette[index], 3u);
used[slot] = true;
}
}
for (slot = 0; !(used[slot] && memcmp(palette[slot], image.palette[index], 3u) == 0); slot++) {
}
remap[index] = (uint8_t)slot;
}
// Python's floor division: a tileset image too small for one tile has
// no columns or rows.
columns = tileset->columns;
if (columns == 0) {
x = (long)image.width - 2 * tileset->margin + tileset->spacing;
columns = x >= 0 ? x / (TILE_SIDE + tileset->spacing) : -((-x + TILE_SIDE + tileset->spacing - 1) / (TILE_SIDE + tileset->spacing));
}
y = (long)image.height - 2 * tileset->margin + tileset->spacing;
rows = y >= 0 ? y / (TILE_SIDE + tileset->spacing) : -((-y + TILE_SIDE + tileset->spacing - 1) / (TILE_SIDE + tileset->spacing));
tileset->tileCount = (columns > 0 && rows > 0) ? (size_t)(columns * rows) : 0u;
tileset->tiles = (uint8_t *)malloc(tileset->tileCount * TILE_PIXELS + 1u);
if (tileset->tiles == NULL) {
toolFail("out of memory");
}
t = 0u;
for (row = 0; row < rows; row++) {
for (column = 0; column < columns; column++) {
x0 = tileset->margin + column * (TILE_SIDE + tileset->spacing);
y0 = tileset->margin + row * (TILE_SIDE + tileset->spacing);
if (x0 < 0 || y0 < 0 || x0 + TILE_SIDE > (long)image.width || y0 + TILE_SIDE > (long)image.height) {
toolFail("tileset %s's tiles run past its image %s", pyRepr(tileset->name), tileset->image);
}
for (y = 0; y < TILE_SIDE; y++) {
for (x = 0; x < TILE_SIDE; x++) {
tileset->tiles[t * TILE_PIXELS + (size_t)(y * TILE_SIDE + x)] = remap[image.pixels[(size_t)(y0 + y) * image.width + (size_t)(x0 + x)]];
}
}
t++;
}
}
toolImageFree(&image);
}
static void usage(void) {
fprintf(stderr, "usage: %s [--header OUT.h] MAP SHEET.png MAPDIR\n", TOOL_NAME);
fprintf(stderr, " MAP: a Tiled map (TMX or TMJ), orthogonal, finite, 8x8 tiles, indexed PNG tilesets\n");
fprintf(stderr, " SHEET.png: the tiles its layers use, in a row; MAPDIR: a LAYER.map for each tile layer\n");
exit(USAGE_EXIT);
}
// The C header: the map's size and tile count as constants named for the
// map, and its layers' files.
static void writeHeader(const char *path, const char *mapPath, long width, long height, size_t count, const MapT *map, char **files) {
const char *base;
const char *mapBase;
const char *dot;
size_t stemLen;
size_t i;
FILE *f;
bool ok;
base = strrchr(path, '/') != NULL ? strrchr(path, '/') + 1 : path;
mapBase = strrchr(mapPath, '/') != NULL ? strrchr(mapPath, '/') + 1 : mapPath;
// The map's name without its extension (Python's splitext: a name that
// is all dots before its last has none).
dot = strrchr(mapBase, '.');
stemLen = strlen(mapBase);
if (dot != NULL) {
for (i = 0; mapBase + i < dot && mapBase[i] == '.'; i++) {
}
if (mapBase + i < dot) {
stemLen = (size_t)(dot - mapBase);
}
}
f = fopen(path, "w");
if (f == NULL) {
toolFail("cannot write %s", path);
}
fprintf(f, "// Generated by tools/tiled2joey from %s: do not edit.\n#ifndef ", mapBase);
pyUpperIdent(base, strlen(base), f);
fprintf(f, "\n#define ");
pyUpperIdent(base, strlen(base), f);
fprintf(f, "\n\n#define ");
pyUpperIdent(mapBase, stemLen, f);
fprintf(f, "_WIDTH %ldu // tiles across\n#define ", width);
pyUpperIdent(mapBase, stemLen, f);
fprintf(f, "_HEIGHT %ldu // tiles down\n#define ", height);
pyUpperIdent(mapBase, stemLen, f);
fprintf(f, "_TILES %zuu // tiles in the bank\n\n// The layers' map files, in the map's order:\n", count);
for (i = 0; i < map->layerCount; i++) {
fprintf(f, "// %s (%s)\n", files[i], map->layers[i].name != NULL ? map->layers[i].name : "None");
}
fprintf(f, "\n#endif\n");
ok = !ferror(f);
ok = fclose(f) == 0 && ok;
if (!ok) {
toolFail("cannot write %s", path);
}
}
// The tile layers among element and everything under it, in document
// order (ElementTree's iter("layer")).
static void xmlLayers(const ToolXmlT *element, MapT *map) {
const ToolXmlT *data;
const char *name;
LayerT layer;
size_t i;
if (strcmp(element->name, "layer") == 0) {
name = toolXmlAttr(element, "name");
data = toolXmlChild(element, "data");
if (data == NULL) {
toolFail("layer %s has no data", pyRepr(name));
}
if (toolXmlChild(data, "chunk") != NULL) {
toolFail("layer %s is in chunks (an infinite map); make the map finite", pyRepr(name));
}
memset(&layer, 0, sizeof(layer));
if (toolXmlChild(data, "tile") != NULL) {
for (i = 0; i < data->childCount; i++) {
if (strcmp(data->children[i]->name, "tile") == 0) {
gidAdd(&layer, (uint32_t)xmlLong(data->children[i], "gid", 0, false));
}
}
} else {
decodeData(name, data->text, NULL, toolXmlAttr(data, "encoding"), toolXmlAttr(data, "compression"), &layer);
}
layerAdd(map, name, &layer);
}
for (i = 0; i < element->childCount; i++) {
xmlLayers(element->children[i], map);
}
}
// An XML attribute as a number: fallback when it is missing and not
// required; a fatal error when it is required and missing, or no number.
static long xmlLong(const ToolXmlT *element, const char *attr, long fallback, bool required) {
const char *text;
long value;
text = toolXmlAttr(element, attr);
if (text == NULL) {
if (required) {
toolFail("<%s> %s has no %s", element->name, pyRepr(toolXmlAttr(element, "name")), attr);
}
return fallback;
}
if (!toolParseInt(text, strlen(text), &value)) {
toolFail("<%s> %s has %s=\"%s\", not a number", element->name, pyRepr(toolXmlAttr(element, "name")), attr, text);
}
return value;
}
int main(int argc, char **argv) {
const char *header;
const char *mapPath;
const char *sheetPath;
const char *mapDir;
MapT map;
TileKeyT keys[MAX_TILES];
uint8_t tiles[MAX_TILES][TILE_PIXELS];
uint8_t palette[MAX_COLOURS][3];
bool used[MAX_COLOURS];
uint8_t **cells;
char **taken;
char **files;
char fileName[PATH_MAX_BYTES];
char path[PATH_MAX_BYTES];
size_t tileCount;
size_t colours;
size_t l;
size_t c;
size_t s;
size_t k;
size_t best;
size_t local;
uint32_t gid;
uint32_t flags;
int a;
int x;
int y;
int sx;
int sy;
int t;
ToolImageT sheet;
toolFileName(TOOL_NAME);
header = NULL;
a = 1;
if (argc - a >= 2 && strcmp(argv[a], "--header") == 0) {
header = argv[a + 1];
a += 2;
}
if (argc - a != 3) {
usage();
}
mapPath = argv[a];
sheetPath = argv[a + 1];
mapDir = argv[a + 2];
loadMap(mapPath, &map);
memset(palette, 0, sizeof(palette));
memset(used, 0, sizeof(used));
for (s = 0; s < map.tilesetCount; s++) {
tilesetPixels(&map.tilesets[s], palette, used, s == 0u);
}
cells = (uint8_t **)calloc(map.layerCount, sizeof(uint8_t *));
if (cells == NULL) {
toolFail("out of memory");
}
tileCount = 0u;
for (l = 0; l < map.layerCount; l++) {
cells[l] = (uint8_t *)malloc(map.layers[l].gidCount + 1u);
if (cells[l] == NULL) {
toolFail("out of memory");
}
for (c = 0; c < map.layers[l].gidCount; c++) {
gid = map.layers[l].gids[c];
if ((gid & ROTATE_HEX) != 0u) {
toolFail("layer %s rotates a tile 120 degrees (hexagonal maps only)", pyRepr(map.layers[l].name));
}
flags = gid & (FLIP_H | FLIP_V | FLIP_D);
gid &= GID_MASK;
if (gid == 0u) {
cells[l][c] = SKIP;
continue;
}
// The tileset with the greatest firstgid not past gid (the
// first of equals).
best = map.tilesetCount;
for (s = 0; s < map.tilesetCount; s++) {
if (map.tilesets[s].firstGid <= (long)gid && (best == map.tilesetCount || map.tilesets[s].firstGid > map.tilesets[best].firstGid)) {
best = s;
}
}
if (best == map.tilesetCount) {
toolFail("layer %s places gid %u, before its first tileset", pyRepr(map.layers[l].name), gid);
}
local = (size_t)((long)gid - map.tilesets[best].firstGid);
if (local >= map.tilesets[best].tileCount) {
toolFail("layer %s places gid %u, past its tileset", pyRepr(map.layers[l].name), gid);
}
for (k = 0; k < tileCount && !(keys[k].firstGid == map.tilesets[best].firstGid && keys[k].local == local && keys[k].flags == flags); k++) {
}
if (k == tileCount) {
if (tileCount == MAX_TILES) {
toolFail("the map uses more than %u distinct tiles and orientations", MAX_TILES);
}
keys[k].firstGid = map.tilesets[best].firstGid;
keys[k].local = local;
keys[k].flags = flags;
// As Tiled draws it: the diagonal flip (x and y swapped)
// first, then the horizontal, then the vertical.
for (y = 0; y < TILE_SIDE; y++) {
for (x = 0; x < TILE_SIDE; x++) {
sx = (flags & FLIP_H) != 0u ? TILE_SIDE - 1 - x : x;
sy = (flags & FLIP_V) != 0u ? TILE_SIDE - 1 - y : y;
if ((flags & FLIP_D) != 0u) {
t = sx;
sx = sy;
sy = t;
}
tiles[k][y * TILE_SIDE + x] = map.tilesets[best].tiles[local * TILE_PIXELS + (size_t)(sy * TILE_SIDE + sx)];
}
}
tileCount++;
}
cells[l][c] = (uint8_t)k;
}
}
if (tileCount == 0u) {
toolFail("the map's tile layers are empty");
}
if (!toolImageCreate(&sheet, (uint32_t)(TILE_SIDE * tileCount), TILE_SIDE, true)) {
toolFail("out of memory");
}
sheet.paletteCount = MAX_COLOURS;
for (c = 0; c < MAX_COLOURS; c++) {
memcpy(sheet.palette[c], palette[c], 3u);
sheet.palette[c][3] = 0xFFu;
}
for (k = 0; k < tileCount; k++) {
for (y = 0; y < TILE_SIDE; y++) {
memcpy(sheet.pixels + (size_t)y * sheet.width + k * TILE_SIDE, tiles[k] + y * TILE_SIDE, TILE_SIDE);
}
}
if (!toolPngSave(sheetPath, &sheet)) {
toolFail("cannot write %s", sheetPath);
}
toolImageFree(&sheet);
makeDirs(mapDir);
taken = (char **)calloc(map.layerCount, sizeof(char *));
files = (char **)calloc(map.layerCount, sizeof(char *));
if (taken == NULL || files == NULL) {
toolFail("out of memory");
}
for (l = 0; l < map.layerCount; l++) {
layerFileName(map.layers[l].name, taken, l, fileName, sizeof(fileName) - strlen(MAP_EXTENSION));
taken[l] = stringCopy(fileName);
strcat(fileName, MAP_EXTENSION);
files[l] = stringCopy(fileName);
snprintf(path, sizeof(path), "%s/%s", mapDir, fileName);
if (!toolWriteFile(path, cells[l], map.layers[l].gidCount)) {
toolFail("cannot write %s", path);
}
}
if (header != NULL) {
writeHeader(header, mapPath, map.width.value, map.height.value, tileCount, &map, files);
}
colours = 0u;
for (c = 0; c < MAX_COLOURS; c++) {
colours += used[c] ? 1u : 0u;
}
printf("%s: %ldx%ld map, %zu tile%s, %zu colour%s, layers: ", TOOL_NAME, map.width.value, map.height.value, tileCount, tileCount == 1u ? "" : "s", colours, colours == 1u ? "" : "s");
for (l = 0; l < map.layerCount; l++) {
printf("%s%s", l == 0u ? "" : ", ", files[l]);
}
printf("\n");
return 0;
}