diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5f3039..b33c756f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased (next)] +### Added +- Implement `ResourceReader` for appropiate functions. (#272) **Read the README's FAQ for more information about this change.** + ## [Unreleased] ## Changed - Updated `Image` docs. (#270) diff --git a/README.md b/README.md index aaf3464f..e1c722bb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Code contributions are welcome as are bug reports, documentation, suggestions an The minimum supported TMX version is 0.13. -### Example +## Example ```rust use tiled::Loader; @@ -34,7 +34,34 @@ fn main() { ``` -### WASM +## FAQ +### How do I embed a map into my executable? / How do I read a file from anywhere else that isn't the filesystem's OS? +The crate does all of its reading through the `read_from` function of the [`ResourceReader`](https://docs.rs/tiled/latest/tiled/trait.ResourceReader.html) that you create the loader with. By default, this reader is set to [`FilesystemResourceReader`](https://docs.rs/tiled/latest/tiled/struct.FilesystemResourceReader.html) and all files are read through the OS's filesystem. You can however change this. + +Here's an example mostly taken from `Loader::with_reader`'s documentation: +```rust +use tiled::{DefaultResourceCache, Loader}; + +let mut loader = Loader::with_reader( + // Specify the reader to use. We can use anything that implements `ResourceReader`, e.g. FilesystemResourceReader. + // Any function that has the same signature as `ResourceReader::read_from` also implements it. + // Here we define a reader that embeds the map at "assets/tiled_xml.csv" into the executable, and allow + // accessing it only through "/my-map.tmx" + // ALL maps, tilesets and templates will be read through this function, even if you don't explicitly load them + // (They can be dependencies of one you did want to load in the first place). + // Doing this embedding is useful for places where the OS filesystem is not available (e.g. WASM applications). + |path: &std::path::Path| -> std::io::Result<_> { + if path == std::path::Path::new("/my-map.tmx") { + Ok(std::io::Cursor::new(include_bytes!("../assets/tiled_csv.tmx"))) + } else { + Err(std::io::ErrorKind::NotFound.into()) + } + } +); +``` +If the closure approach confuses you or you need more flexibility, you can always implement [`ResourceReader`](https://docs.rs/tiled/latest/tiled/trait.ResourceReader.html) on your own structure. + +### How do I get the crate to work on WASM targets? The crate supports WASM, but since it does not currently support asynchronous loading, there are some gotchas. - First, to make it work on any WASM target, **enable the wasm feature**, like so: @@ -66,7 +93,8 @@ impl tiled::ResourceReader for MyReader { } } ``` -Check the `ResourceReader` docs for more information. +You can also use a function with the same signature as `tiled::ResourceReader::read_from`; check the +`ResourceReader` docs for more information. ### Licences diff --git a/examples/ggez/main.rs b/examples/ggez/main.rs index d9a9e0d9..4a3d3743 100644 --- a/examples/ggez/main.rs +++ b/examples/ggez/main.rs @@ -48,10 +48,7 @@ impl Game { graphics::set_default_filter(ctx, graphics::FilterMode::Nearest); // Load the map, using a loader with `GgezResourceReader` for reading from the ggez filesystem - let mut loader = tiled::Loader::with_cache_and_reader( - tiled::DefaultResourceCache::new(), - GgezResourceReader(ctx), - ); + let mut loader = tiled::Loader::with_reader(GgezResourceReader(ctx)); let map = loader.load_tmx_map("/tiled_base64_external.tmx").unwrap(); let map_handler = MapHandler::new(map, ctx).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 0be8613d..fa68ac9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ mod map; mod objects; mod parse; mod properties; +mod reader; mod template; mod tile; mod tileset; @@ -29,6 +30,7 @@ pub use loader::*; pub use map::*; pub use objects::*; pub use properties::*; +pub use reader::*; pub use template::*; pub use tile::*; pub use tileset::*; diff --git a/src/loader.rs b/src/loader.rs index 2faf917a..3f68878d 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,61 +1,9 @@ use std::{fs::File, io::Read, path::Path}; -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 -/// ``` -/// use std::io::Cursor; -/// -/// /// Basic example reader impl that just keeps a few resources in memory -/// struct MemoryReader; -/// -/// impl tiled::ResourceReader for MemoryReader { -/// type Resource = Cursor<&'static [u8]>; -/// type Error = std::io::Error; -/// -/// fn read_from(&mut self, path: &std::path::Path) -> std::result::Result { -/// if path == std::path::Path::new("my_map.tmx") { -/// Ok(Cursor::new(include_bytes!("../assets/tiled_xml.tmx"))) -/// } else { -/// Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found")) -/// } -/// } -/// } -/// ``` -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 + Send + Sync + '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) - } -} +use crate::{ + DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, + Tileset, +}; /// A type used for loading [`Map`]s and [`Tileset`]s. /// @@ -89,15 +37,63 @@ impl Loader { } } +impl Loader { + /// Creates a new loader using a specific reader and the default resource cache ([`DefaultResourceCache`]). + /// Shorthand for `Loader::with_cache_and_reader(DefaultResourceCache::new(), reader)`. + /// + /// ## Example + /// ``` + /// # fn main() -> tiled::Result<()> { + /// use std::{sync::Arc, path::Path}; + /// + /// use tiled::{Loader, ResourceCache}; + /// + /// let mut loader = Loader::with_reader( + /// // Specify the reader to use. We can use anything that implements `ResourceReader`, e.g. FilesystemResourceReader. + /// // Any function that has the same signature as `ResourceReader::read_from` also implements it. + /// // Here we define a reader that embeds the map at "assets/tiled_xml.csv" into the executable, and allow + /// // accessing it only through "/my-map.tmx" + /// // ALL maps, tilesets and templates will be read through this function, even if you don't explicitly load them + /// // (They can be dependencies of one you did want to load in the first place). + /// // Doing this embedding is useful for places where the OS filesystem is not available (e.g. WASM applications). + /// |path: &std::path::Path| -> std::io::Result<_> { + /// if path == std::path::Path::new("/my-map.tmx") { + /// Ok(std::io::Cursor::new(include_bytes!("../assets/tiled_csv.tmx"))) + /// } else { + /// Err(std::io::ErrorKind::NotFound.into()) + /// } + /// } + /// ); + /// + /// let map = loader.load_tmx_map("/my-map.tmx")?; + /// + /// assert_eq!( + /// map.tilesets()[0].image.as_ref().unwrap().source, + /// Path::new("/tilesheet.png") + /// ); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn with_reader(reader: Reader) -> Self { + Self { + cache: DefaultResourceCache::new(), + reader, + } + } +} + impl Loader { - /// Creates a new loader using a specific resource cache and reader. + /// Creates a new loader using a specific resource cache and reader. In most cases you won't + /// need a custom resource cache; If that is the case you can use [`Loader::with_reader()`] for + /// a less verbose version of this function. /// /// ## Example /// ``` /// # fn main() -> tiled::Result<()> { /// use std::{sync::Arc, path::Path}; /// - /// use tiled::{Loader, ResourceCache, FilesystemResourceReader}; + /// use tiled::{Loader, ResourceCache}; /// /// /// An example resource cache that doesn't actually cache any resources at all. /// struct NoopResourceCache; @@ -130,13 +126,30 @@ impl Loader { /// ) {} /// } /// - /// let mut loader = Loader::with_cache_and_reader(NoopResourceCache, FilesystemResourceReader); + /// let mut loader = Loader::with_cache_and_reader( + /// // Specify the resource cache to use. In this case, the one we defined earlier. + /// NoopResourceCache, + /// // Specify the reader to use. We can use anything that implements `ResourceReader`, e.g. FilesystemResourceReader. + /// // Any function that has the same signature as `ResourceReader::read_from` also implements it. + /// // Here we define a reader that embeds the map at "assets/tiled_xml.csv" into the executable, and allow + /// // accessing it only through "/my-map.tmx" + /// // ALL maps, tilesets and templates will be read through this function, even if you don't explicitly load them + /// // (They can be dependencies of one you did want to load in the first place). + /// // Doing this embedding is useful for places where the OS filesystem is not available (e.g. WASM applications). + /// |path: &std::path::Path| -> std::io::Result<_> { + /// if path == std::path::Path::new("/my-map.tmx") { + /// Ok(std::io::Cursor::new(include_bytes!("../assets/tiled_csv.tmx"))) + /// } else { + /// Err(std::io::ErrorKind::NotFound.into()) + /// } + /// } + /// ); /// - /// let map = loader.load_tmx_map("assets/tiled_base64_external.tmx")?; + /// let map = loader.load_tmx_map("/my-map.tmx")?; /// /// assert_eq!( /// map.tilesets()[0].image.as_ref().unwrap().source, - /// Path::new("assets/tilesheet.png") + /// Path::new("/tilesheet.png") /// ); /// /// # Ok(()) diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 00000000..325cabb9 --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,72 @@ +use std::{fs::File, io::Read, path::Path}; + +/// 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 +/// ``` +/// use std::io::Cursor; +/// +/// /// Basic example reader impl that just keeps a few resources in memory +/// struct MemoryReader; +/// +/// impl tiled::ResourceReader for MemoryReader { +/// type Resource = Cursor<&'static [u8]>; +/// type Error = std::io::Error; +/// +/// fn read_from(&mut self, path: &std::path::Path) -> std::result::Result { +/// if path == std::path::Path::new("my_map.tmx") { +/// Ok(Cursor::new(include_bytes!("../assets/tiled_xml.tmx"))) +/// } else { +/// Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found")) +/// } +/// } +/// } +/// ``` +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 + Send + Sync + '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 { + /// Creates a new [`FilesystemResourceReader`]. + pub 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 { + File::open(path) + } +} + +impl ResourceReader for T +where + T: for<'a> Fn(&'a Path) -> Result, + R: Read, + E: std::error::Error + Send + Sync + 'static, +{ + type Resource = R; + + type Error = E; + + fn read_from(&mut self, path: &Path) -> std::result::Result { + self(path) + } +}