/* Copyright (c) 2023 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "3dmodelio/TextureAtlasData.h" #include "binaryserialization/BinaryDeserializationContext.h" #include "binaryserialization/BinarySerializationContext.h" #include "font/BitmapFont2.h" #include "gamemath/Grid.h" #include "jsonio/JSONEmitter.h" #include "jsonio/JSONParser.h" #include "tileset/ImageCollection.h" #include "tileset/SpriteCollection.h" #include "tileset/TileAdjacencyBehaviorSet.h" #include "tileset/TilesetAdjacencyBlendMap.h" #include "tileset/TileMapEditData.h" #include "tileset/TileZoneMap.h" #include "utilities/FileBundle.h" #include "utilities/HashTable.h" #include "utilities/IOUtilities.h" #include #include #include #include #include #include #include #include #define STRIP_FLAG_DOCUMENT_NAME 0x1 #define STRIP_FLAG_ITEM_NAME 0x2 #define STRIP_FLAG_SCHEMA 0x4 #define STRIP_FLAG_LAYER_CROP 0x8 #define REPLACEMENT_ASCII_MIN ' ' #define REPLACEMENT_ASCII_MAX '~' enum fileType { FILE_TYPE_IMAGE_COLLECTION_BUNDLE, FILE_TYPE_TILESET, FILE_TYPE_TILE_MAP, FILE_TYPE_TILE_MAP_BUNDLE, FILE_TYPE_BEHAVIOR_SET, FILE_TYPE_BLEND_MAP, FILE_TYPE_SPRITE_COLLECTION, FILE_TYPE_TILE_ZONE_MAP, FILE_TYPE_BITMAP_FONT_2, FILE_TYPE_TEXTURE_ATLAS_DATA }; #define BUNDLE_FILE_TYPE_TILE_MAP 1002 #define BUNDLE_FILE_TYPE_ZONE_MAP 1007 struct typedFileData { enum fileType fileType; union { struct { FileBundle * fileBundle; ImageCollection * imageCollection; TextureAtlasData * atlasData; } imageCollectionBundle; TilesetEditData * tileset; TileMapEditData * tileMap; struct { FileBundle * fileBundle; TileZoneMap * zoneMap; unsigned int tileMapCount; TileMapEditData ** tileMaps; } tileMapBundle; TileAdjacencyBehaviorSet * behaviorSet; TilesetAdjacencyBlendMap * blendMap; SpriteCollection * spriteCollection; TileZoneMap * zoneMap; BitmapFont2 * bitmapFont2; TextureAtlasData * atlasData; void * untyped; } data; }; struct substitutions { HashTable * atlasEntry; HashTable * metadata; }; static void replaceTextureAtlasDataInFileBundle(FileBundle * bundle, TextureAtlasData * atlasData, const char * entryName) { BinarySerializationContext * context = BinarySerializationContext_create(false); TextureAtlasData_serialize(atlasData, context); size_t atlasDataBinarySize; void * atlasDataBinary = BinarySerializationContext_writeToBytes(context, &atlasDataBinarySize); BinarySerializationContext_dispose(context); FileBundle_replaceFileContents(bundle, entryName, atlasDataBinary, atlasDataBinarySize, true, false); } static void replaceImageCollectionInFileBundle(FileBundle * bundle, ImageCollection * imageCollection, const char * entryName) { BinarySerializationContext * context = BinarySerializationContext_create(false); ImageCollection_serialize(imageCollection, context); size_t imageCollectionDataSize; void * imageCollectionData = BinarySerializationContext_writeToBytes(context, &imageCollectionDataSize); BinarySerializationContext_dispose(context); FileBundle_replaceFileContents(bundle, entryName, imageCollectionData, imageCollectionDataSize, true, false); } static void replaceTileMapInFileBundle(FileBundle * bundle, TileMapEditData * tileMap, unsigned int entryIndex) { BinarySerializationContext * context = BinarySerializationContext_create(false); TileMapEditData_serialize(tileMap, context); size_t tileMapDataSize; void * tileMapData = BinarySerializationContext_writeToBytes(context, &tileMapDataSize); BinarySerializationContext_dispose(context); FileBundle_replaceFileContentsAtIndex(bundle, entryIndex, tileMapData, tileMapDataSize, true, false); } static void replaceZoneMapInFileBundle(FileBundle * bundle, TileZoneMap * zoneMap, unsigned int entryIndex) { BinarySerializationContext * context = BinarySerializationContext_create(false); TileZoneMap_serialize(zoneMap, context); size_t zoneMapDataSize; void * zoneMapData = BinarySerializationContext_writeToBytes(context, &zoneMapDataSize); BinarySerializationContext_dispose(context); FileBundle_replaceFileContentsAtIndex(bundle, entryIndex, zoneMapData, zoneMapDataSize, true, false); } static ImageCollection * readImageCollectionFromFileBundle(FileBundle * bundle, const char * entryName) { uint32_t dataSize; const void * data = FileBundle_getFile(bundle, entryName, &dataSize); if (data == NULL) { return NULL; } BinaryDeserializationContext * context = BinaryDeserializationContext_createWithBytes(data, dataSize); ImageCollection * imageCollection = ImageCollection_deserialize(context); BinaryDeserializationContext_dispose(context); return imageCollection; } static TextureAtlasData * readTextureAtlasDataFromFileBundle(FileBundle * bundle, const char * entryName) { uint32_t dataSize; const void * data = FileBundle_getFile(bundle, entryName, &dataSize); if (data == NULL) { return NULL; } BinaryDeserializationContext * context = BinaryDeserializationContext_createWithBytes(data, dataSize); TextureAtlasData * atlasData = TextureAtlasData_deserialize(context); BinaryDeserializationContext_dispose(context); return atlasData; } static bool unpackImageCollectionBundle(FileBundle * bundle, ImageCollection ** outImageCollection, TextureAtlasData ** outAtlasData) { ImageCollection * imageCollection = readImageCollectionFromFileBundle(bundle, "image_collection"); if (imageCollection == NULL) { return NULL; } TextureAtlasData * atlasData = readTextureAtlasDataFromFileBundle(bundle, "atlas"); if (atlasData == NULL) { ImageCollection_dispose(imageCollection); return false; } *outImageCollection = imageCollection; *outAtlasData = atlasData; return true; } static bool unpackTileMapBundle(FileBundle * bundle, unsigned int * outTileMapCount, TileMapEditData *** outTileMaps, TileZoneMap ** outZoneMap) { unsigned int fileCount = FileBundle_getFileCount(bundle); TileZoneMap * zoneMap = NULL; TileMapEditData ** tileMaps = malloc(fileCount * sizeof(*tileMaps)); unsigned int tileMapCount = 0; for (unsigned int fileIndex = 0; fileIndex < fileCount; fileIndex++) { uint32_t fileSize; const void * fileData = FileBundle_getFileAtIndex(bundle, fileIndex, &fileSize); FileBundle_fileType fileType = FileBundle_getFileTypeAtIndex(bundle, fileIndex); BinaryDeserializationContext * context = BinaryDeserializationContext_createWithBytes(fileData, fileSize); if (fileType == BUNDLE_FILE_TYPE_TILE_MAP) { tileMaps[tileMapCount] = TileMapEditData_deserialize(context); if (tileMaps[tileMapCount] == NULL) { BinaryDeserializationContext_dispose(context); for (unsigned int tileMapIndex = 0; tileMapIndex < tileMapCount; tileMapIndex++) { TileMapEditData_dispose(tileMaps[tileMapIndex]); } if (zoneMap != NULL) { TileZoneMap_dispose(zoneMap); } return false; } tileMapCount++; } else if (fileType == BUNDLE_FILE_TYPE_ZONE_MAP) { if (zoneMap != NULL) { return false; } zoneMap = TileZoneMap_deserialize(context); if (zoneMap == NULL) { BinaryDeserializationContext_dispose(context); for (unsigned int tileMapIndex = 0; tileMapIndex < tileMapCount; tileMapIndex++) { TileMapEditData_dispose(tileMaps[tileMapIndex]); } return false; } } else { return false; } BinaryDeserializationContext_dispose(context); } *outZoneMap = zoneMap; *outTileMaps = tileMaps; *outTileMapCount = tileMapCount; return true; } #define readBinaryFileImplementation(type, ...) \ BinaryDeserializationContext * context; \ if (filePath == NULL || !strcmp(filePath, "-")) { \ size_t length; \ void * data = readStdinSimple(&length); \ if (data == NULL) { \ return NULL; \ } \ context = BinaryDeserializationContext_createWithBytes(data, length); \ free(data); \ } else { \ context = BinaryDeserializationContext_createWithFile(filePath); \ } \ type * result = type##_deserialize(context, ##__VA_ARGS__); \ BinaryDeserializationContext_dispose(context); \ return result; static TilesetEditData * readTilesetFile(const char * filePath) { readBinaryFileImplementation(TilesetEditData); } static TileMapEditData * readTileMapFile(const char * filePath) { readBinaryFileImplementation(TileMapEditData); } static TileAdjacencyBehaviorSet * readBehaviorSetFile(const char * filePath) { readBinaryFileImplementation(TileAdjacencyBehaviorSet); } static TilesetAdjacencyBlendMap * readBlendMapFile(const char * filePath) { readBinaryFileImplementation(TilesetAdjacencyBlendMap); } static SpriteCollection * readSpriteCollectionFile(const char * filePath) { readBinaryFileImplementation(SpriteCollection); } static TileZoneMap * readZoneMapFile(const char * filePath) { readBinaryFileImplementation(TileZoneMap); } static BitmapFont2 * readBitmapFont2File(const char * filePath) { readBinaryFileImplementation(BitmapFont2); } static TextureAtlasData * readTextureAtlasDataFile(const char * filePath) { readBinaryFileImplementation(TextureAtlasData); } static FileBundle * readFileBundle(const char * filePath) { if (filePath == NULL || !strcmp(filePath, "-")) { size_t length; void * data = readStdinSimple(&length); if (data == NULL) { return false; } FileBundle * bundle = FileBundle_loadData(data, length); free(data); return bundle; } return FileBundle_loadFile(filePath); } #define writeBinaryFileImplementation(type, object) \ BinarySerializationContext * context = BinarySerializationContext_create(false); \ type##_serialize(object, context); \ if (context->status != SERIALIZATION_ERROR_OK) { \ fprintf(stderr, "Serialization failed with status %d\n", context->status); \ BinarySerializationContext_dispose(context); \ return false; \ } \ bool success = false; \ if (filePath == NULL || !strcmp(filePath, "-")) { \ size_t length; \ void * data = BinarySerializationContext_writeToBytes(context, &length); \ write(STDOUT_FILENO, data, length); \ free(data); \ success = true; \ } else { \ success = BinarySerializationContext_writeToFile(context, filePath); \ } \ if (!success) { \ fprintf(stderr, "Writing serialized data failed with status %d\n", context->status); \ } \ BinarySerializationContext_dispose(context); \ return success; static bool writeTilesetFile(const char * filePath, TilesetEditData * tileset) { writeBinaryFileImplementation(TilesetEditData, tileset); } static bool writeTileMapFile(const char * filePath, TileMapEditData * tileMap) { writeBinaryFileImplementation(TileMapEditData, tileMap); } static bool writeBehaviorSetFile(const char * filePath, TileAdjacencyBehaviorSet * behaviorSet) { writeBinaryFileImplementation(TileAdjacencyBehaviorSet, behaviorSet); } static bool writeBlendMapFile(const char * filePath, TilesetAdjacencyBlendMap * blendMap) { writeBinaryFileImplementation(TilesetAdjacencyBlendMap, blendMap); } static bool writeSpriteCollectionFile(const char * filePath, SpriteCollection * spriteCollection) { writeBinaryFileImplementation(SpriteCollection, spriteCollection); } static bool writeZoneMapFile(const char * filePath, TileZoneMap * zoneMap) { writeBinaryFileImplementation(TileZoneMap, zoneMap); } static bool writeBitmapFont2File(const char * filePath, BitmapFont2 * bitmapFont2) { writeBinaryFileImplementation(BitmapFont2, bitmapFont2); } static bool writeTextureAtlasDataFile(const char * filePath, TextureAtlasData * atlasData) { writeBinaryFileImplementation(TextureAtlasData, atlasData); } static bool writeFileBundle(const char * filePath, FileBundle * bundle) { if (filePath == NULL || !strcmp(filePath, "-")) { size_t size; void * data = FileBundle_writeData(bundle, &size); write(STDOUT_FILENO, data, size); return true; } return FileBundle_writeFile(bundle, filePath); } static struct typedFileData readDataFile(const char * filePath) { struct typedFileData result = {.data.untyped = NULL}; FileBundle * fileBundle = readFileBundle(filePath); if (fileBundle != NULL) { if (unpackImageCollectionBundle(fileBundle, &result.data.imageCollectionBundle.imageCollection, &result.data.imageCollectionBundle.atlasData)) { result.data.imageCollectionBundle.fileBundle = fileBundle; result.fileType = FILE_TYPE_IMAGE_COLLECTION_BUNDLE; return result; } if (unpackTileMapBundle(fileBundle, &result.data.tileMapBundle.tileMapCount, &result.data.tileMapBundle.tileMaps, &result.data.tileMapBundle.zoneMap)) { result.data.tileMapBundle.fileBundle = fileBundle; result.fileType = FILE_TYPE_TILE_MAP_BUNDLE; return result; } FileBundle_dispose(fileBundle); result.data.untyped = NULL; return result; } result.data.tileset = readTilesetFile(filePath); if (result.data.tileset != NULL) { result.fileType = FILE_TYPE_TILESET; return result; } result.data.tileMap = readTileMapFile(filePath); if (result.data.tileMap != NULL) { result.fileType = FILE_TYPE_TILE_MAP; return result; } result.data.behaviorSet = readBehaviorSetFile(filePath); if (result.data.behaviorSet != NULL) { result.fileType = FILE_TYPE_BEHAVIOR_SET; return result; } result.data.blendMap = readBlendMapFile(filePath); if (result.data.blendMap != NULL) { result.fileType = FILE_TYPE_BLEND_MAP; return result; } result.data.spriteCollection = readSpriteCollectionFile(filePath); if (result.data.spriteCollection != NULL) { result.fileType = FILE_TYPE_SPRITE_COLLECTION; return result; } result.data.zoneMap = readZoneMapFile(filePath); if (result.data.zoneMap != NULL) { result.fileType = FILE_TYPE_TILE_ZONE_MAP; return result; } result.data.bitmapFont2 = readBitmapFont2File(filePath); if (result.data.bitmapFont2 != NULL) { result.fileType = FILE_TYPE_BITMAP_FONT_2; return result; } result.data.atlasData = readTextureAtlasDataFile(filePath); if (result.data.atlasData != NULL) { result.fileType = FILE_TYPE_TEXTURE_ATLAS_DATA; return result; } return result; } static void printUsage(void) { fprintf(stderr, "Usage: stripEditData -i -o \n" " [--strip={DLNS}] [--substitutions ] [--substitutions ...]\n" " [--generate-substitutions] [--generate-metadata-header]\n" "\n" " If -i or -o is unspecified, stdin or stdout is assumed.\n" "\n" " input can be atlas, imagecollection, tileset_edit, tilemap_edit, tilemap_bundle, tileset_adjacency,\n" " tile_behavior, spritecollection, zonemap, or bitmapfont2.\n" "\n" " --strip={flags} specifies information to strip from the file, where {flags} is a set one or more characters:\n" " D: Replaces all top-level name fields with an empty string.\n" " L: For tile map files, crops each layer to its content, eliminating rows and columns of TILE_ID_NONE.\n" " N: Replaces all names of tiles, sprites, and tile map layers with an empty string.\n" " S: Removes all nonessential schema information from tilesets. This includes schemas for everything other\n" " than tiles (since tile and zone maps keep their own copies), and the master list of enumerations.\n" "\n" " substitutions.json contains multiple objects specifying search string keys and replacement string values for\n" " different sections. The top-level object should contain one or more child objects with the following keys:\n" " \"atlas_entry\" (imagecollection item names and atlas keys)\n" " \"metadata\" (tileset_edit metadata keys and their counterparts in tilemap_edit and zonemap)\n" " If multiple substitution files are specified, they will all be applied, with later-specified ones taking\n" " priority over earlier ones in the case of name collisions.\n" "\n" " --generate-substitutions takes an imagecollection or tileset as input, and writes a substitutions.json file\n" " which can be used on all files that reference that imagecollection or tileset (and the imagecollection or\n" " tileset itself) to minimize the number of bytes used for image name/metadata strings, while preserving name\n" " relationships.\n" "\n" " --generate-metadata-header takes a substitutions.json file as input and writes a header file with definitions\n" " for all replaced metadata keys, mapping from editor name to stripped name. Cannot be used together with\n" " --generate-substitutions. Input file is ignored; use --substitutions to specify input.\n"); } static bool addSubstitutions(HashTable * hashTable, struct JSONNode * node) { if (node->type != JSON_TYPE_OBJECT) { fprintf(stderr, "Substitution group \"%s\" is not of object type\n", node->key); return false; } for (unsigned int nodeIndex = 0; nodeIndex < node->value.count; nodeIndex++) { if (node->subitems[nodeIndex].type != JSON_TYPE_STRING) { fprintf(stderr, "Substitution entry \"%s\" in group \"%s\" is not of string type\n", node->subitems[nodeIndex].key, node->key); return false; } char * replaceString = strdup(node->subitems[nodeIndex].value.string); HashTable_set(hashTable, HashTable_stringKey(node->subitems[nodeIndex].key), &replaceString); } return true; } static bool readSubstitutionsFile(const char * filePath, struct substitutions * substitutions) { struct JSONParseError error; struct JSONNode * rootNode = JSONParser_loadFile(filePath, &error); if (rootNode == NULL) { fprintf(stderr, "JSON parse error: %s (%d, line %u char %u)\n", error.description, error.code, error.textPosition.lineIndex, error.textPosition.lineCharIndex); return false; } if (rootNode->type != JSON_TYPE_OBJECT) { fprintf(stderr, "Unexpected substitutions file format (top-level container is not an object)\n"); JSONNode_dispose(rootNode); return false; } for (unsigned int nodeIndex = 0; nodeIndex < rootNode->value.count; nodeIndex++) { if (!strcmp(rootNode->subitems[nodeIndex].key, "atlas_entry")) { if (!addSubstitutions(substitutions->atlasEntry, &rootNode->subitems[nodeIndex])) { JSONNode_dispose(rootNode); return false; } } else if (!strcmp(rootNode->subitems[nodeIndex].key, "metadata")) { if (!addSubstitutions(substitutions->metadata, &rootNode->subitems[nodeIndex])) { JSONNode_dispose(rootNode); return false; } } else { fprintf(stderr, "Unrecognized substitution group \"%s\"\n", rootNode->subitems[nodeIndex].key); JSONNode_dispose(rootNode); return false; } } return true; } static void advanceReplacementString(char * replacementString, size_t replacementStringMaxLength, unsigned int * ioReplacementStringLength) { unsigned int replacementStringLength = *ioReplacementStringLength; for (unsigned int charIndex = replacementStringLength - 1; charIndex < replacementStringLength; charIndex--) { if (replacementString[charIndex] < REPLACEMENT_ASCII_MAX) { replacementString[charIndex]++; return; } replacementString[charIndex] = REPLACEMENT_ASCII_MIN; } replacementString[replacementStringLength++] = REPLACEMENT_ASCII_MIN; if (replacementStringLength >= replacementStringMaxLength) { fprintf(stderr, "Error: Replacement string overflow\n"); exit(EXIT_FAILURE); } replacementString[replacementStringLength] = 0; *ioReplacementStringLength = replacementStringLength; } static int writeTilesetSubstitutionFile(const char * fileName, TilesetEditData * tileset) { struct JSONNode rootNode = {.type = JSON_TYPE_OBJECT, .key = NULL, .keyLength = 0, .subitems = malloc(sizeof(*rootNode.subitems)), .stringLength = 0, .value = {.count = 1}}; struct JSONNode metadataNode = {.type = JSON_TYPE_OBJECT, .key = strdup("metadata"), .keyLength = strlen("metadata"), .subitems = NULL, .stringLength = 0, .value = {.count = 0}}; char replacementString[8] = {REPLACEMENT_ASCII_MIN, 0}; unsigned int replacementStringLength = 1; HashTable * replacedNameSet = HashTable_create(0); for (unsigned int schemaIndex = 0; schemaIndex < tileset->metadataSchemaCount; schemaIndex++) { for (unsigned int fieldIndex = 0; fieldIndex < tileset->metadataSchemas[schemaIndex].schema->fieldCount; fieldIndex++) { DataValueSchemaField * field = &tileset->metadataSchemas[schemaIndex].schema->fields[fieldIndex]; if (HashTable_get(replacedNameSet, HashTable_stringKey(field->name)) == NULL) { struct JSONNode stringNode = {.type = JSON_TYPE_STRING, .key = strdup(field->name), .keyLength = strlen(field->name), .subitems = NULL, .stringLength = replacementStringLength, .value = {.string = strdup(replacementString)}}; metadataNode.subitems = realloc(metadataNode.subitems, (metadataNode.value.count + 1) * sizeof(*metadataNode.subitems)); metadataNode.subitems[metadataNode.value.count++] = stringNode; advanceReplacementString(replacementString, sizeof(replacementString), &replacementStringLength); HashTable_set(replacedNameSet, HashTable_stringKey(field->name), NULL); } } } rootNode.subitems[0] = metadataNode; HashTable_dispose(replacedNameSet); struct JSONEmissionError error; if (!JSONEmitter_writeFile(&rootNode, JSONEmitterFormat_multiLine, fileName, &error)) { fprintf(stderr, "Couldn't write %s: %s (code = %d, errno = %d)\n", fileName, error.description, error.code, errno); return EXIT_FAILURE; } return EXIT_SUCCESS; } static int writeImageCollectionSubstitutionFile(const char * fileName, ImageCollection * imageCollection) { struct JSONNode rootNode = {.type = JSON_TYPE_OBJECT, .key = NULL, .keyLength = 0, .subitems = malloc(sizeof(*rootNode.subitems)), .stringLength = 0, .value = {.count = 1}}; struct JSONNode atlasEntryNode = {.type = JSON_TYPE_OBJECT, .key = strdup("atlas_entry"), .keyLength = strlen("atlas_entry"), .subitems = malloc(imageCollection->imageCount * sizeof(*rootNode.subitems)), .stringLength = 0, .value = {.count = imageCollection->imageCount}}; char replacementString[8] = {REPLACEMENT_ASCII_MIN, 0}; unsigned int replacementStringLength = 1; for (unsigned int imageIndex = 0; imageIndex < imageCollection->imageCount; imageIndex++) { struct JSONNode stringNode = {.type = JSON_TYPE_STRING, .key = strdup(imageCollection->images[imageIndex].name), .keyLength = strlen(imageCollection->images[imageIndex].name), .subitems = NULL, .stringLength = replacementStringLength, .value = {.string = strdup(replacementString)}}; atlasEntryNode.subitems[imageIndex] = stringNode; advanceReplacementString(replacementString, sizeof(replacementString), &replacementStringLength); } rootNode.subitems[0] = atlasEntryNode; struct JSONEmissionError error; if (!JSONEmitter_writeFile(&rootNode, JSONEmitterFormat_multiLine, fileName, &error)) { fprintf(stderr, "Couldn't write %s: %s (code = %d, errno = %d)\n", fileName, error.description, error.code, errno); return EXIT_FAILURE; } return EXIT_SUCCESS; } static bool writeMetadataKeyDefine(HashTable * hashTable, HashTable_key key, void * value, void * context) { int * fd = context; char * replaceString = *(char **) value; size_t replaceStringLength = strlen(replaceString); unsigned int doubleQuoteCount = 0; for (unsigned int charIndex = 0; charIndex < replaceStringLength; charIndex++) { if (replaceString[charIndex] == '"') { doubleQuoteCount++; } } char * keySymbol = createIdentifierFromDisplayString(key.data.string.characters, 64); size_t keySymbolLength = strlen(keySymbol); char defineString[21 + keySymbolLength + 2 + replaceStringLength + doubleQuoteCount + 2]; memcpy(defineString, "#define METADATA_KEY_", 21); memcpy(defineString + 21, keySymbol, keySymbolLength); free(keySymbol); defineString[21 + keySymbolLength] = ' '; defineString[21 + keySymbolLength + 1] = '"'; size_t escapedReplaceStringLength = 0; for (unsigned int charIndex = 0; charIndex < replaceStringLength; charIndex++) { if (replaceString[charIndex] == '"') { defineString[21 + keySymbolLength + 2 + escapedReplaceStringLength++] = '\\'; } defineString[21 + keySymbolLength + 2 + escapedReplaceStringLength++] = replaceString[charIndex]; } defineString[21 + keySymbolLength + 2 + escapedReplaceStringLength] = '"'; defineString[21 + keySymbolLength + 2 + escapedReplaceStringLength + 1] = '\n'; write(*fd, defineString, sizeof(defineString)); return true; } static int writeMetadataHeaderFile(const char * fileName, HashTable * substitutionTable) { int fd; if (fileName == NULL) { fd = STDOUT_FILENO; } else { fd = creat(fileName, 0777); if (fd == -1) { fprintf(stderr, "Error: Couldn't open %s for writing (errno = %d)\n", fileName, errno); return EXIT_FAILURE; } } char preamble[] = "#ifndef __MetadataKeys_H__\n" "#define __MetadataKeys_H__\n" "\n"; write(fd, preamble, sizeof(preamble) - 1); HashTable_foreach(substitutionTable, writeMetadataKeyDefine, &fd); char postamble[] = "\n" "#endif\n"; write(fd, postamble, sizeof(postamble) - 1); close(fd); return EXIT_SUCCESS; } static void stripString(char ** ioString) { free(*ioString); *ioString = strdup(""); } static void substituteString(char ** ioString, HashTable * substitutionTable) { char ** replacementString = HashTable_get(substitutionTable, HashTable_stringKey(*ioString)); if (replacementString != NULL) { free(*ioString); *ioString = strdup(*replacementString); } } static void substituteMetadataKeys(DataHashTable ** metadata, HashTable * substitutionTable) { if (*metadata != NULL) { DataHashTable * newMetadata = hashCreate(); size_t fieldCount = hashGetCount(*metadata); for (size_t fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) { const char * key = hashGetKeyAtIndex(*metadata, fieldIndex); const char * newKey = key; char ** replacementString = HashTable_get(substitutionTable, HashTable_stringKey(key)); if (replacementString != NULL) { newKey = *replacementString; } if (hashHas(newMetadata, newKey)) { fprintf(stderr, "Warning: Name collision detected in metadata; attempting to replace key \"%s\" with \"%s\", which already exists\n", key, newKey); } hashSet(newMetadata, newKey, valueCopy(hashGet(*metadata, key))); // TODO: Hierarchical metadata } hashDispose(*metadata); *metadata = newMetadata; } } static void substituteSchemaFieldNames(DataValueSchema * schema, HashTable * substitutionTable) { for (unsigned int fieldIndex = 0; fieldIndex < schema->fieldCount; fieldIndex++) { substituteString(&schema->fields[fieldIndex].name, substitutionTable); // TODO: Hierarchical metadata } } static void stripImageCollection(ImageCollection * imageCollection, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&imageCollection->name); } if (substitutions->atlasEntry->count > 0) { for (unsigned int imageIndex = 0; imageIndex < imageCollection->imageCount; imageIndex++) { substituteString(&imageCollection->images[imageIndex].name, substitutions->atlasEntry); } } } static void stripTilesetEditData(TilesetEditData * tileset, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&tileset->name); } if (stripFlags & STRIP_FLAG_ITEM_NAME) { for (unsigned int tileIndex = 0; tileIndex < tileset->tileCount; tileIndex++) { stripString(&tileset->tiles[tileIndex].name); } } if (stripFlags & STRIP_FLAG_SCHEMA) { for (unsigned int schemaIndex = 0; schemaIndex < tileset->metadataSchemaCount; schemaIndex++) { stripString(&tileset->metadataSchemas[schemaIndex].name); } for (unsigned int enumerationIndex = 0; enumerationIndex < tileset->enumerationCount; enumerationIndex++) { free(tileset->enumerations[enumerationIndex].name); for (unsigned int valueIndex = 0; valueIndex < tileset->enumerations[enumerationIndex].valueCount; valueIndex++) { free(tileset->enumerations[enumerationIndex].values[valueIndex].name); } free(tileset->enumerations[enumerationIndex].values); } free(tileset->enumerations); tileset->enumerations = NULL; tileset->enumerationCount = 0; unsigned int schemaOffset = 0; for (unsigned int schemaIndex = 0; schemaIndex < tileset->metadataSchemaCount; schemaIndex++) { tileset->metadataSchemas[schemaIndex] = tileset->metadataSchemas[schemaIndex + schemaOffset]; if (tileset->metadataSchemas[schemaIndex].identifier != tileset->tileSchemaID) { free(tileset->metadataSchemas[schemaIndex].name); DataValueSchema_dispose(tileset->metadataSchemas[schemaIndex].schema); schemaOffset++; tileset->metadataSchemaCount--; schemaIndex--; } } } if (substitutions->metadata->count > 0) { for (unsigned int schemaIndex = 0; schemaIndex < tileset->metadataSchemaCount; schemaIndex++) { substituteSchemaFieldNames(tileset->metadataSchemas[schemaIndex].schema, substitutions->metadata); for (unsigned int tileIndex = 0; tileIndex < tileset->tileCount; tileIndex++) { substituteMetadataKeys(&tileset->tiles[tileIndex].metadata, substitutions->metadata); } } } } static bool substituteSchemaInUseFields(HashTable * hashTable, HashTable_key key, void * value, void * context) { DataValueSchema * schema = *(DataValueSchema **) value; HashTable * substitutionTable = context; substituteSchemaFieldNames(schema, substitutionTable); return true; } static Rect4i calculateTileGridContentBounds(TileInstanceGrid * tileGrid, Vector2i offset) { unsigned int width = tileGrid->size.x; unsigned int height = tileGrid->size.y; Rect4i result = {0, width, 0, height}; while (result.yMin < result.yMax) { for (unsigned int columnIndex = 0; columnIndex < width; columnIndex++) { if (tileGrid->tileInstances[result.yMin * width + columnIndex].tileID != TILE_ID_NONE) { goto minRowFound; } } result.yMin++; } return result; minRowFound: result.yMax--; while (result.yMax > result.yMin) { for (unsigned int columnIndex = 0; columnIndex < width; columnIndex++) { if (tileGrid->tileInstances[result.yMax * width + columnIndex].tileID != TILE_ID_NONE) { goto maxRowFound; } } result.yMax--; } maxRowFound: result.yMax++; while (result.xMin < result.xMax) { for (unsigned int rowIndex = result.yMin; rowIndex < (unsigned int) result.yMax; rowIndex++) { if (tileGrid->tileInstances[rowIndex * width + result.xMin].tileID != TILE_ID_NONE) { goto minColumnFound; } } result.xMin++; } minColumnFound: result.xMax--; while (result.xMax > result.xMin) { for (unsigned int rowIndex = result.yMin; rowIndex < (unsigned int) result.yMax; rowIndex++) { if (tileGrid->tileInstances[rowIndex * width + result.xMax].tileID != TILE_ID_NONE) { goto maxColumnFound; } } result.xMax--; } maxColumnFound: result.xMax++; return Rect4i_offset(result, offset); } static void copyTileInstanceGridRect(TileInstanceGrid * toTileGrid, TileInstanceGrid * fromTileGrid, int sourceX, int sourceY, int targetX, int targetY, unsigned int width, unsigned int height) { unsigned int startX = 0, startY = 0; unsigned int sourceWidth = fromTileGrid->size.x, targetWidth = toTileGrid->size.x; TileInstance * fromTileInstances = fromTileGrid->tileInstances, * toTileInstances = toTileGrid->tileInstances; clampGridCopyRegion(sourceX, sourceY, targetX, targetY, fromTileGrid->size.x, fromTileGrid->size.y, toTileGrid->size.x, toTileGrid->size.y, &width, &height, &startX, &startY); for (unsigned int rowIndex = startY; rowIndex < height; rowIndex++) { for (unsigned int columnIndex = startX; columnIndex < width; columnIndex++) { unsigned int sourceIndex = (sourceY + rowIndex) * sourceWidth + sourceX + columnIndex; unsigned int targetIndex = (targetY + rowIndex) * targetWidth + targetX + columnIndex; TileID tileID = fromTileInstances[sourceIndex].tileID; toTileInstances[targetIndex].tileID = tileID; if (toTileInstances[targetIndex].metadata != NULL) { hashDispose(toTileInstances[targetIndex].metadata); } if (fromTileInstances[sourceIndex].metadata == NULL) { toTileInstances[targetIndex].metadata = NULL; } else { toTileInstances[targetIndex].metadata = hashCopy(fromTileInstances[sourceIndex].metadata); } } } } static void moveResizeTileInstanceGrid(TileInstanceGrid * tileGrid, Vector2i move, Vector2i resize) { assert((int) tileGrid->size.x + resize.x >= 0); assert((int) tileGrid->size.y + resize.y >= 0); TileInstanceGrid newTileGrid = TileInstanceGrid_create(tileGrid->size.x + resize.x, tileGrid->size.y + resize.y); copyTileInstanceGridRect(&newTileGrid, tileGrid, 0, 0, move.x, move.y, tileGrid->size.x, tileGrid->size.y); free(tileGrid->tileInstances); *tileGrid = newTileGrid; } static void stripTileMapEditData(TileMapEditData * tileMap, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&tileMap->name); } if (stripFlags & STRIP_FLAG_ITEM_NAME) { for (unsigned int layerIndex = 0; layerIndex < tileMap->layerCount; layerIndex++) { stripString(&tileMap->layers[layerIndex].name); } } if (stripFlags & STRIP_FLAG_LAYER_CROP) { for (unsigned int layerIndex = 0; layerIndex < tileMap->layerCount; layerIndex++) { Rect4i contentBounds = calculateTileGridContentBounds(&tileMap->layers[layerIndex].grid, tileMap->layers[layerIndex].offset); if (contentBounds.xMin != tileMap->layers[layerIndex].offset.x || contentBounds.yMin != tileMap->layers[layerIndex].offset.y || (contentBounds.xMax - contentBounds.xMin) != (int) tileMap->layers[layerIndex].grid.size.x || (contentBounds.yMax - contentBounds.yMin) != (int) tileMap->layers[layerIndex].grid.size.y) { if (contentBounds.xMax <= contentBounds.xMin || contentBounds.yMax <= contentBounds.yMin) { contentBounds.xMin = tileMap->layers[layerIndex].offset.x; contentBounds.yMin = tileMap->layers[layerIndex].offset.y; contentBounds.xMax = contentBounds.xMin + 1; contentBounds.yMax = contentBounds.yMin + 1; } moveResizeTileInstanceGrid(&tileMap->layers[layerIndex].grid, VECTOR2i(tileMap->layers[layerIndex].offset.x - contentBounds.xMin, tileMap->layers[layerIndex].offset.y - contentBounds.yMin), VECTOR2i((contentBounds.xMax - contentBounds.xMin) - tileMap->layers[layerIndex].grid.size.x, (contentBounds.yMax - contentBounds.yMin) - tileMap->layers[layerIndex].grid.size.y)); tileMap->layers[layerIndex].offset = VECTOR2i(contentBounds.xMin, contentBounds.yMin); } } } if (substitutions->metadata->count > 0) { HashTable_foreach(tileMap->schemasInUse, substituteSchemaInUseFields, substitutions->metadata); substituteMetadataKeys(&tileMap->metadata, substitutions->metadata); for (unsigned int layerIndex = 0; layerIndex < tileMap->layerCount; layerIndex++) { substituteMetadataKeys(&tileMap->layers[layerIndex].metadata, substitutions->metadata); unsigned int tileCount = tileMap->layers[layerIndex].grid.size.x * tileMap->layers[layerIndex].grid.size.y; for (unsigned int tileIndex = 0; tileIndex < tileCount; tileIndex++) { substituteMetadataKeys(&tileMap->layers[layerIndex].grid.tileInstances[tileIndex].metadata, substitutions->metadata); } } } } static void stripTileAdjacencyBehaviorSet(TileAdjacencyBehaviorSet * behaviorSet, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&behaviorSet->name); } } static void stripTilesetAdjacencyBlendMap(TilesetAdjacencyBlendMap * blendMap, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&blendMap->name); } } static void stripSpriteCollection(SpriteCollection * spriteCollection, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&spriteCollection->name); } if (stripFlags & STRIP_FLAG_ITEM_NAME) { for (unsigned int spriteIndex = 0; spriteIndex < spriteCollection->spriteCount; spriteIndex++) { stripString(&spriteCollection->sprites[spriteIndex].name); } } } static void stripTileZoneMap(TileZoneMap * zoneMap, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&zoneMap->name); } if (substitutions->metadata->count > 0) { if (zoneMap->zoneSchemaInUse != NULL) { substituteSchemaFieldNames(zoneMap->zoneSchemaInUse, substitutions->metadata); } substituteMetadataKeys(&zoneMap->metadata, substitutions->metadata); } } static void stripBitmapFont2(BitmapFont2 * bitmapFont2, unsigned int stripFlags, struct substitutions * substitutions) { if (substitutions->atlasEntry->count > 0) { for (unsigned int characterIndex = 0; characterIndex < bitmapFont2->characterCount; characterIndex++) { substituteString(&bitmapFont2->characters[characterIndex].glyphName, substitutions->atlasEntry); } } } static void stripTextureAtlasData(TextureAtlasData * atlasData, unsigned int stripFlags, struct substitutions * substitutions) { if (stripFlags & STRIP_FLAG_DOCUMENT_NAME) { stripString(&atlasData->textureName); } if (substitutions->atlasEntry->count > 0) { for (unsigned int entryIndex = 0; entryIndex < atlasData->entryCount; entryIndex++) { substituteString(&atlasData->entries[entryIndex].name, substitutions->atlasEntry); } } } static void parseArgs(int argc, char ** argv, const char ** outInFile, const char ** outOutFile, unsigned int * outStripFlags, struct substitutions * substitutions, bool * outGenerateSubstitutionFile, bool * outGenerateMetadataHeader) { for (int argIndex = 1; argIndex < argc; argIndex++) { if (!strcmp(argv[argIndex], "-i")) { if (argc <= argIndex + 1) { fprintf(stderr, "Error: -i specified at the end of argv\n"); printUsage(); exit(EXIT_FAILURE); } argIndex++; if (*outInFile != NULL) { fprintf(stderr, "Error: -i specified more than once (already have \"%s\" and encountered \"%s\")\n", *outInFile, argv[argIndex]); } *outInFile = argv[argIndex]; } else if (!strcmp(argv[argIndex], "-o")) { if (argc <= argIndex + 1) { fprintf(stderr, "Error: -o specified at the end of argv\n"); printUsage(); exit(EXIT_FAILURE); } argIndex++; if (*outOutFile != NULL) { fprintf(stderr, "Error: -o specified more than once (already have \"%s\" and encountered \"%s\")\n", *outOutFile, argv[argIndex]); } *outOutFile = argv[argIndex]; } else if (!strncmp(argv[argIndex], "--strip=", strlen("--strip="))) { for (unsigned int charIndex = 8; argv[argIndex][charIndex] != 0; charIndex++) { switch (argv[argIndex][charIndex]) { case 'D': *outStripFlags |= STRIP_FLAG_DOCUMENT_NAME; break; case 'N': *outStripFlags |= STRIP_FLAG_ITEM_NAME; break; case 'S': *outStripFlags |= STRIP_FLAG_SCHEMA; break; case 'L': *outStripFlags |= STRIP_FLAG_LAYER_CROP; break; default: fprintf(stderr, "Unrecognized --strip= flag: %c\n", argv[argIndex][charIndex]); printUsage(); exit(EXIT_FAILURE); break; } } } else if (!strcmp(argv[argIndex], "--substitutions")) { if (argc <= argIndex + 1) { fprintf(stderr, "Error: --substitutions specified at the end of argv\n"); printUsage(); exit(EXIT_FAILURE); } argIndex++; if (!readSubstitutionsFile(argv[argIndex], substitutions)) { fprintf(stderr, "Error: Couldn't read \"%s\" as a substitutions.json file\n", argv[argIndex]); } } else if (!strcmp(argv[argIndex], "--generate-substitutions")) { *outGenerateSubstitutionFile = true; } else if (!strcmp(argv[argIndex], "--generate-metadata-header")) { *outGenerateMetadataHeader = true; } else if (!strcmp(argv[argIndex], "--help")) { printUsage(); exit(EXIT_SUCCESS); } else { fprintf(stderr, "Error: Unknown argument \"%s\"\n", argv[argIndex]); printUsage(); exit(EXIT_FAILURE); } } } int main(int argc, char ** argv) { const char * inFile = NULL, * outFile = NULL; unsigned int stripFlags = 0; struct substitutions substitutions = {HashTable_create(sizeof(char *)), HashTable_create(sizeof(char *))}; bool generateSubstitutionFile = false, generateMetadataHeader = false; parseArgs(argc, argv, &inFile, &outFile, &stripFlags, &substitutions, &generateSubstitutionFile, &generateMetadataHeader); if (generateSubstitutionFile) { TilesetEditData * tileset = readTilesetFile(inFile); if (tileset != NULL) { return writeTilesetSubstitutionFile(outFile, tileset); } FileBundle * bundle = readFileBundle(inFile); if (bundle != NULL) { ImageCollection * imageCollection; TextureAtlasData * atlasData; if (!unpackImageCollectionBundle(bundle, &imageCollection, &atlasData)) { fprintf(stderr, "Error: Couldn't read %s as an ImageCollection bundle\n", inFile); return EXIT_FAILURE; } return writeImageCollectionSubstitutionFile(outFile, imageCollection); } fprintf(stderr, "Error: Couldn't read %s as an ImageCollection or TilesetEditData\n", inFile); return EXIT_FAILURE; } if (generateMetadataHeader) { return writeMetadataHeaderFile(outFile, substitutions.metadata); } struct typedFileData inData = readDataFile(inFile); if (inData.data.untyped == NULL) { fprintf(stderr, "Error: Couldn't read \"%s\" as any recognized type of file (errno = %d)\n", inFile, errno); return EXIT_FAILURE; } bool success = false; switch (inData.fileType) { case FILE_TYPE_IMAGE_COLLECTION_BUNDLE: stripImageCollection(inData.data.imageCollectionBundle.imageCollection, stripFlags, &substitutions); stripTextureAtlasData(inData.data.imageCollectionBundle.atlasData, stripFlags, &substitutions); replaceImageCollectionInFileBundle(inData.data.imageCollectionBundle.fileBundle, inData.data.imageCollectionBundle.imageCollection, "image_collection"); replaceTextureAtlasDataInFileBundle(inData.data.imageCollectionBundle.fileBundle, inData.data.imageCollectionBundle.atlasData, "atlas"); success = writeFileBundle(outFile, inData.data.imageCollectionBundle.fileBundle); break; case FILE_TYPE_TILESET: stripTilesetEditData(inData.data.tileset, stripFlags, &substitutions); success = writeTilesetFile(outFile, inData.data.tileset); break; case FILE_TYPE_TILE_MAP: stripTileMapEditData(inData.data.tileMap, stripFlags, &substitutions); success = writeTileMapFile(outFile, inData.data.tileMap); break; case FILE_TYPE_TILE_MAP_BUNDLE: stripTileZoneMap(inData.data.tileMapBundle.zoneMap, stripFlags, &substitutions); replaceZoneMapInFileBundle(inData.data.tileMapBundle.fileBundle, inData.data.tileMapBundle.zoneMap, 0); for (unsigned int tileMapIndex = 0; tileMapIndex < inData.data.tileMapBundle.tileMapCount; tileMapIndex++) { stripTileMapEditData(inData.data.tileMapBundle.tileMaps[tileMapIndex], stripFlags, &substitutions); replaceTileMapInFileBundle(inData.data.tileMapBundle.fileBundle, inData.data.tileMapBundle.tileMaps[tileMapIndex], tileMapIndex + 1); } success = writeFileBundle(outFile, inData.data.tileMapBundle.fileBundle); break; case FILE_TYPE_BEHAVIOR_SET: stripTileAdjacencyBehaviorSet(inData.data.behaviorSet, stripFlags, &substitutions); success = writeBehaviorSetFile(outFile, inData.data.behaviorSet); break; case FILE_TYPE_BLEND_MAP: stripTilesetAdjacencyBlendMap(inData.data.blendMap, stripFlags, &substitutions); success = writeBlendMapFile(outFile, inData.data.blendMap); break; case FILE_TYPE_SPRITE_COLLECTION: stripSpriteCollection(inData.data.spriteCollection, stripFlags, &substitutions); success = writeSpriteCollectionFile(outFile, inData.data.spriteCollection); break; case FILE_TYPE_TILE_ZONE_MAP: stripTileZoneMap(inData.data.zoneMap, stripFlags, &substitutions); success = writeZoneMapFile(outFile, inData.data.zoneMap); break; case FILE_TYPE_BITMAP_FONT_2: stripBitmapFont2(inData.data.bitmapFont2, stripFlags, &substitutions); success = writeBitmapFont2File(outFile, inData.data.bitmapFont2); break; case FILE_TYPE_TEXTURE_ATLAS_DATA: stripTextureAtlasData(inData.data.atlasData, stripFlags, &substitutions); success = writeTextureAtlasDataFile(outFile, inData.data.atlasData); break; } if (!success) { fprintf(stderr, "Error: File output failed (errno = %d)\n", errno); return EXIT_FAILURE; } return EXIT_SUCCESS; }