diff options
Diffstat (limited to 'source/utility/tileset_updater.cpp')
-rw-r--r-- | source/utility/tileset_updater.cpp | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/source/utility/tileset_updater.cpp b/source/utility/tileset_updater.cpp new file mode 100644 index 0000000..e94ac53 --- /dev/null +++ b/source/utility/tileset_updater.cpp @@ -0,0 +1,326 @@ +#include "StarLogging.hpp" +#include "tileset_updater.hpp" + +using namespace Star; + +String const InvalidTileImage = "../packed/invalid.png"; +String const AssetsTilesetDirectory = "tilesets"; +String const TileImagesDirectory = "../../../../tiled"; +int const Indentation = 2; + +String unixFileJoin(String const& dirname, String const& filename) { + return (dirname.trimEnd("\\/") + '/' + filename.trimBeg("\\/")).replace("\\", "/"); +} + +TileDatabase::TileDatabase(String const& name) : m_name(name) {} + +void TileDatabase::defineTile(TilePtr const& tile) { + m_tiles[tile->name] = tile; +} + +TilePtr TileDatabase::getTile(String const& tileName) const { + return m_tiles.maybe(tileName).value({}); +} + +String TileDatabase::name() const { + return m_name; +} + +StringSet TileDatabase::tileNames() const { + return StringSet::from(m_tiles.keys()); +} + +Tileset::Tileset(String const& source, String const& name, TileDatabasePtr const& database) + : m_source(source), m_name(name), m_tiles(), m_database(database) {} + +void Tileset::defineTile(TilePtr const& tile) { + // Each tileset must be exported from a single database. When a tile switches + // to another tileset (e.g. because an object has changed category), we allow + // it to stay in the previous tileset to avoid breaking maps. + // This means that if we exported a mix of, e.g. materials, liquids and + // objects (which would cause the assertion failure below) it'd be harder to + // check if a tile still exists in the database and should be exported + // despite no longer belonging to the tileset. + starAssert(m_source == tile->source); + starAssert(m_database->name() == tile->database); + + m_tiles.append(tile); +} + +Maybe<pair<String, String>> parseAssetSource(String const& source) { + if (!File::isDirectory(source)) + return {}; + + String sourcePath = source.trimEnd("/\\"); + String sourceName = sourcePath.splitAny("/\\").last(); + + return make_pair(sourceName, sourcePath); +} + +String tilesetExportDir(String const& sourcePath, String const& sourceName) { + return StringList{sourcePath, AssetsTilesetDirectory, sourceName}.join("/"); +} + +void Tileset::exportTileset() const { + auto parsedSource = parseAssetSource(m_source); + if (!parsedSource) + // Don't export tilesets into packed assets + return; + + String sourceName, sourcePath; + tie(sourceName, sourcePath) = *parsedSource; + String exportDir = tilesetExportDir(sourcePath, sourceName); + String tilesetPath = unixFileJoin(exportDir, m_name + ".json"); + File::makeDirectoryRecursive(File::dirName(tilesetPath)); + Logger::info("Updating tileset at %s", tilesetPath); + + exportTilesetImages(exportDir); + + Json root = getTilesetJson(tilesetPath); + JsonObject tileImages = JsonObject{}; + JsonObject tileProperties = root.getObject("tileproperties", JsonObject{}); + + // Scan the tiles already in the tileset + StringMap<size_t> existingTiles; + size_t nextId = 0; + tie(existingTiles, nextId) = indexExistingTiles(root); + + // Add new tiles and update existing ones + StringSet updatedTiles = updateTiles(tileProperties, tileImages, existingTiles, nextId, tilesetPath); + + // Mark all tiles that (a) already existed and (b) were not updated as invalid + // as they are no longer in the assets database. + StringSet invalidTiles = StringSet::from(existingTiles.keys()).difference(updatedTiles); + invalidateTiles(invalidTiles, existingTiles, tileProperties, tileImages, tilesetPath); + + // We have some broken tile indices because of something strange happening + // in the old .tsx files (manual editing? faulty merges?). + // Cover up the holes so that Tiled doesn't barf on them. + for (size_t id = 0; id < nextId; ++id) { + String idKey = toString(id); + if (!tileProperties.contains(idKey)) + tileProperties[idKey] = JsonObject{{"invalid", "true"}}; + if (!tileImages.contains(idKey)) + tileImages[idKey] = imageFileReference(InvalidTileImage); + } + + root = root.set("tiles", tileImages).set("tileproperties", tileProperties); + root = root.set("tilecount", nextId); + File::writeFile(root.printJson(Indentation, true), tilesetPath); +} + +String Tileset::name() const { + return m_name; +} + +TileDatabasePtr Tileset::database() const { + return m_database; +} + +String imageExportDirName(String const& baseExportDir, String const& assetSourceName) { + String dir = unixFileJoin(baseExportDir, TileImagesDirectory); + return unixFileJoin(dir, assetSourceName); +} + +String Tileset::imageDirName(String const& baseExportDir) const { + String sourceName = parseAssetSource(m_source)->first; + return imageExportDirName(baseExportDir, sourceName); +} + +String Tileset::relativePathBase() const { + int subdirs = m_name.splitAny("\\/").size() - 1; + String relativePathBase; + if (subdirs == 0) { + relativePathBase = "."; + } else { + StringList path; + for (int i = 0; i < subdirs; ++i) + path.append(".."); + relativePathBase = path.join("/"); + } + return relativePathBase; +} + +Json Tileset::imageFileReference(String const& fileName) const { + String tileImagePath = unixFileJoin(imageDirName(relativePathBase()), fileName); + return JsonObject{{"image", tileImagePath}}; +} + +Json Tileset::tileImageReference(String const& tileName, String const& database) const { + String tileImageName = unixFileJoin(database, tileName + ".png"); + return imageFileReference(tileImageName); +} + +void Tileset::exportTilesetImages(String const& exportDir) const { + for (auto const& tile : m_tiles) { + String imageDir = unixFileJoin(imageDirName(exportDir), tile->database); + File::makeDirectoryRecursive(imageDir); + String imageName = unixFileJoin(imageDir, tile->name + ".png"); + Logger::info("Updating image %s", imageName); + tile->image->writePng(File::open(imageName, IOMode::Write)); + } +} + +Json Tileset::getTilesetJson(String const& tilesetPath) const { + if (File::exists(tilesetPath)) { + return Json::parseJson(File::readFileString(tilesetPath)); + } else { + Logger::warn( + "Tileset %s wasn't already present. Creating it from scratch. Any maps already using this tileset may be " + "broken.", + tilesetPath); + return JsonObject{{"margin", 0}, + {"name", m_name}, + {"properties", JsonObject{}}, + {"spacing", 0}, + {"tilecount", m_tiles.size()}, + {"tileheight", TilePixels}, + {"tilewidth", TilePixels}, + {"tiles", JsonObject{}}, + {"tileproperties", JsonObject{}}}; + } +} + +pair<StringMap<size_t>, size_t> Tileset::indexExistingTiles(Json tileset) const { + StringMap<size_t> existingTiles; + size_t nextId = 0; + for (auto const& entry : tileset.getObject("tileproperties")) { + size_t id = lexicalCast<size_t>(entry.first); + Tiled::Properties properties = entry.second; + if (properties.contains("//name")) { + existingTiles[properties.get<String>("//name")] = id; + nextId = max(id + 1, nextId); + } + } + return make_pair(existingTiles, nextId); +} + +StringSet Tileset::updateTiles(JsonObject& tileProperties, + JsonObject& tileImages, + StringMap<size_t> const& existingTiles, + size_t& nextId, + String const& tilesetPath) const { + StringSet updatedTiles; + for (TilePtr const& tile : m_tiles) { + Tiled::Properties properties = tile->properties; + size_t id = 0; + + if (existingTiles.contains(tile->name)) { + id = existingTiles.get(tile->name); + } else { + coutf("Adding '%s' to %s\n", tile->name, tilesetPath); + id = nextId++; + } + + tileProperties[toString(id)] = properties.toJson(); + tileImages[toString(id)] = tileImageReference(tile->name, tile->database); + + updatedTiles.add(tile->name); + } + return updatedTiles; +} + +void Tileset::invalidateTiles(StringSet const& invalidTiles, + StringMap<size_t> const& existingTiles, + JsonObject& tileProperties, + JsonObject& tileImages, + String const& tilesetPath) const { + for (String tileName : invalidTiles) { + size_t id = existingTiles.get(tileName); + + if (TilePtr const& tile = m_database->getTile(tileName)) { + // Tile has moved category, but we're leaving it in this tileset to avoid + // breaking existing maps. + tileProperties[toString(id)] = tile->properties.toJson(); + tileImages[toString(id)] = tileImageReference(tile->name, tile->database); + } else { + if (!tileProperties[toString(id)].contains("invalid")) + coutf("Removing '%s' from %s\n", tileName, tilesetPath); + tileProperties[toString(id)] = JsonObject{{"//name", tileName}, {"invalid", "true"}}; + tileImages[toString(id)] = imageFileReference(InvalidTileImage); + } + } +} + +void TilesetUpdater::defineAssetSource(String const& source) { + auto parsedSource = parseAssetSource(source); + if (!parsedSource) + // Don't change anything about images in packed assets + return; + + String sourceName; + String sourcePath; + tie(sourceName, sourcePath) = *parsedSource; + String tilesetDir = tilesetExportDir(sourcePath, sourceName); + String imageDir = imageExportDirName(tilesetDir, sourceName); + + Logger::info("Scanning %s for images...", imageDir); + if (!File::isDirectory(imageDir)) + return; + + for (pair<String, bool> entry : File::dirList(imageDir)) { + if (entry.second) { + String databaseName = entry.first; + String databasePath = unixFileJoin(imageDir, databaseName); + Logger::info("Scanning database %s...", databaseName); + for (pair<String, bool> image : File::dirList(databasePath)) { + starAssert(!image.second); + starAssert(image.first.endsWith(".png")); + String tileName = image.first.substr(0, image.first.findLast(".png")); + m_preexistingImages[sourceName][databaseName].add(tileName); + } + } + } +} + +void TilesetUpdater::defineTile(TilePtr const& tile) { + getDatabase(tile)->defineTile(tile); + getTileset(tile)->defineTile(tile); +} + +void TilesetUpdater::exportTilesets() { + for (auto const& tilesets : m_tilesets) { + auto parsedAssetSource = parseAssetSource(tilesets.first); + if (!parsedAssetSource) { + Logger::info("Not updating tilesets in %s because it is packed", tilesets.first); + continue; + } + String sourceName; + String sourcePath; + tie(sourceName, sourcePath) = *parsedAssetSource; + + String tilesetDir = tilesetExportDir(sourcePath, sourceName); + String imageDir = imageExportDirName(tilesetDir, sourceName); + + for (auto const& tileset : tilesets.second.values()) { + tileset->exportTileset(); + } + + for (auto const& database : m_databases[tilesets.first].values()) { + String databaseImagePath = unixFileJoin(imageDir, database->name()); + StringSet unusedImages = m_preexistingImages[sourceName][database->name()].difference(database->tileNames()); + for (String tileName : unusedImages) { + String tileImagePath = unixFileJoin(databaseImagePath, tileName + ".png"); + starAssert(File::isFile(tileImagePath)); + coutf("Removing unused tile image tiled/%s/%s/%s.png\n", sourceName, database->name(), tileName); + File::remove(tileImagePath); + } + m_preexistingImages[sourceName][database->name()] = database->tileNames(); + } + } +} + +TileDatabasePtr const& TilesetUpdater::getDatabase(TilePtr const& tile) { + auto& databases = m_databases[tile->source]; + if (!databases.contains(tile->database)) + databases[tile->database] = make_shared<TileDatabase>(tile->database); + return databases[tile->database]; +} + +TilesetPtr const& TilesetUpdater::getTileset(TilePtr const& tile) { + TileDatabasePtr database = getDatabase(tile); + auto& tilesets = m_tilesets[tile->source]; + if (!tilesets.contains(tile->tileset)) + tilesets[tile->tileset] = make_shared<Tileset>(tile->source, tile->tileset, database); + return tilesets[tile->tileset]; +} |