osb/source/utility/update_tilesets.cpp
2023-06-26 11:58:35 -07:00

263 lines
9.8 KiB
C++

#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<String> 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>(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<StringSet>(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<ObjectOrientationPtr> 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<ImageConstPtr> layers;
unsigned width = 0, height = 0;
for (auto const& imageLayer : orientation->imageLayers) {
String imageName = AssetPath::join(imageLayer.imagePart().image).replaceTags(StringMap<String>{}, 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<Image>(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>(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 %s 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<Image>(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>(Tile{assetSource, "liquids", "liquids", liquid->name, image, properties});
updater.defineTile(tile);
ImagePtr sourceImage = make_shared<Image>(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>(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: \"%s\"", source);
updater.defineAssetSource(source);
}
scanMaterials(updater);
scanObjects(updater);
scanLiquids(updater);
updater.exportTilesets();
return 0;
} catch (std::exception const& e) {
cerrf("exception caught: %s\n", outputException(e, true));
return 1;
}
}