Skip to content

Commit

Permalink
YY plugin: Adjusted to changes in GameMaker file format (#4143)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bjorn authored Jan 17, 2025
1 parent 9591005 commit cc77ce6
Show file tree
Hide file tree
Showing 5 changed files with 592 additions and 432 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
20 changes: 10 additions & 10 deletions docs/manual/export-yy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

<div class="new new-prev">Since Tiled 1.5</div>

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.
Expand All @@ -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:

Expand Down
73 changes: 50 additions & 23 deletions src/plugins/yy/jsonwriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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()
Expand All @@ -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();
}
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions src/plugins/yy/jsonwriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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; }

Expand All @@ -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);
Expand All @@ -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)
Expand Down
Loading

0 comments on commit cc77ce6

Please sign in to comment.