view tools/tileset/main.cpp @ 140:a83fff870983

Client: prepare state, #712 Add update and draw function in states to allow future development of predefined states.
author David Demelier <markand@malikania.fr>
date Wed, 27 Sep 2017 21:02:41 +0200
parents 0addfab87b17
children 4b292c20124c
line wrap: on
line source

/*
 * main.cpp -- create malikania tilesets 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.
 */

#include <cassert>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
#include <stdexcept>

#include <boost/filesystem.hpp>
#include <boost/property_tree/xml_parser.hpp>

#include <json.hpp>

#include <malikania/util.hpp>

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

namespace {

/*
 * load_tileset
 * ------------------------------------------------------------------
 *
 * In tiled maps, tilesets may be bundled in the map itself or can be in an
 * other file referenced by a path.
 *
 * This function check for the two case and return the XML data.
 */
ptree::ptree load_tileset(const fs::path& path)
{
    ptree::ptree xml;

    ptree::read_xml(path.string(), xml);

    if (path.extension() == ".tmx") {
        auto tileset = xml.get_child_optional("map.tileset");

        if (!tileset)
            throw std::runtime_error("no <tileset> node found");

        // Check if it's external or embeded.
        auto source = tileset->get_optional<std::string>("<xmlattr>.source");

        if (source) {
            fs::path source_path(*source);

            if (source_path.is_relative())
                source_path = path.parent_path() / *source;

            ptree::read_xml(source_path.string(), xml);

            xml = xml.get_child("tileset");
        } else
            xml = *tileset;
    } else
        xml = xml.get_child("tileset");

    return xml;
}

/*
 * read_tileset_properties
 * ------------------------------------------------------------------
 *
 * Extract all properties from the XML and return a JSON object.
 *
 * The XML looks like this:
 *
 * <tile id="123">
 *      <properties>
 *          <property name="abc" value"def" />
 *      </properties>
 * </tile>
 */
nlohmann::json read_tileset_properties(const ptree::ptree& tree)
{
    auto properties = tree.get_child_optional("properties");
    auto json = nlohmann::json::object();

    if (properties) {
        for (const auto& prop : *properties) {
            auto name = prop.second.get<std::string>("<xmlattr>.name");
            auto value = prop.second.get<std::string>("<xmlattr>.value");

            json[name] = value;
        }
    }

    return json;
}

/*
 * save_tileset_properties
 * ------------------------------------------------------------------
 *
 * Read properties and add them to the JSON tile description.
 */
void save_tileset_properties(nlohmann::json& tiles,
                             const std::string& id,
                             const ptree::ptree& tree)
{
    tiles[id]["properties"] = read_tileset_properties(tree);
}

/*
 * read_tileset_collisions
 * ------------------------------------------------------------------
 *
 * Extract the collisions from the XML object description.
 *
 * This function is used for rectangle and ellipses because they use the same
 * attributes.
 */
nlohmann::json read_tileset_collision(const ptree::ptree& tree)
{
    auto ret = nlohmann::json::object({
        { "x", tree.get<double>("<xmlattr>.x") },
        { "y", tree.get<double>("<xmlattr>.y") },
        { "width", tree.get<double>("<xmlattr>.width") },
        { "height", tree.get<double>("<xmlattr>.height") }
    });

    if (tree.count("ellipse") > 0)
        ret["type"] = "ellipse";
    else
        ret["type"] = "rectangle";

    return ret;
}

/*
 * read_tileset_collision_polyline
 * ------------------------------------------------------------------
 *
 * Extract the collision description from a object of type polyline.
 */
nlohmann::json read_tileset_collision_polyline(const ptree::ptree& tree)
{
    auto points = tree.get<std::string>("polyline.<xmlattr>.points");
    auto array = nlohmann::json::array();

    for (const auto& p : mlk::util::split(points, " "))
        array.push_back(std::stod(p));

    return {
        { "type", "polyline" },
        { "points", std::move(array) }
    };
}

/*
 * save_tileset_collisions
 * ------------------------------------------------------------------
 *
 * Read the collisions from the given XML tree and save them into the JSON
 * description.
 */
void save_tileset_collisions(nlohmann::json& tiles,
                             const std::string& id,
                             const ptree::ptree& tree)
{
    auto objectgroup = tree.get_child_optional("objectgroup");

    if (!objectgroup)
        return;

    auto array = nlohmann::json::array();

    for (const auto& obj : *objectgroup) {
        if (obj.first != "object")
            continue;

        nlohmann::json collision;

        /*
         * Ellipse and rectangles are defined the same way, only check for
         * polylines at the moment.
         */
        if (obj.second.count("polyline") > 0)
            collision = read_tileset_collision_polyline(obj.second);
        else
            collision = read_tileset_collision(obj.second);

        array.push_back(collision);
    }

    tiles[id]["collisions"] = std::move(array);
}

/*
 * save_tileset
 * ------------------------------------------------------------------
 *
 * Save the tileset at the given path from the XML description.
 */
void save_tileset(const fs::path& path, const ptree::ptree& xml)
{
    std::ofstream out(path.string(), std::ofstream::trunc);

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

    nlohmann::json json{
        { "image", xml.get<std::string>("image.<xmlattr>.source") },
        {
            "cell", {
                { "width", xml.get<int>("<xmlattr>.tilewidth"),     },
                { "height", xml.get<int>("<xmlattr>.tileheight")    }
            },
        }
    };

    nlohmann::json tiles = nlohmann::json::object();

    for (const auto& p : xml) {
        if (p.first == "tile") {
            auto id = p.second.get<std::string>("<xmlattr>.id");

            save_tileset_properties(tiles, id, p.second);
            save_tileset_collisions(tiles, id, p.second);
        }
    }

    if (!tiles.empty())
        json["tiles"] = tiles;

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

} // !namespace

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

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

    try {
        fs::path input(argv[0]);
        fs::path output;

        if (argc >= 2)
            output = argv[1];
        else
            output = fs::path(input).replace_extension(".json");

        save_tileset(output, load_tileset(input));
    } catch (const std::exception& ex) {
        std::cerr << ex.what() << std::endl;
        return 1;
    }
}