From cc77ce62b128ff40e985de70f540564245b593cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Fri, 17 Jan 2025 11:17:16 +0100 Subject: [PATCH] YY plugin: Adjusted to changes in GameMaker file format (#4143) An update to GameMaker has broken compatibility in several ways: * It now requires a "type tag field" at the start of applicable JSON records. * The name is expected to follow the "type tag field" as a "%Name" field, in addition to the regular "name" field. * The JSON fields are now required to be ordered alphabetically (case-insentively). Other minor differences include: * The "tags" field is left out when there are no tags. * There is no longer a space included after JSON field names. * Some new fields were added. Also, the plugin now uses SaveFile again rather than QFile, because it seems that recent GameMaker versions will reload fine when using safe writing of files. Updated the manual to no longer say "GameMaker Studio 2.3". Closes #4132 --- NEWS.md | 1 + docs/manual/export-yy.rst | 20 +- src/plugins/yy/jsonwriter.cpp | 73 ++- src/plugins/yy/jsonwriter.h | 9 +- src/plugins/yy/yyplugin.cpp | 921 +++++++++++++++++++--------------- 5 files changed, 592 insertions(+), 432 deletions(-) diff --git a/NEWS.md b/NEWS.md index b114e31f4a..6d792eb134 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ### Unreleased +* YY plugin: Fixed compatibility with GameMaker 2024 (#4132) * snap: Fixed crash on startup on Wayland * Raised minimum supported Qt version from 5.12 to 5.15 diff --git a/docs/manual/export-yy.rst b/docs/manual/export-yy.rst index deb33e51a7..d21d0d5e6e 100644 --- a/docs/manual/export-yy.rst +++ b/docs/manual/export-yy.rst @@ -4,11 +4,11 @@
Since Tiled 1.5
-GameMaker Studio 2.3 -==================== +GameMaker +========= -GameMaker Studio 2.3 uses a JSON-based format to store its rooms, and Tiled -ships with a plugin to export maps in this format. +GameMaker uses a JSON-based format to store its rooms, and Tiled ships with a +plugin to export maps in this format. This plugin will do its best to export the map as accurately as possible, mapping Tiled's various features to the matching GameMaker features. @@ -20,13 +20,13 @@ as background layers. .. warning:: - Since GameMaker's "Add Existing" action doesn't work at this point in time - (2.3.1) the easiest way to export a Tiled map to your GameMaker Project is - to overwrite an already existing ``room.yy`` file. + Since it's not possible to add a room to a project by selecting a .yy file, + the easiest way to export a Tiled map to your GameMaker project is to create + a new room in GameMaker and then overwrite its ``room.yy`` file when + exporting from Tiled. - Starting with Tiled 1.8, it is no longer necessary to deactivate the "Use - safe writing of files" option, since the GameMaker export now ignores it to - avoid reload issues in GameMaker. + In 2024, GameMaker made minor but incompatible changes to its file format. + Tiled 1.12 ships with an updated plugin that uses the new format. .. _yy-asset-references: diff --git a/src/plugins/yy/jsonwriter.cpp b/src/plugins/yy/jsonwriter.cpp index ce417d36d6..6be4efafaa 100644 --- a/src/plugins/yy/jsonwriter.cpp +++ b/src/plugins/yy/jsonwriter.cpp @@ -59,13 +59,16 @@ void JsonWriter::writeStartScope(Scope scope, const char *name) m_valueWritten = false; } -void JsonWriter::writeEndScope(Scope scope) +void JsonWriter::writeEndScope(Scope scope, bool forceNewLine) { Q_ASSERT(m_scopes.last() == scope); m_scopes.pop(); if (m_valueWritten) { write(m_valueSeparator); // This is not JSON-conform, but it's what GameMaker does - writeNewline(); + + // GameMaker minimization logic + if (m_scopes.size() < 2 || forceNewLine) + writeNewline(forceNewLine); } write(scope == Object ? '}' : ']'); m_newLine = false; @@ -74,17 +77,10 @@ void JsonWriter::writeEndScope(Scope scope) void JsonWriter::writeValue(double value) { - if (qIsFinite(value)) { - // Force at least one decimal to avoid double values from being written - // as integer, which may confuse GameMaker. - if (std::ceil(value) == value) { - writeUnquotedValue(QByteArray::number(value, 'f', 1)); - } else { - writeUnquotedValue(QByteArray::number(value, 'g', QLocale::FloatingPointShortest)); - } - } else { + if (qIsFinite(value)) + writeUnquotedValue(QByteArray::number(value, 'g', QLocale::FloatingPointShortest)); + else writeUnquotedValue("null"); // +INF || -INF || NaN (see RFC4627#section2.4) - } } void JsonWriter::writeValue(const QByteArray &value) @@ -114,20 +110,51 @@ void JsonWriter::writeValue(const QJsonValue &value) writeValue(value.toString()); break; case QJsonValue::Array: { - writeStartArray(); const QJsonArray array = value.toArray(); + + bool arrayContainedObject = false; + qsizetype index = 0; + + writeStartArray(); for (auto v : array) { - prepareNewLine(); + arrayContainedObject |= v.isObject(); + + if (m_tileSerialiseWidth > 0) { + // force new line when starting a new row of tiles + prepareNewLine(index % m_tileSerialiseWidth == 0); + } else { + // force new line if value is an object + prepareNewLine(v.isObject()); + } + writeValue(v); + ++index; } - writeEndArray(); + writeEndArray(arrayContainedObject || m_tileSerialiseWidth > 0); break; } case QJsonValue::Object: { - writeStartObject(); const QJsonObject object = value.toObject(); - for (auto it = object.begin(); it != object.end(); ++it) - writeMember(it.key().toLatin1().constData(), it.value()); + + // GameMaker 2024 requires the keys to be sorted case-insensitively + auto keys = object.keys(); + keys.sort(Qt::CaseInsensitive); + + writeStartObject(); + for (const auto &key : keys) { + const auto value = object.value(key); + + const bool writingTiles = key == QLatin1String("tiles"); + if (writingTiles) { + const auto tilesObject = value.toObject(); + m_tileSerialiseWidth = tilesObject.value(QLatin1String("SerialiseWidth")).toInt(); + } + + writeMember(key.toLatin1().constData(), value); + + if (writingTiles) + m_tileSerialiseWidth = 0; + } writeEndObject(); break; } @@ -215,13 +242,13 @@ QString JsonWriter::quote(const QString &str) return quoted; } -void JsonWriter::prepareNewLine() +void JsonWriter::prepareNewLine(bool forceNewLine) { if (m_valueWritten) { write(m_valueSeparator); m_valueWritten = false; } - writeNewline(); + writeNewline(forceNewLine); } void JsonWriter::prepareNewValue() @@ -238,10 +265,10 @@ void JsonWriter::writeIndent() write(" "); } -void JsonWriter::writeNewline() +void JsonWriter::writeNewline(bool force) { if (!m_newLine) { - if (!m_minimize && !m_suppressNewlines) { + if (force || (!m_minimize && !m_suppressNewlines && m_scopes.size() < 3)) { write('\n'); writeIndent(); } @@ -255,7 +282,7 @@ void JsonWriter::writeKey(const char *key) prepareNewLine(); write('"'); write(key); - write(m_minimize ? "\":" : "\": "); + write("\":"); } void JsonWriter::write(const char *bytes, qint64 length) diff --git a/src/plugins/yy/jsonwriter.h b/src/plugins/yy/jsonwriter.h index a41abe1b75..d0db7fb95e 100644 --- a/src/plugins/yy/jsonwriter.h +++ b/src/plugins/yy/jsonwriter.h @@ -53,7 +53,7 @@ class JsonWriter void writeStartArray() { writeStartScope(Array); } void writeStartArray(const char *name) { writeStartScope(Array, name); } - void writeEndArray() { writeEndScope(Array); } + void writeEndArray(bool forceNewLine = false) { writeEndScope(Array, forceNewLine); } void writeValue(int value); void writeValue(unsigned value); @@ -83,7 +83,7 @@ class JsonWriter void setMinimize(bool minimize); bool minimize() const; - void prepareNewLine(); + void prepareNewLine(bool forceNewLine = false); bool hasError() const { return m_error; } @@ -92,12 +92,12 @@ class JsonWriter private: void writeStartScope(Scope scope); void writeStartScope(Scope scope, const char *name); - void writeEndScope(Scope scope); + void writeEndScope(Scope scope, bool forceNewLine = false); void prepareNewValue(); void writeIndent(); - void writeNewline(); + void writeNewline(bool force = false); void writeKey(const char *key); void write(const char *bytes, qint64 length); void write(const char *bytes); @@ -113,6 +113,7 @@ class JsonWriter bool m_newLine { true }; bool m_valueWritten { false }; bool m_error { false }; + int m_tileSerialiseWidth { 0 }; }; inline void JsonWriter::writeValue(int value) diff --git a/src/plugins/yy/yyplugin.cpp b/src/plugins/yy/yyplugin.cpp index 7c817593a5..1bb8090e86 100644 --- a/src/plugins/yy/yyplugin.cpp +++ b/src/plugins/yy/yyplugin.cpp @@ -91,6 +91,7 @@ enum ResourceType GMRPathLayerType, GMRSpriteGraphicType, GMRTileLayerType, + GMRoomType, }; static const char *resourceTypeStr(ResourceType type) @@ -107,47 +108,139 @@ static const char *resourceTypeStr(ResourceType type) case GMRPathLayerType: return "GMRPathLayer"; case GMRSpriteGraphicType: return "GMRSpriteGraphic"; case GMRTileLayerType: return "GMRTileLayer"; + case GMRoomType: return "GMRoom"; } return "Unknown"; } +static const char *resourceTypeTagValue(ResourceType type) +{ + switch (type) { + case GMOverriddenPropertyType: return "v1"; + case GMPathType: return ""; + case GMRAssetLayerType: return ""; + case GMRBackgroundLayerType: return ""; + case GMRGraphicType: return ""; + case GMRInstanceLayerType: return ""; + case GMRInstanceType: return "v1"; + case GMRLayerType: return ""; + case GMRPathLayerType: return ""; + case GMRSpriteGraphicType: return ""; + case GMRTileLayerType: return ""; + case GMRoomType: return "v1"; + } + + return ""; +} + +static QJsonValue idValue(const QString &id, const QString &scope) +{ + if (id.isEmpty()) + return QJsonValue(QJsonValue::Null); + + return QJsonObject { + { "name", id }, + { "path", QStringLiteral("%1/%2/%2.yy").arg(scope, id) } + }; +} + +static unsigned colorToAbgr(const QColor &color) +{ + const QRgb rgba = color.rgba(); + return qRgba(qBlue(rgba), qGreen(rgba), qRed(rgba), qAlpha(rgba)); +} + +static double colorToAbgrF(const QColor &color) +{ + return colorToAbgr(color); +} + + struct GMRView { bool inherit = false; bool visible = false; int xview = 0; int yview = 0; - int wview = 1366; + int wview = 1024; int hview = 768; int xport = 0; int yport = 0; - int wport = 1366; + int wport = 1024; int hport = 768; int hborder = 32; int vborder = 32; int hspeed = -1; int vspeed = -1; QString objectId; + + QJsonObject toJson() const; }; +QJsonObject GMRView::toJson() const +{ + return { + { "inherit", inherit }, + { "visible", visible }, + { "xview", xview }, + { "yview", yview }, + { "wview", wview }, + { "hview", hview }, + { "xport", xport }, + { "yport", yport }, + { "wport", wport }, + { "hport", hport }, + { "hborder", hborder }, + { "vborder", vborder }, + { "hspeed", hspeed }, + { "vspeed", vspeed }, + { "objectId", idValue(objectId, QStringLiteral("objects")) } + }; +} + + struct GMResource { GMResource(ResourceType type) : resourceType(type) {} virtual ~GMResource() = default; - QString resourceVersion = QStringLiteral("1.0"); + virtual QJsonObject toJson() const; + + QString resourceVersion = QStringLiteral("2.0"); QString name; QStringList tags; ResourceType resourceType; }; +QJsonObject GMResource::toJson() const +{ + QJsonObject json; + + const char *type = resourceTypeStr(resourceType); + + json[QByteArray("$") + type] = resourceTypeTagValue(resourceType); + json["%Name"] = name; + json["resourceVersion"] = resourceVersion; + json["name"] = name; + + if (!tags.isEmpty()) + json["tags"] = QJsonArray::fromStringList(tags); + + json["resourceType"] = type; + + return json; +} + + struct GMRGraphic final : GMResource { GMRGraphic(bool isSprite) : GMResource(isSprite ? GMRSpriteGraphicType : GMRGraphicType) {} + QJsonObject toJson() const override; + QString spriteId; union { @@ -181,19 +274,80 @@ struct GMRGraphic final : GMResource double y = 0.0; }; +QJsonObject GMRGraphic::toJson() const +{ + QJsonObject json = GMResource::toJson(); + + json["spriteId"] = idValue(spriteId, QStringLiteral("sprites")); + + if (resourceType == GMRSpriteGraphicType) { + json["headPosition"] = headPosition; + json["rotation"] = rotation; + json["scaleX"] = scaleX; + json["scaleY"] = scaleY; + json["animationSpeed"] = animationSpeed; + } else { + json["w"] = w; + json["h"] = h; + json["u0"] = u0; + json["v0"] = v0; + json["u1"] = u1; + json["v1"] = v1; + } + + json["colour"] = colorToAbgrF(colour); + + if (inheritedItemId.isEmpty()) { + json["inheritedItemId"] = QJsonValue(QJsonValue::Null); + } else { + json["inheritedItemId"] = QJsonObject { + { "name", inheritedItemId }, + { "path", inheritedItemPath } + }; + } + + json["frozen"] = frozen; + json["ignore"] = ignore; + json["inheritItemSettings"] = inheritItemSettings; + json["x"] = x; + json["y"] = y; + + return json; +} + + struct GMOverriddenProperty final : GMResource { GMOverriddenProperty() : GMResource(GMOverriddenPropertyType) {} + QJsonObject toJson() const override; + QString propertyId; QString objectId; QString value; }; +QJsonObject GMOverriddenProperty::toJson() const +{ + QJsonObject json = GMResource::toJson(); + + json["propertyId"] = QJsonObject { + { "name", propertyId }, + { "path", QStringLiteral("%1/%2/%2.yy").arg(QStringLiteral("objects"), objectId) } + }; + json["objectId"] = idValue(objectId, QStringLiteral("objects")); + json["value"] = value; + + return json; +} + + struct GMRInstance final : GMResource { GMRInstance() : GMResource(GMRInstanceType) {} + QJsonObject toJson() const override; + std::vector properties; bool isDnd = false; QString objectId; @@ -214,20 +368,94 @@ struct GMRInstance final : GMResource double y = 0.0; }; +QJsonObject GMRInstance::toJson() const +{ + QJsonObject json = GMResource::toJson(); + + QJsonArray propertiesJson; + for (const GMOverriddenProperty &prop : properties) + propertiesJson.append(prop.toJson()); + json["properties"] = propertiesJson; + + json["isDnd"] = isDnd; + + json["objectId"] = idValue(objectId, QStringLiteral("objects")); + + json["inheritCode"] = inheritCode; + json["hasCreationCode"] = hasCreationCode; + json["colour"] = colorToAbgrF(colour); + json["rotation"] = rotation; + json["scaleX"] = scaleX; + json["scaleY"] = scaleY; + json["imageIndex"] = imageIndex; + json["imageSpeed"] = imageSpeed; + + if (inheritedItemId.isEmpty()) { + json["inheritedItemId"] = QJsonValue(QJsonValue::Null); + } else { + json["inheritedItemId"] = QJsonObject { + { "name", inheritedItemId }, + { "path", inheritedItemPath } + }; + } + + json["frozen"] = frozen; + json["ignore"] = ignore; + json["inheritItemSettings"] = inheritItemSettings; + json["x"] = x; + json["y"] = y; + + return json; +} + + struct GMPath final : GMResource { GMPath() : GMResource(GMPathType) {} + QJsonObject toJson() const override; + int kind = 0; bool closed = false; int precision = 4; QVector points; }; +QJsonObject GMPath::toJson() const +{ + QJsonObject json = GMResource::toJson(); + + json["kind"] = kind; + json["closed"] = closed; + json["precision"] = precision; + + // todo: + // "parent":{ + // "name":"Rooms", + // "path":"folders/Rooms.yy", + // }, + + QJsonArray pointsJson; + for (const QPointF &point : points) { + pointsJson.append(QJsonObject { + { "speed", 100.0 }, + { "x", point.x() }, + { "y", point.y() } + }); + } + + json["points"] = pointsJson; + + return json; +} + + struct GMRLayer : GMResource { GMRLayer(ResourceType type = GMRLayerType) : GMResource(type) {} + QJsonObject toJson() const override; + bool visible = true; int depth = 0; bool userdefinedDepth = false; @@ -239,10 +467,40 @@ struct GMRLayer : GMResource bool hierarchyFrozen = false; }; +QJsonObject GMRLayer::toJson() const +{ + QJsonObject json = GMResource::toJson(); + + json["visible"] = visible; + json["depth"] = depth; + json["userdefinedDepth"] = userdefinedDepth; + json["inheritLayerDepth"] = inheritLayerDepth; + json["inheritLayerSettings"] = inheritLayerSettings; + json["inheritSubLayers"] = true; + json["inheritVisibility"] = true; + json["gridX"] = gridX; + json["gridY"] = gridY; + json["effectEnabled"] = true; + json["effectType"] = QJsonValue(QJsonValue::Null); + + QJsonArray layersJson; + for (const std::unique_ptr &layer : layers) + layersJson.append(layer->toJson()); + + json["layers"] = layersJson; + json["hierarchyFrozen"] = hierarchyFrozen; + json["properties"] = QJsonArray(); + + return json; +} + + struct GMRTileLayer final : GMRLayer { GMRTileLayer() : GMRLayer(GMRTileLayerType) {} + QJsonObject toJson() const override; + QString tilesetId; int x = 0; int y = 0; @@ -251,32 +509,101 @@ struct GMRTileLayer final : GMRLayer std::vector tiles; }; +QJsonObject GMRTileLayer::toJson() const +{ + QJsonObject json = GMRLayer::toJson(); + + json["tilesetId"] = idValue(tilesetId, QStringLiteral("tilesets")); + json["x"] = x; + json["y"] = y; + + QJsonArray tilesJson; + for (size_t index = 0; index < tiles.size(); ++index) + tilesJson.append((double) tiles.at(index)); + + json["tiles"] = QJsonObject { + { "SerialiseWidth", SerialiseWidth }, + { "SerialiseHeight", SerialiseHeight }, + { "TileSerialiseData", tilesJson } + }; + + return json; +} + + struct GMRAssetLayer final : GMRLayer { GMRAssetLayer() : GMRLayer(GMRAssetLayerType) {} + QJsonObject toJson() const override; + std::vector assets; }; +QJsonObject GMRAssetLayer::toJson() const +{ + QJsonObject json = GMRLayer::toJson(); + + QJsonArray assetsJson; + for (const auto &asset : assets) + assetsJson.append(asset.toJson()); + + json["assets"] = assetsJson; + + return json; +} + + struct GMRInstanceLayer final : GMRLayer { GMRInstanceLayer() : GMRLayer(GMRInstanceLayerType) {} + QJsonObject toJson() const override; + std::vector instances; }; +QJsonObject GMRInstanceLayer::toJson() const +{ + QJsonObject json = GMRLayer::toJson(); + + QJsonArray instancesJson; + for (const auto &instance : instances) + instancesJson.append(instance.toJson()); + + json["instances"] = instancesJson; + + return json; +} + + struct GMRPathLayer final : GMRLayer { GMRPathLayer() : GMRLayer(GMRPathLayerType) {} + QJsonObject toJson() const; + QString pathId; QColor colour = Qt::red; }; +QJsonObject GMRPathLayer::toJson() const +{ + QJsonObject json = GMRLayer::toJson(); + + json["pathId"] = idValue(pathId, QStringLiteral("paths")); + json["colour"] = colorToAbgrF(colour); + + return json; +} + + struct GMRBackgroundLayer final : GMRLayer { GMRBackgroundLayer() : GMRLayer(GMRBackgroundLayerType) {} + QJsonObject toJson() const; + QString spriteId; QColor colour = Qt::white; int x = 0; @@ -291,6 +618,27 @@ struct GMRBackgroundLayer final : GMRLayer bool userdefinedAnimFPS = false; }; +QJsonObject GMRBackgroundLayer::toJson() const +{ + QJsonObject json = GMRLayer::toJson(); + + json["spriteId"] = idValue(spriteId, QStringLiteral("sprites")); + json["colour"] = colorToAbgrF(colour); + json["x"] = x; + json["y"] = y; + json["htiled"] = htiled; + json["vtiled"] = vtiled; + json["hspeed"] = hspeed; + json["vspeed"] = vspeed; + json["stretch"] = stretch; + json["animationFPS"] = animationFPS; + json["animationSpeedType"] = animationSpeedType; + json["userdefinedAnimFPS"] = userdefinedAnimFPS; + + return json; +} + + struct InstanceCreation { QString name; @@ -300,11 +648,120 @@ struct InstanceCreation { return creationOrder < other.creationOrder; } }; -struct Context + +struct GMRoom final : GMResource { + GMRoom() : GMResource(GMRoomType) {} + + QJsonObject toJson() const override; + + bool isDnd = false; + double volume = 1.0; std::vector views; - std::vector paths; + std::vector> layers; + bool inheritLayers = false; + QString creationCodeFile; + bool inheritCode = false; std::vector instanceCreationOrder; + bool inheritCreationOrder = false; + + struct { + bool inheritRoomSettings = false; + int Width = 0; + int Height = 0; + bool persistent = false; + } roomSettings; + + struct { + bool inheritViewSettings = false; + bool enableViews = false; + bool clearViewBackground = false; + bool clearDisplayBuffer = false; + } viewSettings; + + struct { + bool inheritPhysicsSettings = false; + bool PhysicsWorld = false; + double PhysicsWorldGravityX = 0.0; + double PhysicsWorldGravityY = 10.0; + double PhysicsWorldPixToMetres = 0.1; + } physicsSettings; + + QString parent = QStringLiteral("Rooms"); + QString roomPathInProject; +}; + +QJsonObject GMRoom::toJson() const +{ + QJsonObject json = GMResource::toJson(); + + json["isDnd"] = isDnd; + json["volume"] = volume; + json["parentRoom"] = QJsonValue(QJsonValue::Null); // TODO: Provide a way to set this? + + QJsonArray viewsJson; + for (const GMRView &view : views) + viewsJson.append(view.toJson()); + + json["views"] = viewsJson; + + QJsonArray layersJson; + for (const auto &layer : layers) + layersJson.append(layer->toJson()); + + json["layers"] = layersJson; + + json["inheritLayers"] = inheritLayers; + json["creationCodeFile"] = creationCodeFile; + json["inheritCode"] = inheritCode; + + QJsonArray instanceCreationOrderJson; + for (const InstanceCreation &creation : instanceCreationOrder) { + instanceCreationOrderJson.append(QJsonObject { + { "name", creation.name }, + { "path", roomPathInProject } + }); + } + + json["instanceCreationOrder"] = instanceCreationOrderJson; + json["inheritCreationOrder"] = inheritCreationOrder; + json["sequenceId"] = QJsonValue(QJsonValue::Null); + + json["roomSettings"] = QJsonObject { + { "inheritRoomSettings", roomSettings.inheritRoomSettings }, + { "Width", roomSettings.Width }, + { "Height", roomSettings.Height }, + { "persistent", roomSettings.persistent } + }; + + json["viewSettings"] = QJsonObject { + { "inheritViewSettings", viewSettings.inheritViewSettings }, + { "enableViews", viewSettings.enableViews }, + { "clearViewBackground", viewSettings.clearViewBackground }, + { "clearDisplayBuffer", viewSettings.clearDisplayBuffer } + }; + + json["physicsSettings"] = QJsonObject { + { "inheritPhysicsSettings", physicsSettings.inheritPhysicsSettings }, + { "PhysicsWorld", physicsSettings.PhysicsWorld }, + { "PhysicsWorldGravityX", physicsSettings.PhysicsWorldGravityX }, + { "PhysicsWorldGravityY", physicsSettings.PhysicsWorldGravityY }, + { "PhysicsWorldPixToMetres", physicsSettings.PhysicsWorldPixToMetres } + }; + + json["parent"] = QJsonObject { + { "name", QFileInfo(parent).fileName() }, + { "path", QStringLiteral("folders/%1.yy").arg(parent) } + }; + + return json; +} + + +struct Context +{ + GMRoom room; + std::vector paths; std::unique_ptr renderer; ExportContext exportContext; @@ -354,37 +811,25 @@ struct Context } // namespace Yy template -static T optionalProperty(const Object *object, const QString &name, const T &def) +static void readProperty(const Object *object, const QString &name, T &out) { const QVariant var = object->resolvedProperty(name); - return var.isValid() ? var.value() : def; + if (var.isValid()) + out = var.value(); } template -static T takeProperty(Properties &properties, const QString &name, const T &def) +static T optionalProperty(const Object *object, const QString &name, const T &def) { - const QVariant var = properties.take(name); + const QVariant var = object->resolvedProperty(name); return var.isValid() ? var.value() : def; } template -static void writeProperty(JsonWriter &json, - const Object *object, - const QString &propertyName, - const char *memberName, - const T &def) -{ - const T value = optionalProperty(object, propertyName, def); - json.writeMember(memberName, value); -} - -template -static void writeProperty(JsonWriter &json, - const Object *object, - const char *name, - const T &def) +static T takeProperty(Properties &properties, const QString &name, const T &def) { - writeProperty(json, object, QString::fromLatin1(name), name, def); + const QVariant var = properties.take(name); + return var.isValid() ? var.value() : def; } static QStringList readTags(const Object *object) @@ -394,36 +839,6 @@ static QStringList readTags(const Object *object) return tagList; } -static void writeTags(JsonWriter &json, const QStringList &tags) -{ - json.writeMember("tags", QJsonArray::fromStringList(tags)); -} - -static void writeTags(JsonWriter &json, const Object *object) -{ - writeTags(json, readTags(object)); -} - -static void writeResourceProperties(JsonWriter &json, const GMResource &resource) -{ - json.writeMember("resourceVersion", resource.resourceVersion); - json.writeMember("name", resource.name); - writeTags(json, resource.tags); - json.writeMember("resourceType", resourceTypeStr(resource.resourceType)); -} - -static void writeId(JsonWriter &json, const char *member, const QString &id, const QString &scope) -{ - if (id.isEmpty()) { - json.writeMember(member, QJsonValue(QJsonValue::Null)); - } else { - json.writeStartObject(member); - json.writeMember("name", id); - json.writeMember("path", QStringLiteral("%1/%2/%2.yy").arg(scope, id)); - json.writeEndObject(); - } -} - static QString spriteId(const Object *object, const QUrl &imageUrl, Context &context) { // If the custom property "sprite" exist use it instead of crawling the file system @@ -434,15 +849,6 @@ static QString spriteId(const Object *object, const QUrl &imageUrl, Context &con return context.resourceId(imageUrl.path()); } -static unsigned colorToAbgr(const QColor &color) -{ - const QRgb rgba = color.rgba(); - return ((qAlpha(rgba) & 0xffu) << 24) | - ((qBlue(rgba) & 0xffu) << 16) | - ((qGreen(rgba) & 0xffu) << 8) | - (qRed(rgba) & 0xffu); -} - static QString toOverriddenPropertyValue(const QVariant &value, Context &context) { if (value.userType() == objectRefTypeId()) { @@ -469,208 +875,6 @@ static QString toOverriddenPropertyValue(const QVariant &value, Context &context } } -static void writeLayers(JsonWriter &json, const std::vector> &layers) -{ - json.writeStartArray("layers"); - - for (const std::unique_ptr &layer : layers) { - json.prepareNewLine(); - json.writeStartObject(); - - switch (layer->resourceType) { - case GMRAssetLayerType: { - auto &assetLayer = static_cast(*layer); - - json.writeStartArray("assets"); - - for (const auto &asset : assetLayer.assets) { - json.prepareNewLine(); - json.writeStartObject(); - const bool wasMinimize = json.minimize(); - json.setMinimize(true); - - writeId(json, "spriteId", asset.spriteId, QStringLiteral("sprites")); - - if (asset.resourceType == GMRSpriteGraphicType) { - json.writeMember("headPosition", asset.headPosition); - json.writeMember("rotation", asset.rotation); - json.writeMember("scaleX", asset.scaleX); - json.writeMember("scaleY", asset.scaleY); - json.writeMember("animationSpeed", asset.animationSpeed); - } else { - json.writeMember("w", asset.w); - json.writeMember("h", asset.h); - json.writeMember("u0", asset.u0); - json.writeMember("v0", asset.v0); - json.writeMember("u1", asset.u1); - json.writeMember("v1", asset.v1); - } - json.writeMember("colour", colorToAbgr(asset.colour)); - if (asset.inheritedItemId.isEmpty()) { - json.writeMember("inheritedItemId", QJsonValue(QJsonValue::Null)); - } else { - json.writeStartObject("inheritedItemId"); - json.writeMember("name", asset.inheritedItemId); - json.writeMember("path", asset.inheritedItemPath); - json.writeEndObject(); - } - json.writeMember("frozen", asset.frozen); - json.writeMember("ignore", asset.ignore); - json.writeMember("inheritItemSettings", asset.inheritItemSettings); - json.writeMember("x", asset.x); - json.writeMember("y", asset.y); - - writeResourceProperties(json, asset); - - json.writeEndObject(); - json.setMinimize(wasMinimize); - } - - json.writeEndArray(); // assets - break; - } - case GMRBackgroundLayerType: { - auto &backgroundLayer = static_cast(*layer); - - writeId(json, "spriteId", backgroundLayer.spriteId, QStringLiteral("sprites")); - - json.writeMember("colour", colorToAbgr(backgroundLayer.colour)); - json.writeMember("x", backgroundLayer.x); - json.writeMember("y", backgroundLayer.y); - json.writeMember("htiled", backgroundLayer.htiled); - json.writeMember("vtiled", backgroundLayer.vtiled); - json.writeMember("hspeed", backgroundLayer.hspeed); - json.writeMember("vspeed", backgroundLayer.vspeed); - json.writeMember("stretch", backgroundLayer.stretch); - json.writeMember("animationFPS", backgroundLayer.animationFPS); - json.writeMember("animationSpeedType", backgroundLayer.animationSpeedType); - json.writeMember("userdefinedAnimFPS", backgroundLayer.userdefinedAnimFPS); - break; - } - case GMRInstanceLayerType: { - auto &instanceLayer = static_cast(*layer); - json.writeStartArray("instances"); - - for (const auto &instance : instanceLayer.instances) { - json.prepareNewLine(); - json.writeStartObject(); - const bool wasMinimize = json.minimize(); - json.setMinimize(true); - - json.writeStartArray("properties"); - for (const GMOverriddenProperty &prop : instance.properties) { - json.writeStartObject(); - - json.writeStartObject("propertyId"); - json.writeMember("name", prop.propertyId); - json.writeMember("path", QStringLiteral("%1/%2/%2.yy").arg(QStringLiteral("objects"), prop.objectId)); - json.writeEndObject(); // propertyId - - writeId(json, "objectId", prop.objectId, "objects"); - - json.writeMember("value", prop.value); - - writeResourceProperties(json, prop); - - json.writeEndObject(); - } - json.writeEndArray(); // properties - - json.writeMember("isDnd", instance.isDnd); - - writeId(json, "objectId", instance.objectId, QStringLiteral("objects")); - - json.writeMember("inheritCode", instance.inheritCode); - json.writeMember("hasCreationCode", instance.hasCreationCode); - json.writeMember("colour", colorToAbgr(instance.colour)); - json.writeMember("rotation", instance.rotation); - json.writeMember("scaleX", instance.scaleX); - json.writeMember("scaleY", instance.scaleY); - json.writeMember("imageIndex", instance.imageIndex); - json.writeMember("imageSpeed", instance.imageSpeed); - if (instance.inheritedItemId.isEmpty()) { - json.writeMember("inheritedItemId", QJsonValue(QJsonValue::Null)); - } else { - json.writeStartObject("inheritedItemId"); - json.writeMember("name", instance.inheritedItemId); - json.writeMember("path", instance.inheritedItemPath); - json.writeEndObject(); - } - json.writeMember("frozen", instance.frozen); - json.writeMember("ignore", instance.ignore); - json.writeMember("inheritItemSettings", instance.inheritItemSettings); - json.writeMember("x", instance.x); - json.writeMember("y", instance.y); - - writeResourceProperties(json, instance); - - json.writeEndObject(); - json.setMinimize(wasMinimize); - } - - json.writeEndArray(); // instances - break; - } - case GMRPathLayerType: { - auto &pathLayer = static_cast(*layer); - - writeId(json, "pathId", pathLayer.pathId, QStringLiteral("paths")); - - json.writeMember("colour", colorToAbgr(pathLayer.colour)); - break; - } - case GMRTileLayerType: { - auto &tileLayer = static_cast(*layer); - - writeId(json, "tilesetId", tileLayer.tilesetId, QStringLiteral("tilesets")); - - json.writeMember("x", tileLayer.x); - json.writeMember("y", tileLayer.y); - - json.writeStartObject("tiles"); - json.writeMember("SerialiseWidth", tileLayer.SerialiseWidth); - json.writeMember("SerialiseHeight", tileLayer.SerialiseHeight); - json.writeStartArray("TileSerialiseData"); - - size_t index = 0; - - for (int y = 0; y < tileLayer.SerialiseHeight; ++y) { - json.prepareNewLine(); - - for (int x = 0; x < tileLayer.SerialiseWidth; ++x) { - json.writeValue(tileLayer.tiles.at(index)); - ++index; - } - } - - json.writeEndArray(); // TileSerialiseData - json.writeEndObject(); // tiles - break; - } - default: - break; - } - - json.writeMember("visible", layer->visible); - json.writeMember("depth", layer->depth); - json.writeMember("userdefinedDepth", layer->userdefinedDepth); - json.writeMember("inheritLayerDepth", layer->inheritLayerDepth); - json.writeMember("inheritLayerSettings", layer->inheritLayerSettings); - json.writeMember("gridX", layer->gridX); - json.writeMember("gridY", layer->gridY); - - writeLayers(json, layer->layers); - - json.writeMember("hierarchyFrozen", layer->hierarchyFrozen); - - writeResourceProperties(json, *layer); - - json.writeEndObject(); - } - - json.writeEndArray(); // layers -} - static void fillTileLayer(GMRTileLayer &gmrTileLayer, const TileLayer *tileLayer, const Tileset *tileset) { const auto layerOffset = tileLayer->totalOffset().toPoint(); @@ -825,7 +1029,7 @@ static void createAssetsFromTiles(std::vector &assets, g.colour = color; g.frozen = frozen; - g.ignore = optionalProperty(tileLayer, "ignore", g.ignore); + readProperty(tileLayer, "ignore", g.ignore); g.x = pos.x(); g.y = pos.y() - size.height(); @@ -926,15 +1130,16 @@ static std::unique_ptr processObjectGroup(const ObjectGroup *objectGro if (className == QLatin1String("view")) { // GM only has 8 views so drop anything more than that - if (context.views.size() > 7) { + if (context.room.views.size() > 7) { Tiled::ERROR(QLatin1String("YY plugin: Can't export more than 8 views."), Tiled::JumpToObject { mapObject }); continue; } - GMRView &view = context.views.emplace_back(); + // Last view in Object layer is the first view in the room + GMRView &view = context.room.views.emplace_back(); - view.inherit = optionalProperty(mapObject, "inherit", false); + readProperty(mapObject, "inherit", view.inherit); view.visible = mapObject->isVisible(); // Note: GM only supports ints for positioning // so views could be off if user doesn't align to whole number @@ -951,7 +1156,7 @@ static std::unique_ptr processObjectGroup(const ObjectGroup *objectGro view.vborder = qRound(optionalProperty(mapObject, "vborder", 32.0)); view.hspeed = qRound(optionalProperty(mapObject, "hspeed", -1.0)); view.vspeed = qRound(optionalProperty(mapObject, "vspeed", -1.0)); - view.objectId = optionalProperty(mapObject, "objectId", QString()); + readProperty(mapObject, "objectId", view.objectId); } else if (!className.isEmpty()) { @@ -1024,7 +1229,7 @@ static std::unique_ptr processObjectGroup(const ObjectGroup *objectGro instance.tags = readTags(mapObject); props.remove(QStringLiteral("tags")); - InstanceCreation &instanceCreation = context.instanceCreationOrder.emplace_back(); + InstanceCreation &instanceCreation = context.room.instanceCreationOrder.emplace_back(); instanceCreation.name = instance.name; instanceCreation.creationOrder = takeProperty(props, "creationOrder", 0); @@ -1073,8 +1278,8 @@ static std::unique_ptr processObjectGroup(const ObjectGroup *objectGro } // Allow overriding the scale using custom properties - g.scaleX = optionalProperty(mapObject, "scaleX", g.scaleX); - g.scaleY = optionalProperty(mapObject, "scaleY", g.scaleY); + readProperty(mapObject, "scaleX", g.scaleX); + readProperty(mapObject, "scaleY", g.scaleY); g.animationSpeed = optionalProperty(mapObject, "animationSpeed", 1.0); } else { @@ -1208,11 +1413,11 @@ static std::unique_ptr processImageLayer(const ImageLayer *imageLayer, gmrBackgroundLayer->htiled = optionalProperty(imageLayer, "htiled", imageLayer->repeatX()); gmrBackgroundLayer->vtiled = optionalProperty(imageLayer, "vtiled", imageLayer->repeatY()); - gmrBackgroundLayer->hspeed = optionalProperty(imageLayer, "hspeed", gmrBackgroundLayer->hspeed); - gmrBackgroundLayer->vspeed = optionalProperty(imageLayer, "vspeed", gmrBackgroundLayer->vspeed); - gmrBackgroundLayer->stretch = optionalProperty(imageLayer, "stretch", gmrBackgroundLayer->stretch); - gmrBackgroundLayer->animationFPS = optionalProperty(imageLayer, "animationFPS", gmrBackgroundLayer->animationFPS); - gmrBackgroundLayer->animationSpeedType = optionalProperty(imageLayer, "animationSpeedType", gmrBackgroundLayer->animationSpeedType); + readProperty(imageLayer, "hspeed", gmrBackgroundLayer->hspeed); + readProperty(imageLayer, "vspeed", gmrBackgroundLayer->vspeed); + readProperty(imageLayer, "stretch", gmrBackgroundLayer->stretch); + readProperty(imageLayer, "animationFPS", gmrBackgroundLayer->animationFPS); + readProperty(imageLayer, "animationSpeedType", gmrBackgroundLayer->animationSpeedType); gmrBackgroundLayer->userdefinedAnimFPS = imageLayer->resolvedProperty(QStringLiteral("animationFPS")).isValid(); return gmrBackgroundLayer; @@ -1339,146 +1544,72 @@ YyPlugin::YyPlugin() bool YyPlugin::write(const Map *map, const QString &fileName, Options options) { - // Not using SaveFile here, because GameMaker's reload functionality does - // not work correctly when the file is replaced. - QFile file(fileName); + SaveFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { mError = QCoreApplication::translate("File Errors", "Could not open file for writing."); return false; } - const QString baseName = QFileInfo(fileName).completeBaseName(); - - JsonWriter json(&file); - - json.setMinimize(options.testFlag(WriteMinimized)); - - json.writeStartObject(); - - writeProperty(json, map, "isDnd", false); - writeProperty(json, map, "volume", 1.0); - json.writeMember("parentRoom", QJsonValue(QJsonValue::Null)); // TODO: Provide a way to set this? - Context context; context.renderer = MapRenderer::create(map); + GMRoom &room = context.room; - std::vector> layers; - processLayers(layers, map->layers(), context); + room.name = QFileInfo(fileName).completeBaseName();; + room.roomPathInProject = QStringLiteral("rooms/%1/%1.yy").arg(room.name); + + readProperty(map, QStringLiteral("name"), room.name); + readProperty(map, QStringLiteral("isDnd"), room.isDnd); + readProperty(map, QStringLiteral("volume"), room.volume); + + processLayers(room.layers, map->layers(), context); // If a valid background color is set, create a background layer with this color. if (map->backgroundColor().isValid()) { auto gmrBackgroundLayer = std::make_unique(); gmrBackgroundLayer->name = QStringLiteral("Background_Color"); gmrBackgroundLayer->colour = map->backgroundColor(); - layers.push_back(std::move(gmrBackgroundLayer)); + room.layers.push_back(std::move(gmrBackgroundLayer)); } - autoAssignDepth(layers); - - const bool enableViews = !context.views.empty(); - - // Write out views - // Last view in Object layer is the first view in the room - json.writeStartArray("views"); - context.views.resize(8); // GameMaker always stores 8 views - for (const GMRView &view : std::as_const(context.views)) { - json.prepareNewLine(); - json.writeStartObject(); - const bool wasMinimize = json.minimize(); - json.setMinimize(true); - - json.writeMember("inherit", view.inherit); - json.writeMember("visible", view.visible); - json.writeMember("xview", view.xview); - json.writeMember("yview", view.yview); - json.writeMember("wview", view.wview); - json.writeMember("hview", view.hview); - json.writeMember("xport", view.xport); - json.writeMember("yport", view.yport); - json.writeMember("wport", view.wport); - json.writeMember("hport", view.hport); - json.writeMember("hborder", view.hborder); - json.writeMember("vborder", view.vborder); - json.writeMember("hspeed", view.hspeed); - json.writeMember("vspeed", view.vspeed); - - writeId(json, "objectId", view.objectId, "objects"); - - json.writeEndObject(); - json.setMinimize(wasMinimize); - } + autoAssignDepth(room.layers); - json.writeEndArray(); // views + room.viewSettings.enableViews = !room.views.empty(); + room.views.resize(8); // GameMaker always stores 8 views - writeLayers(json, layers); + std::stable_sort(room.instanceCreationOrder.begin(), + room.instanceCreationOrder.end()); - writeProperty(json, map, "inheritLayers", false); - writeProperty(json, map, "creationCodeFile", QString()); - writeProperty(json, map, "inheritCode", false); + readProperty(map, QStringLiteral("inheritLayers"), room.inheritLayers); + readProperty(map, QStringLiteral("creationCodeFile"), room.creationCodeFile); + readProperty(map, QStringLiteral("inheritCode"), room.inheritCode); + readProperty(map, QStringLiteral("inheritCreationOrder"), room.inheritCreationOrder); - const QString currentRoomPath = QStringLiteral("rooms/%1/%1.yy").arg(baseName); + readProperty(map, QStringLiteral("inheritRoomSettings"), room.roomSettings.inheritRoomSettings); + room.roomSettings.Width = map->tileWidth() * map->width(); + room.roomSettings.Height = map->tileHeight() * map->height(); + readProperty(map, QStringLiteral("persistent"), room.roomSettings.persistent); - std::stable_sort(context.instanceCreationOrder.begin(), - context.instanceCreationOrder.end()); + readProperty(map, QStringLiteral("inheritViewSettings"), room.viewSettings.inheritViewSettings); + readProperty(map, QStringLiteral("enableViews"), room.viewSettings.enableViews); + readProperty(map, QStringLiteral("clearViewBackground"), room.viewSettings.clearViewBackground); + readProperty(map, QStringLiteral("clearDisplayBuffer"), room.viewSettings.clearDisplayBuffer); - json.writeStartArray("instanceCreationOrder"); - for (const auto &creation : context.instanceCreationOrder) { - json.prepareNewLine(); - json.writeStartObject(); - const bool wasMinimize = json.minimize(); - json.setMinimize(true); - json.writeMember("name", creation.name); - json.writeMember("path", currentRoomPath); - json.writeEndObject(); - json.setMinimize(wasMinimize); - } - json.writeEndArray(); - - writeProperty(json, map, "inheritCreationOrder", false); - json.writeMember("sequenceId", QJsonValue(QJsonValue::Null)); - - const int mapPixelWidth = map->tileWidth() * map->width(); - const int mapPixelHeight = map->tileHeight() * map->height(); - - json.writeStartObject("roomSettings"); - writeProperty(json, map, "inheritRoomSettings", false); - json.writeMember("Width", mapPixelWidth); - json.writeMember("Height", mapPixelHeight); - writeProperty(json, map, "persistent", false); - json.writeEndObject(); - - json.writeStartObject("viewSettings"); - writeProperty(json, map, "inheritViewSettings", false); - writeProperty(json, map, "enableViews", enableViews); - writeProperty(json, map, "clearViewBackground", false); - writeProperty(json, map, "clearDisplayBuffer", true); - json.writeEndObject(); - - json.writeStartObject("physicsSettings"); - writeProperty(json, map, "inheritPhysicsSettings", false); - writeProperty(json, map, "PhysicsWorld", false); - writeProperty(json, map, "PhysicsWorldGravityX", 0.0); - writeProperty(json, map, "PhysicsWorldGravityY", 10.0); - writeProperty(json, map, "PhysicsWorldPixToMetres", 0.1); - json.writeEndObject(); - - json.writeStartObject("parent"); - const QString parent = optionalProperty(map, "parent", QStringLiteral("Rooms")); - json.writeMember("name", QFileInfo(parent).fileName()); - json.writeMember("path", QStringLiteral("folders/%1.yy").arg(parent)); - json.writeEndObject(); - - writeProperty(json, map, "resourceVersion", QString("1.0")); - writeProperty(json, map, "name", baseName); - writeTags(json, map); - json.writeMember("resourceType", "GMRoom"); - - json.writeEndObject(); - json.writeEndDocument(); + readProperty(map, QStringLiteral("inheritPhysicsSettings"), room.physicsSettings.inheritPhysicsSettings); + readProperty(map, QStringLiteral("PhysicsWorld"), room.physicsSettings.PhysicsWorld); + readProperty(map, QStringLiteral("PhysicsWorldGravityX"), room.physicsSettings.PhysicsWorldGravityX); + readProperty(map, QStringLiteral("PhysicsWorldGravityY"), room.physicsSettings.PhysicsWorldGravityY); + readProperty(map, QStringLiteral("PhysicsWorldPixToMetres"), room.physicsSettings.PhysicsWorldPixToMetres); - file.flush(); + readProperty(map, QStringLiteral("parent"), room.parent); + + room.tags = readTags(map); + + JsonWriter json(file.device()); + json.setMinimize(options.testFlag(WriteMinimized)); + json.writeValue(room.toJson()); + json.writeEndDocument(); - if (file.error() != QFileDevice::NoError) { + if (!file.commit()) { mError = file.errorString(); return false; }