diff options
Diffstat (limited to 'src/resources/mapreader.cpp')
-rw-r--r-- | src/resources/mapreader.cpp | 723 |
1 files changed, 723 insertions, 0 deletions
diff --git a/src/resources/mapreader.cpp b/src/resources/mapreader.cpp new file mode 100644 index 000000000..cf26830f2 --- /dev/null +++ b/src/resources/mapreader.cpp @@ -0,0 +1,723 @@ +/* + * The Mana Client + * Copyright (C) 2004-2009 The Mana World Development Team + * Copyright (C) 2009-2010 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 <http://www.gnu.org/licenses/>. + */ + +#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/gettext.h" +#include "utils/stringutils.h" +#include "utils/xml.h" + +#include <cassert> +#include <iostream> +#include <zlib.h> + +int inflateMemory(unsigned char *in, unsigned int inLength, + unsigned char *&out, unsigned int &outLength); + +int inflateMemory(unsigned char *in, unsigned int inLength, + unsigned char *&out); + +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; +} + +/** + * Inflates either zlib or gzip deflated memory. The inflated memory is + * expected to be freed by the caller. + */ +int inflateMemory(unsigned char *in, unsigned int inLength, + unsigned char *&out, unsigned int &outLength) +{ + int bufferSize = 256 * 1024; + int ret; + z_stream strm; + + out = (unsigned char*) malloc(bufferSize); + + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.next_in = in; + strm.avail_in = inLength; + strm.next_out = out; + strm.avail_out = bufferSize; + + ret = inflateInit2(&strm, 15 + 32); + + if (ret != Z_OK) + return ret; + + do + { + if (strm.next_out == NULL) + { + inflateEnd(&strm); + return Z_MEM_ERROR; + } + + ret = inflate(&strm, Z_NO_FLUSH); + assert(ret != Z_STREAM_ERROR); + + switch (ret) + { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; + case Z_DATA_ERROR: + case Z_MEM_ERROR: + (void) inflateEnd(&strm); + return ret; + default: + break; + } + + if (ret != Z_STREAM_END) + { + out = (unsigned char*) realloc(out, bufferSize * 2); + + if (out == NULL) + { + inflateEnd(&strm); + return Z_MEM_ERROR; + } + + strm.next_out = out + bufferSize; + strm.avail_out = bufferSize; + bufferSize *= 2; + } + } + while (ret != Z_STREAM_END); + assert(strm.avail_in == 0); + + outLength = bufferSize - strm.avail_out; + (void) inflateEnd(&strm); + return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; +} + +int inflateMemory(unsigned char *in, unsigned int inLength, + unsigned char *&out) +{ + unsigned int outLength = 0; + int ret = inflateMemory(in, inLength, out, outLength); + + if (ret != Z_OK || out == NULL) + { + if (ret == Z_MEM_ERROR) + { + logger->log1("Error: Out of memory while decompressing map data!"); + } + else if (ret == Z_VERSION_ERROR) + { + logger->log1("Error: Incompatible zlib version!"); + } + else if (ret == Z_DATA_ERROR) + { + logger->log1("Error: Incorrect zlib compressed data!"); + } + else + { + logger->log1("Error: Unknown error while decompressing map data!"); + } + + free(out); + out = NULL; + outLength = 0; + } + + return outLength; +} + +Map *MapReader::readMap(const std::string &filename) +{ + logger->log("Attempting to read map %s", filename.c_str()); + // Load the file through resource manager + ResourceManager *resman = ResourceManager::getInstance(); + int fileSize; + void *buffer = resman->loadFile(filename, fileSize); + Map *map = NULL; + + if (buffer == NULL) + { + logger->log("Map file not found (%s)", filename.c_str()); + return NULL; + } + + unsigned char *inflated; + unsigned int inflatedSize; + + if (filename.find(".gz", filename.length() - 3) != std::string::npos) + { + // Inflate the gzipped map data + inflatedSize = + inflateMemory((unsigned char*) buffer, fileSize, inflated); + free(buffer); + + if (inflated == NULL) + { + logger->log("Could not decompress map file (%s)", + filename.c_str()); + return NULL; + } + } + else + { + inflated = (unsigned char*) buffer; + inflatedSize = fileSize; + } + + XML::Document doc((char*) inflated, inflatedSize); + free(inflated); + + xmlNodePtr node = doc.rootNode(); + + // Parse the inflated map data + if (node) + { + if (!xmlStrEqual(node->name, BAD_CAST "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(xmlNodePtr node, const std::string &path) +{ + // Take the filename off the path + const std::string pathDir = path.substr(0, path.rfind("/") + 1); + + const int w = XML::getProperty(node, "width", 0); + const int h = XML::getProperty(node, "height", 0); + const int tilew = XML::getProperty(node, "tilewidth", -1); + const int tileh = XML::getProperty(node, "tileheight", -1); + + bool showWarps = config.getBoolValue("warpParticle"); + const std::string warpPath = paths.getStringValue("particles") + + paths.getStringValue("portalEffectFile"); + + if (tilew < 0 || tileh < 0) + { + logger->log("MapReader: Warning: " + "Unitialized tile width or height value for map: %s", + path.c_str()); + return 0; + } + + Map *map = new Map(w, h, tilew, tileh); + + for_each_xml_child_node(childNode, node) + { + if (xmlStrEqual(childNode->name, BAD_CAST "tileset")) + { + Tileset *tileset = readTileset(childNode, pathDir, map); + if (tileset) + { + map->addTileset(tileset); + } + } + else if (xmlStrEqual(childNode->name, BAD_CAST "layer")) + { + readLayer(childNode, map); + } + else if (xmlStrEqual(childNode->name, BAD_CAST "properties")) + { + readProperties(childNode, map); + } + else if (xmlStrEqual(childNode->name, BAD_CAST "objectgroup")) + { + // The object group offset is applied to each object individually + const int tileOffsetX = XML::getProperty(childNode, "x", 0); + const int tileOffsetY = XML::getProperty(childNode, "y", 0); + const int offsetX = tileOffsetX * tilew; + const int offsetY = tileOffsetY * tileh; + + for_each_xml_child_node(objectNode, childNode) + { + if (xmlStrEqual(objectNode->name, BAD_CAST "object")) + { + std::string objType = XML::getProperty( + objectNode, "type", ""); + + objType = toUpper(objType); + +/* + if (objType == "NPC" || + objType == "SCRIPT") + { + logger->log("hidden obj: " + objType); + // Silently skip server-side objects. + continue; + } +*/ + + const std::string objName = XML::getProperty( + objectNode, "name", ""); + const int objX = XML::getProperty(objectNode, "x", 0); + const int objY = XML::getProperty(objectNode, "y", 0); + const int objW = XML::getProperty(objectNode, "width", 0); + const int objH = XML::getProperty(objectNode, "height", 0); + + logger->log("- Loading object name: %s type: %s at %d:%d" + " (%dx%d)", objName.c_str(), objType.c_str(), + objX, objY, objW, objH); + + if (objType == "PARTICLE_EFFECT") + { + if (objName.empty()) + { + logger->log1(" Warning: No particle file given"); + continue; + } + + map->addParticleEffect(objName, + objX + offsetX, + objY + offsetY, + objW, objH); + } + else if (objType == "WARP") + { + if (showWarps) + { + map->addParticleEffect(warpPath, + objX, objY, objW, objH); + } + map->addPortal(objName, MapItem::PORTAL, + objX, objY, objW, objH); + } + else if (objType == "SPAWN") + { +// map->addPortal(_("Spawn: ") + objName, MapItem::PORTAL, +// objX, objY, objW, objH); + } + else + { + logger->log1(" Warning: Unknown object type"); + } + } + } + } + } + + map->initializeAmbientLayers(); + + return map; +} + +void MapReader::readProperties(xmlNodePtr node, Properties *props) +{ + if (!props) + return; + + for_each_xml_child_node(childNode, node) + { + if (!xmlStrEqual(childNode->name, BAD_CAST "property")) + continue; + + // Example: <property name="name" value="value"/> + const std::string name = XML::getProperty(childNode, "name", ""); + const std::string value = XML::getProperty(childNode, "value", ""); + + if (!name.empty() && !value.empty()) + props->setProperty(name, value); + } +} + +static void setTile(Map *map, MapLayer *layer, int x, int y, int gid) +{ + const Tileset * const set = map->getTilesetWithGid(gid); + if (layer) + { + // Set regular tile on a layer + Image * const img = set ? set->get(gid - set->getFirstGid()) : 0; + layer->setTile(x, y, img); + } + else + { + // Set collision tile +// if (set && (gid - set->getFirstGid() == 1)) buggy update + if (set && (gid - set->getFirstGid() != 0)) + map->blockTile(x, y, Map::BLOCKTYPE_WALL); + } +} + +void MapReader::readLayer(xmlNodePtr node, Map *map) +{ + // Layers are not necessarily the same size as the map + const int w = XML::getProperty(node, "width", map->getWidth()); + const int h = XML::getProperty(node, "height", map->getHeight()); + const int offsetX = XML::getProperty(node, "x", 0); + const int offsetY = XML::getProperty(node, "y", 0); + std::string name = XML::getProperty(node, "name", ""); + name = toLower(name); + + const bool isFringeLayer = (name.substr(0, 6) == "fringe"); + const bool isCollisionLayer = (name.substr(0, 9) == "collision"); + + MapLayer *layer = 0; + + if (!isCollisionLayer) + { + layer = new MapLayer(offsetX, offsetY, w, h, isFringeLayer); + map->addLayer(layer); + } + + logger->log("- Loading layer \"%s\"", name.c_str()); + int x = 0; + int y = 0; + + // Load the tile data + for_each_xml_child_node(childNode, node) + { + if (!xmlStrEqual(childNode->name, BAD_CAST "data")) + continue; + + const std::string encoding = + XML::getProperty(childNode, "encoding", ""); + const std::string compression = + XML::getProperty(childNode, "compression", ""); + + if (encoding == "base64") + { + if (!compression.empty() && compression != "gzip") + { + logger->log1("Warning: only gzip layer" + " compression supported!"); + return; + } + + // Read base64 encoded map file + xmlNodePtr dataChild = childNode->xmlChildrenNode; + if (!dataChild) + continue; + + int len = static_cast<int>(strlen( + (const char*)dataChild->content) + 1); + unsigned char *charData = new unsigned char[len + 1]; + const char *charStart = (const char*)dataChild->content; + 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, + static_cast<int>(strlen((char*)charData)), &binLen); + + delete[] charData; + + if (binData) + { + if (compression == "gzip") + { + // Inflate the gzipped layer data + unsigned char *inflated; + unsigned int inflatedSize = + inflateMemory(binData, binLen, inflated); + + free(binData); + binData = inflated; + binLen = inflatedSize; + + if (!inflated) + { + logger->log1("Error: Could not decompress layer!"); + return; + } + } + + for (int i = 0; i < binLen - 3; i += 4) + { + const int gid = binData[i] | + binData[i + 1] << 8 | + binData[i + 2] << 16 | + binData[i + 3] << 24; + + setTile(map, layer, x, y, gid); + + TileAnimation* ani = map->getAnimationForGid(gid); + if (ani) + ani->addAffectedTile(layer, x + y * w); + + 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 + { + // Read plain XML map file + for_each_xml_child_node(childNode2, childNode) + { + if (!xmlStrEqual(childNode2->name, BAD_CAST "tile")) + continue; + + const int gid = XML::getProperty(childNode2, "gid", -1); + 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; + } + +/* + if (!layer) + return; + + for (int y = 0; y < layer->getHeight(); y ++) + { + for (int x = 0 ; x < layer->getWidth() ; x ++) + { + int width; + int c = layer->getTileDrawWidth(x, y, layer->getWidth(), width); + layer->setTileInfo(x, y, width, c); + } + } +*/ + +/* + Image *img1 = 0; + for (int y = 0; y < layer->getHeight(); y ++) + { + int skipWidth = 0; + int skipCount = 0; + img1 = layer->getTile(0, y); + layer->setTileInfo(layer->getWidth() - 1, y, skipWidth, skipCount); + for (int x = layer->getWidth() - 1 ; x > 0 ; x --) + { + Image *img = layer->getTile(x, y); + if (img) + { + if (img != img1) + { // different tile + skipWidth = 0; + skipCount = 0; + } + else + { // same tile + skipWidth += img1->getWidth(); + skipCount ++; + } + } + else + { + skipWidth = 0; + skipCount = 0; + } + layer->setTileInfo(x, y, skipWidth, skipCount); + img1 = img; + } + } +*/ +} + +Tileset *MapReader::readTileset(xmlNodePtr node, const std::string &path, + Map *map) +{ + if (!map) + return NULL; + + int firstGid = XML::getProperty(node, "firstgid", 0); + int margin = XML::getProperty(node, "margin", 0); + int spacing = XML::getProperty(node, "spacing", 0); + XML::Document* doc = NULL; + Tileset *set = NULL; + std::string pathDir(path); + + if (xmlHasProp(node, BAD_CAST "source")) + { + std::string filename = XML::getProperty(node, "source", ""); + 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 = XML::getProperty(node, "tilewidth", map->getTileWidth()); + const int th = XML::getProperty(node, "tileheight", map->getTileHeight()); + + for_each_xml_child_node(childNode, node) + { + if (xmlStrEqual(childNode->name, BAD_CAST "image")) + { + const std::string source = XML::getProperty( + childNode, "source", ""); + + if (!source.empty()) + { + std::string sourceStr = resolveRelativePath(pathDir, source); + + ResourceManager *resman = ResourceManager::getInstance(); + Image* tilebmp = resman->getImage(sourceStr); + + if (tilebmp) + { + set = new Tileset(tilebmp, tw, th, firstGid, margin, + spacing); + tilebmp->decRef(); + } + else + { + logger->log("Warning: Failed to load tileset (%s)", + source.c_str()); + } + } + } + else if (xmlStrEqual(childNode->name, BAD_CAST "tile")) + { + for_each_xml_child_node(tileNode, childNode) + { + if (!xmlStrEqual(tileNode->name, BAD_CAST "properties")) + continue; + + int tileGID = firstGid + XML::getProperty(childNode, "id", 0); + + // read tile properties to a map for simpler handling + std::map<std::string, int> tileProperties; + for_each_xml_child_node(propertyNode, tileNode) + { + if (!xmlStrEqual(propertyNode->name, BAD_CAST "property")) + continue; + std::string name = XML::getProperty( + propertyNode, "name", ""); + int value = XML::getProperty(propertyNode, "value", 0); + tileProperties[name] = value; + logger->log("Tile Prop of %d \"%s\" = \"%d\"", + tileGID, name.c_str(), value); + } + + // create animation + if (!set) + continue; + + Animation *ani = new Animation; + for (int i = 0; ; i++) + { + std::map<std::string, int>::iterator iFrame, iDelay; + iFrame = tileProperties.find( + "animation-frame" + toString(i)); + iDelay = tileProperties.find( + "animation-delay" + toString(i)); + if (iFrame != tileProperties.end() + && iDelay != tileProperties.end()) + { + ani->addFrame(set->get(iFrame->second), + iDelay->second, 0, 0); + } + else + { + break; + } + } + + if (ani->getLength() > 0) + { + map->addAnimation(tileGID, new TileAnimation(ani)); + logger->log("Animation length: %d", ani->getLength()); + } + else + { + delete ani; + ani = 0; + } + } + } + } + + delete doc; + + return set; +} |