1 /*
2  * Replica Island Tiled Plugin
3  * Copyright 2011, Eric Kidd <eric@kiddsoftware.com>
4  * Copyright 2011, seeseekey <seeseekey@googlemail.com>
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 "replicaislandplugin.h"
23 
24 #include "map.h"
25 #include "savefile.h"
26 #include "tile.h"
27 #include "tileset.h"
28 #include "tilelayer.h"
29 #include "compression.h"
30 
31 #include <QCoreApplication>
32 #include <QFile>
33 #include <QtEndian>
34 
35 using namespace ReplicaIsland;
36 
37 
tilesetForLayer(int type,int tileIndex,const QVector<Tiled::SharedTileset> & typeTilesets,const QVector<Tiled::SharedTileset> & tileIndexTilesets)38 static Tiled::SharedTileset tilesetForLayer(int type, int tileIndex,
39                                             const QVector<Tiled::SharedTileset> &typeTilesets,
40                                             const QVector<Tiled::SharedTileset> &tileIndexTilesets)
41 {
42     if (type == 0)
43         return tileIndexTilesets[tileIndex];
44     else
45         return typeTilesets[type];
46 }
47 
ReplicaIslandPlugin()48 ReplicaIslandPlugin::ReplicaIslandPlugin()
49 {
50 }
51 
read(const QString & fileName)52 std::unique_ptr<Tiled::Map> ReplicaIslandPlugin::read(const QString &fileName)
53 {
54     using namespace Tiled;
55 
56     // Read data.
57     QFile file(fileName);
58     if (!file.open(QIODevice::ReadOnly)) {
59         mError = tr("Cannot open Replica Island map file!");
60         return nullptr;
61     }
62     QDataStream in(&file);
63     in.setByteOrder(QDataStream::LittleEndian);
64     in.setFloatingPointPrecision(QDataStream::SinglePrecision);
65 
66     // Parse file header.
67     quint8 mapSignature, layerCount, backgroundIndex;
68     in >> mapSignature >> layerCount >> backgroundIndex;
69     if (in.status() == QDataStream::ReadPastEnd || mapSignature != 96) {
70         mError = tr("Can't parse file header!");
71         return nullptr;
72     }
73 
74     // Create our map, leaving width and height to 0 until we load a layer.
75     Map::Parameters mapParameters;
76     mapParameters.tileWidth = 32;
77     mapParameters.tileHeight = 32;
78     auto map = std::make_unique<Map>(mapParameters);
79     map->setProperty("background_index", QString::number(backgroundIndex));
80 
81     // Load our Tilesets.
82     QVector<SharedTileset> typeTilesets, tileIndexTilesets;
83     loadTilesetsFromResources(map.get(), typeTilesets, tileIndexTilesets);
84 
85     // Load each of our layers.
86     for (quint8 i = 0; i < layerCount; i++) {
87         // Parse layer header.
88         quint8 type, tileIndex, levelSignature;
89         float scrollSpeed;
90         qint32 width, height;
91         in >> type >> tileIndex >> scrollSpeed
92            >> levelSignature >> width >> height;
93         if (in.status() == QDataStream::ReadPastEnd || levelSignature != 42) {
94             mError = tr("Can't parse layer header!");
95             return nullptr;
96         }
97 
98         // Make sure our width and height are consistent.
99         if (map->width() == 0)
100             map->setWidth(width);
101         if (map->height() == 0)
102             map->setHeight(height);
103         if (map->width() != width || map->height() != height) {
104             mError = tr("Inconsistent layer sizes!");
105             return nullptr;
106         }
107 
108         // Create a layer object.
109         TileLayer *layer =
110             new TileLayer(layerTypeToName(type), 0, 0, width, height);
111         layer->setProperty("type", QString::number(type));
112         layer->setProperty("tile_index", QString::number(tileIndex));
113         layer->setProperty("scroll_speed", QString::number(scrollSpeed, 'f'));
114         map->addLayer(layer);
115 
116         // Look up the tileset for this layer.
117         SharedTileset tileset =
118             tilesetForLayer(type, tileIndex, typeTilesets, tileIndexTilesets);
119 
120         // Read our tile data all at once.
121         QByteArray tileData(width*height, '\0');
122         int bytesRead = in.readRawData(tileData.data(), tileData.size());
123         if (bytesRead != tileData.size()) {
124             mError = tr("File ended in middle of layer!");
125             return nullptr;
126         }
127         quint8 *tp = reinterpret_cast<quint8 *>(tileData.data());
128 
129         // Add the tiles to our layer.
130         for (int y = 0; y < height; y++) {
131             for (int x = 0; x < width; x++) {
132                 quint8 tile_id = *tp++;
133                 if (tile_id != 255) {
134                     Tile *tile = tileset->findTile(tile_id);
135                     layer->setCell(x, y, Cell(tile));
136                 }
137             }
138         }
139     }
140 
141     // Make sure we read the entire *.bin file.
142     if (in.status() != QDataStream::Ok || !in.atEnd()) {
143         mError = tr("Unexpected data at end of file!");
144         return nullptr;
145     }
146 
147     return map;
148 }
149 
loadTilesetsFromResources(Tiled::Map * map,QVector<Tiled::SharedTileset> & typeTilesets,QVector<Tiled::SharedTileset> & tileIndexTilesets)150 void ReplicaIslandPlugin::loadTilesetsFromResources(
151         Tiled::Map *map,
152         QVector<Tiled::SharedTileset> &typeTilesets,
153         QVector<Tiled::SharedTileset> &tileIndexTilesets)
154 {
155     // Create tilesets for type 0 to 3, inclusive.
156     typeTilesets.append(Tiled::SharedTileset()); // Use a tileIndexTileset.
157     typeTilesets.append(loadTilesetFromResource("collision_map"));
158     typeTilesets.append(loadTilesetFromResource("objects"));
159     typeTilesets.append(loadTilesetFromResource("hotspots"));
160     addTilesetsToMap(map, typeTilesets);
161 
162     // Create tilesets for tileIndex 0 to 7, inclusive.
163     tileIndexTilesets.append(loadTilesetFromResource("grass"));
164     tileIndexTilesets.append(loadTilesetFromResource("island"));
165     tileIndexTilesets.append(loadTilesetFromResource("sewage"));
166     tileIndexTilesets.append(loadTilesetFromResource("cave"));
167     tileIndexTilesets.append(loadTilesetFromResource("lab"));
168     // The titletileset is also known as "lighting".
169     tileIndexTilesets.append(loadTilesetFromResource("titletileset"));
170     tileIndexTilesets.append(loadTilesetFromResource("tutorial"));
171     addTilesetsToMap(map, tileIndexTilesets);
172 }
173 
174 Tiled::SharedTileset
loadTilesetFromResource(const QString & name)175 ReplicaIslandPlugin::loadTilesetFromResource(const QString &name)
176 {
177     using namespace Tiled;
178 
179     SharedTileset tileset(Tileset::create(name, 32, 32));
180     tileset->loadFromImage(QImage(":/" + name + ".png"), name + ".png");
181     return tileset;
182 }
183 
addTilesetsToMap(Tiled::Map * map,const QVector<Tiled::SharedTileset> & tilesets)184 void ReplicaIslandPlugin::addTilesetsToMap(Tiled::Map *map,
185                                            const QVector<Tiled::SharedTileset> &tilesets)
186 {
187     using namespace Tiled;
188 
189     for (const Tiled::SharedTileset &tileset : tilesets)
190         if (tileset)
191             map->addTileset(tileset);
192 }
193 
layerTypeToName(char type)194 QString ReplicaIslandPlugin::layerTypeToName(char type)
195 {
196     switch (type) {
197         case 0: return "Background";
198         case 1: return "Collision";
199         case 2: return "Objects";
200         case 3: return "Hot spots";
201         default: return "Unknown layer type";
202     }
203 }
204 
nameFilter() const205 QString ReplicaIslandPlugin::nameFilter() const
206 {
207     return tr("Replica Island map files (*.bin)");
208 }
209 
shortName() const210 QString ReplicaIslandPlugin::shortName() const
211 {
212     return QStringLiteral("replicaisland");
213 }
214 
supportsFile(const QString & fileName) const215 bool ReplicaIslandPlugin::supportsFile(const QString &fileName) const
216 {
217     // Check the file extension first.
218     if (!fileName.endsWith(QLatin1String(".bin"), Qt::CaseInsensitive))
219         return false;
220 
221     // Since we may have lots of Android-related *.bin files that aren't
222     // maps, check our signature byte, too.
223     QFile f(fileName);
224     if (!f.open(QIODevice::ReadOnly))
225         return false;
226     char signature;
227     qint64 read = f.read(&signature, 1);
228     return (read == 1 || signature == 96);
229 }
230 
errorString() const231 QString ReplicaIslandPlugin::errorString() const
232 {
233     return mError;
234 }
235 
write(const Tiled::Map * map,const QString & fileName,Options options)236 bool ReplicaIslandPlugin::write(const Tiled::Map *map, const QString &fileName, Options options)
237 {
238     Q_UNUSED(options)
239 
240     using namespace Tiled;
241 
242     // Open up a temporary file for saving the level.
243     SaveFile file(fileName);
244     if (!file.open(QIODevice::WriteOnly)) {
245         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
246         return false;
247     }
248 
249     // Create an output stream for serializing data.
250     QDataStream out(file.device());
251     out.setByteOrder(QDataStream::LittleEndian);
252     out.setFloatingPointPrecision(QDataStream::SinglePrecision);
253 
254     // Write out the signature and file header.
255     out << static_cast<quint8>(96); // Signature.
256     out << static_cast<quint8>(map->layerCount());
257     bool ok;
258     out << static_cast<quint8>(map->property("background_index").toInt(&ok));
259     if (!ok) {
260         mError = tr("You must define a background_index property on the map!");
261         return false;
262     }
263 
264     // Write out each layer.
265     for (int i = 0; i < map->layerCount(); i++) {
266         TileLayer *layer = map->layerAt(i)->asTileLayer();
267         if (!layer) {
268             mError = tr("Can't save non-tile layer!");
269             return false;
270         }
271         if (!writeLayer(out, layer))
272             return false;
273     }
274 
275     if (!file.commit()) {
276         mError = file.errorString();
277         return false;
278     }
279 
280     return true;
281 }
282 
writeLayer(QDataStream & out,Tiled::TileLayer * layer)283 bool ReplicaIslandPlugin::writeLayer(QDataStream &out, Tiled::TileLayer *layer)
284 {
285     using namespace Tiled;
286 
287     // Write out the layer header.
288     bool ok;
289     out << static_cast<quint8>(layer->property("type").toInt(&ok));
290     if (!ok) {
291         mError = tr("You must define a type property on each layer!");
292         return false;
293     }
294     out << static_cast<quint8>(layer->property("tile_index").toInt(&ok));
295     if (!ok) {
296         mError = tr("You must define a tile_index property on each layer!");
297         return false;
298     }
299     out << layer->property("scroll_speed").toFloat(&ok);
300     if (!ok) {
301         mError = tr("You must define a scroll_speed property on each layer!");
302         return false;
303     }
304     out << static_cast<quint8>(42); // Layer signature.
305     out << static_cast<qint32>(layer->width());
306     out << static_cast<qint32>(layer->height());
307 
308     // Write out the raw tile data.  We assume that the user has used the
309     // correct tileset for this layer.
310     for (int y = 0; y < layer->height(); y++) {
311         for (int x = 0; x < layer->width(); x++) {
312             Tile *tile = layer->cellAt(x, y).tile();
313             if (tile)
314                 out << static_cast<quint8>(tile->id());
315             else
316                 out << static_cast<quint8>(255);
317         }
318     }
319 
320     return true;
321 }
322