Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/utility/tileset_updater.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/utility/tileset_updater.cpp')
-rw-r--r--source/utility/tileset_updater.cpp326
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];
+}