Skip to content

Commit

Permalink
Improve ResourceReader's API, add better docs (#272)
Browse files Browse the repository at this point in the history
* Improve ResourceReader's API, add better docs

* Update changelog

* Fix formatting

* Address PR comments

* Run rustfmt
  • Loading branch information
aleokdev authored Jun 12, 2023
1 parent 4f94470 commit c67f108
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 68 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down
5 changes: 1 addition & 4 deletions examples/ggez/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod map;
mod objects;
mod parse;
mod properties;
mod reader;
mod template;
mod tile;
mod tileset;
Expand All @@ -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::*;
135 changes: 74 additions & 61 deletions src/loader.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Resource, Self::Error> {
/// 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<Self::Resource, Self::Error>;
}

/// 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<Self::Resource, Self::Error> {
std::fs::File::open(path)
}
}
use crate::{
DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result,
Tileset,
};

/// A type used for loading [`Map`]s and [`Tileset`]s.
///
Expand Down Expand Up @@ -89,15 +37,63 @@ impl Loader {
}
}

impl<Reader: ResourceReader> Loader<DefaultResourceCache, Reader> {
/// 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<Cache: ResourceCache, Reader: ResourceReader> Loader<Cache, Reader> {
/// 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;
Expand Down Expand Up @@ -130,13 +126,30 @@ impl<Cache: ResourceCache, Reader: ResourceReader> Loader<Cache, Reader> {
/// ) {}
/// }
///
/// 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(())
Expand Down
72 changes: 72 additions & 0 deletions src/reader.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Resource, Self::Error> {
/// 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<Self::Resource, Self::Error>;
}

/// 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<Self::Resource, Self::Error> {
File::open(path)
}
}

impl<T, R, E> ResourceReader for T
where
T: for<'a> Fn(&'a Path) -> Result<R, E>,
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::Resource, Self::Error> {
self(path)
}
}

0 comments on commit c67f108

Please sign in to comment.