1 /*
2  * DefoldCollection Tiled Plugin
3  * Copyright 2019, CodeSpartan
4  * Based on Defold Tiled Plugin by Nikita Razdobreev and Thorbjørn Lindeijer
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 "defoldcollectionplugin.h"
23 
24 #include "layer.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 #include "grouplayer.h"
32 
33 #include <QCoreApplication>
34 #include <QDir>
35 #include <QFileInfo>
36 #include <QTextStream>
37 
38 #include <cmath>
39 
40 namespace DefoldCollection {
41 
42 static const char cellTemplate[] =
43 R"(  cell {
44     x: {{x}}
45     y: {{y}}
46     tile: {{tile}}
47     h_flip: {{h_flip}}
48     v_flip: {{v_flip}}
49   }
50 )";
51 
52 static const char layerTemplate[] =
53 R"(layers {
54   id: "{{id}}"
55   z: {{z}}
56   is_visible: {{is_visible}}
57 {{cells}}}
58 )";
59 
60 static const char tileMapTemplate[] =
61 R"(tile_set: "{{tile_set}}"
62 {{layers}}
63 material: "{{material}}"
64 blend_mode: {{blend_mode}}
65 )";
66 
67 static const char collectionTemplate[] =
68 R"(name: "default"
69 scale_along_z: 0
70 {{embedded-instances}}
71 
72 )";
73 
74 static const char componentTemplate[] =
75 R"(  "components {\n"
76   "  id: \"{{tilemap_name}}\"\n"
77   "  component: \"{{tilemap_rel_path}}\"\n"
78   "  position {\n"
79   "    x: 0.0\n"
80   "    y: 0.0\n"
81   "    z: 0.0\n"
82   "  }\n"
83   "  rotation {\n"
84   "    x: 0.0\n"
85   "    y: 0.0\n"
86   "    z: 0.0\n"
87   "    w: 1.0\n"
88   "  }\n"
89   "}\n"
90 )";
91 
92 static const char childTemplate[] =
93 R"(  children: "{{child-name}}"
94 )";
95 
96 static const char emdeddedInstanceTemplate[] =
97 R"(embedded_instances {
98   id: "{{instance-name}}"
99 {{children}}  data: {{components}}
100   ""
101   position {
102     x: {{pos-x}}
103     y: {{pos-y}}
104     z: 0.0
105   }
106   rotation {
107     x: 0.0
108     y: 0.0
109     z: 0.0
110     w: 1.0
111   }
112   scale3 {
113     x: 1.0
114     y: 1.0
115     z: 1.0
116   }
117 }
118 )";
119 
replaceTags(QString context,const QVariantHash & map)120 static QString replaceTags(QString context, const QVariantHash &map)
121 {
122     QHashIterator<QString,QVariant> it{map};
123     while (it.hasNext()) {
124         it.next();
125         context.replace(QLatin1String("{{") + it.key() + QLatin1String("}}"),
126                         it.value().toString());
127     }
128     return context;
129 }
130 
DefoldCollectionPlugin()131 DefoldCollectionPlugin::DefoldCollectionPlugin()
132 {
133 }
134 
135 QString DefoldCollectionPlugin::nameFilter() const
136 {
137     return tr("Defold collection (*.collection)");
138 }
139 
140 QString DefoldCollectionPlugin::shortName() const
141 {
142     return QStringLiteral("defoldcollection");
143 }
144 
145 QString DefoldCollectionPlugin::errorString() const
146 {
147     return mError;
148 }
149 
150 /*
151  * Returns a new filepath relative to the root of the Defold project if we're in one.
152  * Determines the root of the project by looking for a file called "game.project".
153  * If no such file is found by going up the hierarchy, return filename from the \a filePath.
154  */
155 static QString tilesetRelativePath(const QString &filePath)
156 {
157     QString gameproject = "game.project";
158     QFileInfo fi(filePath);
159     QDir dir = fi.dir();
160 
161     while (dir.exists() && !dir.isRoot()) {
162         if (dir.exists(gameproject)) {
163             // return relative path
164             return filePath.right(filePath.length() - dir.path().length());
165         } else if (!dir.cdUp()) {
166             break;
167         }
168     }
169     return fi.fileName();
170 }
171 
172 /*
173  * Returns z-Index for a layer, depending on its order in the map
174  */
175 static float zIndexForLayer(const Tiled::Map &map, const Tiled::Layer &inLayer, bool isTopLayer)
176 {
177     if (isTopLayer) {
178         int topLayerOrder = 0;
179         for (auto layer : map.layers()) {
180             if (layer->layerType() != Tiled::Layer::GroupLayerType && layer->layerType() != Tiled::Layer::TileLayerType)
181                 continue;
182             if (&inLayer == layer)
183                 return qBound(0, topLayerOrder, 9999) * 0.0001f;
184             topLayerOrder++;
185         }
186     } else if (inLayer.parentLayer()) {
187         float zIndex = zIndexForLayer(map, *inLayer.parentLayer(), true);
188         int subLayerOrder = 0;
189         for (auto subLayer : inLayer.parentLayer()->layers()) {
190             if (subLayer == &inLayer) {
191                 zIndex += qBound(0, subLayerOrder, 9999) * 0.00000001f;
192                 return zIndex;
193             }
194             subLayerOrder++;
195         }
196     }
197     return 0;
198 }
199 
200 /*
201  * Writes a .collection file, as well as multiple .tilemap files required by this collection
202  */
203 bool DefoldCollectionPlugin::write(const Tiled::Map *map, const QString &collectionFile, Options options)
204 {
205     Q_UNUSED(options)
206 
207     QFileInfo fi(collectionFile);
208     QString outputFilePath = fi.filePath();
209     QString outputFileName = fi.fileName();
210     QString mapName = fi.completeBaseName();
211 
212     QVariantHash collectionHash;
213     QString embeddedInstances;
214 
215     QVariantHash mainEmbeddedInstanceHash;
216     mainEmbeddedInstanceHash["instance-name"] = "tilemaps";
217     mainEmbeddedInstanceHash["pos-x"] = map->property(QLatin1String("x-offset")).toInt();
218     mainEmbeddedInstanceHash["pos-y"] = map->property(QLatin1String("y-offset")).toInt();
219     QString topLevelChildren;
220     QString topLevelComponents;
221 
222     QString tilesetFileDir = outputFilePath;
223     tilesetFileDir.chop(outputFileName.length());
224 
225     // dealing with top-level tile layers here only
226     // create a tilemap file for each tileset this map uses, and for each of them create a "component" in the main embedded instance
227     for (auto &tileset : map->tilesets()) {
228         QString tilemapFilePath = tilesetFileDir;
229         tilemapFilePath.append(mapName + "-" + tileset->name() + ".tilemap");
230 
231         QVariantHash componentHash;
232         componentHash["tilemap_name"] = mapName + "-" + tileset->name();
233         componentHash["tilemap_rel_path"] = tilesetRelativePath(tilemapFilePath);
234 
235         QVariantHash tileMapHash;
236 
237         bool tilemapHasCells = false;
238 
239         int componentCells = 0;
240         QString layers;
241         for (auto layer : map->layers()) {
242             if (layer->layerType() != Tiled::Layer::TileLayerType)
243                 continue;
244             auto tileLayer = static_cast<Tiled::TileLayer*>(layer);
245 
246             QVariantHash layerHash;
247             layerHash["id"] = tileLayer->name();
248             layerHash["z"] = zIndexForLayer(*map, *tileLayer, true);
249             layerHash["is_visible"] = tileLayer->isVisible() ? 1 : 0;
250             QString cells;
251 
252             for (int x = 0; x < tileLayer->width(); ++x) {
253                 for (int y = 0; y < tileLayer->height(); ++y) {
254                     const Tiled::Cell &cell = tileLayer->cellAt(x, y);
255                     if (cell.isEmpty())
256                         continue;
257                     if (cell.tileset() != tileset) // skip cell if it doesn't belong to current tileset
258                         continue;
259 
260                     tilemapHasCells = true;
261                     componentCells++;
262                     QVariantHash cellHash;
263                     cellHash["x"] = x;
264                     cellHash["y"] = tileLayer->height() - y - 1;
265                     cellHash["tile"] = cell.tileId();
266                     cellHash["h_flip"] = cell.flippedHorizontally() ? 1 : 0;
267                     cellHash["v_flip"] = cell.flippedVertically() ? 1 : 0;
268                     cells.append(replaceTags(QLatin1String(cellTemplate), cellHash));
269 
270                     // Create a component for this embedded instance only when the first cell of this component is found.
271                     // If 0 cells are found, this component is not necessary.
272                     // If more than 1 cells are found, recreating it would be redundant.
273                     if (componentCells == 1)
274                         topLevelComponents.append(replaceTags(QLatin1String(componentTemplate), componentHash));
275                 }
276             }
277             layerHash["cells"] = cells;
278 
279             // only add this layer to the .tilemap if it has any cells
280             if (!cells.isEmpty())
281                 layers.append(replaceTags(QLatin1String(layerTemplate), layerHash));
282         }
283         // make a check that this tilemap has cells at all, or no .tilesource file is necessary
284         if (tilemapHasCells) {
285             tileMapHash["layers"] = layers;
286             tileMapHash["material"] = "/builtins/materials/tile_map.material";
287             tileMapHash["blend_mode"] = "BLEND_MODE_ALPHA";
288             // Below, we input a value that's not necessarily correct in Defold, but it lets the user know what tilesource to link this tilemap with manually.
289             // However, if the user keeps all tilesources in /tilesources/ and the name of the tilesource corresponds with the name of the tileset in Defold,
290             // the value will be automatically correct.
291             tileMapHash["tile_set"] = "/tilesources/" + tileset->name() + ".tilesource";
292 
293             QString result = replaceTags(QLatin1String(tileMapTemplate), tileMapHash);
294             Tiled::SaveFile mapFile(tilemapFilePath);
295             if (!mapFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
296                 mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
297                 return false;
298             }
299             QTextStream stream(mapFile.device());
300             stream << result;
301 
302             if (mapFile.error() != QFileDevice::NoError) {
303                 mError = mapFile.errorString();
304                 return false;
305             }
306 
307             if (!mapFile.commit()) {
308                 mError = mapFile.errorString();
309                 return false;
310             }
311         }
312     }
313 
314     // For each Group Layer, create a "GameObject" parented to the "tilemaps" GO
315     // and create tilemaps for layers (as components of this GO)
316     for (auto layer : map->layers()) {
317         if (layer->layerType() != Tiled::Layer::GroupLayerType)
318             continue;
319         auto groupLayer = static_cast<Tiled::GroupLayer*>(layer);
320 
321         QVariantHash childHash;
322         childHash["child-name"] = layer->name();
323         topLevelChildren.append(replaceTags(QLatin1String(childTemplate), childHash));
324 
325         QVariantHash emdeddedInstanceHash;
326         emdeddedInstanceHash["instance-name"] = layer->name();
327         emdeddedInstanceHash["pos-x"] = 0;
328         emdeddedInstanceHash["pos-y"] = 0;
329         emdeddedInstanceHash["children"] = "";
330 
331         QString components;
332 
333         // write as many tilemaps as there are tilesets per group layer
334         for (auto &tileset : map->tilesets()) {
335             QString tilemapFilePath = tilesetFileDir;
336             tilemapFilePath.append(mapName + "-" + layer->name() + "-" + tileset->name() + ".tilemap");
337 
338             QVariantHash tileMapHash;
339             QString layers;
340 
341             int componentCells = 0;
342             for (auto subLayer : groupLayer->layers()) {
343                 auto tileLayer = subLayer->asTileLayer();
344                 if (!tileLayer)
345                     continue;
346 
347                 QVariantHash layerHash;
348                 layerHash["id"] = tileLayer->name();
349                 layerHash["z"] = zIndexForLayer(*map, *subLayer, false);
350 
351                 layerHash["is_visible"] = layer->isVisible() ? 1 : 0;
352                 QString cells;
353 
354                 for (int x = 0; x < tileLayer->width(); ++x) {
355                     for (int y = 0; y < tileLayer->height(); ++y) {
356                         const Tiled::Cell &cell = tileLayer->cellAt(x, y);
357                         if (cell.isEmpty() || cell.tileset() != tileset) // skip cell if it doesn't belong to current tileset
358                             continue;
359 
360                         QVariantHash cellHash;
361                         cellHash["x"] = x;
362                         cellHash["y"] = tileLayer->height() - y - 1;
363                         cellHash["tile"] = cell.tileId();
364                         cellHash["h_flip"] = cell.flippedHorizontally() ? 1 : 0;
365                         cellHash["v_flip"] = cell.flippedVertically() ? 1 : 0;
366                         cells.append(replaceTags(QLatin1String(cellTemplate), cellHash));
367                         componentCells++;
368 
369                         // Create a component for this embedded instance only when the first cell of this component is found.
370                         // If 0 cells are found, this component is not necessary.
371                         // If more than 1 cells are found, recreating it would be redundant.
372                         if (componentCells == 1) {
373                             QVariantHash componentHash;
374                             componentHash["tilemap_name"] = mapName + "-" + layer->name() + "-" + tileset->name();
375                             componentHash["tilemap_rel_path"] = tilesetRelativePath(tilemapFilePath);
376                             components.append(replaceTags(QLatin1String(componentTemplate), componentHash));
377                         }
378                     }
379                 }
380 
381                 if (!cells.isEmpty()) {
382                     layerHash["cells"] = cells;
383                     layers.append(replaceTags(QLatin1String(layerTemplate), layerHash));
384                 }
385             }
386 
387             // no need to save a tilemap with 0 cells
388             if (layers.isEmpty())
389                 continue;
390 
391             tileMapHash["layers"] = layers;
392             tileMapHash["material"] = "/builtins/materials/tile_map.material";
393             tileMapHash["blend_mode"] = "BLEND_MODE_ALPHA";
394             // Below, we input a value that's not necessarily correct in Defold, but it lets the user know what tilesource to link this tilemap with manually.
395             // However, if the user keeps all tilesources in /tilesources/ and the name of the tilesource corresponds with the name of the tileset in Defold,
396             // the value will be automatically correct.
397             tileMapHash["tile_set"] = "/tilesources/" + tileset->name() + ".tilesource";
398 
399             QString result = replaceTags(QLatin1String(tileMapTemplate), tileMapHash);
400             Tiled::SaveFile mapFile(tilemapFilePath);
401             if (!mapFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
402                 mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
403                 return false;
404             }
405 
406             QTextStream stream(mapFile.device());
407             stream << result;
408 
409             if (mapFile.error() != QFileDevice::NoError) {
410                 mError = mapFile.errorString();
411                 return false;
412             }
413 
414             if (!mapFile.commit()) {
415                 mError = mapFile.errorString();
416                 return false;
417             }
418         }
419         emdeddedInstanceHash["components"] = components;
420         embeddedInstances.append(replaceTags(QLatin1String(emdeddedInstanceTemplate), emdeddedInstanceHash));
421     }
422 
423     mainEmbeddedInstanceHash["components"] = topLevelComponents;
424     mainEmbeddedInstanceHash["children"] = topLevelChildren;
425     embeddedInstances.prepend(replaceTags(QLatin1String(emdeddedInstanceTemplate), mainEmbeddedInstanceHash));
426     collectionHash["embedded-instances"] = embeddedInstances;
427 
428     QString result = replaceTags(QLatin1String(collectionTemplate), collectionHash);
429     Tiled::SaveFile mapFile(collectionFile);
430     if (!mapFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
431         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
432         return false;
433     }
434     QTextStream stream(mapFile.device());
435     stream << result;
436 
437     if (mapFile.error() != QFileDevice::NoError) {
438         mError = mapFile.errorString();
439         return false;
440     }
441 
442     if (!mapFile.commit()) {
443         mError = mapFile.errorString();
444         return false;
445     }
446 
447     return true;
448 }
449 
450 } // namespace DefoldCollection
451