|
|
|
@ -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<Layer>,
|
|
|
|
|
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<u32>
|
|
|
|
|
},
|
|
|
|
|
Collision {
|
|
|
|
|
tiles: Vec<u32>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result<Option<Layer>, Box<dyn Error>> {
|
|
|
|
|
let layer_id = layer.attribute( "id" ).ok_or( "invalid file: on layer: no id" )?;
|
|
|
|
|
let layer_name = layer.attribute( "name" ).unwrap_or( "<no name>" );
|
|
|
|
|
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<Node> = 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<u32> = data.into_iter().map( | string | Ok( string.trim().parse()? ) ).collect::< Result< Vec<u32>, Box<dyn Error> > >()?;
|
|
|
|
|
|
|
|
|
|
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<TiledTilemap, Box<dyn Error>> {
|
|
|
|
|
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<Layer> = map.descendants()
|
|
|
|
|
.filter( | node | node.tag_name() == "layer".into() )
|
|
|
|
|
.map( | node | get_layer( node, width, height ) )
|
|
|
|
|
.collect::< Result< Vec<Option<Layer>>, Box<dyn Error> > >()?
|
|
|
|
|
.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" )?
|
|
|
|
|
}
|
|
|
|
|
}
|