view tools/tileset/main.cpp @ 189:f28cb6d04731

Misc: extreme refactoring
author David Demelier <markand@malikania.fr>
date Thu, 25 Oct 2018 21:36:14 +0200
parents 3107ce017c3a
children 61dd98874d82
line wrap: on
line source

/*
 * main.cpp -- create malikania tilesets from tiled
 *
 * Copyright (c) 2013-2018 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.
 */
auto load_tileset(const fs::path& path) -> ptree::ptree
{
	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.
		const 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>
 */
auto read_tileset_properties(const ptree::ptree& tree) -> nlohmann::json
{
	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.
 */
auto read_tileset_collision(const ptree::ptree& tree) -> nlohmann::json
{
	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.
 */
auto read_tileset_collision_polyline(const ptree::ptree& tree) -> nlohmann::json
{
	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;
	}
}