v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
326
source/utility/tileset_updater.cpp
Normal file
326
source/utility/tileset_updater.cpp
Normal file
|
|
@ -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];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue