496 lines
25 KiB
Rust
496 lines
25 KiB
Rust
use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow, collections::HashSet};
|
|
use euclid::default::Rect;
|
|
use image::{DynamicImage, GenericImageView};
|
|
use linked_hash_map::LinkedHashMap;
|
|
use roxmltree::Node;
|
|
use crate::reskit::{utility::print_warning, cli::settings::TileOrder};
|
|
|
|
#[derive(Debug)]
|
|
pub struct TiledTilemap {
|
|
pub tileset: Vec<TiledTileset>,
|
|
pub metatiles: Vec<Metatile>,
|
|
pub layers: Vec<Layer>,
|
|
pub objects: Vec<Object>,
|
|
pub collision: Vec<Rect<u16>>,
|
|
pub width: usize,
|
|
pub height: usize
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct Metatile {
|
|
pub id: u32,
|
|
pub source: String,
|
|
pub width: u16,
|
|
pub height: u16,
|
|
pub tiles: Vec<u32>
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Object {
|
|
pub id: String,
|
|
pub attributes: LinkedHashMap<String, String>
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SpriteMetadata {
|
|
pub id: String,
|
|
pub width: u8,
|
|
pub height: u8,
|
|
pub palette: u16,
|
|
pub anim_interval: Option<u16>
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TiledTileset {
|
|
pub source: String,
|
|
pub image: DynamicImage,
|
|
pub palettes: Vec<Option<u8>>,
|
|
pub tile_order: TileOrder,
|
|
pub sprite_metadata: Option<SpriteMetadata>
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum SystemPlane {
|
|
MdPlaneA,
|
|
MdPlaneB
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Layer {
|
|
Metatile {
|
|
system_plane: SystemPlane,
|
|
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::Metatile {
|
|
system_plane: SystemPlane::MdPlaneA,
|
|
tiles
|
|
} ) ),
|
|
"b" => Ok( Some( Layer::Metatile {
|
|
system_plane: SystemPlane::MdPlaneB,
|
|
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 )
|
|
}
|
|
}
|
|
|
|
fn get_tiles( tileset: Node, seen_sources: &mut HashSet<String>, working_directory: &str ) -> Result<Option<TiledTileset>, Box<dyn Error>> {
|
|
// Get the image for the tileset and its source
|
|
let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?;
|
|
let source = image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )?.to_string();
|
|
|
|
// If the file path ends in .tmx, it's a metatile definition. We need to hop to the .tmx file in source, then redefine layer
|
|
// to the single tileset in that metatile .tmx.
|
|
let full_path = format!( "{}/{}", working_directory, source );
|
|
let formatted_path = Path::new( &full_path );
|
|
if let Some( extension ) = formatted_path.extension() {
|
|
let extension = extension.to_string_lossy();
|
|
if extension == "tmx" {
|
|
// Case where source is a nested tileset. Open the .tmx file
|
|
let file = read_to_string( full_path.clone() )?;
|
|
let tmx_document = roxmltree::Document::parse( &file )?;
|
|
|
|
// Obtain the single (first) <tileset>
|
|
let tileset = tmx_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: metatile has no tileset" )?;
|
|
let tileset_source = tileset.attribute( "source" ).ok_or( "invalid file: metatile tileset has no source" )?.to_string();
|
|
|
|
// tileset_source should already point to the _actual_ source we need. Was this tileset source already seen? If so, bail
|
|
if seen_sources.contains( &tileset_source ) {
|
|
return Ok( None )
|
|
} else {
|
|
seen_sources.insert( tileset_source.clone() );
|
|
}
|
|
|
|
// Load nested tileset into roxmltree document
|
|
let file = read_to_string( format!( "{}/{}", working_directory, tileset_source ) )?;
|
|
let nested_tileset = roxmltree::Document::parse( &file )?;
|
|
let tileset = nested_tileset.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: metatile tileset has no tileset" )?;
|
|
|
|
// Return nested tileset from tmx file
|
|
let result = get_tiles( tileset, seen_sources, working_directory )?;
|
|
if let Some( mut result ) = result {
|
|
result.source = tileset_source.to_owned();
|
|
return Ok( Some( result ) )
|
|
}
|
|
|
|
return Ok( result )
|
|
}
|
|
}
|
|
|
|
// Case where source is an actual image
|
|
let image = image::open( full_path )?;
|
|
|
|
// Image must be a multiple of 8 (--system md)
|
|
if image.width() % 8 != 0 { return Err( "invalid file: tileset width not multiple of 8" )? }
|
|
if image.height() % 8 != 0 { return Err( "invalid file: tileset height not multiple of 8" )? }
|
|
|
|
let tile_count: usize = tileset.attribute( "tilecount" ).ok_or( "invalid file: no tilecount attribute on tileset" )?.parse()?;
|
|
let mut palettes: Vec<Option<u8>> = vec![ None; tile_count ];
|
|
|
|
let defined_tiles = tileset.descendants().filter( | node | node.tag_name() == "tile".into() );
|
|
for defined_tile in defined_tiles {
|
|
let tile_id: usize = defined_tile.attribute( "id" ).ok_or( "invalid file: id attribute not defined on a tile" )?.parse()?;
|
|
let properties = defined_tile.descendants().find( | node | node.tag_name() == "properties".into() ).ok_or( "invalid file: no properties descendant in tileset" )?;
|
|
let property = properties.descendants().find( | node | node.tag_name() == "property".into() && node.attribute( "name" ).unwrap_or( "" ) == "reskit-palette" );
|
|
if let Some( property ) = property {
|
|
let property_type = property.attribute( "type" ).unwrap_or( "string" );
|
|
if property_type == "int" {
|
|
let palette_value: u8 = property.attribute( "value" ).ok_or( "invalid file: reskit-palette property has no value" )?.parse()?;
|
|
// --system md
|
|
if palette_value > 3 {
|
|
print_warning( &format!( "reskit-palette property on tile {} is not valid palette (0 to 3) - leaving palette unset, this is probably not what you want...", tile_id ) );
|
|
} else {
|
|
palettes[ tile_id ] = Some( palette_value );
|
|
}
|
|
} else {
|
|
print_warning( &format!( "reskit-palette property on tile {} is not int type - leaving palette unset, this is probably not what you want...", tile_id ) )
|
|
}
|
|
}
|
|
}
|
|
|
|
let sprite_metadata;
|
|
|
|
// Retrieve the tile order as specified by reskit-tile-order (if none is specified, fallback on Tile)
|
|
let tile_order = if let Some( properties ) = tileset.descendants().find( | node | node.tag_name() == "properties".into() ) {
|
|
if let Some( tile_order_property ) = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-tile-order" ) ) {
|
|
let tile_order_property = tile_order_property.attribute( "value" ).expect( "invalid file: no reskit-tile-order value" );
|
|
match tile_order_property.to_lowercase().as_str() {
|
|
"sprite" => {
|
|
// If sprite, reskit-sprite-height and reskit-sprite-width must be defined
|
|
let reskit_sprite_height = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-sprite-height" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-sprite-height and reskit-sprite-width must be defined." )?;
|
|
let reskit_sprite_width = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-sprite-width" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-sprite-height and reskit-sprite-width must be defined." )?;
|
|
let reskit_sprite_id = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-sprite-id" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-sprite-id must be defined." )?;
|
|
let reskit_palette = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-palette" ) ).ok_or( "invalid file: for reskit-tile-order \"sprite\", reskit-palette must be defined." )?;
|
|
let reskit_anim_interval = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-anim-interval" ) );
|
|
|
|
let reskit_sprite_height = reskit_sprite_height.attribute( "value" ).ok_or( "invalid file: no reskit-sprite-height value" )?;
|
|
let reskit_sprite_width = reskit_sprite_width.attribute( "value" ).ok_or( "invalid file: no reskit-sprite-width value" )?;
|
|
let reskit_palette = reskit_palette.attribute( "value" ).ok_or( "invalid file: no reskit-palette value" )?;
|
|
|
|
let id: String = reskit_sprite_id.attribute( "value" ).ok_or( "invalid file: no reskit-sprite-id value" )?.to_owned();
|
|
let width: u8 = reskit_sprite_width.parse()?;
|
|
let height: u8 = reskit_sprite_height.parse()?;
|
|
let palette: u16 = reskit_palette.parse()?;
|
|
|
|
let anim_interval = if let Some( property ) = reskit_anim_interval {
|
|
let value = property.attribute( "value" ).ok_or( "invalid file: no reskit-anim-interval value" )?;
|
|
Some( value.parse()? )
|
|
} else {
|
|
None
|
|
};
|
|
|
|
sprite_metadata = Some( SpriteMetadata { id, width, height, palette, anim_interval } );
|
|
|
|
TileOrder::Sprite
|
|
},
|
|
"tile" => {
|
|
sprite_metadata = None;
|
|
TileOrder::Tile
|
|
},
|
|
invalid => {
|
|
sprite_metadata = None;
|
|
print_warning( &format!( "invalid setting for property reskit-tile-order: {}. falling back on \"tile\"", invalid ) );
|
|
TileOrder::Tile
|
|
}
|
|
}
|
|
} else {
|
|
sprite_metadata = None;
|
|
TileOrder::Tile
|
|
}
|
|
} else {
|
|
sprite_metadata = None;
|
|
TileOrder::Tile
|
|
};
|
|
|
|
Ok( Some( TiledTileset { source: format!( "" ), image, palettes, tile_order, sprite_metadata } ) )
|
|
}
|
|
|
|
pub fn get_metatile( id: u32, tileset: Node, working_directory: &str ) -> Result<Option<Metatile>, Box<dyn Error>> {
|
|
// Get the image for the tileset and its source
|
|
let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?;
|
|
let source = image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )?.to_string();
|
|
let full_path = format!( "{}/{}", working_directory, source );
|
|
let formatted_path = Path::new( &full_path );
|
|
if let Some( extension ) = formatted_path.extension() {
|
|
let extension = extension.to_string_lossy();
|
|
if extension == "tmx" {
|
|
// Case where source is a nested tileset. Open the .tmx file
|
|
let file = read_to_string( full_path.clone() )?;
|
|
let tmx_document = roxmltree::Document::parse( &file )?;
|
|
|
|
// Get width and height from <map> item
|
|
let map_item = tmx_document.descendants().find( | node | node.tag_name() == "map".into() ).ok_or( "invalid file: no map element in metatile" )?;
|
|
let width: u16 = map_item.attribute( "width" ).ok_or( "invalid file: map of metatile has no width attribute" )?.parse()?;
|
|
let height: u16 = map_item.attribute( "height" ).ok_or( "invalid file: map of metatile has no height attribute" )?.parse()?;
|
|
|
|
// Obtain the single (first) <tileset>
|
|
let tileset_count = tmx_document.descendants().filter( | node | node.tag_name() == "tileset".into() ).count();
|
|
if tileset_count > 1 {
|
|
return Err( "invalid file: only a single tileset is valid for a metatile file" )?
|
|
}
|
|
let tileset = tmx_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: metatile has no tileset" )?;
|
|
let source = tileset.attribute( "source" ).ok_or( "invalid file: metatile tileset has no source" )?.to_string();
|
|
|
|
// Create metatile object here
|
|
let data = tmx_document.descendants().find( | node | node.tag_name() == "data".into() ).ok_or( "invalid file: metatile has no data" )?;
|
|
if data.attribute( "encoding" ) != Some( "csv" ) {
|
|
return Err( "invalid file: metatile data not in csv format" )?
|
|
}
|
|
let data = data.text().ok_or( "invalid file: data node has no text" )?;
|
|
let values: Vec<&str> = data.split( "," ).collect();
|
|
// Subtract 1 to each value to account for firstgid. This is a sane assumption because a valid metatile file only contains a single tileset.
|
|
let tiles: Vec<u32> = values.into_iter().map( | string | {
|
|
let val = string.trim().parse::<u32>().expect( "fatal: non-u32 value in metatile" );
|
|
if val == 0 {
|
|
val
|
|
} else {
|
|
val - 1
|
|
}
|
|
} ).collect();
|
|
return Ok( Some( Metatile { id, source, width, height, tiles } ) )
|
|
}
|
|
}
|
|
|
|
Ok( None )
|
|
}
|
|
|
|
pub fn get_objs( node: &Node, object_fields: &Vec<&str> ) -> Result<Vec<Object>, Box<dyn Error>> {
|
|
let mut result = Vec::new();
|
|
|
|
let objects = node.descendants().filter( | node | node.tag_name() == "object".into() );
|
|
for object in objects {
|
|
let object_id = object.attribute( "id" ).ok_or( "invalid file: object in objectgroup has no id attribute" )?;
|
|
|
|
if let Some( properties ) = object.descendants().find( | node | node.tag_name() == "properties".into() ) {
|
|
if let Some( object_id_property ) = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-object-id" ) ) {
|
|
let id = object_id_property.attribute( "value" ).ok_or( "invalid file: property has no value attribute" )?.to_owned();
|
|
|
|
let mut attributes = LinkedHashMap::new();
|
|
|
|
// Get position x and y as beginning attributes
|
|
let ( x, y ) = (
|
|
object.attribute( "x" ).ok_or( "invalid file: object has no x position" )?.to_owned(),
|
|
object.attribute( "y" ).ok_or( "invalid file: object has no y position" )?.to_owned()
|
|
);
|
|
|
|
attributes.insert( "x".trim().to_owned(), x );
|
|
attributes.insert( "y".trim().to_owned(), y );
|
|
|
|
// Custom object fields - push these in order into the LinkedHashMap
|
|
for field in object_fields {
|
|
let property = properties.descendants().find( | node | node.attribute( "name" ) == Some( &format!( "reskit-field[{}]", field ) ) );
|
|
if let Some( property ) = property {
|
|
let value = property.attribute( "value" ).ok_or( "invalid file: property has no value attribute" )?.to_owned();
|
|
attributes.insert( field.to_string(), value );
|
|
} else {
|
|
print_warning( &format!( "object {} does not define a value for struct field \"{}\", this field will be filled in with 0x00 at export", id, field ) );
|
|
attributes.insert( field.to_string(), "0".to_owned() );
|
|
}
|
|
}
|
|
|
|
result.push( Object { id, attributes } );
|
|
} else {
|
|
print_warning( &format!( "object {} has no \"reskit-object-id\" property....ignoring this object. this is probably not what you want.", object_id ) );
|
|
}
|
|
} else {
|
|
print_warning( &format!( "object {} has no properties....ignoring this object. this is probably not what you want", object_id ) );
|
|
}
|
|
}
|
|
|
|
Ok( result )
|
|
}
|
|
|
|
pub fn get_tiled_tilemap( path: &str, object_fields: &Vec<&str> ) -> Result<TiledTilemap, Box<dyn Error>> {
|
|
let file = read_to_string( path )?;
|
|
let document = roxmltree::Document::parse( &file )?;
|
|
let working_directory = {
|
|
let result = Path::new( path ).parent().unwrap_or( Path::new( "." ) ).to_string_lossy();
|
|
|
|
if result == "" {
|
|
Cow::from( "." )
|
|
} else {
|
|
result
|
|
}
|
|
};
|
|
|
|
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()?
|
|
);
|
|
|
|
// --system md is 8x8
|
|
if tile_width != 8 {
|
|
return Err( "invalid file: tile width is not 8 for --system md" )?
|
|
}
|
|
if tile_height != 8 {
|
|
return Err( "invalid file: tile height is not 8 for --system md" )?
|
|
}
|
|
|
|
// Build tilesets (current version assumes one tileset per level)
|
|
let mut tilesets: Vec<TiledTileset> = vec![];
|
|
let mut metatiles: Vec<Metatile> = vec![];
|
|
let mut seen_sources: HashSet<String> = HashSet::new();
|
|
for tileset in map.descendants().filter( | node | node.tag_name() == "tileset".into() ) {
|
|
let tileset_first_gid = tileset.attribute( "firstgid" ).ok_or( "invalid file: no tileset firstgid" )?;
|
|
let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?;
|
|
let tileset_file = read_to_string( format!( "{}/{}", working_directory, 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" )?;
|
|
|
|
// Tilesets may instead contain metatiles
|
|
// First, load tilesets instead of metatiles
|
|
// If tileset_source_path was seen, don't proceed
|
|
if !seen_sources.contains( tileset_source_path ) {
|
|
seen_sources.insert( tileset_source_path.to_owned() );
|
|
|
|
if let Some( mut tiles ) = get_tiles( tileset.clone(), &mut seen_sources, &working_directory )? {
|
|
if tiles.source == "" {
|
|
tiles.source = tileset_source_path.to_owned();
|
|
}
|
|
|
|
tilesets.push( tiles );
|
|
}
|
|
}
|
|
|
|
// Then, check for metatiles
|
|
if let Some( metatile ) = get_metatile( tileset_first_gid.parse()?, tileset, &working_directory )? {
|
|
metatiles.push( metatile );
|
|
}
|
|
}
|
|
|
|
let tileset = tilesets;
|
|
if tileset.is_empty() {
|
|
return Err( "invalid file: at least one tileset must be present in file" )?
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Get the entity-component system
|
|
let object_group = map.descendants().find( | node |
|
|
node.tag_name() == "objectgroup".into() &&
|
|
node.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-layer" ) && node.attribute( "value" ) == Some( "object" ) ).is_some()
|
|
);
|
|
let objects = if let Some( object_group ) = object_group {
|
|
get_objs( &object_group, object_fields )?
|
|
} else {
|
|
print_warning( "no object layer in this file, this is probably not what you want..." );
|
|
Vec::new()
|
|
};
|
|
|
|
// Get collision
|
|
let collision_group = map.descendants().find( | node |
|
|
node.tag_name() == "objectgroup".into() &&
|
|
node.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-layer" ) && node.attribute( "value" ) == Some( "collision" ) ).is_some()
|
|
);
|
|
let collision = if let Some( collision_group ) = collision_group {
|
|
let mut result = Vec::new();
|
|
let objects = collision_group.descendants().filter( | node | node.tag_name() == "object".into() );
|
|
|
|
for object in objects {
|
|
let x: u16 = object.attribute( "x" ).ok_or( "invalid file: no \"x\" attribute in collision bounding box" )?.parse()?;
|
|
let y: u16 = object.attribute( "y" ).ok_or( "invalid file: no \"y\" attribute in collision bounding box" )?.parse()?;
|
|
let width: u16 = object.attribute( "width" ).ok_or( "invalid file: no \"width\" attribute in collision bounding box" )?.parse()?;
|
|
let height: u16 = object.attribute( "height" ).ok_or( "invalid file: no \"height\" attribute in collision bounding box" )?.parse()?;
|
|
|
|
result.push( euclid::rect( x, y, width, height ) );
|
|
}
|
|
|
|
if result.is_empty() {
|
|
print_warning( "collision layer present but no bounding boxes are defined, this is probably not what you want..." );
|
|
}
|
|
|
|
result
|
|
} else {
|
|
print_warning( "no collision layer in this file, this is probably not what you want..." );
|
|
Vec::new()
|
|
};
|
|
|
|
Ok( TiledTilemap { tileset, metatiles, layers, objects, collision, width, height } )
|
|
} else {
|
|
Err( "invalid file: this does not appear to be valid Tiled .tmx file" )?
|
|
}
|
|
} |