diff --git a/Cargo.toml b/Cargo.toml index 89c88e3..2d00d1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ hound = "3.5.0" pitch_shift = "1.0.0" linked_hash_set = "0.1.4" linked-hash-map = "0.5.6" -convert_case = "0.6.0" \ No newline at end of file +convert_case = "0.6.0" +roxmltree = "0.18.0" \ No newline at end of file diff --git a/src/reskit/cli/evaluator.rs b/src/reskit/cli/evaluator.rs index bc6c7c2..1330b70 100644 --- a/src/reskit/cli/evaluator.rs +++ b/src/reskit/cli/evaluator.rs @@ -1,6 +1,6 @@ use std::{error::Error, fs::File, io::Write, path::Path}; use clap::Parser; -use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::{print_good, print_error}}; +use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::{print_good, print_error}, level::converter::get_tiled_tilemap}; use super::settings::{Args, Tools, TileOutputFormat, TileOrder}; pub fn run_command() -> Result<(), Box> { @@ -65,8 +65,10 @@ pub fn run_command() -> Result<(), Box> { print_good( "all files converted successfully" ); } - Tools::Level { input_file, configuration_file, console, tile_size } => { + Tools::Level { input_file, output_directory, console, tile_size } => { + let tiled_file = get_tiled_tilemap( &input_file )?; + print_good( "file opened without errors" ); print_error( "unimplemented" ); } }; diff --git a/src/reskit/cli/settings.rs b/src/reskit/cli/settings.rs index c185a69..9b6d0e9 100644 --- a/src/reskit/cli/settings.rs +++ b/src/reskit/cli/settings.rs @@ -105,9 +105,9 @@ pub enum Tools { #[arg(short, long)] input_file: String, - /// Configuration file (TOML, see doc) - #[arg(short, long)] - configuration_file: String, + /// Output directory for artifacts + #[arg(short, long, default_value_t=String::from("./"))] + output_directory: String, /// Console system type #[arg(short, long, value_enum, default_value_t=SystemType::Md)] diff --git a/src/reskit/level/converter.rs b/src/reskit/level/converter.rs new file mode 100644 index 0000000..7d3ebd4 --- /dev/null +++ b/src/reskit/level/converter.rs @@ -0,0 +1,181 @@ +use std::{error::Error, fs::read_to_string}; +use image::DynamicImage; +use roxmltree::Node; + +use crate::reskit::utility::print_warning; + +pub struct TiledTilemap { + tileset: TiledTileset, + layers: Vec, + width: usize, + height: usize, + tile_width: usize, + tile_height: usize +} + +pub struct TiledTileset { + image: DynamicImage +} + +pub enum SystemPlane { + MdPlaneA, + MdPlaneB +} + +pub enum Layer { + Tile { + system_plane: SystemPlane, + tiles: Vec + }, + Collision { + tiles: Vec + } +} + +fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result, Box> { + let layer_id = layer.attribute( "id" ).ok_or( "invalid file: on layer: no id" )?; + let layer_name = layer.attribute( "name" ).unwrap_or( "" ); + let layer_name = format!( "({}, ID: {})", layer_name, layer_id ); + + // Validate layer is same as total map size + let ( layer_width, layer_height ): ( usize, usize ) = ( + layer.attribute( "width" ).ok_or( format!( "invalid file: on layer {}: no width attribute", layer_name ) )?.parse()?, + layer.attribute( "height" ).ok_or( format!( "invalid file: on layer {}: no height attribute", layer_name ) )?.parse()? + ); + + if layer_width != map_width || layer_height != map_height { + return Err( format!( "invalid file: on layer {}: layer width must match map width", layer_name ) )? + } + + let properties = layer.descendants().find( | node | node.tag_name() == "properties".into() ); + if let Some( properties ) = properties { + let properties: Vec = properties.descendants().filter( | node | { + if let Some( name ) = node.attribute( "name" ) { + name == "reskit-layer" + } else { + false + } + } ).collect(); + + // Should be either one or none + if let Some( layer_property ) = properties.first() { + let layer_type = layer_property.attribute( "value" ).ok_or( format!( "invalid file: on layer {}: no value for property", layer_name ) )?; + + let data = layer.descendants() + .find( | node | node.tag_name() == "data".into() ) + .ok_or( format!( "invalid file: on layer {}: no data for layer", layer_name ) )?; + + let encoding = data.attribute( "encoding" ).ok_or( format!( "invalid file: on layer {}: no encoding attribute", layer_name ) )?; + if encoding != "csv" { + return Err( format!( "invalid file: on layer {}: only csv is supported for layer encoding", layer_name ) )? + } + + let data: Vec<&str> = data.text().ok_or( format!( "invalid file: on layer {}: no layer data", layer_name ) )?.split( "," ).collect(); + let tiles: Vec = data.into_iter().map( | string | Ok( string.trim().parse()? ) ).collect::< Result< Vec, Box > >()?; + + match layer_type.to_lowercase().as_str() { + "a" => Ok( Some( Layer::Tile { + system_plane: SystemPlane::MdPlaneA, + tiles + } ) ), + "b" => Ok( Some( Layer::Tile { + system_plane: SystemPlane::MdPlaneB, + tiles + } ) ), + "collision" => Ok( Some( Layer::Collision { tiles } ) ), + _ => { + print_warning( &format!( "on layer {}: invalid reskit-layer value {}; ignoring this layer", layer_name, layer_type ) ); + Ok( None ) + } + } + } else { + print_warning( &format!( "on layer {}: no reskit-layer property defining hardware layer or collision; ignoring this layer", layer_name ) ); + Ok( None ) + } + } else { + print_warning( &format!( "on layer {}: no properties defining hardware layer or collision; ignoring this layer", layer_name ) ); + Ok( None ) + } +} + +pub fn get_tiled_tilemap( path: &str ) -> Result> { + let file = read_to_string( path )?; + let document = roxmltree::Document::parse( &file )?; + + let map = document.descendants().find( | node | node.tag_name() == "map".into() ); + if let Some( map ) = map { + // Validate version + let version = map.attribute( "version" ).ok_or( "invalid file: no version attribute" )?; + if version < "1.10" || !version.starts_with( "1." ) { + return Err( "invalid file: unsupported version" )? + } + + // Validate orientation and render order + let orientation = map.attribute( "orientation" ).ok_or( "invalid file: no orientation attribute" )?; + if orientation != "orthogonal" { + return Err( "invalid file: only orthogonal orientation is supported" )? + } + let render_order = map.attribute( "renderorder" ).ok_or( "invalid file: no renderorder attribute" )?; + if render_order != "left-down" { + return Err( "invalid file: only left-down orientation is supported" )? + } + + let ( width, height ): ( usize, usize ) = ( + map.attribute( "width" ).ok_or( "invalid file: no width attribute" )?.parse()?, + map.attribute( "height" ).ok_or( "invalid file: no height attribute" )?.parse()? + ); + + let ( tile_width, tile_height ): ( usize, usize ) = ( + map.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?, + map.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()? + ); + + // Build tileset (current version assumes one tileset per level) + let tileset = map.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset" )?; + let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?; + let tileset_file = read_to_string( tileset_source_path )?; + let tileset_document = roxmltree::Document::parse( &tileset_file )?; + let tileset = tileset_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset origin object" )?; + + // Validate referenced tileset dimensions match map dimensions + // E.g. if your .tmx specifies 16x16, the acompanying .tsx file should agree + let ( tsx_tile_width, tsx_tile_height ): ( usize, usize ) = ( + tileset.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?, + tileset.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()? + ); + if tsx_tile_width != tile_width || tsx_tile_height != tile_height { + return Err( "invalid file: referenced tilemap doesn't have same per-tile width and height of parent tilemap" )? + } + + // Get the image for the tileset + let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?; + let image = image::open( image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )? )?; + + // Get the layers + let layers: Vec = map.descendants() + .filter( | node | node.tag_name() == "layer".into() ) + .map( | node | get_layer( node, width, height ) ) + .collect::< Result< Vec>, Box > >()? + .into_iter() + .filter_map( | option | option ) + .collect(); + + // Print warning if there is no collision layer + if let None = layers.iter().find( | layer | matches!( layer, Layer::Collision { tiles: _ } ) ) { + print_warning( "tile map has no \"collision\" layer: this probably is not what you want" ); + } + + Ok( + TiledTilemap { + tileset: TiledTileset { image }, + layers, + width, + height, + tile_width, + tile_height + } + ) + } else { + Err( "invalid file: this does not appear to be valid Tiled .tmx file" )? + } +} \ No newline at end of file diff --git a/src/reskit/level/mod.rs b/src/reskit/level/mod.rs new file mode 100644 index 0000000..9c6a24e --- /dev/null +++ b/src/reskit/level/mod.rs @@ -0,0 +1 @@ +pub mod converter; diff --git a/src/reskit/mod.rs b/src/reskit/mod.rs index 5ca7637..196bf5f 100644 --- a/src/reskit/mod.rs +++ b/src/reskit/mod.rs @@ -1,4 +1,5 @@ pub mod cli; +pub mod level; pub mod soundtrack; pub mod tileset; pub mod utility; \ No newline at end of file