Mana
Loading...
Searching...
No Matches
mapreader.cpp
Go to the documentation of this file.
1/*
2 * The Mana Client
3 * Copyright (C) 2004-2009 The Mana World Development Team
4 * Copyright (C) 2009-2012 The Mana Developers
5 *
6 * This file is part of The Mana Client.
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22#include "resources/mapreader.h"
23
24#include "configuration.h"
25#include "log.h"
26#include "map.h"
27#include "tileset.h"
28
29#include "resources/animation.h"
30#include "resources/image.h"
32
33#include "utils/base64.h"
34#include "utils/stringutils.h"
35#include "utils/zlib.h"
36
37#include <iostream>
38
39static void readProperties(XML::Node node, Properties* props);
40
41static void readLayer(XML::Node node, Map *map);
42
43static Tileset *readTileset(XML::Node node,
44 const std::string &path,
45 Map *map);
46
47static void readTileAnimation(XML::Node tileNode,
48 Tileset *set,
49 unsigned tileGID,
50 Map *map);
51
52static std::string resolveRelativePath(std::string base, std::string relative)
53{
54 // Remove trailing "/", if present
55 size_t i = base.length();
56 if (base.at(i - 1) == '/')
57 base.erase(i - 1, i);
58
59 while (relative.substr(0, 3) == "../")
60 {
61 relative.erase(0, 3); // Remove "../"
62 if (!base.empty()) // If base is already empty, we can't trim anymore
63 {
64 i = base.find_last_of('/');
65 if (i == std::string::npos)
66 i = 0;
67 base.erase(i, base.length()); // Remove deepest folder in base
68 }
69 }
70
71 // Re-add trailing slash, if needed
72 if (!base.empty() && base[base.length() - 1] != '/')
73 base += '/';
74
75 return base + relative;
76}
77
78Map *MapReader::readMap(const std::string &filename)
79{
80 Log::info("Attempting to read map %s", filename.c_str());
81 Map *map = nullptr;
82
83 XML::Document doc(filename);
84
85 XML::Node node = doc.rootNode();
86
87 // Parse the inflated map data
88 if (node)
89 {
90 if (node.name() != "map")
91 {
92 Log::error("Not a map file (%s)!", filename.c_str());
93 }
94 else
95 {
96 map = readMap(node, filename);
97 }
98 }
99 else
100 {
101 Log::info("Error while parsing map file (%s)!", filename.c_str());
102 }
103
104 if (map)
105 map->setProperty("_filename", filename);
106
107 return map;
108}
109
110Map *MapReader::readMap(XML::Node node, const std::string &path)
111{
112 // Take the filename off the path
113 const std::string pathDir = path.substr(0, path.rfind("/") + 1);
114
115 const int w = node.getProperty("width", 0);
116 const int h = node.getProperty("height", 0);
117 const int tilew = node.getProperty("tilewidth", -1);
118 const int tileh = node.getProperty("tileheight", -1);
119
120 if (tilew < 0 || tileh < 0)
121 {
122 Log::info("MapReader: Warning: "
123 "Unitialized tile width or height value for map: %s",
124 path.c_str());
125 return nullptr;
126 }
127
128 Map *map = new Map(w, h, tilew, tileh);
129
130 for (auto childNode : node.children())
131 {
132 if (childNode.name() == "tileset")
133 {
134 Tileset *tileset = readTileset(childNode, pathDir, map);
135 if (tileset)
136 {
137 map->addTileset(tileset);
138 }
139 }
140 else if (childNode.name() == "layer")
141 {
142 readLayer(childNode, map);
143 }
144 else if (childNode.name() == "properties")
145 {
146 readProperties(childNode, map);
147 }
148 else if (childNode.name() == "objectgroup")
149 {
150 // The object group offset is applied to each object individually
151 const int tileOffsetX = childNode.getProperty("x", 0);
152 const int tileOffsetY = childNode.getProperty("y", 0);
153 const int offsetX = tileOffsetX * tilew;
154 const int offsetY = tileOffsetY * tileh;
155
156 for (auto objectNode : childNode.children())
157 {
158 if (objectNode.name() == "object")
159 {
160 std::string objType = objectNode.getProperty("type", "");
161 objType = toUpper(objType);
162
163 if (objType == "NPC" ||
164 objType == "SCRIPT" ||
165 objType == "SPAWN")
166 {
167 // Silently skip server-side objects.
168 continue;
169 }
170
171 const std::string objName = objectNode.getProperty("name", "");
172 const int objX = objectNode.getProperty("x", 0);
173 const int objY = objectNode.getProperty("y", 0);
174 const int objW = objectNode.getProperty("width", 0);
175 const int objH = objectNode.getProperty("height", 0);
176
177 Log::info("- Loading object name: %s type: %s at %d:%d",
178 objName.c_str(), objType.c_str(),
179 objX, objY);
180
181 if (objType == "PARTICLE_EFFECT")
182 {
183 if (objName.empty())
184 {
185 Log::info(" Warning: No particle file given");
186 continue;
187 }
188
189 map->addParticleEffect(objName,
190 objX + offsetX,
191 objY + offsetY,
192 objW, objH);
193 }
194 else if (objType == "WARP")
195 {
196 if (config.showWarps)
197 {
199 paths.getStringValue("particles")
200 + paths.getStringValue("portalEffectFile"),
201 objX, objY, objW, objH);
202 }
203 }
204 else
205 {
206 Log::info(" Warning: Unknown object type");
207 }
208 }
209 }
210 }
211 }
212
214
215 return map;
216}
217
225static void readProperties(XML::Node node, Properties *props)
226{
227 for (auto childNode : node.children())
228 {
229 if (childNode.name() != "property")
230 continue;
231
232 // Example: <property name="name" value="value"/>
233 const std::string name = childNode.getProperty("name", "");
234 const std::string value = childNode.getProperty("value", "");
235
236 if (!name.empty() && !value.empty())
237 props->setProperty(name, value);
238 }
239}
240
241static void setTile(Map *map, MapLayer *layer, int x, int y, unsigned gid)
242{
243 // Bits on the far end of the 32-bit global tile ID are used for tile flags
244 const int FlippedHorizontallyFlag = 0x80000000;
245 const int FlippedVerticallyFlag = 0x40000000;
246 const int FlippedAntiDiagonallyFlag = 0x20000000;
247
248 // Clear the flags
249 // TODO: It would be nice to properly support these flags later
250 gid &= ~(FlippedHorizontallyFlag |
251 FlippedVerticallyFlag |
252 FlippedAntiDiagonallyFlag);
253
254 const Tileset * const set = map->getTilesetWithGid(gid);
255 if (layer)
256 {
257 // Set regular tile on a layer
258 Image * const img = set ? set->get(gid - set->getFirstGid()) : nullptr;
259 layer->setTile(x, y, img);
260
261 if (TileAnimation *ani = map->getAnimationForGid(gid))
262 ani->addAffectedTile(layer, x + y * layer->getWidth());
263 }
264 else
265 {
266 // Set collision tile
267 if (set && (gid - set->getFirstGid() == 1))
268 map->blockTile(x, y, Map::BLOCKTYPE_WALL);
269 }
270}
271
275static void readLayer(XML::Node node, Map *map)
276{
277 // Layers are not necessarily the same size as the map
278 const int w = node.getProperty("width", map->getWidth());
279 const int h = node.getProperty("height", map->getHeight());
280 const int offsetX = node.getProperty("x", 0);
281 const int offsetY = node.getProperty("y", 0);
282 std::string name = node.getProperty("name", "");
283 name = toLower(name);
284
285 const bool isFringeLayer = (name.substr(0,6) == "fringe");
286 const bool isCollisionLayer = (name.substr(0,9) == "collision");
287
288 MapLayer *layer = nullptr;
289
290 if (!isCollisionLayer)
291 {
292 layer = new MapLayer(offsetX, offsetY, w, h, isFringeLayer, map);
293 map->addLayer(layer);
294 }
295
296 Log::info("- Loading layer \"%s\"", name.c_str());
297 int x = 0;
298 int y = 0;
299
300 // Load the tile data
301 for (auto childNode : node.children())
302 {
303 if (childNode.name() == "properties")
304 {
305 for (auto prop : childNode.children())
306 {
307 if (prop.name() != "property")
308 continue;
309
310 const std::string pname = prop.getProperty("name", "");
311 const std::string value = prop.getProperty("value", "");
312
313 // TODO: Consider supporting "Hidden", "Version" and "NotVersion"
314
315 if (pname == "Mask")
316 {
317 layer->setMask(atoi(value.c_str()));
318 }
319 }
320 continue;
321 }
322
323 if (childNode.name() != "data")
324 continue;
325
326 const std::string encoding =
327 childNode.getProperty("encoding", "");
328 const std::string compression =
329 childNode.getProperty("compression", "");
330
331 if (encoding == "base64")
332 {
333 if (!compression.empty() && compression != "gzip"
334 && compression != "zlib")
335 {
336 Log::warn("Only gzip or zlib layer "
337 "compression supported!");
338 return;
339 }
340
341 // Read base64 encoded map file
342 const auto data = childNode.textContent();
343 if (data.empty())
344 continue;
345
346 auto *charStart = data.data();
347 auto *charData = new unsigned char[data.length() + 1];
348 unsigned char *charIndex = charData;
349
350 while (*charStart)
351 {
352 if (*charStart != ' ' &&
353 *charStart != '\t' &&
354 *charStart != '\n')
355 {
356 *charIndex = *charStart;
357 charIndex++;
358 }
359 charStart++;
360 }
361 *charIndex = '\0';
362
363 int binLen;
364 unsigned char *binData =
365 php3_base64_decode(charData,
366 charIndex - charData,
367 &binLen);
368
369 delete[] charData;
370
371 if (binData)
372 {
373 if (compression == "gzip" || compression == "zlib")
374 {
375 // Inflate the gzipped layer data
376 unsigned char *inflated;
377 unsigned int inflatedSize =
378 inflateMemory(binData, binLen, inflated);
379
380 free(binData);
381 binData = inflated;
382 binLen = inflatedSize;
383
384 if (!inflated)
385 {
386 Log::error("Could not decompress layer!");
387 return;
388 }
389 }
390
391 for (int i = 0; i < binLen - 3; i += 4)
392 {
393 const unsigned gid = binData[i] |
394 binData[i + 1] << 8 |
395 binData[i + 2] << 16 |
396 binData[i + 3] << 24;
397
398 setTile(map, layer, x, y, gid);
399
400 x++;
401 if (x == w)
402 {
403 x = 0; y++;
404
405 // When we're done, don't crash on too much data
406 if (y == h)
407 break;
408 }
409 }
410 free(binData);
411 }
412 }
413 else if (encoding == "csv")
414 {
415 const auto data = childNode.textContent();
416 if (data.empty())
417 {
418 Log::error("CSV layer data is empty!");
419 continue;
420 }
421
422 auto *pos = data.data();
423
424 for (;;)
425 {
426 // Try to parse the next number at 'pos'
427 errno = 0;
428 char *end;
429 unsigned gid = strtol(pos, &end, 10);
430 if (pos == end) // No number found
431 break;
432
433 if (errno == ERANGE)
434 {
435 Log::error("Range error in tile layer data!");
436 break;
437 }
438
439 setTile(map, layer, x, y, gid);
440
441 x++;
442 if (x == w)
443 {
444 x = 0; y++;
445
446 // When we're done, don't crash on too much data
447 if (y == h)
448 break;
449 }
450
451 // Skip the comma, or break if we're done
452 pos = strchr(end, ',');
453 if (!pos)
454 {
455 Log::error("CSV layer data too short!");
456 break;
457 }
458 ++pos;
459 }
460 }
461 else
462 {
463 // Read plain XML map file
464 for (auto childNode2 : childNode.children())
465 {
466 if (childNode2.name() != "tile")
467 continue;
468
469 unsigned gid = childNode2.getProperty("gid", 0);
470 setTile(map, layer, x, y, gid);
471
472 x++;
473 if (x == w)
474 {
475 x = 0; y++;
476 if (y >= h)
477 break;
478 }
479 }
480 }
481
482 if (y < h)
483 std::cerr << "TOO SMALL!\n";
484 if (x)
485 std::cerr << "TOO SMALL!\n";
486
487 // There can be only one data element
488 break;
489 }
490}
491
495static Tileset *readTileset(XML::Node node, const std::string &path,
496 Map *map)
497{
498 const unsigned firstGid = node.getProperty("firstgid", 0);
499 const int margin = node.getProperty("margin", 0);
500 const int spacing = node.getProperty("spacing", 0);
501 XML::Document *doc = nullptr;
502 Tileset *set = nullptr;
503 std::string pathDir(path);
504
505 if (node.hasAttribute("source"))
506 {
507 std::string filename = node.getProperty("source", std::string());
508 filename = resolveRelativePath(path, filename);
509
510 doc = new XML::Document(filename);
511 node = doc->rootNode();
512
513 // Reset path to be realtive to the tsx file
514 pathDir = filename.substr(0, filename.rfind("/") + 1);
515 }
516
517 const int tw = node.getProperty("tilewidth", map->getTileWidth());
518 const int th = node.getProperty("tileheight", map->getTileHeight());
519
520 for (auto childNode : node.children())
521 {
522 if (childNode.name() == "image")
523 {
524 const auto source = childNode.getProperty("source", std::string());
525 if (!source.empty())
526 {
527 std::string sourceStr = resolveRelativePath(pathDir, source);
528
530 auto tilebmp = resman->getImage(sourceStr);
531
532 if (tilebmp)
533 {
534 set = new Tileset(tilebmp, tw, th, firstGid, margin,
535 spacing);
536 }
537 else
538 {
539 Log::warn("Failed to load tileset (%s)", source.c_str());
540 }
541 }
542 }
543 else if (set && childNode.name() == "tile")
544 {
545 const int tileGID = firstGid + childNode.getProperty("id", 0);
546
547 for (auto tileNode : childNode.children())
548 {
549 if (tileNode.name() == "animation")
550 readTileAnimation(tileNode, set, tileGID, map);
551 }
552 }
553 }
554
555 delete doc;
556
557 return set;
558}
559
560static void readTileAnimation(XML::Node tileNode,
561 Tileset *set,
562 unsigned tileGID,
563 Map *map)
564{
565 Animation ani;
566 for (auto frameNode : tileNode.children())
567 {
568 if (frameNode.name() == "frame")
569 {
570 const int tileId = frameNode.getProperty("tileid", 0);
571 const int duration = frameNode.getProperty("duration", 0);
572 ani.addFrame(set->get(tileId), duration, 0, 0);
573 }
574 }
575
576 if (ani.getLength() > 0)
577 map->addAnimation(tileGID, TileAnimation(std::move(ani)));
578}
unsigned char * php3_base64_decode(const unsigned char *string, int length, int *ret_length)
Definition base64.cpp:88
An animation consists of several frames, each with their own delay and offset.
Definition animation.h:47
int getLength() const
Returns the length of this animation in frames.
Definition animation.h:70
void addFrame(Image *image, int delay, int offsetX, int offsetY)
Appends a new animation at the end of the sequence.
Definition animation.cpp:29
std::string getStringValue(const std::string &key) const
Image * get(size_t i) const
Definition imageset.cpp:49
Defines a class for loading and storing images.
Definition image.h:45
A map layer.
Definition map.h:81
void setTile(int x, int y, Image *img)
Set tile image, with x and y in layer coordinates.
Definition map.cpp:106
void setMask(int mask)
Definition map.h:131
int getWidth() const
Definition map.h:93
static Map * readMap(const std::string &filename)
Read an XML map from a file.
Definition mapreader.cpp:78
A tile map.
Definition map.h:147
TileAnimation * getAnimationForGid(int gid)
Gets the tile animation for a specific gid.
Definition map.cpp:991
int getHeight() const
Returns the height of this map in tiles.
Definition map.h:260
int getTileHeight() const
Returns the tile height used by this map.
Definition map.h:271
void addLayer(MapLayer *layer)
Adds a layer to this map.
Definition map.cpp:284
void addAnimation(int gid, TileAnimation animation)
Adds a tile animation to the map.
Definition map.cpp:982
int getWidth() const
Returns the width of this map in tiles.
Definition map.h:255
@ BLOCKTYPE_WALL
Definition map.h:152
void initializeAmbientLayers()
Initialize ambient layers.
Definition map.cpp:241
int getTileWidth() const
Returns the tile width of this map.
Definition map.h:265
void blockTile(int x, int y, BlockType type)
Marks a tile as occupied.
Definition map.cpp:513
Tileset * getTilesetWithGid(unsigned gid) const
Finds the tile set that a tile with the given global id is part of.
Definition map.cpp:500
void addTileset(Tileset *tileset)
Adds a tileset to this map.
Definition map.cpp:289
void addParticleEffect(const std::string &effectFile, int x, int y, int w=0, int h=0)
Adds a particle effect.
Definition map.cpp:953
A class holding a set of properties.
Definition properties.h:32
void setProperty(const std::string &name, const std::string &value)
Set a map property.
Definition properties.h:133
A class for loading and managing resources.
static ResourceManager * getInstance()
Returns an instance of the class, creating one if it does not already exist.
ResourceRef< Image > getImage(const std::string &idPath)
Loads the Image resource found at the given identifier path.
Animation cycle of a tile image which changes the map accordingly.
Definition map.h:61
A tileset, which is basically just an image set but it stores a firstgid.
Definition tileset.h:30
unsigned getFirstGid() const
Returns the first gid.
Definition tileset.h:42
A helper class for parsing an XML document, which also cleans it up again (RAII).
Definition xml.h:190
Node rootNode() const
Returns the root node of the document (or NULL if there was a load error).
Definition xml.h:213
std::string_view name() const
Definition xml.h:46
int getProperty(const char *name, int def) const
Definition xml.h:144
Children children() const
Definition xml.h:97
bool hasAttribute(const char *name) const
Definition xml.h:139
Config config
Global settings (config.xml)
Definition client.cpp:97
Configuration paths
XML default paths information reader.
Definition client.cpp:99
void warn(const char *log_text,...) LOG_PRINTF_ATTR
void info(const char *log_text,...) LOG_PRINTF_ATTR
void error(const char *log_text,...) LOG_PRINTF_ATTR
std::string & toUpper(std::string &str)
Converts the given string to upper case.
std::string & toLower(std::string &str)
Converts the given string to lower case.
bool showWarps
int inflateMemory(unsigned char *in, unsigned int inLength, unsigned char *&out, unsigned int &outLength)
Inflates either zlib or gzip deflated memory.
Definition zlib.cpp:34