1 /*
2  * The T-Engine 4 Tiled Plugin
3  * Copyright 2010, Mikolai Fajer <mfajer@gmail.com>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "tengineplugin.h"
22 
23 #include "map.h"
24 #include "mapobject.h"
25 #include "objectgroup.h"
26 #include "properties.h"
27 #include "savefile.h"
28 #include "tile.h"
29 #include "tilelayer.h"
30 
31 #include <QCoreApplication>
32 #include <QHash>
33 #include <QList>
34 #if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
35 #include <QStringView>
36 #endif
37 #include <QTextStream>
38 
39 #include <QtMath>
40 
41 #include "qtcompat_p.h"
42 
43 using namespace Tengine;
44 
TenginePlugin()45 TenginePlugin::TenginePlugin()
46 {
47 }
48 
write(const Tiled::Map * map,const QString & fileName,Options options)49 bool TenginePlugin::write(const Tiled::Map *map, const QString &fileName, Options options)
50 {
51     Q_UNUSED(options)
52 
53     using namespace Tiled;
54 
55     SaveFile file(fileName);
56     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
57         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
58         return false;
59     }
60     QTextStream out(file.device());
61 
62     // Write the header
63     const QString header = map->property("header").toString();
64 #if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
65     const auto lines = QStringView(header).split(QStringLiteral("\\n"));
66 #else
67     const auto lines = header.splitRef("\\n");
68 #endif
69     for (const auto &line : lines)
70         out << line << Qt::endl;
71 
72     const int width = map->width();
73     const int height = map->height();
74 
75     QList<QString> asciiMap;
76     QHash<QString, Tiled::Properties> cachedTiles;
77     QList<QString> propertyOrder;
78     propertyOrder.append("terrain");
79     propertyOrder.append("object");
80     propertyOrder.append("actor");
81     propertyOrder.append("trap");
82     propertyOrder.append("status");
83     propertyOrder.append("spot");
84     // Ability to handle overflow and strings for display
85     bool outputLists = false;
86     char asciiDisplay = ASCII_MIN;
87     int overflowDisplay = 1;
88     QHash<QString, Tiled::Properties>::const_iterator i;
89     // Add the empty tile
90     int numEmptyTiles = 0;
91     Properties emptyTile;
92     emptyTile["display"] = "?";
93     cachedTiles["?"] = emptyTile;
94     // Process the map, collecting used display strings as we go
95     for (int y = 0; y < height; ++y) {
96         for (int x = 0; x < width; ++x) {
97             Properties currentTile = cachedTiles["?"];
98             for (Layer *layer : map->layers()) {
99                 // If the layer name does not start with one of the tile properties, skip it
100                 QString layerKey;
101                 QListIterator<QString> propertyIterator = propertyOrder;
102                 while (propertyIterator.hasNext()) {
103                     QString currentProperty = propertyIterator.next();
104                     if (layer->name().startsWith(currentProperty, Qt::CaseInsensitive)) {
105                         layerKey = currentProperty;
106                         break;
107                     }
108                 }
109 
110                 if (layerKey.isEmpty())
111                     continue;
112 
113                 // Process the Tile Layer
114                 if (TileLayer *tileLayer = layer->asTileLayer()) {
115                     if (Tile *tile = tileLayer->cellAt(x, y).tile()) {
116                         currentTile["display"] = tile->property("display");
117                         currentTile[layerKey] = tile->property("value");
118                     }
119                 // Process the Object Layer
120                 } else if (ObjectGroup *objectLayer = layer->asObjectGroup()) {
121                     for (const MapObject *obj : objectLayer->objects()) {
122                         if (floor(obj->y()) <= y && y <= floor(obj->y() + obj->height())) {
123                             if (floor(obj->x()) <= x && x <= floor(obj->x() + obj->width())) {
124                                 // Check the Object Layer properties if either display or value was missing
125                                 if (!obj->property("display").isNull()) {
126                                     currentTile["display"] = obj->property("display");
127                                 } else if (!objectLayer->property("display").isNull()) {
128                                     currentTile["display"] = objectLayer->property("display");
129                                 }
130                                 if (!obj->property("value").isNull()) {
131                                     currentTile[layerKey] = obj->property("value");
132                                 } else if (!objectLayer->property("value").isNull()) {
133                                     currentTile[layerKey] = objectLayer->property("value");
134                                 }
135                             }
136                         }
137                     }
138                 }
139             }
140             // If the currentTile does not exist in the cache, add it
141             if (!cachedTiles.contains(currentTile["display"].toString())) {
142                 cachedTiles[currentTile["display"].toString()] = currentTile;
143             // Otherwise check that it EXACTLY matches the cached one
144             // and if not...
145             } else if (currentTile != cachedTiles[currentTile["display"].toString()]) {
146                 // Search the cached tiles for a match
147                 bool foundInCache = false;
148                 QString displayString;
149                 for (i = cachedTiles.constBegin(); i != cachedTiles.constEnd(); ++i) {
150                     displayString = i.key();
151                     currentTile["display"].setValue(displayString);
152                     if (currentTile == i.value()) {
153                         foundInCache = true;
154                         break;
155                     }
156                 }
157                 // If we haven't found a match then find a random display string
158                 // and cache it
159                 if (!foundInCache) {
160                     while (true) {
161                         // First try to use the ASCII characters
162                         if (asciiDisplay < ASCII_MAX) {
163                             displayString = QString(QChar::fromLatin1(asciiDisplay));
164                             asciiDisplay++;
165                         // Then fall back onto integers
166                         } else {
167                             displayString = QString::number(overflowDisplay);
168                             overflowDisplay++;
169                         }
170                         currentTile["display"] = displayString;
171                         if (!cachedTiles.contains(displayString)) {
172                             cachedTiles[displayString] = currentTile;
173                             break;
174                         } else if (currentTile == cachedTiles[currentTile["display"].toString()]) {
175                             break;
176                         }
177                     }
178                 }
179             }
180             // Check the output type
181             if (currentTile["display"].toString().length() > 1) {
182                 outputLists = true;
183             }
184             // Check if we are still the emptyTile
185             if (currentTile == emptyTile) {
186                 numEmptyTiles++;
187             }
188             // Finally add the character to the asciiMap
189             asciiMap.append(currentTile["display"].toString());
190         }
191     }
192     // Write the definitions to the file
193     out << "-- defineTile section" << Qt::endl;
194     for (i = cachedTiles.constBegin(); i != cachedTiles.constEnd(); ++i) {
195         QString displayString = i.key();
196         // Only print the emptyTile definition if there were empty tiles
197         if (displayString == QLatin1String("?") && numEmptyTiles == 0) {
198             continue;
199         }
200         // Need to escape " and \ characters
201         displayString.replace(QLatin1Char('\\'), "\\\\");
202         displayString.replace(QLatin1Char('"'), "\\\"");
203         QString args = constructArgs(i.value(), propertyOrder);
204         if (!args.isEmpty()) {
205             args = QString(", %1").arg(args);
206         }
207         out << QString("defineTile(\"%1\"%2)").arg(displayString, args) << Qt::endl;
208     }
209 
210     // Check for an ObjectGroup named AddSpot
211     out << Qt::endl << "-- addSpot section" << Qt::endl;
212     for (Layer *layer : map->layers()) {
213         ObjectGroup *objectLayer = layer->asObjectGroup();
214         if (objectLayer && objectLayer->name().startsWith("addspot", Qt::CaseInsensitive)) {
215             for (const MapObject *obj : objectLayer->objects()) {
216                 QList<QString> propertyOrder;
217                 propertyOrder.append("type");
218                 propertyOrder.append("subtype");
219                 propertyOrder.append("additional");
220                 QString args = constructArgs(obj->properties(), propertyOrder);
221                 if (!args.isEmpty()) {
222                     args = QString(", %1").arg(args);
223                 }
224                 for (int y = qFloor(obj->y()); y <= qFloor(obj->y() + obj->height()); ++y) {
225                     for (int x = qFloor(obj->x()); x <= qFloor(obj->x() + obj->width()); ++x) {
226                         out << QString("addSpot({%1, %2}%3)").arg(x).arg(y).arg(args) << Qt::endl;
227                     }
228                 }
229             }
230         }
231     }
232 
233     // Check for an ObjectGroup named AddZone
234     out << Qt::endl << "-- addZone section" << Qt::endl;
235     for (Layer *layer : map->layers()) {
236         ObjectGroup *objectLayer = layer->asObjectGroup();
237         if (objectLayer && objectLayer->name().startsWith("addzone", Qt::CaseInsensitive)) {
238             for (MapObject *obj : objectLayer->objects()) {
239                 QList<QString> propertyOrder;
240                 propertyOrder.append("type");
241                 propertyOrder.append("subtype");
242                 propertyOrder.append("additional");
243                 QString args = constructArgs(obj->properties(), propertyOrder);
244                 if (!args.isEmpty()) {
245                     args = QString(", %1").arg(args);
246                 }
247                 int top_left_x = qFloor(obj->x());
248                 int top_left_y = qFloor(obj->y());
249                 int bottom_right_x = qFloor(obj->x() + obj->width());
250                 int bottom_right_y = qFloor(obj->y() + obj->height());
251                 out << QString("addZone({%1, %2, %3, %4}%5)").arg(top_left_x).arg(top_left_y).arg(bottom_right_x).arg(bottom_right_y).arg(args) << Qt::endl;
252             }
253         }
254     }
255 
256     // Write the map
257     QString returnStart;
258     QString returnStop;
259     QString lineStart;
260     QString lineStop;
261     QString itemStart;
262     QString itemStop;
263     QString seperator;
264     if (outputLists) {
265         returnStart = "{";
266         returnStop = "}";
267         lineStart = "{";
268         lineStop = "},";
269         itemStart = "[[";
270         itemStop = "]]";
271         seperator = ",";
272     } else {
273         returnStart = "[[";
274         returnStop = "]]";
275         lineStart = "";
276         lineStop = "";
277         itemStart = "";
278         itemStop = "";
279         seperator = "";
280     }
281     out << Qt::endl << "-- ASCII map section" << Qt::endl;
282     out << "return " << returnStart << Qt::endl;
283     for (int y = 0; y < height; ++y) {
284         out << lineStart;
285         for (int x = 0; x < width; ++x) {
286             out << itemStart << asciiMap[x + (y * width)] << itemStop << seperator;
287         }
288         if (y == height - 1) {
289             out << lineStop << returnStop;
290         } else {
291             out << lineStop << Qt::endl;
292         }
293     }
294 
295     if (!file.commit()) {
296         mError = file.errorString();
297         return false;
298     }
299 
300     return true;
301 }
302 
nameFilter() const303 QString TenginePlugin::nameFilter() const
304 {
305     return tr("T-Engine4 map files (*.lua)");
306 }
307 
shortName() const308 QString TenginePlugin::shortName() const
309 {
310     return QStringLiteral("te4");
311 }
312 
errorString() const313 QString TenginePlugin::errorString() const
314 {
315     return mError;
316 }
317 
constructArgs(const Tiled::Properties & props,const QList<QString> & propOrder) const318 QString TenginePlugin::constructArgs(const Tiled::Properties &props,
319                                      const QList<QString> &propOrder) const
320 {
321     QString argString;
322     // We work backwards so we don't have to include a bunch of nils
323     for (int i = propOrder.size() - 1; i >= 0; --i) {
324         QString currentValue = props[propOrder[i]].toString();
325         // Special handling of the "additional" property
326         if ((propOrder[i] == "additional") && currentValue.isEmpty()) {
327             currentValue = constructAdditionalTable(props, propOrder);
328         }
329         if (!argString.isEmpty()) {
330             if (currentValue.isEmpty()) {
331                 currentValue = "nil";
332             }
333             argString = QString("%1, %2").arg(currentValue, argString);
334         }  else if (!currentValue.isEmpty()) {
335             argString = currentValue;
336         }
337     }
338     return argString;
339 }
340 
341 // Finds unhandled properties and bundles them into a Lua table
constructAdditionalTable(const Tiled::Properties & props,const QList<QString> & propOrder) const342 QString TenginePlugin::constructAdditionalTable(const Tiled::Properties &props,
343                                                 const QList<QString> &propOrder) const
344 {
345     QString tableString;
346     QMap<QString, QVariant> unhandledProps = QMap<QString, QVariant>(props);
347 
348     // Remove handled properties
349     for (const QString &prop : propOrder)
350         unhandledProps.remove(prop);
351 
352     // Construct the Lua string
353     if (unhandledProps.size() > 0) {
354         tableString = "{";
355         QMapIterator<QString, QVariant> i(unhandledProps);
356         while (i.hasNext()) {
357             i.next();
358             tableString = QString("%1%2=%3,").arg(tableString, i.key(), i.value().toString());
359         }
360         tableString = QString("%1}").arg(tableString);
361     }
362 
363     return tableString;
364 }
365