1206 lines
44 KiB
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 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 <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_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;
|
|
}
|