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