#include "StarAssets.hpp" #include "StarLiquidsDatabase.hpp" #include "StarMaterialDatabase.hpp" #include "StarObject.hpp" #include "StarObjectDatabase.hpp" #include "StarRootLoader.hpp" #include "tileset_updater.hpp" using namespace Star; String const InboundNode = "/tilesets/inboundnode.png"; String const OutboundNode = "/tilesets/outboundnode.png"; Vec3B const SourceLiquidBorderColor(0x80, 0x80, 0x00); void scanMaterials(TilesetUpdater& updater) { auto& root = Root::singleton(); auto materials = root.materialDatabase(); for (String materialName : materials->materialNames()) { MaterialId id = materials->materialId(materialName); Maybe path = materials->materialPath(id); if (!path) continue; String source = root.assets()->assetSource(*path); auto renderProfile = materials->materialRenderProfile(id); if (renderProfile == nullptr) continue; String tileset = materials->materialCategory(id); String imagePath = renderProfile->pieceImage(renderProfile->representativePiece, 0); ImageConstPtr image = root.assets()->image(imagePath); Tiled::Properties properties; properties.set("material", materialName); properties.set("//name", materialName); properties.set("//shortdescription", materials->materialShortDescription(id)); properties.set("//description", materials->materialDescription(id)); auto tile = make_shared(Tile{source, "materials", tileset.toLower(), materialName, image, properties}); updater.defineTile(tile); } } // imagePosition might not be aligned to a whole number, i.e. the image origin // might not align with the tile grid. We do, however want Tile Objects in Tiled // to be grid-aligned (valid positions are offset relative to the grid not // completely free-form), so we correct the alignment by adding padding to the // image that we export. // We're going to ignore the fact that some objects have imagePositions that // aren't even aligned _to pixels_ (e.g. giftsmallmonsterbox). Vec2U objectPositionPadding(Vec2I imagePosition) { int pixelsX = imagePosition.x(); int pixelsY = imagePosition.y(); // Unsigned modulo operation gives the padding to use (in pixels) unsigned padX = (unsigned)pixelsX % TilePixels; unsigned padY = (unsigned)pixelsY % TilePixels; return Vec2U(padX, padY); } StringSet categorizeObject(String const& objectName, Vec2U imageSize) { if (imageSize[0] >= 256 || imageSize[1] >= 256) return StringSet{"huge-objects"}; auto& root = Root::singleton(); auto assets = root.assets(); auto objects = root.objectDatabase(); Json defaultCategories = assets->json("/objects/defaultCategories.config"); auto objectConfig = objects->getConfig(objectName); StringSet categories; if (objectConfig->category != defaultCategories.getString("category")) categories.insert("objects-by-category/" + objectConfig->category); for (String const& tag : objectConfig->colonyTags) categories.insert("objects-by-colonytag/" + tag); if (objectConfig->type != defaultCategories.getString("objectType")) categories.insert("objects-by-type/" + objectConfig->type); if (objectConfig->race != defaultCategories.getString("race")) categories.insert("objects-by-race/" + objectConfig->race); if (categories.size() == 0) categories.insert("objects-uncategorized"); return transform(categories, [](String const& category) { return category.toLower(); }); } void drawNodes(ImagePtr const& image, Vec2I imagePosition, JsonArray nodes, String nodeImagePath) { ImageConstPtr nodeImage = Root::singleton().assets()->image(nodeImagePath); for (Json const& node : nodes) { Vec2I nodePos = jsonToVec2I(node) * TilePixels + Vec2I(0, TilePixels - nodeImage->height()); Vec2U nodeImagePos = Vec2U(nodePos - imagePosition); image->drawInto(nodeImagePos, *nodeImage); } } void defineObjectOrientation(TilesetUpdater& updater, String const& objectName, List const& orientations, int orientationIndex) { auto& root = Root::singleton(); auto assets = root.assets(); auto objects = root.objectDatabase(); ObjectOrientationPtr orientation = orientations[orientationIndex]; Vec2I imagePosition = Vec2I(orientation->imagePosition * TilePixels); List layers; unsigned width = 0, height = 0; for (auto const& imageLayer : orientation->imageLayers) { String imageName = AssetPath::join(imageLayer.imagePart().image).replaceTags(StringMap{}, true, "default"); ImageConstPtr image = assets->image(imageName); layers.append(image); width = max(width, image->width()); height = max(height, image->height()); } Vec2U imagePadding = objectPositionPadding(imagePosition); imagePosition -= Vec2I(imagePadding); // Padding is added to the right hand side as well as the left so that // when objects are flipped in the editor, they're still aligned correctly. Vec2U imageSize(width + 2 * imagePadding.x(), height + imagePadding.y()); ImagePtr combinedImage = make_shared(imageSize, PixelFormat::RGBA32); combinedImage->fill(Vec4B(0, 0, 0, 0)); for (ImageConstPtr const& layer : layers) { combinedImage->drawInto(imagePadding, *layer); } // Overlay the image with the wiring nodes: auto objectConfig = objects->getConfig(objectName); drawNodes(combinedImage, imagePosition, objectConfig->config.getArray("inputNodes", {}), InboundNode); drawNodes(combinedImage, imagePosition, objectConfig->config.getArray("outputNodes", {}), OutboundNode); ObjectPtr example = objects->createObject(objectName); Tiled::Properties properties; properties.set("object", objectName); properties.set("imagePositionX", imagePosition.x()); properties.set("imagePositionY", imagePosition.y()); properties.set("//shortdescription", example->shortDescription()); properties.set("//description", example->description()); if (orientation->directionAffinity.isValid()) { Direction direction = *orientation->directionAffinity; if (orientation->flipImages) direction = -direction; properties.set("tilesetDirection", DirectionNames.getRight(direction)); } StringSet tilesets = categorizeObject(objectName, imageSize); // tileName becomes part of the filename for the tile's image. Different // orientations require different images, so the tileName must be different // for each orientation. String tileName = objectName; if (orientationIndex != 0) tileName += "_orientation" + toString(orientationIndex); properties.set("//name", tileName); String source = assets->assetSource(objectConfig->path); for (String const& tileset : tilesets) { TilePtr tile = make_shared(Tile{source, "objects", tileset, tileName, combinedImage, properties}); updater.defineTile(tile); } } void scanObjects(TilesetUpdater& updater) { auto& root = Root::singleton(); auto objects = root.objectDatabase(); for (String const& objectName : objects->allObjects()) { auto orientations = objects->getOrientations(objectName); if (orientations.size() < 1) { Logger::warn("Object {} has no orientations and will not be exported", objectName); continue; } // Always export the first orientation ObjectOrientationPtr orientation = orientations[0]; defineObjectOrientation(updater, objectName, orientations, 0); // If there are more than 2 orientations or the imagePositions are different // then horizontal flipping in the editor is not enough to get all the // orientations and display them correctly, so we export each orientation // as a separate tile. for (unsigned i = 1; i < orientations.size(); ++i) { if (i >= 2 || orientation->imagePosition != orientations[i]->imagePosition) defineObjectOrientation(updater, objectName, orientations, i); } } } void scanLiquids(TilesetUpdater& updater) { auto& root = Root::singleton(); auto liquids = root.liquidsDatabase(); auto assets = root.assets(); Vec2U imageSize(TilePixels, TilePixels); for (auto liquid : liquids->allLiquidSettings()) { ImagePtr image = make_shared(imageSize, PixelFormat::RGBA32); image->fill(liquid->liquidColor); String assetSource = assets->assetSource(liquid->path); Tiled::Properties properties; properties.set("liquid", liquid->name); properties.set("//name", liquid->name); auto tile = make_shared(Tile{assetSource, "liquids", "liquids", liquid->name, image, properties}); updater.defineTile(tile); ImagePtr sourceImage = make_shared(imageSize, PixelFormat::RGBA32); sourceImage->copyInto(Vec2U(), *image.get()); sourceImage->fillRect(Vec2U(), Vec2U(image->width(), 1), SourceLiquidBorderColor); sourceImage->fillRect(Vec2U(), Vec2U(1, image->height()), SourceLiquidBorderColor); String sourceName = liquid->name + "_source"; properties.set("source", true); properties.set("//name", sourceName); properties.set("//shortdescription", "Endless " + liquid->name); auto sourceTile = make_shared(Tile{assetSource, "liquids", "liquids", sourceName, sourceImage, properties}); updater.defineTile(sourceTile); } } int main(int argc, char** argv) { try { RootLoader rootLoader({{}, {}, {}, LogLevel::Error, false, {}}); rootLoader.setSummary("Updates Tiled JSON tilesets in unpacked assets directories"); RootUPtr root; OptionParser::Options options; tie(root, options) = rootLoader.commandInitOrDie(argc, argv); TilesetUpdater updater; for (String source : root->assets()->assetSources()) { Logger::info("Assets source: \"{}\"", source); updater.defineAssetSource(source); } scanMaterials(updater); scanObjects(updater); scanLiquids(updater); updater.exportTilesets(); return 0; } catch (std::exception const& e) { cerrf("exception caught: {}\n", outputException(e, true)); return 1; } }