From 884eb7df2f02cb72ce26e38aed2deca71d1f7c7b Mon Sep 17 00:00:00 2001 From: William Cahill Date: Wed, 9 Feb 2022 19:32:01 -0500 Subject: [PATCH] Merging GID changes --- CHANGELOG.md | 8 +- README.md | 2 + assets/tiled_image_layers.tmx | 4 +- assets/tiled_object_groups.tmx | 5 +- examples/main.rs | 58 ++++- src/error.rs | 53 +++-- src/image.rs | 11 +- src/layers.rs | 344 ------------------------------ src/layers/group.rs | 107 ++++++++-- src/layers/mod.rs | 11 +- src/lib.rs | 2 + src/map.rs | 236 +++++++++++++++++---- src/objects.rs | 63 ++++-- src/properties.rs | 71 ++++--- src/tile.rs | 43 ++-- src/tileset.rs | 159 +++++++------- src/util.rs | 208 ++---------------- tests/lib.rs | 374 ++++++++++++++++++++------------- 18 files changed, 844 insertions(+), 915 deletions(-) delete mode 100644 src/layers.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a04f3f..a3330712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Tileset::source` for obtaining where the tileset actually came from. - `Tileset::columns`. +- `Color::alpha`. - `Layer::id`, `Layer::width`, `Layer::height`, `Layer::parallax_x` and `Layer::parallax_y`. - Support for 'object'-type properties. - Support for multiline string properties. @@ -18,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MIT license file. ### Changed +- **Set the minimum Tiled TMX version to 0.13.** +- `Tileset::tilecount` is no longer optional. - `Layer` has been renamed to `TileLayer`, and the original `Layer` structure is now used for common data from all layer types. - `Map` now has a single `layers` member which contains layers of all types in order. @@ -28,7 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `parse_with_path` -> `Map::parse_reader`. - `parse_tileset` -> `Tileset::parse`. - All mentions of `Colour` have been changed to `Color` for consistency with the Tiled dataformat. -- `Map::get_tileset_by_gid` -> `Map::tileset_by_gid`. - `Layer::tiles` changed from `Vec>` to `Vec`. - Tile now has `image` instead of `images`. ([Issue comment](https://github.com/mapeditor/rs-tiled/issues/103#issuecomment-940773123)) - Tileset now has `image` instead of `images`. @@ -39,6 +41,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped `zstd` to `0.9`. - Fixed markdown formatting in the `CONTRIBUTORS` file. +### Fixed +- `Color` parsing. + + ## [0.9.5] - 2021-05-02 ### Added - Support for file properties. diff --git a/README.md b/README.md index fd40af5d..33eae93c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ tiled = "0.9.5" to the dependencies section of your Cargo.toml. +The minimum supported TMX version is 0.13. + ### Example ```rust diff --git a/assets/tiled_image_layers.tmx b/assets/tiled_image_layers.tmx index 6ee077b4..6c3128ff 100644 --- a/assets/tiled_image_layers.tmx +++ b/assets/tiled_image_layers.tmx @@ -3,8 +3,8 @@ - - + + diff --git a/assets/tiled_object_groups.tmx b/assets/tiled_object_groups.tmx index ae6c190c..7f8fe073 100644 --- a/assets/tiled_object_groups.tmx +++ b/assets/tiled_object_groups.tmx @@ -1,5 +1,5 @@ - + 0,0,0,0,0,0,0,0,0,0, @@ -15,6 +15,9 @@ + + + diff --git a/examples/main.rs b/examples/main.rs index d39dd523..75c822f6 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,7 +1,57 @@ -use tiled::Map; +use std::path::PathBuf; + +use tiled::{FilesystemResourceCache, Map}; fn main() { - let map = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap(); - println!("{:?}", map); - println!("{:?}", map.tileset_by_gid(22)); + // Create a new resource cache. This is a structure that holds references to loaded + // assets such as tilesets so that they only get loaded once. + // [`FilesystemResourceCache`] is a implementation of [`tiled::ResourceCache`] that + // identifies resources by their path in the filesystem. + let mut cache = FilesystemResourceCache::new(); + + let map_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("assets/tiled_base64_zlib.tmx"); + let map = Map::parse_file(map_path, &mut cache).unwrap(); + + for layer in map.layers() { + print!("Layer \"{}\":\n\t", layer.data().name); + + match layer.layer_type() { + tiled::LayerType::TileLayer(layer) => match layer.data() { + tiled::TileLayerData::Finite(data) => println!( + "Finite tile layer with width = {} and height = {}; ID of tile @ (0,0): {}", + data.width(), + data.height(), + layer.get_tile(0, 0).unwrap().id + ), + tiled::TileLayerData::Infinite(data) => { + // This is prone to change! Infinite layers will be refactored before 0.10.0 + // releases. + println!("Infinite tile layer with {} chunks", data.chunks.len()) + } + }, + + tiled::LayerType::ObjectLayer(layer) => { + println!("Object layer with {} objects", layer.data().objects.len()) + }, + + tiled::LayerType::ImageLayer(layer) => { + println!( + "Image layer with {}", + match &layer.data().image { + Some(img) => + format!("an image with source = {}", img.source.to_string_lossy()), + None => "no image".to_owned(), + } + ) + }, + + tiled::LayerType::GroupLayer(layer) => { + println!( + "Group layer with {} sublayers", + layer.layers().len() + ) + } + } + } } diff --git a/src/error.rs b/src/error.rs index 55c4d032..3b0f789a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, path::PathBuf}; #[derive(Debug, Copy, Clone)] pub enum ParseTileError { @@ -23,23 +23,46 @@ pub enum TiledError { SourceRequired { object_to_parse: String, }, + /// The path given is invalid because it isn't contained in any folder. + PathIsNotFile, + CouldNotOpenFile { + path: PathBuf, + err: std::io::Error, + }, + /// There was an invalid tile in the map parsed. + InvalidTileFound, Other(String), } impl fmt::Display for TiledError { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match *self { - TiledError::MalformedAttributes(ref s) => write!(fmt, "{}", s), - TiledError::DecompressingError(ref e) => write!(fmt, "{}", e), - TiledError::Base64DecodingError(ref e) => write!(fmt, "{}", e), - TiledError::XmlDecodingError(ref e) => write!(fmt, "{}", e), - TiledError::PrematureEnd(ref e) => write!(fmt, "{}", e), + match self { + TiledError::MalformedAttributes(s) => write!(fmt, "{}", s), + TiledError::DecompressingError(e) => write!(fmt, "{}", e), + TiledError::Base64DecodingError(e) => write!(fmt, "{}", e), + TiledError::XmlDecodingError(e) => write!(fmt, "{}", e), + TiledError::PrematureEnd(e) => write!(fmt, "{}", e), TiledError::SourceRequired { ref object_to_parse, } => { write!(fmt, "Tried to parse external {} without a file location, e.g. by using Map::parse_reader.", object_to_parse) } - TiledError::Other(ref s) => write!(fmt, "{}", s), + TiledError::PathIsNotFile => { + write!( + fmt, + "The path given is invalid because it isn't contained in any folder." + ) + } + TiledError::CouldNotOpenFile { path, err } => { + write!( + fmt, + "Could not open '{}'. Error: {}", + path.to_string_lossy(), + err + ) + } + TiledError::InvalidTileFound => write!(fmt, "Invalid tile found in map being parsed"), + TiledError::Other(s) => write!(fmt, "{}", s), } } } @@ -47,14 +70,12 @@ impl fmt::Display for TiledError { // This is a skeleton implementation, which should probably be extended in the future. impl std::error::Error for TiledError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match *self { - TiledError::MalformedAttributes(_) => None, - TiledError::DecompressingError(ref e) => Some(e as &dyn std::error::Error), - TiledError::Base64DecodingError(ref e) => Some(e as &dyn std::error::Error), - TiledError::XmlDecodingError(ref e) => Some(e as &dyn std::error::Error), - TiledError::PrematureEnd(_) => None, - TiledError::SourceRequired { .. } => None, - TiledError::Other(_) => None, + match self { + TiledError::DecompressingError(e) => Some(e as &dyn std::error::Error), + TiledError::Base64DecodingError(e) => Some(e as &dyn std::error::Error), + TiledError::XmlDecodingError(e) => Some(e as &dyn std::error::Error), + TiledError::CouldNotOpenFile { err, .. } => Some(err as &dyn std::error::Error), + _ => None, } } } diff --git a/src/image.rs b/src/image.rs index a7cf8eff..7399f380 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,9 +1,6 @@ -use std::{ - io::Read, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; -use xml::{attribute::OwnedAttribute, EventReader}; +use xml::attribute::OwnedAttribute; use crate::{error::TiledError, properties::Color, util::*}; @@ -24,8 +21,8 @@ pub struct Image { } impl Image { - pub(crate) fn new( - parser: &mut EventReader, + pub(crate) fn new( + parser: &mut impl Iterator, attrs: Vec, path_relative_to: impl AsRef, ) -> Result { diff --git a/src/layers.rs b/src/layers.rs deleted file mode 100644 index d23e790b..00000000 --- a/src/layers.rs +++ /dev/null @@ -1,344 +0,0 @@ -use std::{collections::HashMap, io::Read, path::Path}; - -use xml::{attribute::OwnedAttribute, EventReader}; - -use crate::{ - error::TiledError, - image::Image, - objects::Object, - properties::{parse_properties, Color, Properties}, - util::*, -}; - -/// Stores the proper tile gid, along with how it is flipped. -// Maybe PartialEq and Eq should be custom, so that it ignores tile-flipping? -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LayerTile { - pub gid: u32, - pub flip_h: bool, - pub flip_v: bool, - pub flip_d: bool, -} - -const FLIPPED_HORIZONTALLY_FLAG: u32 = 0x80000000; -const FLIPPED_VERTICALLY_FLAG: u32 = 0x40000000; -const FLIPPED_DIAGONALLY_FLAG: u32 = 0x20000000; -const ALL_FLIP_FLAGS: u32 = - FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG; - -impl LayerTile { - pub fn new(id: u32) -> LayerTile { - let flags = id & ALL_FLIP_FLAGS; - let gid = id & !ALL_FLIP_FLAGS; - let flip_d = flags & FLIPPED_DIAGONALLY_FLAG == FLIPPED_DIAGONALLY_FLAG; // Swap x and y axis (anti-diagonally) [flips over y = -x line] - let flip_h = flags & FLIPPED_HORIZONTALLY_FLAG == FLIPPED_HORIZONTALLY_FLAG; // Flip tile over y axis - let flip_v = flags & FLIPPED_VERTICALLY_FLAG == FLIPPED_VERTICALLY_FLAG; // Flip tile over x axis - - LayerTile { - gid, - flip_h, - flip_v, - flip_d, - } - } -} - -#[derive(Clone, PartialEq, Debug)] -pub enum LayerType { - TileLayer(TileLayer), - ObjectLayer(ObjectLayer), - ImageLayer(ImageLayer), - GroupLayer(GroupLayer), -} - -#[derive(Clone, Copy)] -pub(crate) enum LayerTag { - TileLayer, - ObjectLayer, - ImageLayer, - GroupLayer, -} - -#[derive(Clone, PartialEq, Debug)] -pub struct Layer { - pub name: String, - pub id: u32, - pub visible: bool, - pub offset_x: f32, - pub offset_y: f32, - pub parallax_x: f32, - pub parallax_y: f32, - pub opacity: f32, - pub properties: Properties, - pub layer_type: LayerType, -} - -impl Layer { - pub(crate) fn new( - parser: &mut EventReader, - attrs: Vec, - tag: LayerTag, - infinite: bool, - path_relative_to: Option<&Path>, - ) -> Result { - let ((opacity, visible, offset_x, offset_y, parallax_x, parallax_y, name, id), ()) = get_attrs!( - attrs, - optionals: [ - ("opacity", opacity, |v:String| v.parse().ok()), - ("visible", visible, |v:String| v.parse().ok().map(|x:i32| x == 1)), - ("offsetx", offset_x, |v:String| v.parse().ok()), - ("offsety", offset_y, |v:String| v.parse().ok()), - ("parallaxx", parallax_x, |v:String| v.parse().ok()), - ("parallaxy", parallax_y, |v:String| v.parse().ok()), - ("name", name, |v| Some(v)), - ("id", id, |v:String| v.parse().ok()), - ], - required: [ - ], - - TiledError::MalformedAttributes("layer parsing error, no id attribute found".to_string()) - ); - - let (ty, properties) = match tag { - LayerTag::TileLayer => { - let (ty, properties) = TileLayer::new(parser, attrs, infinite)?; - (LayerType::TileLayer(ty), properties) - } - LayerTag::ObjectLayer => { - let (ty, properties) = ObjectLayer::new(parser, attrs)?; - (LayerType::ObjectLayer(ty), properties) - } - LayerTag::ImageLayer => { - let (ty, properties) = ImageLayer::new(parser, path_relative_to)?; - (LayerType::ImageLayer(ty), properties) - }, - LayerTag::GroupLayer => { - let (ty, properties) = GroupLayer::new(parser, infinite, path_relative_to)?; - (LayerType::GroupLayer(ty), properties) - }, - }; - - Ok(Self { - visible: visible.unwrap_or(true), - offset_x: offset_x.unwrap_or(0.0), - offset_y: offset_y.unwrap_or(0.0), - parallax_x: parallax_x.unwrap_or(1.0), - parallax_y: parallax_y.unwrap_or(1.0), - opacity: opacity.unwrap_or(1.0), - name: name.unwrap_or_default(), - id: id.unwrap_or(0), - properties, - layer_type: ty, - }) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct TileLayer { - pub width: u32, - pub height: u32, - /// The tiles are arranged in rows. Each tile is a number which can be used - /// to find which tileset it belongs to and can then be rendered. - pub tiles: LayerData, -} - -impl TileLayer { - pub(crate) fn new( - parser: &mut EventReader, - attrs: Vec, - infinite: bool, - ) -> Result<(TileLayer, Properties), TiledError> { - let ((), (w, h)) = get_attrs!( - attrs, - optionals: [ - ], - required: [ - ("width", width, |v: String| v.parse().ok()), - ("height", height, |v: String| v.parse().ok()), - ], - TiledError::MalformedAttributes("layer parsing error, width and height attributes required".to_string()) - ); - let mut tiles: LayerData = LayerData::Finite(Default::default()); - let mut properties = HashMap::new(); - parse_tag!(parser, "layer", { - "data" => |attrs| { - if infinite { - tiles = parse_infinite_data(parser, attrs)?; - } else { - tiles = parse_data(parser, attrs)?; - } - Ok(()) - }, - "properties" => |_| { - properties = parse_properties(parser)?; - Ok(()) - }, - }); - - Ok(( - TileLayer { - width: w, - height: h, - tiles: tiles, - }, - properties, - )) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub enum LayerData { - Finite(Vec), - Infinite(HashMap<(i32, i32), Chunk>), -} - -#[derive(Debug, PartialEq, Clone)] -pub struct ImageLayer { - pub image: Option, -} - -impl ImageLayer { - pub(crate) fn new( - parser: &mut EventReader, - path_relative_to: Option<&Path>, - ) -> Result<(ImageLayer, Properties), TiledError> { - let mut image: Option = None; - let mut properties = HashMap::new(); - - parse_tag!(parser, "imagelayer", { - "image" => |attrs| { - image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); - Ok(()) - }, - "properties" => |_| { - properties = parse_properties(parser)?; - Ok(()) - }, - }); - Ok((ImageLayer { image }, properties)) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct ObjectLayer { - pub objects: Vec, - pub colour: Option, -} - -impl ObjectLayer { - pub(crate) fn new( - parser: &mut EventReader, - attrs: Vec, - ) -> Result<(ObjectLayer, Properties), TiledError> { - let (c, ()) = get_attrs!( - attrs, - optionals: [ - ("color", colour, |v:String| v.parse().ok()), - ], - required: [], - // this error should never happen since there are no required attrs - TiledError::MalformedAttributes("object group parsing error".to_string()) - ); - let mut objects = Vec::new(); - let mut properties = HashMap::new(); - parse_tag!(parser, "objectgroup", { - "object" => |attrs| { - objects.push(Object::new(parser, attrs)?); - Ok(()) - }, - "properties" => |_| { - properties = parse_properties(parser)?; - Ok(()) - }, - }); - Ok(( - ObjectLayer { - objects: objects, - colour: c, - }, - properties, - )) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct GroupLayer { - pub layers: Vec, -} - -impl GroupLayer { - pub(crate) fn new( - parser: &mut EventReader, - infinite: bool, - path_relative_to: Option<&Path>, - ) -> Result<(GroupLayer, Properties), TiledError> { - let mut properties = HashMap::new(); - let mut layers = Vec::new(); - parse_tag!(parser, "group", { - "properties" => |_| { - properties = parse_properties(parser)?; - Ok(()) - }, - "layer" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::TileLayer, infinite, path_relative_to)?); - Ok(()) - }, - "imagelayer" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ImageLayer, infinite, path_relative_to)?); - Ok(()) - }, - "objectgroup" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ObjectLayer, infinite, path_relative_to)?); - Ok(()) - }, - "group" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::GroupLayer, infinite, path_relative_to)?); - Ok(()) - }, - }); - Ok(( - GroupLayer { layers }, - properties, - )) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct Chunk { - pub x: i32, - pub y: i32, - pub width: u32, - pub height: u32, - pub tiles: Vec, -} - -impl Chunk { - pub(crate) fn new( - parser: &mut EventReader, - attrs: Vec, - encoding: Option, - compression: Option, - ) -> Result { - let ((), (x, y, width, height)) = get_attrs!( - attrs, - optionals: [], - required: [ - ("x", x, |v: String| v.parse().ok()), - ("y", y, |v: String| v.parse().ok()), - ("width", width, |v: String| v.parse().ok()), - ("height", height, |v: String| v.parse().ok()), - ], - TiledError::MalformedAttributes("layer must have a name".to_string()) - ); - - let tiles = parse_data_line(encoding, compression, parser)?; - - Ok(Chunk { - x, - y, - width, - height, - tiles, - }) - } -} diff --git a/src/layers/group.rs b/src/layers/group.rs index c1980af1..130eafed 100644 --- a/src/layers/group.rs +++ b/src/layers/group.rs @@ -1,53 +1,122 @@ -use std::io::Read; use std::path::Path; use std::collections::HashMap; -use xml::{attribute::OwnedAttribute, EventReader}; use crate:: { - Layer, + layers::{LayerData, LayerTag}, error::TiledError, properties::{parse_properties, Properties}, + map::MapTilesetGid, util::*, + MapWrapper, Layer, Map }; #[derive(Debug, PartialEq, Clone)] -pub struct GroupLayer<'map> { - pub layers: Vec>, +pub struct GroupLayerData { + layers: Vec, } -impl<'map> GroupLayer<'map> { - pub(crate) fn new( - parser: &mut EventReader, +impl GroupLayerData { + + pub(crate) fn new( + parser: &mut impl Iterator, infinite: bool, - path_relative_to: Option<&Path>, - ) -> Result<(GroupLayer<'map>, Properties), TiledError> { + map_path: &Path, + tilesets: &[MapTilesetGid], + ) -> Result<(GroupLayerData, Properties), TiledError> { let mut properties = HashMap::new(); let mut layers = Vec::new(); parse_tag!(parser, "group", { - "properties" => |_| { - properties = parse_properties(parser)?; - Ok(()) - }, "layer" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::TileLayer, infinite, source_path)?); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::TileLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, "imagelayer" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ImageLayer, infinite, path_relative_to)?); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::ImageLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, "objectgroup" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ObjectLayer, infinite, path_relative_to)?); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::ObjectLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, "group" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::GroupLayer, infinite, path_relative_to)?); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::GroupLayer, + infinite, + map_path, + &tilesets, + )?); + Ok(()) + }, + "properties" => |_| { + properties = parse_properties(parser)?; Ok(()) }, }); Ok(( - GroupLayer { layers }, + GroupLayerData { layers }, properties, )) } +} + +pub type GroupLayer<'map> = MapWrapper<'map, GroupLayerData>; + +impl<'map> GroupLayer<'map> { + pub fn layers(&self) -> GroupLayerIter { + GroupLayerIter::new(self.map(), self.data()) + } + pub fn get_layer(&self, index: usize) -> Option { + self.data().layers.get(index).map(|data| Layer::new(self.map(), data)) + } +} + +/// An iterator that iterates over all the layers in a group layer, obtained via [`GroupLayer::layers`]. +pub struct GroupLayerIter<'map> { + map: &'map Map, + group: &'map GroupLayerData, + index: usize, +} + +impl<'map> GroupLayerIter<'map> { + fn new(map: &'map Map, group: &'map GroupLayerData) -> Self { + Self { map, group, index: 0 } + } +} + +impl<'map> Iterator for GroupLayerIter<'map> { + type Item = Layer<'map>; + fn next(&mut self) -> Option { + let layer_data = self.group.layers.get(self.index)?; + self.index += 1; + Some(Layer::new(self.map, layer_data)) + } +} + +impl<'map> ExactSizeIterator for GroupLayerIter<'map> { + fn len(&self) -> usize { + self.group.layers.len() - self.index + } } \ No newline at end of file diff --git a/src/layers/mod.rs b/src/layers/mod.rs index 471a9cbf..ddebcf65 100644 --- a/src/layers/mod.rs +++ b/src/layers/mod.rs @@ -18,7 +18,7 @@ pub enum LayerDataType { TileLayer(TileLayerData), ObjectLayer(ObjectLayerData), ImageLayer(ImageLayerData), - GroupLayer(GroupLayer), + GroupLayer(GroupLayerData), } #[derive(Clone, Copy)] @@ -26,6 +26,7 @@ pub(crate) enum LayerTag { TileLayer, ObjectLayer, ImageLayer, + GroupLayer } #[derive(Clone, PartialEq, Debug)] @@ -52,6 +53,7 @@ impl LayerData { map_path: &Path, tilesets: &[MapTilesetGid], ) -> Result { + println!("Attrs are {:?}", attrs); let ( (opacity, tint_color, visible, offset_x, offset_y, parallax_x, parallax_y, name, id), (), @@ -87,6 +89,10 @@ impl LayerData { let (ty, properties) = ImageLayerData::new(parser, map_path)?; (LayerDataType::ImageLayer(ty), properties) } + LayerTag::GroupLayer => { + let (ty, properties) = GroupLayerData::new(parser, infinite, map_path, tilesets)?; + (LayerDataType::GroupLayer(ty), properties) + } }; Ok(Self { @@ -118,7 +124,7 @@ pub enum LayerType<'map> { TileLayer(TileLayer<'map>), ObjectLayer(ObjectLayer<'map>), ImageLayer(ImageLayer<'map>), - // TODO: Support group layers + GroupLayer(GroupLayer<'map>), } impl<'map> LayerType<'map> { @@ -127,6 +133,7 @@ impl<'map> LayerType<'map> { LayerDataType::TileLayer(data) => Self::TileLayer(TileLayer::new(map, data)), LayerDataType::ObjectLayer(data) => Self::ObjectLayer(ObjectLayer::new(map, data)), LayerDataType::ImageLayer(data) => Self::ImageLayer(ImageLayer::new(map, data)), + LayerDataType::GroupLayer(data) => Self::GroupLayer(GroupLayer::new(map, data)), } } } diff --git a/src/lib.rs b/src/lib.rs index ff45bb16..45a03f3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod animation; +mod cache; mod error; mod image; mod layers; @@ -10,6 +11,7 @@ mod tileset; mod util; pub use animation::*; +pub use cache::*; pub use error::*; pub use image::*; pub use layers::*; diff --git a/src/map.rs b/src/map.rs index 51f21901..4325eb20 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,17 +1,23 @@ -use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, str::FromStr}; +use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, rc::Rc, str::FromStr}; use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader}; use crate::{ error::{ParseTileError, TiledError}, - layers::{Layer, LayerTag}, + layers::{LayerData, LayerTag}, properties::{parse_properties, Color, Properties}, tileset::Tileset, - util::{get_attrs, parse_tag}, + util::{get_attrs, parse_tag, XmlEventResult}, + EmbeddedParseResultType, Layer, ResourceCache, }; +pub(crate) struct MapTilesetGid { + pub first_gid: Gid, + pub tileset: Rc, +} + /// All Tiled map files will be parsed into this. Holds all the layers and tilesets. -#[derive(Debug, PartialEq, Clone)] +#[derive(PartialEq, Clone, Debug)] pub struct Map { /// The TMX format version this map was saved to. pub version: String, @@ -24,10 +30,10 @@ pub struct Map { pub tile_width: u32, /// Tile height, in pixels. pub tile_height: u32, - /// The tilesets present in this map. - pub tilesets: Vec, + /// The tilesets present on this map. + tilesets: Vec>, /// The layers present in this map. - pub layers: Vec, + layers: Vec, /// The custom properties of this map. pub properties: Properties, /// The background color of this map, if any. @@ -41,10 +47,16 @@ impl Map { /// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing, /// in which case this function may be required. /// - /// The path is used for external dependencies such as tilesets or images, and may be skipped - /// if the map is fully embedded (Doesn't refer to external files). If a map *does* refer to - /// external files and a path is not given, the function will return [TiledError::SourceRequired]. - pub fn parse_reader(reader: R, path: Option<&Path>) -> Result { + /// The path is used for external dependencies such as tilesets or images. It is required. + /// If the map if fully embedded and doesn't refer to external files, you may input an arbitrary path; + /// the library won't read from the filesystem if it is not required to do so. + /// + /// The tileset cache is used to store and refer to any tilesets found along the way. + pub fn parse_reader( + reader: R, + path: impl AsRef, + cache: &mut impl ResourceCache, + ) -> Result { let mut parser = EventReader::new(reader); loop { match parser.next().map_err(TiledError::XmlDecodingError)? { @@ -52,7 +64,12 @@ impl Map { name, attributes, .. } => { if name.local_name == "map" { - return Self::parse_xml(&mut parser, attributes, path); + return Self::parse_xml( + &mut parser.into_iter(), + attributes, + path.as_ref(), + cache, + ); } } XmlEvent::EndDocument => { @@ -67,16 +84,69 @@ impl Map { /// Parse a file hopefully containing a Tiled map and try to parse it. All external /// files will be loaded relative to the path given. - pub fn parse_file(path: impl AsRef) -> Result { - let file = File::open(path.as_ref()) + /// + /// The tileset cache is used to store and refer to any tilesets found along the way. + pub fn parse_file( + path: impl AsRef, + cache: &mut impl ResourceCache, + ) -> Result { + let reader = File::open(path.as_ref()) .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path.as_ref())))?; - Self::parse_reader(file, Some(path.as_ref())) + Self::parse_reader(reader, path.as_ref(), cache) + } +} + +impl Map { + /// Get a reference to the map's tilesets. + pub fn tilesets(&self) -> &[Rc] { + self.tilesets.as_ref() + } + + /// Get an iterator over all the layers in the map in ascending order of their layer index. + pub fn layers(&self) -> MapLayerIter { + MapLayerIter::new(self) + } + + /// Returns the layer that has the specified index, if it exists. + pub fn get_layer(&self, index: usize) -> Option { + self.layers.get(index).map(|data| Layer::new(self, data)) + } +} + +/// An iterator that iterates over all the layers in a map, obtained via [`Map::layers`]. +pub struct MapLayerIter<'map> { + map: &'map Map, + index: usize, +} + +impl<'map> MapLayerIter<'map> { + fn new(map: &'map Map) -> Self { + Self { map, index: 0 } + } +} + +impl<'map> Iterator for MapLayerIter<'map> { + type Item = Layer<'map>; + + fn next(&mut self) -> Option { + let layer_data = self.map.layers.get(self.index)?; + self.index += 1; + Some(Layer::new(self.map, layer_data)) } +} + +impl<'map> ExactSizeIterator for MapLayerIter<'map> { + fn len(&self) -> usize { + self.map.layers.len() - self.index + } +} - fn parse_xml( - parser: &mut EventReader, +impl Map { + fn parse_xml( + parser: &mut impl Iterator, attrs: Vec, - map_path: Option<&Path>, + map_path: &Path, + cache: &mut impl ResourceCache, ) -> Result { let ((c, infinite), (v, o, w, h, tw, th)) = get_attrs!( attrs, @@ -96,37 +166,84 @@ impl Map { ); let infinite = infinite.unwrap_or(false); - let source_path = map_path.and_then(|p| p.parent()); - let mut tilesets = Vec::new(); + // We can only parse sequentally, but tilesets are guaranteed to appear before layers. + // So we can pass in tileset data to layer construction without worrying about unfinished + // data usage. let mut layers = Vec::new(); let mut properties = HashMap::new(); + let mut tilesets = Vec::new(); + parse_tag!(parser, "map", { "tileset" => |attrs| { - tilesets.push(Tileset::parse_xml(parser, attrs, source_path)?); + let res = Tileset::parse_xml_in_map(parser, attrs, map_path)?; + match res.result_type { + EmbeddedParseResultType::ExternalReference { tileset_path } => { + let file = File::open(&tileset_path).map_err(|err| TiledError::CouldNotOpenFile{path: tileset_path.clone(), err })?; + let tileset = cache.get_or_try_insert_tileset_with(tileset_path.clone(), || Tileset::new_external(file, Some(&tileset_path)))?; + tilesets.push(MapTilesetGid{first_gid: res.first_gid, tileset}); + } + EmbeddedParseResultType::Embedded { tileset } => { + tilesets.push(MapTilesetGid{first_gid: res.first_gid, tileset: Rc::new(tileset)}); + }, + }; Ok(()) }, "layer" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::TileLayer, infinite, source_path)?); + println!("Tile layer 1"); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::TileLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, "imagelayer" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ImageLayer, infinite, source_path)?); - Ok(()) - }, - "properties" => |_| { - properties = parse_properties(parser)?; + layers.push(LayerData::new( + parser, + attrs, + LayerTag::ImageLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, "objectgroup" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ObjectLayer, infinite, source_path)?); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::ObjectLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, "group" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::GroupLayer, infinite, source_path)?); + println!("Group layer 1"); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::GroupLayer, + infinite, + map_path, + &tilesets, + )?); + Ok(()) + }, + "properties" => |_| { + properties = parse_properties(parser)?; Ok(()) }, }); + + // We do not need first GIDs any more + let tilesets = tilesets.into_iter().map(|ts| ts.tileset).collect(); + Ok(Map { version: v, orientation: o, @@ -141,19 +258,6 @@ impl Map { infinite, }) } - - /// This function will return the correct Tileset given a GID. - pub fn tileset_by_gid(&self, gid: u32) -> Option<&Tileset> { - let mut maximum_gid: i32 = -1; - let mut maximum_ts = None; - for tileset in self.tilesets.iter() { - if tileset.first_gid as i32 > maximum_gid && tileset.first_gid <= gid { - maximum_gid = tileset.first_gid as i32; - maximum_ts = Some(tileset); - } - } - maximum_ts - } } /// Represents the way tiles are laid out in a map. @@ -189,3 +293,51 @@ impl fmt::Display for Orientation { } } } + +/// A Tiled global tile ID. +/// +/// These are used to identify tiles in a map. Since the map may have more than one tileset, an +/// unique mapping is required to convert the tiles' local tileset ID to one which will work nicely +/// even if there is more than one tileset. +/// +/// Tiled also treats GID 0 as empty space, which means that the first tileset in the map will have +/// a starting GID of 1. +/// +/// See also: https://doc.mapeditor.org/en/latest/reference/global-tile-ids/ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct Gid(pub u32); + +impl Gid { + /// The GID representing an empty tile in the map. + #[allow(dead_code)] + pub const EMPTY: Gid = Gid(0); +} + +/// A wrapper over a naive datatype that holds a reference to the parent map as well as the type's data. +#[derive(Clone, PartialEq, Debug)] +pub struct MapWrapper<'map, DataT> +where + DataT: Clone + PartialEq + std::fmt::Debug, +{ + map: &'map Map, + data: &'map DataT, +} + +impl<'map, DataT> MapWrapper<'map, DataT> +where + DataT: Clone + PartialEq + std::fmt::Debug, +{ + pub(crate) fn new(map: &'map Map, data: &'map DataT) -> Self { + Self { map, data } + } + + /// Get the wrapper's data. + pub fn data(&self) -> &'map DataT { + self.data + } + + /// Get the wrapper's map. + pub fn map(&self) -> &'map Map { + self.map + } +} diff --git a/src/objects.rs b/src/objects.rs index 6569f333..5c4e1fb9 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -1,11 +1,12 @@ -use std::{collections::HashMap, io::Read}; +use std::collections::HashMap; -use xml::{attribute::OwnedAttribute, EventReader}; +use xml::attribute::OwnedAttribute; use crate::{ error::TiledError, properties::{parse_properties, Properties}, - util::{get_attrs, parse_tag}, + util::{get_attrs, parse_tag, XmlEventResult}, + LayerTile, LayerTileData, MapTilesetGid, MapWrapper, }; #[derive(Debug, PartialEq, Clone)] @@ -18,9 +19,9 @@ pub enum ObjectShape { } #[derive(Debug, PartialEq, Clone)] -pub struct Object { +pub struct ObjectData { pub id: u32, - pub gid: u32, + tile: Option, pub name: String, pub obj_type: String, pub width: f32, @@ -33,12 +34,15 @@ pub struct Object { pub properties: Properties, } -impl Object { - pub(crate) fn new( - parser: &mut EventReader, +impl ObjectData { + /// If it is known that the object has no tile images in it (i.e. collision data) + /// then we can pass in [`None`] as the tilesets + pub(crate) fn new( + parser: &mut impl Iterator, attrs: Vec, - ) -> Result { - let ((id, gid, n, t, w, h, v, r), (x, y)) = get_attrs!( + tilesets: Option<&[MapTilesetGid]>, + ) -> Result { + let ((id, bits, n, t, w, h, v, r), (x, y)) = get_attrs!( attrs, optionals: [ ("id", id, |v:String| v.parse().ok()), @@ -61,7 +65,7 @@ impl Object { let h = h.unwrap_or(0f32); let r = r.unwrap_or(0f32); let id = id.unwrap_or(0u32); - let gid = gid.unwrap_or(0u32); + let tile = bits.and_then(|bits| LayerTileData::from_bits(bits, tilesets?)); let n = n.unwrap_or(String::new()); let t = t.unwrap_or(String::new()); let mut shape = None; @@ -76,15 +80,15 @@ impl Object { Ok(()) }, "polyline" => |attrs| { - shape = Some(Object::new_polyline(attrs)?); + shape = Some(ObjectData::new_polyline(attrs)?); Ok(()) }, "polygon" => |attrs| { - shape = Some(Object::new_polygon(attrs)?); + shape = Some(ObjectData::new_polygon(attrs)?); Ok(()) }, "point" => |_| { - shape = Some(Object::new_point(x, y)?); + shape = Some(ObjectData::new_point(x, y)?); Ok(()) }, "properties" => |_| { @@ -98,22 +102,24 @@ impl Object { height: h, }); - Ok(Object { - id: id, - gid: gid, + Ok(ObjectData { + id, + tile, name: n.clone(), obj_type: t.clone(), width: w, height: h, - x: x, - y: y, + x, + y, rotation: r, visible: v, - shape: shape, - properties: properties, + shape, + properties, }) } +} +impl ObjectData { fn new_polyline(attrs: Vec) -> Result { let ((), s) = get_attrs!( attrs, @@ -123,7 +129,7 @@ impl Object { ], TiledError::MalformedAttributes("A polyline must have points".to_string()) ); - let points = Object::parse_points(s)?; + let points = ObjectData::parse_points(s)?; Ok(ObjectShape::Polyline { points: points }) } @@ -136,7 +142,7 @@ impl Object { ], TiledError::MalformedAttributes("A polygon must have points".to_string()) ); - let points = Object::parse_points(s)?; + let points = ObjectData::parse_points(s)?; Ok(ObjectShape::Polygon { points: points }) } @@ -165,3 +171,14 @@ impl Object { Ok(points) } } + +pub type Object<'map> = MapWrapper<'map, ObjectData>; + +impl<'map> Object<'map> { + /// Returns the tile that the object is using as image, if any. + pub fn get_tile<'res: 'map>(&self) -> Option> { + self.data() + .tile + .map(|tile| LayerTile::from_data(&tile, self.map())) + } +} diff --git a/src/properties.rs b/src/properties.rs index 7e8c0bd9..928b7f91 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -1,38 +1,57 @@ -use std::{collections::HashMap, io::Read, str::FromStr}; +use std::{collections::HashMap, str::FromStr}; -use xml::{EventReader, attribute::OwnedAttribute, reader::XmlEvent}; +use xml::{attribute::OwnedAttribute, reader::XmlEvent}; use crate::{ - error::{ParseTileError, TiledError}, - util::{get_attrs, parse_tag}, + error::TiledError, + util::{get_attrs, parse_tag, XmlEventResult}, }; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct Color { + pub alpha: u8, pub red: u8, pub green: u8, pub blue: u8, } impl FromStr for Color { - type Err = ParseTileError; + type Err = (); - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { let s = if s.starts_with("#") { &s[1..] } else { s }; - if s.len() != 6 { - return Err(ParseTileError::ColorError); - } - let r = u8::from_str_radix(&s[0..2], 16); - let g = u8::from_str_radix(&s[2..4], 16); - let b = u8::from_str_radix(&s[4..6], 16); - if r.is_ok() && g.is_ok() && b.is_ok() { - return Ok(Color { - red: r.unwrap(), - green: g.unwrap(), - blue: b.unwrap(), - }); + match s.len() { + 6 => { + let r = u8::from_str_radix(&s[0..2], 16); + let g = u8::from_str_radix(&s[2..4], 16); + let b = u8::from_str_radix(&s[4..6], 16); + match (r, g, b) { + (Ok(red), Ok(green), Ok(blue)) => Ok(Color { + alpha: 0xFF, + red, + green, + blue, + }), + _ => Err(()), + } + } + 8 => { + let a = u8::from_str_radix(&s[0..2], 16); + let r = u8::from_str_radix(&s[2..4], 16); + let g = u8::from_str_radix(&s[4..6], 16); + let b = u8::from_str_radix(&s[6..8], 16); + match (a, r, g, b) { + (Ok(alpha), Ok(red), Ok(green), Ok(blue)) => Ok(Color { + alpha, + red, + green, + blue, + }), + _ => Err(()), + } + } + _ => Err(()), } - Err(ParseTileError::ColorError) } } @@ -87,8 +106,8 @@ impl PropertyValue { pub type Properties = HashMap; -pub(crate) fn parse_properties( - parser: &mut EventReader, +pub(crate) fn parse_properties( + parser: &mut impl Iterator, ) -> Result { let mut p = HashMap::new(); parse_tag!(parser, "properties", { @@ -105,13 +124,15 @@ pub(crate) fn parse_properties( TiledError::MalformedAttributes("property must have a name and a value".to_string()) ); let t = t.unwrap_or("string".into()); - - let v = match v_attr { + + let v: String = match v_attr { Some(val) => val, None => { // if the "value" attribute was missing, might be a multiline string - match parser.next().map_err(TiledError::XmlDecodingError)? { - XmlEvent::Characters(s) => Ok(s), + match parser.next() { + Some(Ok(XmlEvent::Characters(s))) => Ok(s), + Some(Err(err)) => Err(TiledError::XmlDecodingError(err)), + None => unreachable!(), // EndDocument or error must come first _ => Err(TiledError::MalformedAttributes(format!("property '{}' is missing a value", k))), }? } diff --git a/src/tile.rs b/src/tile.rs index a6d623b2..0493fb50 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -1,33 +1,34 @@ -use std::{collections::HashMap, io::Read, path::Path}; +use std::{collections::HashMap, path::Path}; -use xml::{attribute::OwnedAttribute, EventReader}; +use xml::attribute::OwnedAttribute; use crate::{ animation::Frame, error::TiledError, image::Image, - layers::ObjectLayer, + layers::ObjectLayerData, properties::{parse_properties, Properties}, - util::{get_attrs, parse_animation, parse_tag}, + util::{get_attrs, parse_animation, parse_tag, XmlEventResult}, }; -#[derive(Debug, PartialEq, Clone)] +pub type TileId = u32; + +#[derive(Debug, PartialEq, Clone, Default)] pub struct Tile { - pub id: u32, pub image: Option, pub properties: Properties, - pub collision: Option, + pub collision: Option, pub animation: Option>, pub tile_type: Option, pub probability: f32, } impl Tile { - pub(crate) fn new( - parser: &mut EventReader, + pub(crate) fn new( + parser: &mut impl Iterator, attrs: Vec, path_relative_to: Option<&Path>, - ) -> Result { + ) -> Result<(TileId, Tile), TiledError> { let ((tile_type, probability), id) = get_attrs!( attrs, optionals: [ @@ -46,7 +47,7 @@ impl Tile { let mut animation = None; parse_tag!(parser, "tile", { "image" => |attrs| { - image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_owned()})?)?); + image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse:"Image".to_owned()})?)?); Ok(()) }, "properties" => |_| { @@ -54,7 +55,7 @@ impl Tile { Ok(()) }, "objectgroup" => |attrs| { - objectgroup = Some(ObjectLayer::new(parser, attrs)?.0); + objectgroup = Some(ObjectLayerData::new(parser, attrs, None)?.0); Ok(()) }, "animation" => |_| { @@ -62,14 +63,16 @@ impl Tile { Ok(()) }, }); - Ok(Tile { + Ok(( id, - image, - properties, - collision: objectgroup, - animation, - tile_type, - probability: probability.unwrap_or(1.0), - }) + Tile { + image, + properties, + collision: objectgroup, + animation, + tile_type, + probability: probability.unwrap_or(1.0), + }, + )) } } diff --git a/src/tileset.rs b/src/tileset.rs index 2b120ecd..1efcb5db 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; @@ -11,19 +10,17 @@ use crate::error::TiledError; use crate::image::Image; use crate::properties::{parse_properties, Properties}; use crate::tile::Tile; -use crate::util::*; +use crate::{util::*, Gid}; /// A tileset, usually the tilesheet image. #[derive(Debug, PartialEq, Clone)] pub struct Tileset { - /// The GID of the first tile stored. - pub first_gid: u32, pub name: String, pub tile_width: u32, pub tile_height: u32, pub spacing: u32, pub margin: u32, - pub tilecount: Option, + pub tilecount: u32, pub columns: u32, /// A tileset can either: @@ -34,70 +31,60 @@ pub struct Tileset { /// - Source: [tiled issue #2117](https://github.com/mapeditor/tiled/issues/2117) /// - Source: [`columns` documentation](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset) pub image: Option, - pub tiles: Vec, + + /// All the tiles present in this tileset, indexed by their local IDs. + pub tiles: HashMap, + + /// The custom properties of the tileset. pub properties: Properties, /// Where this tileset was loaded from. - /// If fully embedded (loaded with path = `None`), this will return `None`. + /// If fully embedded, this will return `None`. pub source: Option, } +pub(crate) enum EmbeddedParseResultType { + ExternalReference { tileset_path: PathBuf }, + Embedded { tileset: Tileset }, +} + +pub(crate) struct EmbeddedParseResult { + pub first_gid: Gid, + pub result_type: EmbeddedParseResultType, +} + /// Internal structure for holding mid-parse information. struct TilesetProperties { spacing: Option, margin: Option, - tilecount: Option, + tilecount: u32, columns: Option, - first_gid: u32, name: String, tile_width: u32, tile_height: u32, + /// The path all non-absolute paths are relative to. path_relative_to: Option, source: Option, } impl Tileset { /// Parse a buffer hopefully containing the contents of a Tiled tileset. - /// - /// External tilesets do not have a firstgid attribute. That lives in the - /// map. You must pass in `first_gid`. If you do not need to use gids for anything, - /// passing in 1 will work fine. - pub fn parse(reader: R, first_gid: u32) -> Result { - Tileset::new_external(reader, first_gid, None) + pub fn parse(reader: R) -> Result { + Tileset::new_external(reader, None) } /// Parse a buffer hopefully containing the contents of a Tiled tileset. - /// - /// External tilesets do not have a firstgid attribute. That lives in the - /// map. You must pass in `first_gid`. If you do not need to use gids for anything, - /// passing in 1 will work fine. - pub fn parse_with_path( - reader: R, - first_gid: u32, - path: impl AsRef, - ) -> Result { - Tileset::new_external(reader, first_gid, Some(path.as_ref())) + pub fn parse_with_path(reader: R, path: impl AsRef) -> Result { + Tileset::new_external(reader, Some(path.as_ref())) } - pub(crate) fn parse_xml( - parser: &mut EventReader, - attrs: Vec, - path_relative_to: Option<&Path>, - ) -> Result { - Tileset::parse_xml_embedded(parser, &attrs, path_relative_to).or_else(|err| { - if matches!(err, TiledError::MalformedAttributes(_)) { - Tileset::parse_xml_reference(&attrs, path_relative_to) - } else { - Err(err) - } - }) + pub fn get_tile(&self, id: u32) -> Option<&Tile> { + self.tiles.get(&id) } +} - pub(crate) fn new_external( - file: R, - first_gid: u32, - path: Option<&Path>, - ) -> Result { +impl Tileset { + pub(crate) fn new_external(file: R, path: Option<&Path>) -> Result { let mut tileset_parser = EventReader::new(file); loop { match tileset_parser @@ -109,8 +96,7 @@ impl Tileset { } => { if name.local_name == "tileset" { return Self::parse_external_tileset( - first_gid, - &mut tileset_parser, + &mut tileset_parser.into_iter(), &attributes, path, ); @@ -126,21 +112,37 @@ impl Tileset { } } - fn parse_xml_embedded( - parser: &mut EventReader, + pub(crate) fn parse_xml_in_map( + parser: &mut impl Iterator, + attrs: Vec, + map_path: &Path, + ) -> Result { + let path_relative_to = map_path.parent(); + Tileset::parse_xml_embedded(parser, &attrs, path_relative_to).or_else(|err| { + if matches!(err, TiledError::MalformedAttributes(_)) { + Tileset::parse_xml_reference(&attrs, path_relative_to) + } else { + Err(err) + } + }) + } + + /// Returns both the tileset and its first gid in the corresponding map. + fn parse_xml_embedded( + parser: &mut impl Iterator, attrs: &Vec, path_relative_to: Option<&Path>, - ) -> Result { - let ((spacing, margin, tilecount, columns), (first_gid, name, tile_width, tile_height)) = get_attrs!( + ) -> Result { + let ((spacing, margin, columns), (tilecount, first_gid, name, tile_width, tile_height)) = get_attrs!( attrs, optionals: [ ("spacing", spacing, |v:String| v.parse().ok()), ("margin", margin, |v:String| v.parse().ok()), - ("tilecount", tilecount, |v:String| v.parse().ok()), ("columns", columns, |v:String| v.parse().ok()), ], required: [ - ("firstgid", first_gid, |v:String| v.parse().ok()), + ("tilecount", tilecount, |v:String| v.parse().ok()), + ("firstgid", first_gid, |v:String| v.parse().ok().map(|n| Gid(n))), ("name", name, |v| Some(v)), ("tilewidth", width, |v:String| v.parse().ok()), ("tileheight", height, |v:String| v.parse().ok()), @@ -159,21 +161,24 @@ impl Tileset { tilecount, tile_height, tile_width, - first_gid, source: None, }, ) + .map(|tileset| EmbeddedParseResult { + first_gid, + result_type: EmbeddedParseResultType::Embedded { tileset }, + }) } fn parse_xml_reference( attrs: &Vec, path_relative_to: Option<&Path>, - ) -> Result { + ) -> Result { let ((), (first_gid, source)) = get_attrs!( attrs, optionals: [], required: [ - ("firstgid", first_gid, |v:String| v.parse().ok()), + ("firstgid", first_gid, |v:String| v.parse().ok().map(|n| Gid(n))), ("source", name, |v| Some(v)), ], TiledError::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string()) @@ -184,35 +189,32 @@ impl Tileset { object_to_parse: "Tileset".to_string(), })? .join(source); - let file = File::open(&tileset_path).map_err(|_| { - TiledError::Other(format!( - "External tileset file not found: {:?}", - tileset_path - )) - })?; - Tileset::new_external(file, first_gid, Some(&tileset_path)) + + Ok(EmbeddedParseResult { + first_gid, + result_type: EmbeddedParseResultType::ExternalReference { tileset_path }, + }) } - fn parse_external_tileset( - first_gid: u32, - parser: &mut EventReader, + fn parse_external_tileset( + parser: &mut impl Iterator, attrs: &Vec, path: Option<&Path>, ) -> Result { - let ((spacing, margin, tilecount, columns), (name, tile_width, tile_height)) = get_attrs!( + let ((spacing, margin, columns), (tilecount, name, tile_width, tile_height)) = get_attrs!( attrs, optionals: [ ("spacing", spacing, |v:String| v.parse().ok()), ("margin", margin, |v:String| v.parse().ok()), - ("tilecount", tilecount, |v:String| v.parse().ok()), ("columns", columns, |v:String| v.parse().ok()), ], required: [ + ("tilecount", tilecount, |v:String| v.parse().ok()), ("name", name, |v| Some(v)), ("tilewidth", width, |v:String| v.parse().ok()), ("tileheight", height, |v:String| v.parse().ok()), ], - TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string()) + TiledError::MalformedAttributes("tileset must have a name, tile width and height with correct types".to_string()) ); let source_path = path.and_then(|p| p.parent().map(Path::to_owned)); @@ -228,19 +230,19 @@ impl Tileset { tilecount, tile_height, tile_width, - first_gid, source: path.map(Path::to_owned), }, ) } - fn finish_parsing_xml( - parser: &mut EventReader, + fn finish_parsing_xml( + parser: &mut impl Iterator, prop: TilesetProperties, - ) -> Result { + ) -> Result { let mut image = Option::None; - let mut tiles = Vec::new(); + let mut tiles = HashMap::with_capacity(prop.tilecount as usize); let mut properties = HashMap::new(); + parse_tag!(parser, "tileset", { "image" => |attrs| { image = Some(Image::new(parser, attrs, prop.path_relative_to.as_ref().ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); @@ -251,20 +253,29 @@ impl Tileset { Ok(()) }, "tile" => |attrs| { - tiles.push(Tile::new(parser, attrs, prop.path_relative_to.as_ref().and_then(|p| Some(p.as_path())))?); + let (id, tile) = Tile::new(parser, attrs, prop.path_relative_to.as_ref().and_then(|p| Some(p.as_path())))?; + tiles.insert(id, tile); Ok(()) }, }); - let (margin, spacing) = (prop.margin.unwrap_or(0), prop.spacing.unwrap_or(0)); + // A tileset is considered an image collection tileset if there is no image attribute (because its tiles do). + let is_image_collection_tileset = image.is_none(); + + if !is_image_collection_tileset { + for tile_id in 0..prop.tilecount { + tiles.entry(tile_id).or_default(); + } + } + let margin = prop.margin.unwrap_or(0); + let spacing = prop.spacing.unwrap_or(0); let columns = prop .columns .map(Ok) .unwrap_or_else(|| Self::calculate_columns(&image, prop.tile_width, margin, spacing))?; Ok(Tileset { - first_gid: prop.first_gid, name: prop.name, tile_width: prop.tile_width, tile_height: prop.tile_height, diff --git a/src/util.rs b/src/util.rs index 500cf3c8..0eafe246 100644 --- a/src/util.rs +++ b/src/util.rs @@ -29,8 +29,8 @@ macro_rules! get_attrs { /// that child. Closes the tag. macro_rules! parse_tag { ($parser:expr, $close_tag:expr, {$($open_tag:expr => $open_method:expr),* $(,)*}) => { - loop { - match $parser.next().map_err(TiledError::XmlDecodingError)? { + while let Some(next) = $parser.next() { + match next.map_err(TiledError::XmlDecodingError)? { xml::reader::XmlEvent::StartElement {name, attributes, ..} => { if false {} $(else if name.local_name == $open_tag { @@ -52,23 +52,14 @@ macro_rules! parse_tag { } } -use std::{ - collections::HashMap, - io::{BufReader, Read}, -}; - pub(crate) use get_attrs; pub(crate) use parse_tag; -use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader}; -use crate::{ - animation::Frame, - error::TiledError, - layers::{Chunk, LayerData, LayerTile}, -}; +use crate::{animation::Frame, error::TiledError, Gid, MapTilesetGid}; -pub(crate) fn parse_animation( - parser: &mut EventReader, +// TODO: Move to animation module +pub(crate) fn parse_animation( + parser: &mut impl Iterator, ) -> Result, TiledError> { let mut animation = Vec::new(); parse_tag!(parser, "animation", { @@ -80,179 +71,16 @@ pub(crate) fn parse_animation( Ok(animation) } -pub(crate) fn parse_infinite_data( - parser: &mut EventReader, - attrs: Vec, -) -> Result { - let ((e, c), ()) = get_attrs!( - attrs, - optionals: [ - ("encoding", encoding, |v| Some(v)), - ("compression", compression, |v| Some(v)), - ], - required: [], - TiledError::MalformedAttributes("data must have an encoding and a compression".to_string()) - ); - - let mut chunks = HashMap::<(i32, i32), Chunk>::new(); - parse_tag!(parser, "data", { - "chunk" => |attrs| { - let chunk = Chunk::new(parser, attrs, e.clone(), c.clone())?; - chunks.insert((chunk.x, chunk.y), chunk); - Ok(()) - } - }); - - Ok(LayerData::Infinite(chunks)) -} - -pub(crate) fn parse_data( - parser: &mut EventReader, - attrs: Vec, -) -> Result { - let ((e, c), ()) = get_attrs!( - attrs, - optionals: [ - ("encoding", encoding, |v| Some(v)), - ("compression", compression, |v| Some(v)), - ], - required: [], - TiledError::MalformedAttributes("data must have an encoding and a compression".to_string()) - ); - - let tiles = parse_data_line(e, c, parser)?; - - Ok(LayerData::Finite(tiles)) -} - -pub(crate) fn parse_data_line( - encoding: Option, - compression: Option, - parser: &mut EventReader, -) -> Result, TiledError> { - match (encoding, compression) { - (None, None) => { - return Err(TiledError::Other( - "XML format is currently not supported".to_string(), - )) - } - (Some(e), None) => match e.as_ref() { - "base64" => return parse_base64(parser).map(|v| convert_to_tiles(&v)), - "csv" => return decode_csv(parser), - e => return Err(TiledError::Other(format!("Unknown encoding format {}", e))), - }, - (Some(e), Some(c)) => match (e.as_ref(), c.as_ref()) { - ("base64", "zlib") => { - return parse_base64(parser) - .and_then(decode_zlib) - .map(|v| convert_to_tiles(&v)) - } - ("base64", "gzip") => { - return parse_base64(parser) - .and_then(decode_gzip) - .map(|v| convert_to_tiles(&v)) - } - #[cfg(feature = "zstd")] - ("base64", "zstd") => { - return parse_base64(parser) - .and_then(decode_zstd) - .map(|v| convert_to_tiles(&v)) - } - (e, c) => { - return Err(TiledError::Other(format!( - "Unknown combination of {} encoding and {} compression", - e, c - ))) - } - }, - _ => return Err(TiledError::Other("Missing encoding format".to_string())), - }; -} - -pub(crate) fn parse_base64(parser: &mut EventReader) -> Result, TiledError> { - loop { - match parser.next().map_err(TiledError::XmlDecodingError)? { - XmlEvent::Characters(s) => { - return base64::decode(s.trim().as_bytes()).map_err(TiledError::Base64DecodingError) - } - XmlEvent::EndElement { name, .. } => { - if name.local_name == "data" { - return Ok(Vec::new()); - } - } - _ => {} - } - } -} - -pub(crate) fn decode_zlib(data: Vec) -> Result, TiledError> { - use libflate::zlib::Decoder; - let mut zd = - Decoder::new(BufReader::new(&data[..])).map_err(|e| TiledError::DecompressingError(e))?; - let mut data = Vec::new(); - match zd.read_to_end(&mut data) { - Ok(_v) => {} - Err(e) => return Err(TiledError::DecompressingError(e)), - } - Ok(data) -} - -pub(crate) fn decode_gzip(data: Vec) -> Result, TiledError> { - use libflate::gzip::Decoder; - let mut zd = - Decoder::new(BufReader::new(&data[..])).map_err(|e| TiledError::DecompressingError(e))?; - - let mut data = Vec::new(); - zd.read_to_end(&mut data) - .map_err(|e| TiledError::DecompressingError(e))?; - Ok(data) -} - -#[cfg(feature = "zstd")] -pub(crate) fn decode_zstd(data: Vec) -> Result, TiledError> { - use std::io::Cursor; - use zstd::stream::read::Decoder; - - let buff = Cursor::new(&data); - let mut zd = Decoder::with_buffer(buff).map_err(|e| TiledError::DecompressingError(e))?; - - let mut data = Vec::new(); - zd.read_to_end(&mut data) - .map_err(|e| TiledError::DecompressingError(e))?; - Ok(data) -} - -pub(crate) fn decode_csv( - parser: &mut EventReader, -) -> Result, TiledError> { - loop { - match parser.next().map_err(TiledError::XmlDecodingError)? { - XmlEvent::Characters(s) => { - let tiles = s - .split(',') - .map(|v| v.trim().parse().unwrap()) - .map(LayerTile::new) - .collect(); - return Ok(tiles); - } - XmlEvent::EndElement { name, .. } => { - if name.local_name == "data" { - return Ok(Vec::new()); - } - } - _ => {} - } - } -} - -pub(crate) fn convert_to_tiles(all: &Vec) -> Vec { - let mut data = Vec::new(); - for chunk in all.chunks_exact(4) { - let n = chunk[0] as u32 - + ((chunk[1] as u32) << 8) - + ((chunk[2] as u32) << 16) - + ((chunk[3] as u32) << 24); - data.push(LayerTile::new(n)); - } - data +pub(crate) type XmlEventResult = xml::reader::Result; + +/// Returns both the tileset and its index +pub(crate) fn get_tileset_for_gid( + tilesets: &[MapTilesetGid], + gid: Gid, +) -> Option<(usize, &MapTilesetGid)> { + tilesets + .iter() + .enumerate() + .rev() + .find(|(_idx, ts)| ts.first_gid <= gid) } diff --git a/tests/lib.rs b/tests/lib.rs index cd30ead8..02e4bb05 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,85 +1,122 @@ -use std::path::Path; -use std::{fs::File, path::PathBuf}; -use tiled::{LayerData, Map, PropertyValue, TiledError, Tileset}; -use tiled::{LayerType, ObjectLayer, TileLayer}; +use std::path::PathBuf; +use tiled::{ + Color, FilesystemResourceCache, FiniteTileLayerData, Layer, LayerDataType, LayerType, Map, + ObjectLayer, PropertyValue, ResourceCache, TileLayer, TileLayerData, GroupLayer, +}; -fn as_tile_layer(layer: &LayerType) -> &TileLayer { - match layer { +fn as_tile_layer<'map>(layer: Layer<'map>) -> TileLayer<'map> { + match layer.layer_type() { LayerType::TileLayer(x) => x, _ => panic!("Not a tile layer"), } } -fn as_object_layer(layer: &LayerType) -> &ObjectLayer { - match layer { +fn as_finite(data: &TileLayerData) -> &FiniteTileLayerData { + match data { + TileLayerData::Finite(data) => data, + TileLayerData::Infinite(_) => panic!("Not a finite tile layer"), + } +} + +fn as_object_layer<'map>(layer: Layer<'map>) -> ObjectLayer<'map> { + match layer.layer_type() { LayerType::ObjectLayer(x) => x, _ => panic!("Not an object layer"), } } -fn parse_map_without_source(p: impl AsRef) -> Result { - let file = File::open(p).unwrap(); - return Map::parse_reader(file, None); +fn as_group_layer<'map>(layer: Layer<'map>) -> GroupLayer<'map> { + match layer.layer_type() { + LayerType::GroupLayer(x) => x, + _ => panic!("Not a group layer"), + } +} + +fn compare_everything_but_tileset_sources(r: &Map, e: &Map) { + assert_eq!(r.version, e.version); + assert_eq!(r.orientation, e.orientation); + assert_eq!(r.width, e.width); + assert_eq!(r.height, e.height); + assert_eq!(r.tile_width, e.tile_width); + assert_eq!(r.tile_height, e.tile_height); + assert_eq!(r.properties, e.properties); + assert_eq!(r.background_color, e.background_color); + assert_eq!(r.infinite, e.infinite); + r.layers() + .zip(e.layers()) + .for_each(|(r, e)| assert_eq!(r.data(), e.data())); } #[test] fn test_gzip_and_zlib_encoded_and_raw_are_the_same() { - let z = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap(); - let g = Map::parse_file("assets/tiled_base64_gzip.tmx").unwrap(); - let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); - let zstd = Map::parse_file("assets/tiled_base64_zstandard.tmx").unwrap(); - let c = Map::parse_file("assets/tiled_csv.tmx").unwrap(); - assert_eq!(z, g); - assert_eq!(z, r); - assert_eq!(z, c); - assert_eq!(z, zstd); - - if let LayerData::Finite(tiles) = &as_tile_layer(&c.layers[0].layer_type).tiles { - assert_eq!(tiles.len(), 100 * 100); - assert_eq!(tiles[0].gid, 35); - assert_eq!(tiles[100].gid, 17); - assert_eq!(tiles[200].gid, 0); - assert_eq!(tiles[200 + 1].gid, 17); - assert!(tiles[9900..9999].iter().map(|t| t.gid).all(|g| g == 0)); - } else { - panic!("It is wrongly recognised as an infinite map"); + let mut cache = FilesystemResourceCache::new(); + let z = Map::parse_file("assets/tiled_base64_zlib.tmx", &mut cache).unwrap(); + let g = Map::parse_file("assets/tiled_base64_gzip.tmx", &mut cache).unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap(); + let zstd = Map::parse_file("assets/tiled_base64_zstandard.tmx", &mut cache).unwrap(); + let c = Map::parse_file("assets/tiled_csv.tmx", &mut cache).unwrap(); + compare_everything_but_tileset_sources(&z, &g); + compare_everything_but_tileset_sources(&z, &r); + compare_everything_but_tileset_sources(&z, &c); + compare_everything_but_tileset_sources(&z, &zstd); + + let layer = as_tile_layer(c.get_layer(0).unwrap()); + { + let data = as_finite(layer.data()); + assert_eq!(data.width(), 100); + assert_eq!(data.height(), 100); } + + assert_eq!(layer.get_tile(0, 0).unwrap().id, 34); + assert_eq!(layer.get_tile(0, 1).unwrap().id, 16); + assert!(layer.get_tile(0, 2).is_none()); + assert_eq!(layer.get_tile(1, 2).unwrap().id, 16); + assert!((0..99).map(|x| layer.get_tile(x, 99)).all(|t| t.is_none())); } #[test] fn test_external_tileset() { - let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); - let mut e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); - e.tilesets[0].source = None; - assert_eq!(r, e); + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap(); + let e = Map::parse_file("assets/tiled_base64_external.tmx", &mut cache).unwrap(); + compare_everything_but_tileset_sources(&r, &e); } #[test] fn test_sources() { - let e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); + let mut cache = FilesystemResourceCache::new(); + + let e = Map::parse_file("assets/tiled_base64_external.tmx", &mut cache).unwrap(); assert_eq!( - e.tilesets[0].source, - Some(PathBuf::from("assets/tilesheet.tsx")) + e.tilesets()[0], + cache.get_tileset("assets/tilesheet.tsx").unwrap() ); assert_eq!( - e.tilesets[0].image.as_ref().unwrap().source, + e.tilesets()[0].image.as_ref().unwrap().source, PathBuf::from("assets/tilesheet.png") ); } #[test] fn test_just_tileset() { - let r = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); - let path = "assets/tilesheet.tsx"; - let t = Tileset::parse_with_path(File::open(path).unwrap(), 1, path).unwrap(); - assert_eq!(r.tilesets[0], t); + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_base64_external.tmx", &mut cache).unwrap(); + assert_eq!( + r.tilesets()[0], + cache.get_tileset("assets/tilesheet.tsx").unwrap() + ); } #[test] fn test_infinite_tileset() { - let r = Map::parse_file("assets/tiled_base64_zlib_infinite.tmx").unwrap(); + let mut cache = FilesystemResourceCache::new(); - if let LayerData::Infinite(chunks) = &as_tile_layer(&r.layers[0].layer_type).tiles { + let r = Map::parse_file("assets/tiled_base64_zlib_infinite.tmx", &mut cache).unwrap(); + + if let TileLayerData::Infinite(inf) = &as_tile_layer(r.get_layer(0).unwrap()).data() { + let chunks = &inf.chunks; assert_eq!(chunks.len(), 4); assert_eq!(chunks[&(0, 0)].width, 32); @@ -94,11 +131,13 @@ fn test_infinite_tileset() { #[test] fn test_image_layers() { - let r = Map::parse_file("assets/tiled_image_layers.tmx").unwrap(); - assert_eq!(r.layers.len(), 2); - let mut image_layers = r.layers.iter().map(|x| { - if let LayerType::ImageLayer(img) = &x.layer_type { - (img, x) + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_image_layers.tmx", &mut cache).unwrap(); + assert_eq!(r.layers().len(), 2); + let mut image_layers = r.layers().map(|layer| layer.data()).map(|layer| { + if let LayerDataType::ImageLayer(img) = &layer.layer_type { + (img, layer) } else { panic!("Found layer that isn't an image layer") } @@ -128,9 +167,14 @@ fn test_image_layers() { #[test] fn test_tile_property() { - let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); - let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = - r.tilesets[0].tiles[0].properties.get("a tile property") + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap(); + let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.tilesets()[0] + .get_tile(1) + .unwrap() + .properties + .get("a tile property") { v.clone() } else { @@ -141,25 +185,31 @@ fn test_tile_property() { #[test] fn test_layer_property() { - let r = Map::parse_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); - let prop_value: String = - if let Some(&PropertyValue::StringValue(ref v)) = r.layers[0].properties.get("prop3") { - v.clone() - } else { - String::new() - }; + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap(); + let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = + r.get_layer(0).unwrap().data().properties.get("prop3") + { + v.clone() + } else { + String::new() + }; assert_eq!("Line 1\r\nLine 2\r\nLine 3,\r\n etc\r\n ", prop_value); } #[test] fn test_object_group_property() { - let r = Map::parse_file("assets/tiled_object_groups.tmx").unwrap(); - let sub_layer = match r.layers[1].layer_type { - LayerType::GroupLayer(ref layer) => &layer.layers[0], - _ => { panic!("Layer was expected to be a group layer"); } - }; - let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = - sub_layer.properties.get("an object group property") + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_object_groups.tmx", &mut cache).unwrap(); + let group_layer = r.get_layer(1).unwrap(); + let group_layer = as_group_layer(group_layer); + let sub_layer = group_layer.get_layer(0).unwrap(); + let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = sub_layer + .data() + .properties + .get("an object group property") { *v } else { @@ -169,9 +219,11 @@ fn test_object_group_property() { } #[test] fn test_tileset_property() { - let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_base64.tmx", &mut cache).unwrap(); let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = - r.tilesets[0].properties.get("tileset property") + r.tilesets()[0].properties.get("tileset property") { v.clone() } else { @@ -181,65 +233,70 @@ fn test_tileset_property() { } #[test] -fn test_flipped_gid() { - let r = Map::parse_file("assets/tiled_flipped.tmx").unwrap(); - - if let LayerData::Finite(tiles) = &as_tile_layer(&r.layers[0].layer_type).tiles { - let t1 = tiles[0]; - let t2 = tiles[1]; - let t3 = tiles[2]; - let t4 = tiles[3]; - assert_eq!(t1.gid, t2.gid); - assert_eq!(t2.gid, t3.gid); - assert_eq!(t3.gid, t4.gid); - assert!(t1.flip_d); - assert!(t1.flip_h); - assert!(t1.flip_v); - assert!(!t2.flip_d); - assert!(!t2.flip_h); - assert!(t2.flip_v); - assert!(!t3.flip_d); - assert!(t3.flip_h); - assert!(!t3.flip_v); - assert!(t4.flip_d); - assert!(!t4.flip_h); - assert!(!t4.flip_v); - } else { - assert!(false, "It is wrongly recognised as an infinite map"); - } +fn test_flipped() { + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_flipped.tmx", &mut cache).unwrap(); + let layer = as_tile_layer(r.get_layer(0).unwrap()); + + let t1 = layer.get_tile(0, 0).unwrap(); + let t2 = layer.get_tile(1, 0).unwrap(); + let t3 = layer.get_tile(0, 1).unwrap(); + let t4 = layer.get_tile(1, 1).unwrap(); + assert_eq!(t1.id, t2.id); + assert_eq!(t2.id, t3.id); + assert_eq!(t3.id, t4.id); + assert!(t1.flip_d); + assert!(t1.flip_h); + assert!(t1.flip_v); + assert!(!t2.flip_d); + assert!(!t2.flip_h); + assert!(t2.flip_v); + assert!(!t3.flip_d); + assert!(t3.flip_h); + assert!(!t3.flip_v); + assert!(t4.flip_d); + assert!(!t4.flip_h); + assert!(!t4.flip_v); } #[test] fn test_ldk_export() { - let r = Map::parse_file("assets/ldk_tiled_export.tmx").unwrap(); - if let LayerData::Finite(tiles) = &as_tile_layer(&r.layers[0].layer_type).tiles { - assert_eq!(tiles.len(), 8 * 8); - assert_eq!(tiles[0].gid, 0); - assert_eq!(tiles[8].gid, 1); - } else { - assert!(false, "It is wrongly recognised as an infinite map"); + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/ldk_tiled_export.tmx", &mut cache).unwrap(); + let layer = as_tile_layer(r.get_layer(0).unwrap()); + { + let data = as_finite(layer.data()); + assert_eq!(data.width(), 8); + assert_eq!(data.height(), 8); } + assert!(layer.get_tile(0, 0).is_none()); + assert_eq!(layer.get_tile(0, 1).unwrap().id, 0); } #[test] fn test_parallax_layers() { - let r = Map::parse_file("assets/tiled_parallax.tmx").unwrap(); - for (i, layer) in r.layers.iter().enumerate() { + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_parallax.tmx", &mut cache).unwrap(); + for (i, layer) in r.layers().enumerate() { + let data = layer.data(); match i { 0 => { - assert_eq!(layer.name, "Background"); - assert_eq!(layer.parallax_x, 0.5); - assert_eq!(layer.parallax_y, 0.75); + assert_eq!(data.name, "Background"); + assert_eq!(data.parallax_x, 0.5); + assert_eq!(data.parallax_y, 0.75); } 1 => { - assert_eq!(layer.name, "Middle"); - assert_eq!(layer.parallax_x, 1.0); - assert_eq!(layer.parallax_y, 1.0); + assert_eq!(data.name, "Middle"); + assert_eq!(data.parallax_x, 1.0); + assert_eq!(data.parallax_y, 1.0); } 2 => { - assert_eq!(layer.name, "Foreground"); - assert_eq!(layer.parallax_x, 2.0); - assert_eq!(layer.parallax_y, 2.0); + assert_eq!(data.name, "Foreground"); + assert_eq!(data.parallax_x, 2.0); + assert_eq!(data.parallax_y, 2.0); } _ => panic!("unexpected layer"), } @@ -248,9 +305,12 @@ fn test_parallax_layers() { #[test] fn test_object_property() { - let r = parse_map_without_source(&Path::new("assets/tiled_object_property.tmx")).unwrap(); + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_object_property.tmx", &mut cache).unwrap(); + let layer = r.get_layer(1).unwrap(); let prop_value = if let Some(PropertyValue::ObjectValue(v)) = - as_object_layer(&r.layers[1].layer_type).objects[0] + as_object_layer(layer).data().objects[0] .properties .get("object property") { @@ -261,49 +321,73 @@ fn test_object_property() { assert_eq!(3, prop_value); } -#[test] -fn test_group_layers() { - let r = Map::parse_file("assets/tiled_group_layers.tmx").unwrap(); - let (layer_tile_1, layer_group_1, layer_group_2) = (&r.layers[0], &r.layers[1], &r.layers[2]); - let layer_tile_2 = match layer_group_1.layer_type { - LayerType::GroupLayer(ref group_layer) => &group_layer.layers[0], - _ => panic!("expected group layer") - }; - let layer_group_3 = match layer_group_2.layer_type { - LayerType::GroupLayer(ref group_layer) => &group_layer.layers[0], - _ => panic!("expected group layer") - }; - let layer_tile_3 = match layer_group_3.layer_type { - LayerType::GroupLayer(ref group_layer) => &group_layer.layers[0], - _ => panic!("expected group layer") - }; - match layer_tile_3.layer_type { - LayerType::TileLayer(ref tile_layer) => {}, - _ => panic!("expected tile layer") - }; +fn test_tint_color() { + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_image_layers.tmx", &mut cache).unwrap(); assert_eq!( - Some(&PropertyValue::StringValue("value1".to_string())), - layer_tile_1.properties.get("key") + r.get_layer(0).unwrap().data().tint_color, + Some(Color { + alpha: 0x12, + red: 0x34, + green: 0x56, + blue: 0x78 + }) ); assert_eq!( - Some(&PropertyValue::StringValue("value2".to_string())), - layer_tile_2.properties.get("key") + r.get_layer(1).unwrap().data().tint_color, + Some(Color { + alpha: 0xFF, + red: 0x12, + green: 0x34, + blue: 0x56 + }) ); +} + +#[test] +fn test_group_layers() { + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_group_layers.tmx", &mut cache).unwrap(); + + // Depth = 0 + let layer_tile_1 = r.get_layer(0).unwrap(); + let layer_group_1 = r.get_layer(1).unwrap(); + let layer_group_2 = r.get_layer(2).unwrap(); + assert_eq!( - Some(&PropertyValue::StringValue("value3".to_string())), - layer_tile_3.properties.get("key") + Some(&PropertyValue::StringValue("value1".to_string())), + layer_tile_1.data().properties.get("key") ); assert_eq!( Some(&PropertyValue::StringValue("value4".to_string())), - layer_group_1.properties.get("key") + layer_group_1.data().properties.get("key") ); assert_eq!( Some(&PropertyValue::StringValue("value5".to_string())), - layer_group_2.properties.get("key") + layer_group_2.data().properties.get("key") + ); + + // Depth = 1 + let layer_group_1 = as_group_layer(layer_group_1); + let layer_tile_2 = layer_group_1.get_layer(0).unwrap(); + let layer_group_2 = as_group_layer(layer_group_2); + let layer_group_3 = layer_group_2.get_layer(0).unwrap(); + assert_eq!( + Some(&PropertyValue::StringValue("value2".to_string())), + layer_tile_2.data().properties.get("key") ); assert_eq!( Some(&PropertyValue::StringValue("value6".to_string())), - layer_group_3.properties.get("key") + layer_group_3.data().properties.get("key") ); - assert_eq!(3, r.layers.len()); -} + + // Depth = 2 + let layer_group_3 = as_group_layer(layer_group_3); + let layer_tile_3 = layer_group_3.get_layer(0).unwrap(); + assert_eq!( + Some(&PropertyValue::StringValue("value3".to_string())), + layer_tile_3.data().properties.get("key") + ); +} \ No newline at end of file