/* * The Mana Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2012 The Mana Developers * * This file is part of The Mana Client. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "resources/mapreader.h" #include "configuration.h" #include "log.h" #include "map.h" #include "tileset.h" #include "resources/animation.h" #include "resources/image.h" #include "resources/resourcemanager.h" #include "utils/base64.h" #include "utils/stringutils.h" #include "utils/zlib.h" #include static void readProperties(XML::Node node, Properties* props); static void readLayer(XML::Node node, Map *map); static Tileset *readTileset(XML::Node node, const std::string &path, Map *map); static void readTileAnimation(XML::Node tileNode, Tileset *set, unsigned tileGID, Map *map); static std::string resolveRelativePath(std::string base, std::string relative) { // Remove trailing "/", if present size_t i = base.length(); if (base.at(i - 1) == '/') base.erase(i - 1, i); while (relative.substr(0, 3) == "../") { relative.erase(0, 3); // Remove "../" if (!base.empty()) // If base is already empty, we can't trim anymore { i = base.find_last_of('/'); if (i == std::string::npos) i = 0; base.erase(i, base.length()); // Remove deepest folder in base } } // Re-add trailing slash, if needed if (!base.empty() && base[base.length() - 1] != '/') base += '/'; return base + relative; } Map *MapReader::readMap(const std::string &filename) { logger->log("Attempting to read map %s", filename.c_str()); Map *map = nullptr; XML::Document doc(filename); XML::Node node = doc.rootNode(); // Parse the inflated map data if (node) { if (node.name() != "map") { logger->log("Error: Not a map file (%s)!", filename.c_str()); } else { map = readMap(node, filename); } } else { logger->log("Error while parsing map file (%s)!", filename.c_str()); } if (map) map->setProperty("_filename", filename); return map; } Map *MapReader::readMap(XML::Node node, const std::string &path) { // Take the filename off the path const std::string pathDir = path.substr(0, path.rfind("/") + 1); const int w = node.getProperty("width", 0); const int h = node.getProperty("height", 0); const int tilew = node.getProperty("tilewidth", -1); const int tileh = node.getProperty("tileheight", -1); if (tilew < 0 || tileh < 0) { logger->log("MapReader: Warning: " "Unitialized tile width or height value for map: %s", path.c_str()); return nullptr; } Map *map = new Map(w, h, tilew, tileh); for (auto childNode : node.children()) { if (childNode.name() == "tileset") { Tileset *tileset = readTileset(childNode, pathDir, map); if (tileset) { map->addTileset(tileset); } } else if (childNode.name() == "layer") { readLayer(childNode, map); } else if (childNode.name() == "properties") { readProperties(childNode, map); } else if (childNode.name() == "objectgroup") { // The object group offset is applied to each object individually const int tileOffsetX = childNode.getProperty("x", 0); const int tileOffsetY = childNode.getProperty("y", 0); const int offsetX = tileOffsetX * tilew; const int offsetY = tileOffsetY * tileh; for (auto objectNode : childNode.children()) { if (objectNode.name() == "object") { std::string objType = objectNode.getProperty("type", ""); objType = toUpper(objType); if (objType == "NPC" || objType == "SCRIPT" || objType == "SPAWN") { // Silently skip server-side objects. continue; } const std::string objName = objectNode.getProperty("name", ""); const int objX = objectNode.getProperty("x", 0); const int objY = objectNode.getProperty("y", 0); const int objW = objectNode.getProperty("width", 0); const int objH = objectNode.getProperty("height", 0); logger->log("- Loading object name: %s type: %s at %d:%d", objName.c_str(), objType.c_str(), objX, objY); if (objType == "PARTICLE_EFFECT") { if (objName.empty()) { logger->log(" Warning: No particle file given"); continue; } map->addParticleEffect(objName, objX + offsetX, objY + offsetY, objW, objH); } else if (objType == "WARP") { if (config.showWarps) { map->addParticleEffect( paths.getStringValue("particles") + paths.getStringValue("portalEffectFile"), objX, objY, objW, objH); } } else { logger->log(" Warning: Unknown object type"); } } } } } map->initializeAmbientLayers(); return map; } /** * Reads the properties element. * * @param node The properties element. * @param props The Properties instance to which the properties will * be assigned. */ static void readProperties(XML::Node node, Properties *props) { for (auto childNode : node.children()) { if (childNode.name() != "property") continue; // Example: const std::string name = childNode.getProperty("name", ""); const std::string value = childNode.getProperty("value", ""); if (!name.empty() && !value.empty()) props->setProperty(name, value); } } static void setTile(Map *map, MapLayer *layer, int x, int y, unsigned gid) { // Bits on the far end of the 32-bit global tile ID are used for tile flags const int FlippedHorizontallyFlag = 0x80000000; const int FlippedVerticallyFlag = 0x40000000; const int FlippedAntiDiagonallyFlag = 0x20000000; // Clear the flags // TODO: It would be nice to properly support these flags later gid &= ~(FlippedHorizontallyFlag | FlippedVerticallyFlag | FlippedAntiDiagonallyFlag); const Tileset * const set = map->getTilesetWithGid(gid); if (layer) { // Set regular tile on a layer Image * const img = set ? set->get(gid - set->getFirstGid()) : nullptr; layer->setTile(x, y, img); if (TileAnimation *ani = map->getAnimationForGid(gid)) ani->addAffectedTile(layer, x + y * layer->getWidth()); } else { // Set collision tile if (set && (gid - set->getFirstGid() == 1)) map->blockTile(x, y, Map::BLOCKTYPE_WALL); } } /** * Reads a map layer and adds it to the given map. */ static void readLayer(XML::Node node, Map *map) { // Layers are not necessarily the same size as the map const int w = node.getProperty("width", map->getWidth()); const int h = node.getProperty("height", map->getHeight()); const int offsetX = node.getProperty("x", 0); const int offsetY = node.getProperty("y", 0); std::string name = node.getProperty("name", ""); name = toLower(name); const bool isFringeLayer = (name.substr(0,6) == "fringe"); const bool isCollisionLayer = (name.substr(0,9) == "collision"); MapLayer *layer = nullptr; if (!isCollisionLayer) { layer = new MapLayer(offsetX, offsetY, w, h, isFringeLayer, map); map->addLayer(layer); } logger->log("- Loading layer \"%s\"", name.c_str()); int x = 0; int y = 0; // Load the tile data for (auto childNode : node.children()) { if (childNode.name() == "properties") { for (auto prop : childNode.children()) { if (prop.name() != "property") continue; const std::string pname = prop.getProperty("name", ""); const std::string value = prop.getProperty("value", ""); // TODO: Consider supporting "Hidden", "Version" and "NotVersion" if (pname == "Mask") { layer->setMask(atoi(value.c_str())); } } continue; } if (childNode.name() != "data") continue; const std::string encoding = childNode.getProperty("encoding", ""); const std::string compression = childNode.getProperty("compression", ""); if (encoding == "base64") { if (!compression.empty() && compression != "gzip" && compression != "zlib") { logger->log("Warning: only gzip or zlib layer " "compression supported!"); return; } // Read base64 encoded map file const auto data = childNode.textContent(); if (data.empty()) continue; auto *charStart = data.data(); auto *charData = new unsigned char[data.length() + 1]; unsigned char *charIndex = charData; while (*charStart) { if (*charStart != ' ' && *charStart != '\t' && *charStart != '\n') { *charIndex = *charStart; charIndex++; } charStart++; } *charIndex = '\0'; int binLen; unsigned char *binData = php3_base64_decode(charData, charIndex - charData, &binLen); delete[] charData; if (binData) { if (compression == "gzip" || compression == "zlib") { // Inflate the gzipped layer data unsigned char *inflated; unsigned int inflatedSize = inflateMemory(binData, binLen, inflated); free(binData); binData = inflated; binLen = inflatedSize; if (!inflated) { logger->log("Error: Could not decompress layer!"); return; } } for (int i = 0; i < binLen - 3; i += 4) { const unsigned gid = binData[i] | binData[i + 1] << 8 | binData[i + 2] << 16 | binData[i + 3] << 24; setTile(map, layer, x, y, gid); x++; if (x == w) { x = 0; y++; // When we're done, don't crash on too much data if (y == h) break; } } free(binData); } } else if (encoding == "csv") { const auto data = childNode.textContent(); if (data.empty()) { logger->log("Error: CSV layer data is empty!"); continue; } auto *pos = data.data(); for (;;) { // Try to parse the next number at 'pos' errno = 0; char *end; unsigned gid = strtol(pos, &end, 10); if (pos == end) // No number found break; if (errno == ERANGE) { logger->log("Error: Range error in tile layer data!"); break; } setTile(map, layer, x, y, gid); x++; if (x == w) { x = 0; y++; // When we're done, don't crash on too much data if (y == h) break; } // Skip the comma, or break if we're done pos = strchr(end, ','); if (!pos) { logger->log("Error: CSV layer data too short!"); break; } ++pos; } } else { // Read plain XML map file for (auto childNode2 : childNode.children()) { if (childNode2.name() != "tile") continue; unsigned gid = childNode2.getProperty("gid", 0); setTile(map, layer, x, y, gid); x++; if (x == w) { x = 0; y++; if (y >= h) break; } } } if (y < h) std::cerr << "TOO SMALL!\n"; if (x) std::cerr << "TOO SMALL!\n"; // There can be only one data element break; } } /** * Reads a tile set. */ static Tileset *readTileset(XML::Node node, const std::string &path, Map *map) { const unsigned firstGid = node.getProperty("firstgid", 0); const int margin = node.getProperty("margin", 0); const int spacing = node.getProperty("spacing", 0); XML::Document *doc = nullptr; Tileset *set = nullptr; std::string pathDir(path); if (node.hasAttribute("source")) { std::string filename = node.getProperty("source", std::string()); filename = resolveRelativePath(path, filename); doc = new XML::Document(filename); node = doc->rootNode(); // Reset path to be realtive to the tsx file pathDir = filename.substr(0, filename.rfind("/") + 1); } const int tw = node.getProperty("tilewidth", map->getTileWidth()); const int th = node.getProperty("tileheight", map->getTileHeight()); for (auto childNode : node.children()) { if (childNode.name() == "image") { const auto source = childNode.getProperty("source", std::string()); if (!source.empty()) { std::string sourceStr = resolveRelativePath(pathDir, source); ResourceManager *resman = ResourceManager::getInstance(); auto tilebmp = resman->getImage(sourceStr); if (tilebmp) { set = new Tileset(tilebmp, tw, th, firstGid, margin, spacing); } else { logger->log("Warning: Failed to load tileset (%s)", source.c_str()); } } } else if (set && childNode.name() == "tile") { const int tileGID = firstGid + childNode.getProperty("id", 0); for (auto tileNode : childNode.children()) { if (tileNode.name() == "animation") readTileAnimation(tileNode, set, tileGID, map); } } } delete doc; return set; } static void readTileAnimation(XML::Node tileNode, Tileset *set, unsigned tileGID, Map *map) { Animation ani; for (auto frameNode : tileNode.children()) { if (frameNode.name() == "frame") { const int tileId = frameNode.getProperty("tileid", 0); const int duration = frameNode.getProperty("duration", 0); ani.addFrame(set->get(tileId), duration, 0, 0); } } if (ani.getLength() > 0) map->addAnimation(tileGID, TileAnimation(std::move(ani))); }