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