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