From e176b4b77f4eb3ea6041bab3630dc83b4942babf Mon Sep 17 00:00:00 2001 From: Alejandro Perea Date: Tue, 22 Mar 2022 12:15:33 +0100 Subject: [PATCH] VFS support (#199) * Add VFS support * Add some docs * Fix tests & docs * Better docs for `Loader::new` * Address code review comments * Misc doc improvements * Rename FilesystemResourceCache --- src/cache.rs | 26 ++++--- src/error.rs | 11 +-- src/image.rs | 6 +- src/loader.rs | 153 ++++++++++++++++++--------------------- src/map.rs | 40 +--------- src/parse/xml/map.rs | 18 ++--- src/parse/xml/tileset.rs | 16 +++- src/tileset.rs | 22 ------ 8 files changed, 118 insertions(+), 174 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index a650dd1d..37ed1922 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -15,6 +15,7 @@ pub type ResourcePathBuf = PathBuf; /// [`ResourcePath`] to prevent loading them more than once. Normally you don't need to use this /// type yourself unless you want to create a custom caching solution to, for instance, integrate /// with your own. +/// /// If you simply want to load a map or tileset, use the [`Loader`](crate::Loader) type. pub trait ResourceCache { /// Obtains a tileset from the cache, if it exists. @@ -22,15 +23,15 @@ pub trait ResourceCache { /// # Example /// ``` /// use std::fs::File; - /// use tiled::{FilesystemResourceCache, ResourceCache, Tileset}; + /// use tiled::{FilesystemResourceReader, Tileset, Loader, ResourceCache}; /// # use tiled::Result; /// # fn main() -> Result<()> { - /// let mut cache = FilesystemResourceCache::new(); + /// let mut loader = Loader::new(); /// let path = "assets/tilesheet.tsx"; /// - /// assert!(cache.get_tileset(path).is_none()); - /// cache.get_or_try_insert_tileset_with(path.to_owned().into(), || Tileset::parse_reader(File::open(path).unwrap(), path))?; - /// assert!(cache.get_tileset(path).is_some()); + /// assert!(loader.cache().get_tileset(path).is_none()); + /// loader.load_tmx_map("assets/tiled_base64_external.tmx"); + /// assert!(loader.cache().get_tileset(path).is_some()); /// # Ok(()) /// # } /// ``` @@ -40,6 +41,11 @@ pub trait ResourceCache { /// result, it will: /// - Insert the object into the cache, if the result was [`Ok`]. /// - Return the error and leave the cache intact, if the result was [`Err`]. + /// + /// ## Note + /// This function is normally only used internally; there are not many instances where it is + /// callable outside of the library implementation, since the cache is normally owned by the + /// loader anyways. fn get_or_try_insert_tileset_with( &mut self, path: ResourcePathBuf, @@ -49,14 +55,14 @@ pub trait ResourceCache { F: FnOnce() -> Result; } -/// A cache that identifies resources by their path in the user's filesystem. +/// A cache that identifies resources by their path, storing a map of them. #[derive(Debug)] -pub struct FilesystemResourceCache { +pub struct DefaultResourceCache { tilesets: HashMap>, } -impl FilesystemResourceCache { - /// Creates an empty [`FilesystemResourceCache`]. +impl DefaultResourceCache { + /// Creates an empty [`DefaultResourceCache`]. pub fn new() -> Self { Self { tilesets: HashMap::new(), @@ -64,7 +70,7 @@ impl FilesystemResourceCache { } } -impl ResourceCache for FilesystemResourceCache { +impl ResourceCache for DefaultResourceCache { fn get_tileset(&self, path: impl AsRef) -> Option> { self.tilesets.get(path.as_ref()).map(Clone::clone) } diff --git a/src/error.rs b/src/error.rs index d83d4922..095bce38 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,12 +18,13 @@ pub enum Error { PrematureEnd(String), /// The path given is invalid because it isn't contained in any folder. PathIsNotFile, - /// Could not open some file due to an I/O error. - CouldNotOpenFile { + /// An error generated by [`ResourceReader`](crate::ResourceReader) while trying to read a + /// resource. + ResourceLoadingError { /// The path to the file that was unable to be opened. path: PathBuf, /// The error that occured when trying to open the file. - err: std::io::Error, + err: Box, }, /// There was an invalid tile in the map parsed. InvalidTileFound, @@ -68,7 +69,7 @@ impl fmt::Display for Error { "The path given is invalid because it isn't contained in any folder." ) } - Error::CouldNotOpenFile { path, err } => { + Error::ResourceLoadingError { path, err } => { write!( fmt, "Could not open '{}'. Error: {}", @@ -103,7 +104,7 @@ impl std::error::Error for Error { Error::DecompressingError(e) => Some(e as &dyn std::error::Error), Error::Base64DecodingError(e) => Some(e as &dyn std::error::Error), Error::XmlDecodingError(e) => Some(e as &dyn std::error::Error), - Error::CouldNotOpenFile { err, .. } => Some(err as &dyn std::error::Error), + Error::ResourceLoadingError { err, .. } => Some(err.as_ref()), _ => None, } } diff --git a/src/image.rs b/src/image.rs index fd2e3ffa..0a922ba5 100644 --- a/src/image.rs +++ b/src/image.rs @@ -31,10 +31,7 @@ pub struct Image { /// use tiled::*; /// /// # fn main() -> std::result::Result<(), Box> { - /// let map = Map::parse_file( - /// "assets/folder/tiled_relative_paths.tmx", - /// &mut FilesystemResourceCache::new(), - /// )?; + /// let map = Loader::new().load_tmx_map("assets/folder/tiled_relative_paths.tmx")?; /// /// let image_layer = match map /// .layers() @@ -67,7 +64,6 @@ pub struct Image { /// Check the assets/tiled_relative_paths.tmx file at the crate root to see the structure of the /// file this example is referring to. // TODO: Embedded images - // TODO: Figure out how to serve crate users paths in a better way pub source: PathBuf, /// The width in pixels of the image. pub width: i32, diff --git a/src/loader.rs b/src/loader.rs index 939e1ff4..6331c6ff 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,39 +1,85 @@ use std::{fs::File, io::Read, path::Path}; -use crate::{Error, FilesystemResourceCache, Map, ResourceCache, Result, Tileset}; +use crate::{DefaultResourceCache, Map, ResourceCache, Result, Tileset}; + +/// A trait defining types that can load data from a [`ResourcePath`](crate::ResourcePath). +/// +/// This trait should be implemented if you wish to load data from a virtual filesystem. +/// +/// ## Example +/// TODO: ResourceReader example +pub trait ResourceReader { + /// The type of the resource that the reader provides. For example, for + /// [`FilesystemResourceReader`], this is defined as [`File`]. + type Resource: Read; + /// The type that is returned if [`read_from()`](Self::read_from()) fails. For example, for + /// [`FilesystemResourceReader`], this is defined as [`std::io::Error`]. + type Error: std::error::Error + 'static; + + /// Try to return a reader object from a path into the resources filesystem. + fn read_from(&mut self, path: &Path) -> std::result::Result; +} + +/// A [`ResourceReader`] that reads from [`File`] handles. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FilesystemResourceReader; + +impl FilesystemResourceReader { + fn new() -> Self { + Self + } +} + +impl ResourceReader for FilesystemResourceReader { + type Resource = File; + type Error = std::io::Error; + + fn read_from(&mut self, path: &Path) -> std::result::Result { + std::fs::File::open(path) + } +} /// A type used for loading [`Map`]s and [`Tileset`]s. /// /// Internally, it holds a [`ResourceCache`] that, as its name implies, caches intermediate loading /// artifacts, most notably map tilesets. /// +/// It also contains a [`ResourceReader`] which is the object in charge of providing read handles +/// to files via a [`ResourcePath`](crate::ResourcePath). +/// /// ## Reasoning /// This type is used for loading operations because they require a [`ResourceCache`] for /// intermediate artifacts, so using a type for creation can ensure that the cache is reused if /// loading more than one object is required. #[derive(Debug, Clone)] -pub struct Loader { +pub struct Loader< + Cache: ResourceCache = DefaultResourceCache, + Reader: ResourceReader = FilesystemResourceReader, +> { cache: Cache, + reader: Reader, } -impl Loader { - /// Creates a new loader, creating a default ([`FilesystemResourceCache`]) resource cache in the process. +impl Loader { + /// Creates a new loader, creating a default resource cache and reader + /// ([`DefaultResourceCache`] & [`FilesystemResourceReader`] respectively) in the process. pub fn new() -> Self { Self { - cache: FilesystemResourceCache::new(), + cache: DefaultResourceCache::new(), + reader: FilesystemResourceReader::new(), } } } -impl Loader { - /// Creates a new loader using a specific resource cache. +impl Loader { + /// Creates a new loader using a specific resource cache and reader. /// /// ## Example /// ``` /// # fn main() -> tiled::Result<()> { /// use std::{sync::Arc, path::Path}; /// - /// use tiled::{Loader, ResourceCache}; + /// use tiled::{Loader, ResourceCache, FilesystemResourceReader}; /// /// /// An example resource cache that doesn't actually cache any resources at all. /// struct NoopResourceCache; @@ -58,7 +104,7 @@ impl Loader { /// } /// } /// - /// let mut loader = Loader::with_cache(NoopResourceCache); + /// let mut loader = Loader::with_cache_and_reader(NoopResourceCache, FilesystemResourceReader); /// /// let map = loader.load_tmx_map("assets/tiled_base64_external.tmx")?; /// @@ -70,8 +116,8 @@ impl Loader { /// # Ok(()) /// # } /// ``` - pub fn with_cache(cache: Cache) -> Self { - Self { cache } + pub fn with_cache_and_reader(cache: Cache, reader: Reader) -> Self { + Self { cache, reader } } /// Parses a file hopefully containing a Tiled map and tries to parse it. All external files @@ -79,84 +125,22 @@ impl Loader { /// /// All intermediate objects such as map tilesets will be stored in the [internal loader cache]. /// - /// If you need to parse a reader object instead, use [Loader::load_tmx_map_from()]. - /// /// [internal loader cache]: Loader::cache() pub fn load_tmx_map(&mut self, path: impl AsRef) -> Result { - let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile { - path: path.as_ref().to_owned(), - err, - })?; - crate::parse::xml::parse_map(reader, path.as_ref(), &mut self.cache) - } - - /// Parses a map out of a reader hopefully containing the contents of a Tiled file. - /// - /// This augments [`load_tmx_map`] with a custom reader: some engines (e.g. Amethyst) simply - /// hand over a byte stream and file location for parsing, in which case this function may be - /// required. - /// - /// If you need to parse a file in the filesystem instead, [`load_tmx_map`] might be - /// more convenient. - /// - /// 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. - /// - /// All intermediate objects such as map tilesets will be stored in the [internal loader cache]. - /// - /// [internal loader cache]: Loader::cache() - /// [`load_tmx_map`]: Loader::load_tmx_map() - pub fn load_tmx_map_from(&mut self, reader: impl Read, path: impl AsRef) -> Result { - crate::parse::xml::parse_map(reader, path.as_ref(), &mut self.cache) + crate::parse::xml::parse_map(path.as_ref(), &mut self.reader, &mut self.cache) } /// Parses a file hopefully containing a Tiled tileset and tries to parse it. All external files /// will be loaded relative to the path given. /// /// Unless you specifically want to load a tileset, you won't need to call this function. If - /// you are trying to load a map, simply use [`Loader::load_tmx_map`] or - /// [`Loader::load_tmx_map_from`]. + /// you are trying to load a map, simply use [`Loader::load_tmx_map`]. /// - /// If you need to parse a reader object instead, use [Loader::load_tsx_tileset_from()]. + /// ## Note + /// This function will **not** cache the tileset inside the internal [`ResourceCache`], since + /// in this context it is not an intermediate object. pub fn load_tsx_tileset(&mut self, path: impl AsRef) -> Result { - let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile { - path: path.as_ref().to_owned(), - err, - })?; - crate::parse::xml::parse_tileset(reader, path.as_ref()) - } - - /// Parses a tileset out of a reader hopefully containing the contents of a Tiled tileset. - /// Uses the `path` parameter as the root for any relative paths found in the tileset. - /// - /// Unless you specifically want to load a tileset, you won't need to call this function. If - /// you are trying to load a map, simply use [`Loader::load_tmx_map`] or - /// [`Loader::load_tmx_map_from`]. - /// - /// ## Example - /// ``` - /// use std::fs::File; - /// use std::path::PathBuf; - /// use std::io::BufReader; - /// use tiled::Loader; - /// - /// let path = "assets/tilesheet.tsx"; - /// // Note: This is just an example, if you actually need to load a file use `load_tsx_tileset` - /// // instead. - /// let reader = BufReader::new(File::open(path).unwrap()); - /// let mut loader = Loader::new(); - /// let tileset = loader.load_tsx_tileset_from(reader, path).unwrap(); - /// - /// assert_eq!(tileset.image.unwrap().source, PathBuf::from("assets/tilesheet.png")); - /// ``` - pub fn load_tsx_tileset_from( - &self, - reader: impl Read, - path: impl AsRef, - ) -> Result { - // This function doesn't need the cache right now, but will do once template support is in - crate::parse::xml::parse_tileset(reader, path.as_ref()) + crate::parse::xml::parse_tileset(path.as_ref(), &mut self.reader) } /// Returns a reference to the loader's internal [`ResourceCache`]. @@ -164,8 +148,13 @@ impl Loader { &self.cache } - /// Consumes the loader and returns its internal [`ResourceCache`]. - pub fn into_cache(self) -> Cache { - self.cache + /// Returns a reference to the loader's internal [`ResourceReader`]. + pub fn reader(&self) -> &Reader { + &self.reader + } + + /// Consumes the loader and returns its internal [`ResourceCache`] and [`ResourceReader`]. + pub fn into_inner(self) -> (Cache, Reader) { + (self.cache, self.reader) } } diff --git a/src/map.rs b/src/map.rs index a8903ee8..75e45003 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,6 +1,6 @@ //! Structures related to Tiled maps. -use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, str::FromStr, sync::Arc}; +use std::{collections::HashMap, fmt, path::Path, str::FromStr, sync::Arc}; use xml::attribute::OwnedAttribute; @@ -10,7 +10,7 @@ use crate::{ properties::{parse_properties, Color, Properties}, tileset::Tileset, util::{get_attrs, parse_tag, XmlEventResult}, - EmbeddedParseResultType, Layer, ResourceCache, + EmbeddedParseResultType, Layer, ResourceCache, ResourceReader, }; pub(crate) struct MapTilesetGid { @@ -60,38 +60,6 @@ pub struct Map { } impl Map { - /// Parse a buffer hopefully containing the contents of a Tiled file and try to - /// parse it. This augments `parse_file` with a custom reader: some engines - /// (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. 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. - #[deprecated(since = "0.10.1", note = "Use `Loader::load_tmx_map_from` instead")] - pub fn parse_reader( - reader: R, - path: impl AsRef, - cache: &mut impl ResourceCache, - ) -> Result { - crate::parse::xml::parse_map(reader, path.as_ref(), cache) - } - - /// Parse a file hopefully containing a Tiled map and try to parse it. All external - /// files will be loaded relative to the path given. - /// - /// The tileset cache is used to store and refer to any tilesets found along the way. - #[deprecated(since = "0.10.1", note = "Use `Loader::load_tmx_map` instead")] - pub fn parse_file(path: impl AsRef, cache: &mut impl ResourceCache) -> Result { - let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile { - path: path.as_ref().to_owned(), - err, - })?; - crate::parse::xml::parse_map(reader, path.as_ref(), cache) - } - /// The TMX format version this map was saved to. Equivalent to the map file's `version` /// attribute. pub fn version(&self) -> &str { @@ -131,6 +99,7 @@ impl Map { attrs: Vec, map_path: &Path, cache: &mut impl ResourceCache, + reader: &mut impl ResourceReader, ) -> Result { let ((c, infinite), (v, o, w, h, tw, th)) = get_attrs!( attrs, @@ -163,8 +132,7 @@ impl Map { 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| Error::CouldNotOpenFile{path: tileset_path.clone(), err })?; - let tileset = cache.get_or_try_insert_tileset_with(tileset_path.clone(), || crate::parse::xml::parse_tileset(file, &tileset_path))?; + let tileset = cache.get_or_try_insert_tileset_with(tileset_path.clone(), || crate::parse::xml::parse_tileset(&tileset_path, reader))?; tilesets.push(MapTilesetGid{first_gid: res.first_gid, tileset}); } EmbeddedParseResultType::Embedded { tileset } => { diff --git a/src/parse/xml/map.rs b/src/parse/xml/map.rs index 05ce67a6..dce54562 100644 --- a/src/parse/xml/map.rs +++ b/src/parse/xml/map.rs @@ -1,27 +1,25 @@ -use std::{io::Read, path::Path}; +use std::{path::Path}; use xml::{reader::XmlEvent, EventReader}; -use crate::{Error, Map, ResourceCache, Result}; +use crate::{Error, Map, ResourceCache, Result, ResourceReader}; pub fn parse_map( - reader: impl Read, path: &Path, + reader: &mut impl ResourceReader, cache: &mut impl ResourceCache, ) -> Result { - let mut parser = EventReader::new(reader); + let mut parser = EventReader::new(reader.read_from(path).map_err(|err| Error::ResourceLoadingError { + path: path.to_owned(), + err: Box::new(err), + })?); loop { match parser.next().map_err(Error::XmlDecodingError)? { XmlEvent::StartElement { name, attributes, .. } => { if name.local_name == "map" { - return Map::parse_xml( - &mut parser.into_iter(), - attributes, - path, - cache, - ); + return Map::parse_xml(&mut parser.into_iter(), attributes, path, cache, reader); } } XmlEvent::EndDocument => { diff --git a/src/parse/xml/tileset.rs b/src/parse/xml/tileset.rs index df6bbe61..cf22d7ec 100644 --- a/src/parse/xml/tileset.rs +++ b/src/parse/xml/tileset.rs @@ -1,11 +1,19 @@ -use std::{io::Read, path::Path}; +use std::path::Path; use xml::{reader::XmlEvent, EventReader}; -use crate::{Error, Result, Tileset}; +use crate::{Error, ResourceReader, Result, Tileset}; -pub fn parse_tileset(reader: R, path: &Path) -> Result { - let mut tileset_parser = EventReader::new(reader); +pub fn parse_tileset(path: &Path, reader: &mut impl ResourceReader) -> Result { + let mut tileset_parser = + EventReader::new( + reader + .read_from(path) + .map_err(|err| Error::ResourceLoadingError { + path: path.to_owned(), + err: Box::new(err), + })?, + ); loop { match tileset_parser.next().map_err(Error::XmlDecodingError)? { XmlEvent::StartElement { diff --git a/src/tileset.rs b/src/tileset.rs index 41bbddd4..4e64edd0 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::io::Read; use std::path::{Path, PathBuf}; use xml::attribute::OwnedAttribute; @@ -81,27 +80,6 @@ struct TilesetProperties { } impl Tileset { - /// Parses a tileset out of a reader hopefully containing the contents of a Tiled tileset. - /// Uses the `path` parameter as the root for any relative paths found in the tileset. - /// - /// ## Example - /// ``` - /// use std::fs::File; - /// use std::path::PathBuf; - /// use std::io::BufReader; - /// use tiled::Tileset; - /// - /// let path = "assets/tilesheet.tsx"; - /// let reader = BufReader::new(File::open(path).unwrap()); - /// let tileset = Tileset::parse_reader(reader, path).unwrap(); - /// - /// assert_eq!(tileset.image.unwrap().source, PathBuf::from("assets/tilesheet.png")); - /// ``` - #[deprecated(since = "0.10.1", note = "Use `Loader::load_tsx_tileset_from` instead")] - pub fn parse_reader(reader: R, path: impl AsRef) -> Result { - crate::parse::xml::parse_tileset(reader, path.as_ref()) - } - /// Gets the tile with the specified ID from the tileset. #[inline] pub fn get_tile(&self, id: TileId) -> Option {