1 /*
2  * GMX Tiled Plugin
3  * Copyright 2016, Jones Blunt <mrjonesblunt@gmail.com>
4  * Copyright 2016-2020, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
5  *
6  * This file is part of Tiled.
7  *
8  * This program is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU General Public License as published by the Free
10  * Software Foundation; either version 2 of the License, or (at your option)
11  * any later version.
12  *
13  * This program is distributed in the hope that it will be useful, but WITHOUT
14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16  * more details.
17  *
18  * You should have received a copy of the GNU General Public License along with
19  * this program. If not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include "gmxplugin.h"
23 
24 #include "logginginterface.h"
25 #include "map.h"
26 #include "mapobject.h"
27 #include "objectgroup.h"
28 #include "savefile.h"
29 #include "tile.h"
30 #include "tilelayer.h"
31 
32 #include <QCoreApplication>
33 #include <QDir>
34 #include <QFileInfo>
35 #include <QRegularExpression>
36 #include <QXmlStreamWriter>
37 
38 #include "qtcompat_p.h"
39 
40 using namespace Tiled;
41 using namespace Gmx;
42 
43 template <typename T>
optionalProperty(const Object * object,const QString & name,const T & def)44 static T optionalProperty(const Object *object, const QString &name, const T &def)
45 {
46     const QVariant var = object->resolvedProperty(name);
47     return var.isValid() ? var.value<T>() : def;
48 }
49 
50 template <typename T>
toString(T number)51 static QString toString(T number)
52 {
53     return QString::number(number);
54 }
55 
toString(bool b)56 static QString toString(bool b)
57 {
58     return QString::number(b ? -1 : 0);
59 }
60 
toString(const QString & string)61 static QString toString(const QString &string)
62 {
63     return string;
64 }
65 
66 template <typename T>
writeProperty(QXmlStreamWriter & writer,const Object * object,const QString & name,const T & def)67 static void writeProperty(QXmlStreamWriter &writer,
68                           const Object *object,
69                           const QString &name,
70                           const T &def)
71 {
72     const T value = optionalProperty(object, name, def);
73     writer.writeTextElement(name, toString(value));
74 }
75 
sanitizeName(QString name)76 static QString sanitizeName(QString name)
77 {
78     static const QRegularExpression regexp(QLatin1String("[^a-zA-Z0-9]"));
79     return name.replace(regexp, QStringLiteral("_"));
80 }
81 
checkIfViewsDefined(const Map * map)82 static bool checkIfViewsDefined(const Map *map)
83 {
84     LayerIterator iterator(map);
85     while (const Layer *layer = iterator.next()) {
86 
87         if (layer->layerType() != Layer::ObjectGroupType)
88             continue;
89 
90         const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);
91 
92         for (const MapObject *object : objectLayer->objects()) {
93             const QString type = object->effectiveType();
94             if (type == "view")
95                 return true;
96         }
97     }
98 
99     return false;
100 }
101 
GmxPlugin()102 GmxPlugin::GmxPlugin()
103 {
104 }
105 
write(const Map * map,const QString & fileName,Options options)106 bool GmxPlugin::write(const Map *map, const QString &fileName, Options options)
107 {
108     SaveFile file(fileName);
109     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
110         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
111         return false;
112     }
113 
114     QXmlStreamWriter stream(file.device());
115 
116     stream.setAutoFormatting(!options.testFlag(WriteMinimized));
117     stream.setAutoFormattingIndent(2);
118 
119     stream.writeComment("This Document is generated by Tiled, if you edit it by hand then you do so at your own risk!");
120 
121     stream.writeStartElement("room");
122 
123     int mapPixelWidth = map->tileWidth() * map->width();
124     int mapPixelHeight = map->tileHeight() * map->height();
125 
126     stream.writeTextElement("width", QString::number(mapPixelWidth));
127     stream.writeTextElement("height", QString::number(mapPixelHeight));
128     stream.writeTextElement("vsnap", QString::number(map->tileHeight()));
129     stream.writeTextElement("hsnap", QString::number(map->tileWidth()));
130     stream.writeTextElement("isometric", toString(map->orientation() == Map::Orientation::Isometric));
131     writeProperty(stream, map, "speed", 30);
132     writeProperty(stream, map, "persistent", false);
133     writeProperty(stream, map, "clearViewBackground", false);
134     writeProperty(stream, map, "clearDisplayBuffer", true);
135     writeProperty(stream, map, "code", QString());
136 
137     // Check if views are defined
138     bool enableViews = checkIfViewsDefined(map);
139     writeProperty(stream, map, "enableViews", enableViews);
140 
141     // Write out views
142     // Last view in Object layer is the first view in the room
143     if (enableViews) {
144         stream.writeStartElement("views");
145         int viewCount = 0;
146         for (const Layer *layer : map->objectGroups()) {
147             const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);
148 
149             for (const MapObject *object : objectLayer->objects()) {
150                 const QString type = object->effectiveType();
151                 if (type != "view")
152                     continue;
153 
154                 // GM only has 8 views so drop anything more than that
155                 if (viewCount > 7) {
156                     Tiled::ERROR(QLatin1String("GMX plugin: Can't export more than 8 views."),
157                                  Tiled::JumpToObject { object });
158                     break;
159                 }
160 
161                 viewCount++;
162                 stream.writeStartElement("view");
163 
164                 stream.writeAttribute("visible", toString(object->isVisible()));
165                 stream.writeAttribute("objName", QString(optionalProperty(object, "objName", QString())));
166                 QPointF pos = object->position();
167                 // Note: GM only supports ints for positioning
168                 // so views could be off if user doesn't align to whole number
169                 stream.writeAttribute("xview", QString::number(qRound(pos.x())));
170                 stream.writeAttribute("yview", QString::number(qRound(pos.y())));
171                 stream.writeAttribute("wview", QString::number(qRound(object->width())));
172                 stream.writeAttribute("hview", QString::number(qRound(object->height())));
173                 // Round these incase user adds properties as floats and not ints
174                 stream.writeAttribute("xport", QString::number(qRound(optionalProperty(object, "xport", 0.0))));
175                 stream.writeAttribute("yport", QString::number(qRound(optionalProperty(object, "yport", 0.0))));
176                 stream.writeAttribute("wport", QString::number(qRound(optionalProperty(object, "wport", 1024.0))));
177                 stream.writeAttribute("hport", QString::number(qRound(optionalProperty(object, "hport", 768.0))));
178                 stream.writeAttribute("hborder", QString::number(qRound(optionalProperty(object, "hborder", 32.0))));
179                 stream.writeAttribute("vborder", QString::number(qRound(optionalProperty(object, "vborder", 32.0))));
180                 stream.writeAttribute("hspeed", QString::number(qRound(optionalProperty(object, "hspeed", -1.0))));
181                 stream.writeAttribute("vspeed", QString::number(qRound(optionalProperty(object, "vspeed", -1.0))));
182 
183                 stream.writeEndElement();
184             }
185         }
186 
187         stream.writeEndElement();
188     }
189 
190     stream.writeStartElement("instances");
191 
192     QSet<QString> usedNames;
193     int layerCount = 0;
194 
195     // Write out object instances
196     LayerIterator iterator(map);
197     while (const Layer *layer = iterator.next()) {
198         ++layerCount;
199 
200         if (layer->layerType() != Layer::ObjectGroupType)
201             continue;
202 
203         const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);
204         const bool locked = !layer->isUnlocked();
205         auto color = layer->effectiveTintColor();
206         color.setAlphaF(color.alphaF() * layer->effectiveOpacity());
207         const auto colorString = QString::number(color.rgba());
208 
209         for (const MapObject *object : objectLayer->objects()) {
210             const QString type = object->effectiveType();
211             if (type.isEmpty())
212                 continue;
213             if (type == "view")
214                 continue;
215 
216             stream.writeStartElement("instance");
217 
218             // The type is used to refer to the name of the object
219             stream.writeAttribute("objName", sanitizeName(type));
220 
221             qreal scaleX = 1;
222             qreal scaleY = 1;
223 
224             QPointF origin(optionalProperty(object, "originX", 0.0),
225                            optionalProperty(object, "originY", 0.0));
226 
227             if (!object->cell().isEmpty()) {
228                 // For tile objects we can support scaling and flipping, though
229                 // flipping in combination with rotation doesn't work in GameMaker.
230                 if (auto tile = object->cell().tile()) {
231                     const QSize tileSize = tile->size();
232                     scaleX = object->width() / tileSize.width();
233                     scaleY = object->height() / tileSize.height();
234 
235                     if (object->cell().flippedHorizontally()) {
236                         scaleX *= -1;
237                         origin += QPointF(object->width() - 2 * origin.x(), 0);
238                     }
239                     if (object->cell().flippedVertically()) {
240                         scaleY *= -1;
241                         origin += QPointF(0, object->height() - 2 * origin.y());
242                     }
243                 }
244 
245                 // Tile objects don't necessarily have top-left origin in Tiled,
246                 // so the position needs to be translated for top-left origin in
247                 // GameMaker, taking into account the rotation.
248                 origin -= alignmentOffset(object->bounds(), object->alignment());
249             }
250 
251             // Allow overriding the scale using custom properties
252             scaleX = optionalProperty(object, "scaleX", scaleX);
253             scaleY = optionalProperty(object, "scaleY", scaleY);
254 
255             // Adjust the position based on the origin
256             QTransform transform;
257             transform.rotate(object->rotation());
258             const QPointF pos = object->position() + transform.map(origin);
259 
260             stream.writeAttribute("x", QString::number(qRound(pos.x())));
261             stream.writeAttribute("y", QString::number(qRound(pos.y())));
262 
263             // Include object ID in the name when necessary because duplicates are not allowed
264             if (object->name().isEmpty()) {
265                 stream.writeAttribute("name", QStringLiteral("inst_%1").arg(object->id()));
266             } else {
267                 QString name = sanitizeName(object->name());
268 
269                 while (usedNames.contains(name))
270                     name += QStringLiteral("_%1").arg(object->id());
271 
272                 usedNames.insert(name);
273                 stream.writeAttribute("name", name);
274             }
275 
276             stream.writeAttribute("locked", toString(locked));
277             stream.writeAttribute("code", optionalProperty(object, "code", QString()));
278             stream.writeAttribute("scaleX", QString::number(scaleX));
279             stream.writeAttribute("scaleY", QString::number(scaleY));
280             stream.writeAttribute("colour", colorString);
281             stream.writeAttribute("rotation", QString::number(-object->rotation()));
282 
283             stream.writeEndElement();
284         }
285     }
286 
287     stream.writeEndElement();
288 
289 
290     stream.writeStartElement("tiles");
291 
292     uint tileId = 0u;
293 
294     // Write out tile instances
295     iterator.toFront();
296     while (const Layer *layer = iterator.next()) {
297         --layerCount;
298         QString depth = QString::number(optionalProperty(layer, QLatin1String("depth"),
299                                                          layerCount + 1000000));
300         const bool locked = !layer->isUnlocked();
301         auto color = layer->effectiveTintColor();
302         color.setAlphaF(color.alphaF() * layer->effectiveOpacity());
303         const auto colorString = QString::number(color.rgba());
304 
305         switch (layer->layerType()) {
306         case Layer::TileLayerType: {
307             auto tileLayer = static_cast<const TileLayer*>(layer);
308 
309             for (int y = 0; y < tileLayer->height(); ++y) {
310                 for (int x = 0; x < tileLayer->width(); ++x) {
311                     const Cell &cell = tileLayer->cellAt(x, y);
312                     if (const Tile *tile = cell.tile()) {
313                         const Tileset *tileset = tile->tileset();
314 
315                         stream.writeStartElement("tile");
316 
317                         int pixelX = x * map->tileWidth();
318                         int pixelY = y * map->tileHeight();
319                         qreal scaleX = 1;
320                         qreal scaleY = 1;
321 
322                         if (cell.flippedHorizontally()) {
323                             scaleX = -1;
324                             pixelX += tile->width();
325                         }
326                         if (cell.flippedVertically()) {
327                             scaleY = -1;
328                             pixelY += tile->height();
329                         }
330                         if (cell.flippedAntiDiagonally()) {
331                             Tiled::WARNING(QStringLiteral("GMX plugin: Rotated tiles are not supported."),
332                                            Tiled::JumpToTile { tileLayer->map(), QPoint(x, y), tileLayer });
333                         }
334 
335                         QString bgName;
336                         int xo = 0;
337                         int yo = 0;
338 
339                         if (tileset->isCollection()) {
340                             bgName = QFileInfo(tile->imageSource().path()).baseName();
341                         } else {
342                             bgName = tileset->name();
343 
344                             const int xInTilesetGrid = tile->id() % tileset->columnCount();
345                             const int yInTilesetGrid = static_cast<int>(tile->id() / tileset->columnCount());
346 
347                             xo = tileset->margin() + (tileset->tileSpacing() + tileset->tileWidth()) * xInTilesetGrid;
348                             yo = tileset->margin() + (tileset->tileSpacing() + tileset->tileHeight()) * yInTilesetGrid;
349                         }
350 
351                         stream.writeAttribute("bgName", bgName);
352                         stream.writeAttribute("x", QString::number(pixelX));
353                         stream.writeAttribute("y", QString::number(pixelY));
354                         stream.writeAttribute("w", QString::number(tile->width()));
355                         stream.writeAttribute("h", QString::number(tile->height()));
356 
357                         stream.writeAttribute("xo", QString::number(xo));
358                         stream.writeAttribute("yo", QString::number(yo));
359 
360                         stream.writeAttribute("id", QString::number(++tileId));
361                         stream.writeAttribute("depth", depth);
362                         stream.writeAttribute("locked", toString(locked));
363                         stream.writeAttribute("colour", colorString);
364 
365                         stream.writeAttribute("scaleX", QString::number(scaleX));
366                         stream.writeAttribute("scaleY", QString::number(scaleY));
367 
368                         stream.writeEndElement();
369                     }
370                 }
371             }
372             break;
373         }
374 
375         case Layer::ObjectGroupType: {
376             auto objectGroup = static_cast<const ObjectGroup*>(layer);
377             auto objects = objectGroup->objects();
378 
379             // Make sure the objects export in the rendering order
380             if (objectGroup->drawOrder() == ObjectGroup::TopDownOrder) {
381                 std::stable_sort(objects.begin(), objects.end(),
382                                  [](const MapObject *a, const MapObject *b) { return a->y() < b->y(); });
383             }
384 
385             for (const MapObject *object : qAsConst(objects)) {
386                 // Objects with types are already exported as instances
387                 if (!object->effectiveType().isEmpty())
388                     continue;
389 
390                 // Non-typed tile objects are exported as tiles. Rotation is
391                 // not supported here, but scaling is.
392                 if (const Tile *tile = object->cell().tile()) {
393                     const Tileset *tileset = tile->tileset();
394 
395                     const QSize tileSize = tile->size();
396                     qreal scaleX = object->width() / tileSize.width();
397                     qreal scaleY = object->height() / tileSize.height();
398                     qreal x = object->x();
399                     qreal y = object->y() - object->height();
400 
401                     if (object->cell().flippedHorizontally()) {
402                         scaleX *= -1;
403                         x += object->width();
404                     }
405                     if (object->cell().flippedVertically()) {
406                         scaleY *= -1;
407                         y += object->height();
408                     }
409 
410                     stream.writeStartElement("tile");
411 
412                     QString bgName;
413                     int xo = 0;
414                     int yo = 0;
415 
416                     if (tileset->isCollection()) {
417                         bgName = QFileInfo(tile->imageSource().path()).baseName();
418                     } else {
419                         bgName = tileset->name();
420 
421                         int xInTilesetGrid = tile->id() % tileset->columnCount();
422                         int yInTilesetGrid = tile->id() / tileset->columnCount();
423 
424                         xo = tileset->margin() + (tileset->tileSpacing() + tileset->tileWidth()) * xInTilesetGrid;
425                         yo = tileset->margin() + (tileset->tileSpacing() + tileset->tileHeight()) * yInTilesetGrid;
426                     }
427 
428                     stream.writeAttribute("bgName", bgName);
429                     stream.writeAttribute("x", QString::number(qRound(x)));
430                     stream.writeAttribute("y", QString::number(qRound(y)));
431                     stream.writeAttribute("w", QString::number(tile->width()));
432                     stream.writeAttribute("h", QString::number(tile->height()));
433 
434                     stream.writeAttribute("xo", QString::number(xo));
435                     stream.writeAttribute("yo", QString::number(yo));
436 
437                     stream.writeAttribute("id", QString::number(++tileId));
438                     stream.writeAttribute("depth", depth);
439                     stream.writeAttribute("locked", toString(locked));
440                     stream.writeAttribute("colour", colorString);
441 
442                     stream.writeAttribute("scaleX", QString::number(scaleX));
443                     stream.writeAttribute("scaleY", QString::number(scaleY));
444 
445                     stream.writeEndElement();
446                 } else {
447                     Tiled::WARNING(QStringLiteral("GMX plugin: Ignoring non-tile object %1 without type.").arg(object->id()),
448                                    Tiled::JumpToObject { object });
449                 }
450             }
451             break;
452         }
453 
454         case Layer::ImageLayerType:
455             // todo: maybe export as backgrounds?
456             Tiled::WARNING(QStringLiteral("GMX plugin: Ignoring image layer \"%1\" (not currently supported).").arg(layer->name()),
457                            Tiled::SelectLayer { layer });
458             break;
459 
460         case Layer::GroupLayerType:
461             // Recursion handled by LayerIterator
462             break;
463         }
464     }
465 
466     stream.writeEndElement();
467 
468     writeProperty(stream, map, "PhysicsWorld", false);
469     writeProperty(stream, map, "PhysicsWorldTop", 0);
470     writeProperty(stream, map, "PhysicsWorldLeft", 0);
471     writeProperty(stream, map, "PhysicsWorldRight", mapPixelWidth);
472     writeProperty(stream, map, "PhysicsWorldBottom", mapPixelHeight);
473     writeProperty(stream, map, "PhysicsWorldGravityX", 0.0);
474     writeProperty(stream, map, "PhysicsWorldGravityY", 10.0);
475     writeProperty(stream, map, "PhysicsWorldPixToMeters", 0.1);
476 
477     stream.writeEndDocument();
478 
479     if (!file.commit()) {
480         mError = file.errorString();
481         return false;
482     }
483 
484     return true;
485 }
486 
errorString() const487 QString GmxPlugin::errorString() const
488 {
489     return mError;
490 }
491 
nameFilter() const492 QString GmxPlugin::nameFilter() const
493 {
494     return tr("GameMaker room files (*.room.gmx)");
495 }
496 
shortName() const497 QString GmxPlugin::shortName() const
498 {
499     return QStringLiteral("gmx");
500 }
501