/* 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 "binaryserialization/BinaryDeserializationContext.h" #include "tileset/TileMapEditData.h" #include "tileset/TileZoneMap.h" #include "utilities/FileBundle.h" #include "utilities/HashTable.h" #include "utilities/IOUtilities.h" #include "stem_core.h" #include #include #include #include #include #define PROJECT_FORMAT_TYPE "tproject" #define PROJECT_FORMAT_VERSION 0 #define PROJECT_ITEM_COUNT_MAX 65536 typedef enum DocumentType { DOCUMENT_TYPE_IMAGE, DOCUMENT_TYPE_TILESET, DOCUMENT_TYPE_TILE_MAP, DOCUMENT_TYPE_TILE_ADJACENCY_MAP, DOCUMENT_TYPE_IMAGE_COLLECTION_BUNDLE, DOCUMENT_TYPE_SPRITE_COLLECTION, DOCUMENT_TYPE_FONT, DOCUMENT_TYPE_ZONE_MAP, DOCUMENT_TYPE_PAINT_PRESETS, DOCUMENT_TYPE_PROJECT_BUNDLE, DOCUMENT_TYPE_UNKNOWN = 10000 } DocumentType; #define FILE_TYPE_TILE_MAP 1002 #define FILE_TYPE_ZONE_MAP 1007 typedef struct ProjectBundleMinimalItem { DocumentType type; char * relativePath; } ProjectBundleMinimalItem; typedef struct ProjectBundleMinimal { unsigned int itemCount; ProjectBundleMinimalItem * items; } ProjectBundleMinimal; ProjectBundleMinimal * ProjectBundleMinimal_create(unsigned int itemCount, ProjectBundleMinimalItem * items, bool copyItems) { ProjectBundleMinimal * project = malloc(sizeof(*project)); project->itemCount = itemCount; project->items = memdup(items, itemCount * sizeof(*items)); if (copyItems) { for (unsigned int itemIndex = 0; itemIndex < project->itemCount; itemIndex++) { project->items[itemIndex].relativePath = strdup(project->items[itemIndex].relativePath); } } return project; } void ProjectBundleMinimal_dispose(ProjectBundleMinimal * project) { for (unsigned int itemIndex = 0; itemIndex < project->itemCount; itemIndex++) { free(project->items[itemIndex].relativePath); } free(project->items); free(project); } #define ITEM_TYPE_ENUM_VALUES { \ {"image", DOCUMENT_TYPE_IMAGE}, \ {"tileset", DOCUMENT_TYPE_TILESET}, \ {"tilemap", DOCUMENT_TYPE_TILE_MAP}, \ {"adjacency", DOCUMENT_TYPE_TILE_ADJACENCY_MAP}, \ {"imagecollection", DOCUMENT_TYPE_IMAGE_COLLECTION_BUNDLE}, \ {"spritecollection", DOCUMENT_TYPE_SPRITE_COLLECTION}, \ {"font", DOCUMENT_TYPE_FONT}, \ {"zonemap", DOCUMENT_TYPE_ZONE_MAP}, \ {"presets", DOCUMENT_TYPE_PAINT_PRESETS} \ } ProjectBundleMinimal * ProjectBundleMinimal_deserializeMinimal(compat_type(DeserializationContext *) deserializationContext, uint16_t formatVersion) { DeserializationContext * context = deserializationContext; if (context->status != SERIALIZATION_ERROR_OK) { return NULL; } if (formatVersion > PROJECT_FORMAT_VERSION) { context->status = SERIALIZATION_ERROR_FORMAT_VERSION_TOO_NEW; return NULL; } call_virtual(readUInt16, context, "tile_map_default_width"); call_virtual(readUInt16, context, "tile_map_default_height"); unsigned int itemCount = call_virtual(beginArray, context, "items"); if (context->status != SERIALIZATION_ERROR_OK || itemCount > PROJECT_ITEM_COUNT_MAX) { return NULL; } Serialization_enumKeyValue itemTypeEnumValues[] = ITEM_TYPE_ENUM_VALUES; ProjectBundleMinimalItem items[itemCount]; memset(items, 0, sizeof(items)); for (unsigned int itemIndex = 0; itemIndex < itemCount; itemIndex++) { call_virtual(beginStructure, context, NULL); items[itemIndex].type = call_virtual(readEnumeration, context, "type", sizeof_count(itemTypeEnumValues), itemTypeEnumValues); items[itemIndex].relativePath = (char *) call_virtual(readString, context, "path"); switch (items[itemIndex].type) { case DOCUMENT_TYPE_TILESET: call_virtual(readUInt32, context, "image_collection_id"); break; case DOCUMENT_TYPE_TILE_MAP: call_virtual(readUInt32, context, "tileset_id"); call_virtual(readUInt32, context, "blend_map_id"); break; case DOCUMENT_TYPE_TILE_ADJACENCY_MAP: call_virtual(readUInt32, context, "tileset_id"); break; case DOCUMENT_TYPE_SPRITE_COLLECTION: call_virtual(readUInt32, context, "image_collection_id"); break; case DOCUMENT_TYPE_FONT: call_virtual(readUInt32, context, "image_collection_id"); break; case DOCUMENT_TYPE_ZONE_MAP: call_virtual(readUInt32, context, "tileset_id"); call_virtual(readUInt32, context, "blend_map_id"); break; case DOCUMENT_TYPE_IMAGE: case DOCUMENT_TYPE_IMAGE_COLLECTION_BUNDLE: case DOCUMENT_TYPE_PAINT_PRESETS: case DOCUMENT_TYPE_PROJECT_BUNDLE: case DOCUMENT_TYPE_UNKNOWN: break; } call_virtual(endStructure, context); } call_virtual(endArray, context); if (context->status != SERIALIZATION_ERROR_OK) { return NULL; } ProjectBundleMinimal * project = ProjectBundleMinimal_create(itemCount, items, true); return project; } ProjectBundleMinimal * ProjectBundleMinimal_deserialize(compat_type(DeserializationContext *) deserializationContext) { deserialize_implementation_v2(ProjectBundleMinimal, PROJECT); } static ProjectBundleMinimal * readProjectBundleFile(const char * filePath) { BinaryDeserializationContext * context = BinaryDeserializationContext_createWithFile(filePath); if (context == NULL || context->status != SERIALIZATION_ERROR_OK) { fprintf(stderr, "Error: Couldn't read %s (deserialization status %d, errno = %d)\n", filePath, context == NULL ? 0 : context->status, errno); } ProjectBundleMinimal * projectBundle = ProjectBundleMinimal_deserialize(context); if (projectBundle == NULL) { fprintf(stderr, "Error: Couldn't read %s as a project bundle (deserialization status %d, errno = %d)\n", filePath, context->status, errno); } BinaryDeserializationContext_dispose(context); return projectBundle; } static TileZoneMap * readTileZoneMapFile(const char * filePath) { BinaryDeserializationContext * context = BinaryDeserializationContext_createWithFile(filePath); if (context->status != SERIALIZATION_ERROR_OK) { BinaryDeserializationContext_dispose(context); return NULL; } TileZoneMap * zoneMap = TileZoneMap_deserialize(context); BinaryDeserializationContext_dispose(context); return zoneMap; } static TileMapEditData * readTileMapFile(const char * filePath) { BinaryDeserializationContext * context = BinaryDeserializationContext_createWithFile(filePath); if (context->status != SERIALIZATION_ERROR_OK) { BinaryDeserializationContext_dispose(context); return NULL; } TileMapEditData * tileMapData = TileMapEditData_deserialize(context); BinaryDeserializationContext_dispose(context); return tileMapData; } static TileZoneMap * readTileZoneMapFileWithID(ProjectBundleMinimal * projectBundle, TileZoneMapID zoneMapID, const char * basePath, unsigned int * outZoneMapItemIndex) { for (unsigned int itemIndex = 0; itemIndex < projectBundle->itemCount; itemIndex++) { if (projectBundle->items[itemIndex].type == DOCUMENT_TYPE_ZONE_MAP) { const char * filePath = getAbsolutePath(basePath, projectBundle->items[itemIndex].relativePath); TileZoneMap * zoneMap = readTileZoneMapFile(filePath); if (zoneMap == NULL) { fprintf(stderr, "Error: Encountered unreadable zone map file \"%s\" in project bundle\n", filePath); exit(EXIT_FAILURE); } if (zoneMap->identifier == zoneMapID) { *outZoneMapItemIndex = itemIndex; return zoneMap; } TileZoneMap_dispose(zoneMap); } } return NULL; } static void addFileToBundle(FileBundle * bundle, const char * basePath, const char * relativePath, FileBundle_fileType fileType) { const char * filePath = getAbsolutePath(basePath, relativePath); size_t size; void * data = readFileSimple(filePath, &size); if (data == NULL) { fprintf(stderr, "Error: Couldn't read file %s (errno = %d)\n", filePath, errno); exit(EXIT_FAILURE); } FileBundle_addFile(bundle, getLastPathComponent(relativePath), fileType, data, size, true, false); } static void printUsage(void) { fprintf(stderr, "Usage: tileMapBundler --bundle-all \n" " tileMapBundler --bundle-zone \n" " tileMapBundler --prereqs \n" " tileMapBundler --zone-prereqs \n"); } static bool printTileMapID(HashTable * hashTable, HashTable_key key, void * value, void * context) { bool * first = context; if (!*first) { fprintf(stderr, ", "); } else { *first = false; } fprintf(stderr, "%u", key.data.uint32); return true; } int main(int argc, char ** argv) { if (argc < 3) { fprintf(stderr, "Too few arguments\n"); printUsage(); return EXIT_FAILURE; } if (!strcmp(argv[1], "--prereqs")) { ProjectBundleMinimal * projectBundle = readProjectBundleFile(argv[2]); if (projectBundle == NULL) { return EXIT_FAILURE; } char * basePath = strdup(getDirectory(argv[2])); for (unsigned int itemIndex = 0; itemIndex < projectBundle->itemCount; itemIndex++) { if (projectBundle->items[itemIndex].type == DOCUMENT_TYPE_TILE_MAP) { const char * filePath = getAbsolutePath(basePath, projectBundle->items[itemIndex].relativePath); printf("%s ", filePath); } } if (isatty(STDOUT_FILENO)) { putchar('\n'); } return EXIT_SUCCESS; } if (!strcmp(argv[1], "--zone-prereqs")) { if (argc < 4) { fprintf(stderr, "Too few arguments to --zone-prereqs\n"); printUsage(); return EXIT_FAILURE; } unsigned int zoneMapID; if (sscanf(argv[2], "%u", &zoneMapID) != 1) { fprintf(stderr, "Error: Couldn't understand \"%s\" as a tile zone map ID\n", argv[2]); printUsage(); return EXIT_FAILURE; } ProjectBundleMinimal * projectBundle = readProjectBundleFile(argv[3]); if (projectBundle == NULL) { return EXIT_FAILURE; } char * basePath = strdup(getDirectory(argv[3])); unsigned int zoneMapItemIndex; TileZoneMap * zoneMap = readTileZoneMapFileWithID(projectBundle, zoneMapID, basePath, &zoneMapItemIndex); if (zoneMap == NULL) { fprintf(stderr, "Error: Couldn't find tile map zone with ID %u\n", zoneMapID); printUsage(); return EXIT_FAILURE; } HashTable * neededTileMapIDs = HashTable_create(0); for (unsigned int roomIndex = 0; roomIndex < zoneMap->roomCount; roomIndex++) { HashTable_set(neededTileMapIDs, HashTable_uint32Key(zoneMap->rooms[roomIndex].tileMapID), NULL); } printf("%s ", getAbsolutePath(basePath, projectBundle->items[zoneMapItemIndex].relativePath)); for (unsigned int itemIndex = 0; itemIndex < projectBundle->itemCount; itemIndex++) { if (projectBundle->items[itemIndex].type == DOCUMENT_TYPE_TILE_MAP) { const char * filePath = getAbsolutePath(basePath, projectBundle->items[itemIndex].relativePath); TileMapEditData * tileMap = readTileMapFile(filePath); if (tileMap == NULL) { fprintf(stderr, "Error: Encountered unreadable tile map file \"%s\" in project bundle\n", filePath); return EXIT_FAILURE; } TileMapID tileMapID = tileMap->identifier; TileMapEditData_dispose(tileMap); if (HashTable_get(neededTileMapIDs, HashTable_uint32Key(tileMapID)) == NULL) { continue; } printf("%s ", getAbsolutePath(basePath, projectBundle->items[itemIndex].relativePath)); HashTable_delete(neededTileMapIDs, HashTable_uint32Key(tileMapID)); } } if (isatty(STDOUT_FILENO)) { putchar('\n'); } if (neededTileMapIDs->count > 0) { fprintf(stderr, "Warning: Zone map specified some tile map IDs that were not found in the project bundle: "); bool first = true; HashTable_foreach(neededTileMapIDs, printTileMapID, &first); fputc('\n', stderr); } return EXIT_SUCCESS; } if (!strcmp(argv[1], "--bundle-all")) { if (argc < 4) { fprintf(stderr, "Too few arguments to --bundle-all\n"); printUsage(); return EXIT_FAILURE; } ProjectBundleMinimal * projectBundle = readProjectBundleFile(argv[2]); if (projectBundle == NULL) { return EXIT_FAILURE; } FileBundle * fileBundle = FileBundle_create(); char * basePath = strdup(getDirectory(argv[2])); for (unsigned int itemIndex = 0; itemIndex < projectBundle->itemCount; itemIndex++) { if (projectBundle->items[itemIndex].type == DOCUMENT_TYPE_TILE_MAP) { addFileToBundle(fileBundle, basePath, projectBundle->items[itemIndex].relativePath, FILE_TYPE_TILE_MAP); } } if (!FileBundle_writeFile(fileBundle, argv[3])) { fprintf(stderr, "Error: Couldn't write %s (errno = %d)\n", argv[3], errno); return EXIT_FAILURE; } return EXIT_SUCCESS; } if (!strcmp(argv[1], "--bundle-zone")) { if (argc < 5) { fprintf(stderr, "Too few arguments to --bundle-zone\n"); printUsage(); return EXIT_FAILURE; } unsigned int zoneMapID; if (sscanf(argv[2], "%u", &zoneMapID) != 1) { fprintf(stderr, "Error: Couldn't understand \"%s\" as a tile zone map ID\n", argv[2]); printUsage(); return EXIT_FAILURE; } ProjectBundleMinimal * projectBundle = readProjectBundleFile(argv[3]); if (projectBundle == NULL) { return EXIT_FAILURE; } char * basePath = strdup(getDirectory(argv[3])); unsigned int zoneMapItemIndex; TileZoneMap * zoneMap = readTileZoneMapFileWithID(projectBundle, zoneMapID, basePath, &zoneMapItemIndex); if (zoneMap == NULL) { fprintf(stderr, "Error: Couldn't find tile map zone with ID %u\n", zoneMapID); printUsage(); return EXIT_FAILURE; } HashTable * neededTileMapIDs = HashTable_create(0); for (unsigned int roomIndex = 0; roomIndex < zoneMap->roomCount; roomIndex++) { HashTable_set(neededTileMapIDs, HashTable_uint32Key(zoneMap->rooms[roomIndex].tileMapID), NULL); } FileBundle * fileBundle = FileBundle_create(); addFileToBundle(fileBundle, basePath, projectBundle->items[zoneMapItemIndex].relativePath, FILE_TYPE_ZONE_MAP); for (unsigned int itemIndex = 0; itemIndex < projectBundle->itemCount; itemIndex++) { if (projectBundle->items[itemIndex].type == DOCUMENT_TYPE_TILE_MAP) { const char * filePath = getAbsolutePath(basePath, projectBundle->items[itemIndex].relativePath); TileMapEditData * tileMap = readTileMapFile(filePath); if (tileMap == NULL) { fprintf(stderr, "Error: Encountered unreadable tile map file \"%s\" in project bundle\n", filePath); return EXIT_FAILURE; } TileMapID tileMapID = tileMap->identifier; TileMapEditData_dispose(tileMap); if (HashTable_get(neededTileMapIDs, HashTable_uint32Key(tileMapID)) == NULL) { continue; } addFileToBundle(fileBundle, basePath, projectBundle->items[itemIndex].relativePath, FILE_TYPE_TILE_MAP); HashTable_delete(neededTileMapIDs, HashTable_uint32Key(tileMapID)); } } if (neededTileMapIDs->count > 0) { fprintf(stderr, "Warning: Zone map specified some tile map IDs that were not found in the project bundle: "); bool first = true; HashTable_foreach(neededTileMapIDs, printTileMapID, &first); fputc('\n', stderr); } if (!FileBundle_writeFile(fileBundle, argv[4])) { fprintf(stderr, "Error: Couldn't write %s (errno = %d)\n", argv[4], errno); return EXIT_FAILURE; } return EXIT_SUCCESS; } fprintf(stderr, "Unrecognized argument \"%s\"\n", argv[1]); printUsage(); return EXIT_FAILURE; }