view tools/map/main.cpp @ 123:fa7466083ba2

Server: get rid of database folder
author David Demelier <markand@malikania.fr>
date Fri, 22 Sep 2017 12:39:00 +0200
parents 0bedc450a9d2
children 4b292c20124c
line wrap: on
line source

/*
 * main.cpp -- create malikania maps from tiled
 *
 * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * For information about the tiled .tmx format, check the following link:
 *
 * http://doc.mapeditor.org/reference/tmx-map-format/
 */

#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>

#include <boost/algorithm/string/trim.hpp>
#include <boost/endian/conversion.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/format.hpp>
#include <boost/iostreams/copy.hpp>
#include <boost/iostreams/device/back_inserter.hpp>
#include <boost/iostreams/filter/gzip.hpp>
#include <boost/iostreams/filter/zlib.hpp>
#include <boost/iostreams/filtering_streambuf.hpp>
#include <boost/property_tree/xml_parser.hpp>

#include <json.hpp>

#include "base64.hpp"

namespace fs = boost::filesystem;
namespace io = boost::iostreams;
namespace ptree = boost::property_tree;

namespace mlk {

namespace {

/*
 * data
 * ------------------------------------------------------------------
 *
 * Store map data.
 */
class data {
public:
    std::string title;
    std::string tileset;
    std::uint64_t width{0};
    std::uint64_t height{0};
    std::uint8_t cell_width{0};
    std::uint8_t cell_height{0};
    std::vector<std::uint32_t> background;
    std::vector<std::uint32_t> foreground;
};

/*
 * load_properties
 * ------------------------------------------------------------------
 *
 * Extract the following properties from the map:
 *
 *  <map>
 *      <properties>
 *          <property name="title" value="..." />
 *      </properties>
 *  </map>
 */
void load_properties(data& result, const ptree::ptree& tree)
{
    for (const auto& p : tree) {
        if (p.first != "property") {
            continue;
        }

        auto name = p.second.get<std::string>("<xmlattr>.name");

        if (name == "title") {
            result.title = p.second.get<std::string>("<xmlattr>.value");
        }
    }
}

/*
 * load_layer_data_xml
 * ------------------------------------------------------------------
 *
 * Convert the <data> node when saved as XML.
 */
std::vector<std::uint32_t> load_layer_data_xml(const ptree::ptree& tree)
{
    std::vector<std::uint32_t> data;

    for (const auto& t : tree) {
        data.push_back(t.second.get<int>("<xmlattr>.gid"));
    }

    return data;
}

/*
 * load_layer_data_csv
 * ------------------------------------------------------------------
 *
 * Convert the <data> node when saved as CSV.
 */
std::vector<std::uint32_t> load_layer_data_csv(const std::string& csv)
{
    std::istringstream iss(csv);
    std::vector<std::uint32_t> data;

    for (std::string line; std::getline(iss, line, ','); ) {
        data.push_back(std::stoi(line));
    }

    return data;
}

/*
 * load_layer_data_array
 * ------------------------------------------------------------------
 *
 * Convert the <data> node when saved as uncompressed base64 data.
 *
 * The data is an array of 32 bits unsigned int in the little endian format.
 */
std::vector<std::uint32_t> load_layer_data_array(std::string bytes)
{
    std::uint32_t* array = reinterpret_cast<std::uint32_t*>(&bytes[0]);
    std::vector<std::uint32_t> data;

    for (auto ptr = array; ptr != &array[bytes.length() / 4]; ++ptr) {
        data.push_back(*ptr);
    }

    return data;
}

/*
 * load_layer_data_array_compressed
 * ------------------------------------------------------------------
 *
 * Decompress the data before parsing, supports zlib and gzip.
 */
std::vector<std::uint32_t> load_layer_data_array_compressed(const std::string& bytes,
                                                            const std::string& compression)
{
    io::filtering_streambuf<io::input> in;
    std::string out;

    if (compression == "zlib") {
        in.push(io::zlib_decompressor());
    } else {
        in.push(io::gzip_decompressor());
    }

    in.push(boost::make_iterator_range(bytes));
    io::copy(in, io::back_inserter(out));

    return load_layer_data_array(std::move(out));
}

/*
 * load_layer_data
 * ------------------------------------------------------------------
 *
 * Extract the <data> node from a <layer>
 */
std::vector<std::uint32_t> load_layer_data(const ptree::ptree& tree)
{
    auto encoding = tree.get_optional<std::string>("<xmlattr>.encoding");
    auto compression = tree.get_optional<std::string>("<xmlattr>.compression");
    std::vector<std::uint32_t> data;

    if (!encoding) {
        data = load_layer_data_xml(tree);
    } else if (*encoding == "csv") {
        data = load_layer_data_csv(boost::trim_copy(tree.get_value<std::string>()));
    } else if (*encoding == "base64") {
        auto b64 = base64::decode(boost::trim_copy(tree.get_value<std::string>()));

        if (!compression) {
            data = load_layer_data_array(std::move(b64));
        } else if (*compression == "zlib" || *compression == "gzip") {
            data = load_layer_data_array_compressed(b64, *compression);
        }

        // Convert endianness if system is not little endian.
        if (boost::endian::order::native != boost::endian::order::little) {
            std::transform(data.begin(), data.end(), data.begin(), [] (auto c) {
                return boost::endian::endian_reverse(c);
            });
        }
    }

    return data;
}

/*
 * load_layer
 * ------------------------------------------------------------------
 *
 * Extract the following properties from the map:
 *
 *  <map>
 *      <layer>
 *          <data></data>
 *      </layer>
 *  </map>
 */
void load_layer(data& map, const ptree::ptree& tree)
{
    auto name = tree.get<std::string>("<xmlattr>.name");

    if (name != "background" && name != "foreground") {
        throw std::runtime_error(boost::str(boost::format(
            "invalid layer name given '%s'\n"
            "(only 'foreground' and 'background' is supported)"
        ) % name));
    }

    auto data = load_layer_data(tree.get_child("data"));

    if (name == "background") {
        map.background = std::move(data);
    } else {
        map.foreground = std::move(data);
    }
}

/*
 * load
 * ------------------------------------------------------------------
 *
 * Load the .tmx file from the path and return it.
 */
data load(const std::string& path)
{
    data result;
    ptree::ptree tree;
    ptree::read_xml(path, tree);

    result.width = tree.get<int>("map.<xmlattr>.width");
    result.height = tree.get<int>("map.<xmlattr>.height");
    result.cell_height = tree.get<int>("map.<xmlattr>.tilewidth");
    result.cell_width = tree.get<int>("map.<xmlattr>.tileheight");
    result.tileset = tree.get<std::string>("map.tileset.image.<xmlattr>.source");

    for (const auto& pair : tree.get_child("map")) {
        if (pair.first == "properties") {
            load_properties(result, pair.second);
        } else if (pair.first == "layer") {
            load_layer(result, pair.second);
        }
    }

    return result;
}

/*
 * save_meta
 * ------------------------------------------------------------------
 *
 * Save map metadata into the specified file.
 */
void save_meta(const data& map, const fs::path& path)
{
    auto json = nlohmann::json::object({
        { "title", map.title },
        { "tileset", map.tileset },
        { "size", { map.width, map.height } },
        { "cell", { map.cell_width, map.cell_height } },
    });

    std::ofstream out(path.string(), std::ofstream::out | std::ofstream::trunc);

    if (!out) {
        throw std::runtime_error(std::strerror(errno));
    }

    out << json.dump(4) << std::endl;
}

/*
 * save_map
 * ------------------------------------------------------------------
 *
 * Save the map tileset info the specified path.
 */
void save_map(const data& map, const fs::path& path)
{
    std::ofstream out(path.string(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);

    if (!out) {
        throw std::runtime_error(std::strerror(errno));
    }

    out.write(
        reinterpret_cast<const char*>(map.background.data()),
        sizeof (map.background[0]) * map.background.size()
    );
    out.write(
        reinterpret_cast<const char*>(map.foreground.data()),
        sizeof (map.foreground[0]) * map.foreground.size()
    );
}

/*
 * save
 * ------------------------------------------------------------------
 *
 * Save the map data into the JSON file and map data.
 *
 * Check MAP.md for the specifications of the JSON file.
 */
void save(const data& map, const fs::path& meta, const fs::path& pack)
{
    save_meta(map, meta);
    save_map(map, pack);
}

} // !namespace

} // !mlk

int main(int argc, char** argv)
{
    -- argc;
    ++ argv;

    if (argc < 1) {
        std::cerr << "usage mlk-map input.xml [output.json [output.map]]" << std::endl;
        return 1;
    }

    try {
        fs::path meta, map;

        if (argc >= 3) {
            meta = argv[1];
            map = argv[2];
        } else if (argc >= 2) {
            meta = argv[1];
            map = meta.replace_extension(".map");
        } else {
            meta = fs::path(argv[0]).replace_extension(".json");
            map = fs::path(argv[0]).replace_extension(".map");
        }

        mlk::save(mlk::load(argv[0]), meta, map);
    } catch (const std::exception& ex) {
        std::cerr << ex.what() << std::endl;
        return 1;
    }
}