// 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 colors make one palette of at most 16 between them: // the first tileset's colors keep their indices (so its index 0 stays the // background), and the others' are matched by color 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 #include #include #include #include #include #include #include #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_COLORS 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_COLORS][3], bool used[MAX_COLORS], 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 colors, // used marks the slots taken). The first tileset's colors take their own // indices (a color it holds twice reads as its first); another's are // matched by color, or take the first free slot. static void tilesetPixels(TilesetT *tileset, uint8_t palette[MAX_COLORS][3], bool used[MAX_COLORS], 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_COLORS) { toolFail("%s uses palette index %u; tiles have %u colors", tileset->image, index, MAX_COLORS); } memcpy(palette[index], image.palette[index], 3u); used[index] = true; } else { for (slot = 0; slot < MAX_COLORS && !(used[slot] && memcmp(palette[slot], image.palette[index], 3u) == 0); slot++) { } if (slot == MAX_COLORS) { for (slot = 0; slot < MAX_COLORS && used[slot]; slot++) { } if (slot == MAX_COLORS) { toolFail("the tilesets' colors make more than %u", MAX_COLORS); } 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_COLORS][3]; bool used[MAX_COLORS]; uint8_t **cells; char **taken; char **files; char fileName[PATH_MAX_BYTES]; char path[PATH_MAX_BYTES]; size_t tileCount; size_t colors; 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_COLORS; for (c = 0; c < MAX_COLORS; 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); } colors = 0u; for (c = 0; c < MAX_COLORS; c++) { colors += used[c] ? 1u : 0u; } printf("%s: %ldx%ld map, %zu tile%s, %zu color%s, layers: ", TOOL_NAME, map.width.value, map.height.value, tileCount, tileCount == 1u ? "" : "s", colors, colors == 1u ? "" : "s"); for (l = 0; l < map.layerCount; l++) { printf("%s%s", l == 0u ? "" : ", ", files[l]); } printf("\n"); return 0; }