From df98e9f35afb6d6a7d3b431537682ee67382fb68 Mon Sep 17 00:00:00 2001 From: Alejandro Perea <alexpro820@gmail.com> Date: Wed, 9 Feb 2022 17:02:45 +0100 Subject: [PATCH] Hide GIDs as internal details (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Partial commit * Partial commit * Partial commit * Some suggested changes and fixed tests (except infinite) * Replaced `tile` member in `LayerTileRef` with a function to get an `Option<&Tile>`. * Replaced `Map::get_tile_by_gid` with `Map::get_tileset_for_gid`, which just returns the `Option<&Tileset>`. It also does a reverse search, which fixes the lookup in case an external tileset has grown since the map was last saved. * Replaced `Tileset::get_tile_by_gid` with `Tileset::get_tile`, since subtracting of the `first_gid` already happens when creating the `LayerTileRef`. Also, we eventually should remove `first_gid` from `Tileset`, since it should be possible to share a single tileset instance betweeen several maps. * Pre-allocate the tiles hash map for the expected number of tiles and use `or_default` instead of `or_insert_with(Default::default)`. * [nonbuilding] Move ownership of tilesets - Moves the ownership of tilesets from Map to an object implementing `TilesetCache` * Clean up * More cleanup * Organize layers into modules * Further modularization * Add layer wrappers * Implement `Clone + PartialEq + Debug` for wrappers * Fix example * Fix all tests except for test_infinite_tileset * Move layer utils to its own module * Better `Map::layers` documentation * `TilesetCache` -> `cache::ResourceCache` * Add `ResourcePath`, rename and add errors * Interface changes - Move embedded tilesets from cache to map - Store `Option<LayerTileData>` instead of `LayerTileData` * parser ->`&mut impl Iterator<Item=XmlEventResult>` * Document that tilesets are ordered by first GID * Fix the layer tiles using GIDs issue * Run `cargo fix` * Fix `test_infinite_tileset` tests * Implement a way to access object tile data * Rename `TiledWrapper` to `MapWrapper` * More efficient `get_or_try_insert_tileset_with` * Add `ResourcePathBuf`, use `Rc<Tileset>` in `Map` * Remove `MapTileset::first_gid` * Run `cargo fix` * Remove unrelated `Tileset` changes * Requested changes * Avoid reference counting when accessing tiles * Store tile data instead of (u32, u32)` in objects * Remove unneeded functions from `ResourceCache` * Address PR comments * Misc improvements in `layers::tile` * Improve example * Convert `LayerTile` properties to fields Co-authored-by: Thorbjørn Lindeijer <bjorn@lindeijer.nl> --- CHANGELOG.md | 3 +- README.md | 2 + examples/main.rs | 51 +++++- src/cache.rs | 55 +++++++ src/error.rs | 53 +++++-- src/image.rs | 11 +- src/layers.rs | 303 ------------------------------------ src/layers/image.rs | 38 +++++ src/layers/mod.rs | 130 ++++++++++++++++ src/layers/object.rs | 59 +++++++ src/layers/tile/finite.rs | 71 +++++++++ src/layers/tile/infinite.rs | 91 +++++++++++ src/layers/tile/mod.rs | 148 ++++++++++++++++++ src/layers/tile/util.rs | 140 +++++++++++++++++ src/lib.rs | 2 + src/map.rs | 223 +++++++++++++++++++++----- src/objects.rs | 63 +++++--- src/properties.rs | 18 ++- src/tile.rs | 43 ++--- src/tileset.rs | 159 ++++++++++--------- src/util.rs | 208 +++---------------------- tests/lib.rs | 285 +++++++++++++++++++-------------- 22 files changed, 1357 insertions(+), 799 deletions(-) create mode 100644 src/cache.rs delete mode 100644 src/layers.rs create mode 100644 src/layers/image.rs create mode 100644 src/layers/mod.rs create mode 100644 src/layers/object.rs create mode 100644 src/layers/tile/finite.rs create mode 100644 src/layers/tile/infinite.rs create mode 100644 src/layers/tile/mod.rs create mode 100644 src/layers/tile/util.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fbbe474..a333071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,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. @@ -29,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<Vec<LayerTile>>` to `Vec<LayerTile>`. - 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`. diff --git a/README.md b/README.md index fd40af5..33eae93 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/examples/main.rs b/examples/main.rs index d39dd52..4269d8d 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,7 +1,50 @@ -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(), + } + ) + } + } + } } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..0a171b6 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,55 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + rc::Rc, +}; + +use crate::Tileset; + +pub type ResourcePath = Path; +pub type ResourcePathBuf = PathBuf; + +pub trait ResourceCache { + fn get_tileset(&self, path: impl AsRef<ResourcePath>) -> Option<Rc<Tileset>>; + fn get_or_try_insert_tileset_with<F, E>( + &mut self, + path: ResourcePathBuf, + f: F, + ) -> Result<Rc<Tileset>, E> + where + F: FnOnce() -> Result<Tileset, E>; +} + +/// A cache that identifies resources by their path in the user's filesystem. +pub struct FilesystemResourceCache { + tilesets: HashMap<ResourcePathBuf, Rc<Tileset>>, +} + +impl FilesystemResourceCache { + pub fn new() -> Self { + Self { + tilesets: HashMap::new(), + } + } +} + +impl ResourceCache for FilesystemResourceCache { + fn get_tileset(&self, path: impl AsRef<ResourcePath>) -> Option<Rc<Tileset>> { + self.tilesets.get(path.as_ref()).map(Clone::clone) + } + + fn get_or_try_insert_tileset_with<F, E>( + &mut self, + path: ResourcePathBuf, + f: F, + ) -> Result<Rc<Tileset>, E> + where + F: FnOnce() -> Result<Tileset, E>, + { + Ok(match self.tilesets.entry(path) { + std::collections::hash_map::Entry::Occupied(o) => o.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => v.insert(Rc::new(f()?)), + } + .clone()) + } +} diff --git a/src/error.rs b/src/error.rs index 55c4d03..3b0f789 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 a7cf8ef..7399f38 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<R: Read>( - parser: &mut EventReader<R>, + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, attrs: Vec<OwnedAttribute>, path_relative_to: impl AsRef<Path>, ) -> Result<Image, TiledError> { diff --git a/src/layers.rs b/src/layers.rs deleted file mode 100644 index e75f345..0000000 --- a/src/layers.rs +++ /dev/null @@ -1,303 +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), - // TODO: Support group layers -} - -#[derive(Clone, Copy)] -pub(crate) enum LayerTag { - TileLayer, - ObjectLayer, - ImageLayer, -} - -#[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 tint_color: Option<Color>, - pub properties: Properties, - pub layer_type: LayerType, -} - -impl Layer { - pub(crate) fn new<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, - tag: LayerTag, - infinite: bool, - path_relative_to: Option<&Path>, - ) -> Result<Self, TiledError> { - let ( - (opacity, tint_color, visible, offset_x, offset_y, parallax_x, parallax_y, name, id), - (), - ) = get_attrs!( - attrs, - optionals: [ - ("opacity", opacity, |v:String| v.parse().ok()), - ("tintcolor", tint_color, |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) - } - }; - - 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), - tint_color, - 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<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, - 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<LayerTile>), - Infinite(HashMap<(i32, i32), Chunk>), -} - -#[derive(Debug, PartialEq, Clone)] -pub struct ImageLayer { - pub image: Option<Image>, -} - -impl ImageLayer { - pub(crate) fn new<R: Read>( - parser: &mut EventReader<R>, - path_relative_to: Option<&Path>, - ) -> Result<(ImageLayer, Properties), TiledError> { - let mut image: Option<Image> = 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<Object>, - pub colour: Option<Color>, -} - -impl ObjectLayer { - pub(crate) fn new<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, - ) -> 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 Chunk { - pub x: i32, - pub y: i32, - pub width: u32, - pub height: u32, - pub tiles: Vec<LayerTile>, -} - -impl Chunk { - pub(crate) fn new<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, - encoding: Option<String>, - compression: Option<String>, - ) -> Result<Chunk, TiledError> { - 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/image.rs b/src/layers/image.rs new file mode 100644 index 0000000..0ea05c0 --- /dev/null +++ b/src/layers/image.rs @@ -0,0 +1,38 @@ +use std::{collections::HashMap, path::Path}; + +use crate::{ + parse_properties, + util::{parse_tag, XmlEventResult}, + Image, MapWrapper, Properties, TiledError, +}; + +#[derive(Debug, PartialEq, Clone)] +pub struct ImageLayerData { + pub image: Option<Image>, +} + +impl ImageLayerData { + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, + map_path: &Path, + ) -> Result<(Self, Properties), TiledError> { + let mut image: Option<Image> = None; + let mut properties = HashMap::new(); + + let path_relative_to = map_path.parent().ok_or(TiledError::PathIsNotFile)?; + + parse_tag!(parser, "imagelayer", { + "image" => |attrs| { + image = Some(Image::new(parser, attrs, path_relative_to)?); + Ok(()) + }, + "properties" => |_| { + properties = parse_properties(parser)?; + Ok(()) + }, + }); + Ok((ImageLayerData { image }, properties)) + } +} + +pub type ImageLayer<'map> = MapWrapper<'map, ImageLayerData>; diff --git a/src/layers/mod.rs b/src/layers/mod.rs new file mode 100644 index 0000000..b05004a --- /dev/null +++ b/src/layers/mod.rs @@ -0,0 +1,130 @@ +use std::path::Path; + +use xml::attribute::OwnedAttribute; + +use crate::{error::TiledError, properties::Properties, util::*, Map, MapTilesetGid, MapWrapper, Color}; + +mod image; +pub use image::*; +mod object; +pub use object::*; +mod tile; +pub use tile::*; + +#[derive(Clone, PartialEq, Debug)] +pub enum LayerDataType { + TileLayer(TileLayerData), + ObjectLayer(ObjectLayerData), + ImageLayer(ImageLayerData), + // TODO: Support group layers +} + +#[derive(Clone, Copy)] +pub(crate) enum LayerTag { + TileLayer, + ObjectLayer, + ImageLayer, +} + +#[derive(Clone, PartialEq, Debug)] +pub struct LayerData { + 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 tint_color: Option<Color>, + pub properties: Properties, + pub layer_type: LayerDataType, +} + +impl LayerData { + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + tag: LayerTag, + infinite: bool, + map_path: &Path, + tilesets: &[MapTilesetGid], + ) -> Result<Self, TiledError> { + let ( + (opacity, tint_color, visible, offset_x, offset_y, parallax_x, parallax_y, name, id), + (), + ) = get_attrs!( + attrs, + optionals: [ + ("opacity", opacity, |v:String| v.parse().ok()), + ("tintcolor", tint_color, |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) = TileLayerData::new(parser, attrs, infinite, tilesets)?; + (LayerDataType::TileLayer(ty), properties) + } + LayerTag::ObjectLayer => { + let (ty, properties) = ObjectLayerData::new(parser, attrs, Some(tilesets))?; + (LayerDataType::ObjectLayer(ty), properties) + } + LayerTag::ImageLayer => { + let (ty, properties) = ImageLayerData::new(parser, map_path)?; + (LayerDataType::ImageLayer(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), + tint_color, + name: name.unwrap_or_default(), + id: id.unwrap_or(0), + properties, + layer_type: ty, + }) + } +} + +pub type Layer<'map> = MapWrapper<'map, LayerData>; + +impl<'map> Layer<'map> { + /// Get the layer's type. + pub fn layer_type(&self) -> LayerType<'map> { + LayerType::new(self.map(), &self.data().layer_type) + } +} + +pub enum LayerType<'map> { + TileLayer(TileLayer<'map>), + ObjectLayer(ObjectLayer<'map>), + ImageLayer(ImageLayer<'map>), + // TODO: Support group layers +} + +impl<'map> LayerType<'map> { + fn new(map: &'map Map, data: &'map LayerDataType) -> Self { + match data { + 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)), + } + } +} diff --git a/src/layers/object.rs b/src/layers/object.rs new file mode 100644 index 0000000..dbae3e8 --- /dev/null +++ b/src/layers/object.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; + +use xml::attribute::OwnedAttribute; + +use crate::{ + parse_properties, + util::{get_attrs, parse_tag, XmlEventResult}, + Color, MapTilesetGid, MapWrapper, Object, ObjectData, Properties, TiledError, +}; + +#[derive(Debug, PartialEq, Clone)] +pub struct ObjectLayerData { + pub objects: Vec<ObjectData>, + pub colour: Option<Color>, +} + +impl ObjectLayerData { + /// If it is known that there are no objects with 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<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + tilesets: Option<&[MapTilesetGid]>, + ) -> Result<(ObjectLayerData, 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(ObjectData::new(parser, attrs, tilesets)?); + Ok(()) + }, + "properties" => |_| { + properties = parse_properties(parser)?; + Ok(()) + }, + }); + Ok((ObjectLayerData { objects, colour: c }, properties)) + } +} + +pub type ObjectLayer<'map> = MapWrapper<'map, ObjectLayerData>; + +impl<'map> ObjectLayer<'map> { + pub fn get_object(&self, idx: usize) -> Option<Object<'map>> { + self.data() + .objects + .get(idx) + .map(|data| Object::new(self.map(), data)) + } +} diff --git a/src/layers/tile/finite.rs b/src/layers/tile/finite.rs new file mode 100644 index 0000000..55e9eb0 --- /dev/null +++ b/src/layers/tile/finite.rs @@ -0,0 +1,71 @@ +use xml::attribute::OwnedAttribute; + +use crate::{ + util::{get_attrs, XmlEventResult}, + LayerTileData, MapTilesetGid, TiledError, +}; + +use super::util::parse_data_line; + +#[derive(PartialEq, Clone, Default)] +pub struct FiniteTileLayerData { + width: u32, + height: u32, + /// The tiles are arranged in rows. + pub(crate) tiles: Vec<Option<LayerTileData>>, +} + +impl std::fmt::Debug for FiniteTileLayerData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FiniteTileLayerData") + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} + +impl FiniteTileLayerData { + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + width: u32, + height: u32, + tilesets: &[MapTilesetGid], + ) -> Result<Self, TiledError> { + 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, tilesets)?; + + Ok(Self { + width, + height, + tiles, + }) + } + + pub(crate) fn get_tile(&self, x: usize, y: usize) -> Option<&LayerTileData> { + if x < self.width as usize && y < self.height as usize { + self.tiles[x + y * self.width as usize].as_ref() + } else { + None + } + } + + /// Get the tile layer's width in tiles. + pub fn width(&self) -> u32 { + self.width + } + + /// Get the tile layer's height in tiles. + pub fn height(&self) -> u32 { + self.height + } +} diff --git a/src/layers/tile/infinite.rs b/src/layers/tile/infinite.rs new file mode 100644 index 0000000..1a50aac --- /dev/null +++ b/src/layers/tile/infinite.rs @@ -0,0 +1,91 @@ +use std::collections::HashMap; + +use xml::attribute::OwnedAttribute; + +use crate::{ + util::{get_attrs, parse_tag, XmlEventResult}, + LayerTileData, MapTilesetGid, TiledError, +}; + +use super::util::parse_data_line; + +#[derive(PartialEq, Clone)] +pub struct InfiniteTileLayerData { + pub chunks: HashMap<(i32, i32), Chunk>, +} + +impl std::fmt::Debug for InfiniteTileLayerData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InfiniteTileLayerData").finish() + } +} + +impl InfiniteTileLayerData { + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + tilesets: &[MapTilesetGid], + ) -> Result<Self, TiledError> { + 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(), tilesets)?; + chunks.insert((chunk.x, chunk.y), chunk); + Ok(()) + } + }); + + Ok(Self { chunks }) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Chunk { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + tiles: Vec<Option<LayerTileData>>, +} + +impl Chunk { + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + encoding: Option<String>, + compression: Option<String>, + tilesets: &[MapTilesetGid], + ) -> Result<Chunk, TiledError> { + 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, tilesets)?; + + Ok(Chunk { + x, + y, + width, + height, + tiles, + }) + } +} diff --git a/src/layers/tile/mod.rs b/src/layers/tile/mod.rs new file mode 100644 index 0000000..49c06b0 --- /dev/null +++ b/src/layers/tile/mod.rs @@ -0,0 +1,148 @@ +use std::collections::HashMap; + +use xml::attribute::OwnedAttribute; + +use crate::{ + parse_properties, + util::{get_attrs, parse_tag, XmlEventResult}, + Gid, Map, MapTilesetGid, MapWrapper, Properties, Tile, TileId, TiledError, Tileset, +}; + +mod finite; +mod infinite; +mod util; + +pub use finite::*; +pub use infinite::*; + +/// Stores the internal tile gid about a layer tile, along with how it is flipped. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct LayerTileData { + /// The index of the tileset this tile's in, relative to the tile's map. + pub(crate) tileset_index: usize, + /// The local ID of the tile in the tileset it's in. + pub(crate) id: TileId, + pub flip_h: bool, + pub flip_v: bool, + pub flip_d: bool, +} + +impl LayerTileData { + const FLIPPED_HORIZONTALLY_FLAG: u32 = 0x80000000; + const FLIPPED_VERTICALLY_FLAG: u32 = 0x40000000; + const FLIPPED_DIAGONALLY_FLAG: u32 = 0x20000000; + const ALL_FLIP_FLAGS: u32 = Self::FLIPPED_HORIZONTALLY_FLAG + | Self::FLIPPED_VERTICALLY_FLAG + | Self::FLIPPED_DIAGONALLY_FLAG; + + /// Creates a new [`LayerTileData`] from a [`GID`] plus its flipping bits. + pub(crate) fn from_bits(bits: u32, tilesets: &[MapTilesetGid]) -> Option<Self> { + let flags = bits & Self::ALL_FLIP_FLAGS; + let gid = Gid(bits & !Self::ALL_FLIP_FLAGS); + let flip_d = flags & Self::FLIPPED_DIAGONALLY_FLAG == Self::FLIPPED_DIAGONALLY_FLAG; // Swap x and y axis (anti-diagonally) [flips over y = -x line] + let flip_h = flags & Self::FLIPPED_HORIZONTALLY_FLAG == Self::FLIPPED_HORIZONTALLY_FLAG; // Flip tile over y axis + let flip_v = flags & Self::FLIPPED_VERTICALLY_FLAG == Self::FLIPPED_VERTICALLY_FLAG; // Flip tile over x axis + + if gid == Gid::EMPTY { + None + } else { + let (tileset_index, tileset) = crate::util::get_tileset_for_gid(tilesets, gid)?; + let id = gid.0 - tileset.first_gid.0; + + Some(Self { + tileset_index, + id, + flip_h, + flip_v, + flip_d, + }) + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum TileLayerData { + Finite(FiniteTileLayerData), + Infinite(InfiniteTileLayerData), +} + +impl TileLayerData { + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + infinite: bool, + tilesets: &[MapTilesetGid], + ) -> Result<(Self, Properties), TiledError> { + let ((), (width, height)) = 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 result = Self::Finite(Default::default()); + let mut properties = HashMap::new(); + parse_tag!(parser, "layer", { + "data" => |attrs| { + if infinite { + result = Self::Infinite(InfiniteTileLayerData::new(parser, attrs, tilesets)?); + } else { + result = Self::Finite(FiniteTileLayerData::new(parser, attrs, width, height, tilesets)?); + } + Ok(()) + }, + "properties" => |_| { + properties = parse_properties(parser)?; + Ok(()) + }, + }); + + Ok((result, properties)) + } + + pub(crate) fn get_tile(&self, x: usize, y: usize) -> Option<&LayerTileData> { + match &self { + Self::Finite(finite) => finite.get_tile(x, y), + Self::Infinite(_) => todo!("Getting tiles from infinite layers"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LayerTile<'map> { + pub tileset: &'map Tileset, + pub id: TileId, + pub flip_h: bool, + pub flip_v: bool, + pub flip_d: bool, +} + +impl<'map> LayerTile<'map> { + pub(crate) fn from_data(data: &LayerTileData, map: &'map Map) -> Self { + Self { + tileset: &*map.tilesets()[data.tileset_index], + id: data.id, + flip_h: data.flip_h, + flip_v: data.flip_v, + flip_d: data.flip_d, + } + } + + /// Get a reference to the layer tile's referenced tile, if it exists. + pub fn get_tile(&self) -> Option<&'map Tile> { + self.tileset.get_tile(self.id) + } +} + +pub type TileLayer<'map> = MapWrapper<'map, TileLayerData>; + +impl<'map> TileLayer<'map> { + pub fn get_tile(&self, x: usize, y: usize) -> Option<LayerTile> { + self.data() + .get_tile(x, y) + .and_then(|data| Some(LayerTile::from_data(data, self.map()))) + } +} diff --git a/src/layers/tile/util.rs b/src/layers/tile/util.rs new file mode 100644 index 0000000..b3c3a00 --- /dev/null +++ b/src/layers/tile/util.rs @@ -0,0 +1,140 @@ +use std::io::{BufReader, Read}; + +use xml::reader::XmlEvent; + +use crate::{util::XmlEventResult, LayerTileData, MapTilesetGid, TiledError}; + +pub(crate) fn parse_data_line( + encoding: Option<String>, + compression: Option<String>, + parser: &mut impl Iterator<Item = XmlEventResult>, + tilesets: &[MapTilesetGid], +) -> Result<Vec<Option<LayerTileData>>, 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, tilesets)), + "csv" => return decode_csv(parser, tilesets), + 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, tilesets)) + } + ("base64", "gzip") => { + return parse_base64(parser) + .and_then(decode_gzip) + .map(|v| convert_to_tiles(&v, tilesets)) + } + #[cfg(feature = "zstd")] + ("base64", "zstd") => { + return parse_base64(parser) + .and_then(decode_zstd) + .map(|v| convert_to_tiles(&v, tilesets)) + } + (e, c) => { + return Err(TiledError::Other(format!( + "Unknown combination of {} encoding and {} compression", + e, c + ))) + } + }, + _ => return Err(TiledError::Other("Missing encoding format".to_string())), + }; +} + +fn parse_base64(parser: &mut impl Iterator<Item = XmlEventResult>) -> Result<Vec<u8>, TiledError> { + while let Some(next) = parser.next() { + match 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()); + } + } + _ => {} + } + } + Err(TiledError::PrematureEnd("Ran out of XML data".to_owned())) +} + +fn decode_zlib(data: Vec<u8>) -> Result<Vec<u8>, 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) +} + +fn decode_gzip(data: Vec<u8>) -> Result<Vec<u8>, 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) +} + +fn decode_zstd(data: Vec<u8>) -> Result<Vec<u8>, 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) +} + +fn decode_csv( + parser: &mut impl Iterator<Item = XmlEventResult>, + tilesets: &[MapTilesetGid], +) -> Result<Vec<Option<LayerTileData>>, TiledError> { + while let Some(next) = parser.next() { + match next.map_err(TiledError::XmlDecodingError)? { + XmlEvent::Characters(s) => { + let tiles = s + .split(',') + .map(|v| v.trim().parse().unwrap()) + .map(|bits| LayerTileData::from_bits(bits, tilesets)) + .collect(); + return Ok(tiles); + } + XmlEvent::EndElement { name, .. } => { + if name.local_name == "data" { + return Ok(Vec::new()); + } + } + _ => {} + } + } + Err(TiledError::PrematureEnd("Ran out of XML data".to_owned())) +} + +fn convert_to_tiles(all: &Vec<u8>, tilesets: &[MapTilesetGid]) -> Vec<Option<LayerTileData>> { + 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(LayerTileData::from_bits(n, tilesets)); + } + data +} diff --git a/src/lib.rs b/src/lib.rs index ff45bb1..45a03f3 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 1cee014..dddb7d2 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<Tileset>, +} + /// 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<Tileset>, + /// The tilesets present on this map. + tilesets: Vec<Rc<Tileset>>, /// The layers present in this map. - pub layers: Vec<Layer>, + layers: Vec<LayerData>, /// 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<R: Read>(reader: R, path: Option<&Path>) -> Result<Self, TiledError> { + /// 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<R: Read>( + reader: R, + path: impl AsRef<Path>, + cache: &mut impl ResourceCache, + ) -> Result<Self, TiledError> { 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<Path>) -> Result<Self, TiledError> { - 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<Path>, + cache: &mut impl ResourceCache, + ) -> Result<Self, TiledError> { + 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<Tileset>] { + 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) -> LayerIter { + LayerIter::new(self) } - fn parse_xml<R: Read>( - parser: &mut EventReader<R>, + /// Returns the layer that has the specified index, if it exists. + pub fn get_layer(&self, index: usize) -> Option<Layer> { + 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 LayerIter<'map> { + map: &'map Map, + index: usize, +} + +impl<'map> LayerIter<'map> { + fn new(map: &'map Map) -> Self { + Self { map, index: 0 } + } +} + +impl<'map> Iterator for LayerIter<'map> { + type Item = Layer<'map>; + + fn next(&mut self) -> Option<Self::Item> { + let layer_data = self.map.layers.get(self.index)?; + self.index += 1; + Some(Layer::new(self.map, layer_data)) + } +} + +impl<'map> ExactSizeIterator for LayerIter<'map> { + fn len(&self) -> usize { + self.map.layers.len() - self.index + } +} + +impl Map { + fn parse_xml( + parser: &mut impl Iterator<Item = XmlEventResult>, attrs: Vec<OwnedAttribute>, - map_path: Option<&Path>, + map_path: &Path, + cache: &mut impl ResourceCache, ) -> Result<Map, TiledError> { let ((c, infinite), (v, o, w, h, tw, th)) = get_attrs!( attrs, @@ -96,33 +166,71 @@ 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)?); + 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)?); + layers.push(LayerData::new( + parser, + attrs, + LayerTag::ImageLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, - "properties" => |_| { - properties = parse_properties(parser)?; + "objectgroup" => |attrs| { + layers.push(LayerData::new( + parser, + attrs, + LayerTag::ObjectLayer, + infinite, + map_path, + &tilesets, + )?); Ok(()) }, - "objectgroup" => |attrs| { - layers.push(Layer::new(parser, attrs, LayerTag::ObjectLayer, infinite, source_path)?); + "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, @@ -137,19 +245,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. @@ -185,3 +280,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 6569f33..5c4e1fb 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<LayerTileData>, 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<R: Read>( - parser: &mut EventReader<R>, +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<Item = XmlEventResult>, attrs: Vec<OwnedAttribute>, - ) -> Result<Object, TiledError> { - let ((id, gid, n, t, w, h, v, r), (x, y)) = get_attrs!( + tilesets: Option<&[MapTilesetGid]>, + ) -> Result<ObjectData, TiledError> { + 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<OwnedAttribute>) -> Result<ObjectShape, TiledError> { 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<LayerTile<'map>> { + self.data() + .tile + .map(|tile| LayerTile::from_data(&tile, self.map())) + } +} diff --git a/src/properties.rs b/src/properties.rs index 949046c..928b7f9 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -1,10 +1,10 @@ -use std::{collections::HashMap, io::Read, str::FromStr}; +use std::{collections::HashMap, str::FromStr}; -use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader}; +use xml::{attribute::OwnedAttribute, reader::XmlEvent}; use crate::{ error::TiledError, - util::{get_attrs, parse_tag}, + util::{get_attrs, parse_tag, XmlEventResult}, }; #[derive(Debug, PartialEq, Eq, Copy, Clone)] @@ -106,8 +106,8 @@ impl PropertyValue { pub type Properties = HashMap<String, PropertyValue>; -pub(crate) fn parse_properties<R: Read>( - parser: &mut EventReader<R>, +pub(crate) fn parse_properties( + parser: &mut impl Iterator<Item = XmlEventResult>, ) -> Result<Properties, TiledError> { let mut p = HashMap::new(); parse_tag!(parser, "properties", { @@ -125,12 +125,14 @@ pub(crate) fn parse_properties<R: Read>( ); 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 a6d623b..0493fb5 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<Image>, pub properties: Properties, - pub collision: Option<ObjectLayer>, + pub collision: Option<ObjectLayerData>, pub animation: Option<Vec<Frame>>, pub tile_type: Option<String>, pub probability: f32, } impl Tile { - pub(crate) fn new<R: Read>( - parser: &mut EventReader<R>, + pub(crate) fn new( + parser: &mut impl Iterator<Item = XmlEventResult>, attrs: Vec<OwnedAttribute>, path_relative_to: Option<&Path>, - ) -> Result<Tile, TiledError> { + ) -> 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 2b120ec..1efcb5d 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<u32>, + 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<Image>, - pub tiles: Vec<Tile>, + + /// All the tiles present in this tileset, indexed by their local IDs. + pub tiles: HashMap<u32, Tile>, + + /// 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<PathBuf>, } +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<u32>, margin: Option<u32>, - tilecount: Option<u32>, + tilecount: u32, columns: Option<u32>, - first_gid: u32, name: String, tile_width: u32, tile_height: u32, + /// The path all non-absolute paths are relative to. path_relative_to: Option<PathBuf>, source: Option<PathBuf>, } 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<R: Read>(reader: R, first_gid: u32) -> Result<Self, TiledError> { - Tileset::new_external(reader, first_gid, None) + pub fn parse<R: Read>(reader: R) -> Result<Self, TiledError> { + 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<R: Read>( - reader: R, - first_gid: u32, - path: impl AsRef<Path>, - ) -> Result<Self, TiledError> { - Tileset::new_external(reader, first_gid, Some(path.as_ref())) + pub fn parse_with_path<R: Read>(reader: R, path: impl AsRef<Path>) -> Result<Self, TiledError> { + Tileset::new_external(reader, Some(path.as_ref())) } - pub(crate) fn parse_xml<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, - path_relative_to: Option<&Path>, - ) -> Result<Tileset, TiledError> { - 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<R: Read>( - file: R, - first_gid: u32, - path: Option<&Path>, - ) -> Result<Self, TiledError> { +impl Tileset { + pub(crate) fn new_external<R: Read>(file: R, path: Option<&Path>) -> Result<Self, TiledError> { 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<R: Read>( - parser: &mut EventReader<R>, + pub(crate) fn parse_xml_in_map( + parser: &mut impl Iterator<Item = XmlEventResult>, + attrs: Vec<OwnedAttribute>, + map_path: &Path, + ) -> Result<EmbeddedParseResult, TiledError> { + 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<Item = XmlEventResult>, attrs: &Vec<OwnedAttribute>, path_relative_to: Option<&Path>, - ) -> Result<Tileset, TiledError> { - let ((spacing, margin, tilecount, columns), (first_gid, name, tile_width, tile_height)) = get_attrs!( + ) -> Result<EmbeddedParseResult, TiledError> { + 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<OwnedAttribute>, path_relative_to: Option<&Path>, - ) -> Result<Tileset, TiledError> { + ) -> Result<EmbeddedParseResult, TiledError> { 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<R: Read>( - first_gid: u32, - parser: &mut EventReader<R>, + fn parse_external_tileset( + parser: &mut impl Iterator<Item = XmlEventResult>, attrs: &Vec<OwnedAttribute>, path: Option<&Path>, ) -> Result<Tileset, TiledError> { - 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<R: Read>( - parser: &mut EventReader<R>, + fn finish_parsing_xml( + parser: &mut impl Iterator<Item = XmlEventResult>, prop: TilesetProperties, - ) -> Result<Self, TiledError> { + ) -> Result<Tileset, TiledError> { 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 500cf3c..0eafe24 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<R: Read>( - parser: &mut EventReader<R>, +// TODO: Move to animation module +pub(crate) fn parse_animation( + parser: &mut impl Iterator<Item = XmlEventResult>, ) -> Result<Vec<Frame>, TiledError> { let mut animation = Vec::new(); parse_tag!(parser, "animation", { @@ -80,179 +71,16 @@ pub(crate) fn parse_animation<R: Read>( Ok(animation) } -pub(crate) fn parse_infinite_data<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, -) -> Result<LayerData, TiledError> { - 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<R: Read>( - parser: &mut EventReader<R>, - attrs: Vec<OwnedAttribute>, -) -> Result<LayerData, TiledError> { - 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<R: Read>( - encoding: Option<String>, - compression: Option<String>, - parser: &mut EventReader<R>, -) -> Result<Vec<LayerTile>, 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<R: Read>(parser: &mut EventReader<R>) -> Result<Vec<u8>, 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<u8>) -> Result<Vec<u8>, 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<u8>) -> Result<Vec<u8>, 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<u8>) -> Result<Vec<u8>, 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<R: Read>( - parser: &mut EventReader<R>, -) -> Result<Vec<LayerTile>, 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<u8>) -> Vec<LayerTile> { - 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<xml::reader::XmlEvent>; + +/// 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 ad8839f..396e01c 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,85 +1,115 @@ -use std::path::Path; -use std::{fs::File, path::PathBuf}; -use tiled::{Color, 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, +}; -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<Path>) -> Result<Map, TiledError> { - let file = File::open(p).unwrap(); - return Map::parse_reader(file, None); +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 +124,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 +160,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,21 +178,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 prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = - r.layers[1].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 prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = r + .layers() + .nth(1) + .unwrap() + .data() + .properties + .get("an object group property") { *v } else { @@ -165,9 +212,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 { @@ -177,65 +226,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"), } @@ -244,9 +298,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") { @@ -259,9 +316,11 @@ fn test_object_property() { #[test] fn test_tint_color() { - let r = Map::parse_file("assets/tiled_image_layers.tmx").unwrap(); + let mut cache = FilesystemResourceCache::new(); + + let r = Map::parse_file("assets/tiled_image_layers.tmx", &mut cache).unwrap(); assert_eq!( - r.layers[0].tint_color, + r.get_layer(0).unwrap().data().tint_color, Some(Color { alpha: 0x12, red: 0x34, @@ -270,7 +329,7 @@ fn test_tint_color() { }) ); assert_eq!( - r.layers[1].tint_color, + r.get_layer(1).unwrap().data().tint_color, Some(Color { alpha: 0xFF, red: 0x12, -- GitLab