view tools/tileset/main.cpp @ 182:3107ce017c3a

Misc: switch back to SDL Qt Quick and QML was an exciting experiment but it's definitely not enough flexible and easy to use for game development. Using SDL2 will let us focusing on our own drawing functions without any kind of overhead. While here, start massive cleanup.
author David Demelier <markand@malikania.fr>
date Fri, 19 Oct 2018 20:18:19 +0200
parents 4b292c20124c
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;
	}
}