1 /*
2  * Flare Tiled Plugin
3  * Copyright 2010, Jaderamiso <jaderamiso@gmail.com>
4  * Copyright 2011, Stefan Beller <stefanbeller@googlemail.com>
5  * Copyright 2011, Clint Bellanger <clintbellanger@gmail.com>
6  *
7  * This file is part of Tiled.
8  *
9  * This program is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU General Public License as published by the Free
11  * Software Foundation; either version 2 of the License, or (at your option)
12  * any later version.
13  *
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
17  * more details.
18  *
19  * You should have received a copy of the GNU General Public License along with
20  * this program. If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 #include "flareplugin.h"
24 
25 #include "gidmapper.h"
26 #include "map.h"
27 #include "mapobject.h"
28 #include "savefile.h"
29 #include "tile.h"
30 #include "tiled.h"
31 #include "tilelayer.h"
32 #include "tileset.h"
33 #include "objectgroup.h"
34 
35 #include <QCoreApplication>
36 #include <QDir>
37 #include <QFileInfo>
38 #include <QStringList>
39 #if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
40 #include <QStringView>
41 #endif
42 #include <QTextStream>
43 
44 #include <memory>
45 
46 using namespace Tiled;
47 
48 namespace Flare {
49 
FlarePlugin()50 FlarePlugin::FlarePlugin()
51 {
52 }
53 
read(const QString & fileName)54 std::unique_ptr<Tiled::Map> FlarePlugin::read(const QString &fileName)
55 {
56     QFile file(fileName);
57 
58     if (!file.open (QIODevice::ReadOnly)) {
59         mError = QCoreApplication::translate("File Errors", "Could not open file for reading.");
60         return nullptr;
61     }
62 
63     // default to values of the original Flare alpha game.
64     Map::Parameters mapParameters;
65     mapParameters.orientation = Map::Isometric;
66     mapParameters.width = 256;
67     mapParameters.height = 256;
68     mapParameters.tileWidth = 64;
69     mapParameters.tileHeight = 32;
70 
71     auto map = std::make_unique<Map>(mapParameters);
72 
73     QTextStream stream (&file);
74     QString line;
75     QString sectionName;
76     bool newsection = false;
77     QString path = QFileInfo(file).absolutePath();
78     int base = 10;
79     GidMapper gidMapper;
80     int gid = 1;
81     TileLayer *tilelayer = nullptr;
82     ObjectGroup *objectgroup = nullptr;
83     MapObject *mapobject = nullptr;
84     bool tilesetsSectionFound = false;
85     bool headerSectionFound = false;
86     bool tilelayerSectionFound = false; // tile layer or objects
87     QColor backgroundColor;
88 
89     while (!stream.atEnd()) {
90         line = stream.readLine();
91 #if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
92         const QStringView lineView(line);
93 #else
94         const QStringRef lineView(&line);
95 #endif
96 
97         if (!line.length())
98             continue;
99 
100         QChar startsWith = line.at(0);
101 
102         if (startsWith == QChar('[')) {
103             sectionName = line.mid(1, line.indexOf(QChar(']')) - 1);
104             newsection = true;
105             continue;
106         }
107         if (sectionName == QLatin1String("header")) {
108             headerSectionFound = true;
109             //get map properties
110             int epos = line.indexOf(QChar('='));
111             if (epos != -1) {
112                 const auto key = lineView.left(epos).trimmed();
113                 const auto value = lineView.mid(epos + 1, -1).trimmed();
114                 if (key == QLatin1String("width"))
115                     map->setWidth(value.toInt());
116                 else if (key == QLatin1String("height"))
117                     map->setHeight(value.toInt());
118                 else if (key == QLatin1String("tilewidth"))
119                     map->setTileWidth(value.toInt());
120                 else if (key == QLatin1String("tileheight"))
121                     map->setTileHeight(value.toInt());
122                 else if (key == QLatin1String("orientation"))
123                     map->setOrientation(orientationFromString(value.toString()));
124                 else if (key == QLatin1String("background_color")){
125                     auto rgbaList = value.split(',');
126 
127                     if (!rgbaList.isEmpty())
128                         backgroundColor.setRed(rgbaList.takeFirst().toInt());
129                     if (!rgbaList.isEmpty())
130                         backgroundColor.setGreen(rgbaList.takeFirst().toInt());
131                     if (!rgbaList.isEmpty())
132                         backgroundColor.setBlue(rgbaList.takeFirst().toInt());
133                     if (!rgbaList.isEmpty())
134                         backgroundColor.setAlpha(rgbaList.takeFirst().toInt());
135 
136                     map->setBackgroundColor(backgroundColor);
137                 }
138                 else
139                     map->setProperty(key.toString(), value.toString());
140             }
141         } else if (sectionName == QLatin1String("tilesets")) {
142             tilesetsSectionFound = true;
143             int epos = line.indexOf(QChar('='));
144             const auto key = lineView.left(epos).trimmed();
145             const auto value = lineView.mid(epos + 1, -1).trimmed();
146             if (key == QLatin1String("tileset")) {
147                 const auto list = value.split(QChar(','));
148 
149                 QString absoluteSource(list.first().toString());
150                 if (QDir::isRelativePath(absoluteSource))
151                     absoluteSource = path + QLatin1Char('/') + absoluteSource;
152 
153                 int tilesetwidth = 0;
154                 int tilesetheight = 0;
155                 if (list.size() > 2) {
156                     tilesetwidth = list[1].toInt();
157                     tilesetheight = list[2].toInt();
158                 }
159 
160                 SharedTileset tileset(Tileset::create(QFileInfo(absoluteSource).fileName(),
161                                                       tilesetwidth, tilesetheight));
162                 bool ok = tileset->loadFromImage(absoluteSource);
163 
164                 if (!ok) {
165                     mError = tr("Error loading tileset %1, which expands to %2. Path not found!")
166                             .arg(list.first().toString(), absoluteSource);
167                     return nullptr;
168                 } else {
169                     if (list.size() > 4)
170                         tileset->setTileOffset(QPoint(list[3].toInt(), list[4].toInt()));
171 
172                     gidMapper.insert(gid, tileset);
173                     if (list.size() > 5) {
174                         gid += list[5].toInt();
175                     } else {
176                         gid += tileset->tileCount();
177                     }
178                     map->addTileset(tileset);
179                 }
180             }
181         } else if (sectionName == QLatin1String("layer")) {
182             if (!tilesetsSectionFound) {
183                 mError = tr("No tilesets section found before layer section.");
184                 return nullptr;
185             }
186             tilelayerSectionFound = true;
187             int epos = line.indexOf(QChar('='));
188             if (epos != -1) {
189                 const auto key = lineView.left(epos).trimmed();
190                 const auto value = lineView.mid(epos + 1, -1).trimmed();
191 
192                 if (key == QLatin1String("type")) {
193                     tilelayer = new TileLayer(value.toString(), 0, 0,
194                                               map->width(),
195                                               map->height());
196                     map->addLayer(tilelayer);
197                 } else if (key == QLatin1String("format")) {
198                     if (value == QLatin1String("dec")) {
199                         base = 10;
200                     } else if (value == QLatin1String("hex")) {
201                         base = 16;
202                     }
203                 } else if (key == QLatin1String("data")) {
204                     for (int y=0; y < map->height(); y++) {
205                         line = stream.readLine();
206                         QStringList l = line.split(QChar(','));
207                         for (int x=0; x < qMin(map->width(), l.size()); x++) {
208                             bool ok;
209                             int tileid = l[x].toInt(nullptr, base);
210                             Cell c = gidMapper.gidToCell(tileid, ok);
211                             if (!ok) {
212                                 mError += tr("Error mapping tile id %1.").arg(tileid);
213                                 return nullptr;
214                             }
215                             tilelayer->setCell(x, y, c);
216                         }
217                     }
218                 } else {
219                     tilelayer->setProperty(key.toString(), value.toString());
220                 }
221             }
222         } else {
223             if (newsection) {
224                 if (map->indexOfLayer(sectionName) == -1) {
225                     objectgroup = new ObjectGroup(sectionName, 0, 0);
226                     map->addLayer(objectgroup);
227                 } else {
228                     objectgroup = dynamic_cast<ObjectGroup*>(map->layerAt(map->indexOfLayer(sectionName)));
229                 }
230                 mapobject = new MapObject();
231                 objectgroup->addObject(mapobject);
232                 newsection = false;
233             }
234             if (!mapobject)
235                 continue;
236 
237             if (startsWith == QChar('#')) {
238                 QString name = lineView.mid(1).trimmed().toString();
239                 mapobject->setName(name);
240             }
241 
242             int epos = line.indexOf(QChar('='));
243             if (epos != -1) {
244                 const auto key = lineView.left(epos).trimmed();
245                 const auto value = lineView.mid(epos + 1, -1).trimmed();
246                 if (key == QLatin1String("type")) {
247                     mapobject->setType(value.toString());
248                 } else if (key == QLatin1String("location")) {
249                     const auto loc = value.split(QChar(','));
250                     qreal x,y;
251                     qreal w,h;
252                     if (map->orientation() == Map::Orthogonal) {
253                         x = loc[0].toDouble() * map->tileWidth();
254                         y = loc[1].toDouble() * map->tileHeight();
255                         if (loc.size() > 3) {
256                             w = loc[2].toInt() * map->tileWidth();
257                             h = loc[3].toInt() * map->tileHeight();
258                         } else {
259                             w = map->tileWidth();
260                             h = map->tileHeight();
261                         }
262                     } else {
263                         x = loc[0].toDouble() * map->tileHeight();
264                         y = loc[1].toDouble() * map->tileHeight();
265                         if (loc.size() > 3) {
266                             w = loc[2].toInt() * map->tileHeight();
267                             h = loc[3].toInt() * map->tileHeight();
268                         } else {
269                             w = h = map->tileHeight();
270                         }
271                     }
272                     mapobject->setPosition(QPointF(x, y));
273                     mapobject->setSize(w, h);
274                 } else {
275                     mapobject->setProperty(key.toString(), value.toString());
276                 }
277             }
278         }
279     }
280 
281     if (!headerSectionFound || !tilesetsSectionFound || !tilelayerSectionFound) {
282         mError = tr("This seems to be no valid flare map. "
283                     "A Flare map consists of at least a header "
284                     "section, a tileset section and one tile layer.");
285         return nullptr;
286     }
287 
288     return map;
289 }
290 
supportsFile(const QString & fileName) const291 bool FlarePlugin::supportsFile(const QString &fileName) const
292 {
293     return fileName.endsWith(QLatin1String(".txt"), Qt::CaseInsensitive);
294 }
295 
nameFilter() const296 QString FlarePlugin::nameFilter() const
297 {
298     return tr("Flare map files (*.txt)");
299 }
300 
shortName() const301 QString FlarePlugin::shortName() const
302 {
303     return QStringLiteral("flare");
304 }
305 
errorString() const306 QString FlarePlugin::errorString() const
307 {
308     return mError;
309 }
310 
write(const Tiled::Map * map,const QString & fileName,Options options)311 bool FlarePlugin::write(const Tiled::Map *map, const QString &fileName, Options options)
312 {
313     Q_UNUSED(options)
314 
315     SaveFile file(fileName);
316 
317     if (!file.open(QFile::WriteOnly | QFile::Text)) {
318         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
319         return false;
320     }
321 
322     QTextStream out(file.device());
323 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
324     out.setCodec("UTF-8");
325 #endif
326 
327     const int mapWidth = map->width();
328     const int mapHeight = map->height();
329     const QColor backgroundColor = map->backgroundColor();
330 
331     // write [header]
332     out << "[header]\n";
333     out << "width=" << mapWidth << "\n";
334     out << "height=" << mapHeight << "\n";
335     out << "tilewidth=" << map->tileWidth() << "\n";
336     out << "tileheight=" << map->tileHeight() << "\n";
337     out << "orientation=" << orientationToString(map->orientation()) << "\n";
338     out << "background_color=" << backgroundColor.red() << "," << backgroundColor.green() << "," << backgroundColor.blue() << "," << backgroundColor.alpha() << "\n";
339 
340     const QDir mapDir = QFileInfo(fileName).absoluteDir();
341 
342     // write all properties for this map
343     Properties::const_iterator it = map->properties().constBegin();
344     Properties::const_iterator it_end = map->properties().constEnd();
345     for (; it != it_end; ++it) {
346         out << it.key() << "=" << toExportValue(it.value(), mapDir).toString() << "\n";
347     }
348     out << "\n";
349 
350     out << "[tilesets]\n";
351     for (const SharedTileset &tileset : map->tilesets()) {
352         QString source = toFileReference(tileset->imageSource(), mapDir);
353         out << "tileset=" << source
354             << "," << tileset->tileWidth()
355             << "," << tileset->tileHeight()
356             << "," << tileset->tileOffset().x()
357             << "," << tileset->tileOffset().y()
358             << "\n";
359     }
360     out << "\n";
361 
362     GidMapper gidMapper(map->tilesets());
363     // write layers
364     for (Layer *layer : map->layers()) {
365         if (TileLayer *tileLayer = layer->asTileLayer()) {
366             out << "[layer]\n";
367             out << "type=" << layer->name() << "\n";
368             out << "data=\n";
369             for (int y = 0; y < mapHeight; ++y) {
370                 for (int x = 0; x < mapWidth; ++x) {
371                     Cell t = tileLayer->cellAt(x, y);
372                     int id = gidMapper.cellToGid(t);
373                     out << id;
374                     if (x < mapWidth - 1)
375                         out << ",";
376                 }
377                 if (y < mapHeight - 1)
378                     out << ",";
379                 out << "\n";
380             }
381             //Write all properties for this layer
382             Properties::const_iterator it = tileLayer->properties().constBegin();
383             Properties::const_iterator it_end = tileLayer->properties().constEnd();
384             for (; it != it_end; ++it) {
385                 out << it.key() << "=" << toExportValue(it.value(), mapDir).toString() << "\n";
386             }
387             out << "\n";
388         }
389         if (ObjectGroup *group = layer->asObjectGroup()) {
390             for (const MapObject *o : group->objects()) {
391                 if (!o->type().isEmpty()) {
392                     out << "[" << group->name() << "]\n";
393 
394                     // display object name as comment
395                     if (!o->name().isEmpty())
396                         out << "# " << o->name() << "\n";
397 
398                     out << "type=" << o->type() << "\n";
399                     int x,y,w,h;
400                     if (map->orientation() == Map::Orthogonal) {
401                         x = o->x()/map->tileWidth();
402                         y = o->y()/map->tileHeight();
403                         w = o->width()/map->tileWidth();
404                         h = o->height()/map->tileHeight();
405                     } else {
406                         x = o->x()/map->tileHeight();
407                         y = o->y()/map->tileHeight();
408                         w = o->width()/map->tileHeight();
409                         h = o->height()/map->tileHeight();
410                     }
411                     out << "location=" << x << "," << y;
412                     out << "," << w << "," << h << "\n";
413 
414                     // write all properties for this object
415                     QVariantMap propsMap = o->properties();
416                     for (QVariantMap::const_iterator it = propsMap.constBegin(); it != propsMap.constEnd(); ++it) {
417                         out << it.key() << "=" << toExportValue(it.value(), mapDir).toString() << "\n";
418                     }
419                     out << "\n";
420                 }
421             }
422         }
423     }
424 
425     if (!file.commit()) {
426         mError = file.errorString();
427         return false;
428     }
429 
430     return true;
431 }
432 
433 } // namespace Flare
434