Skip to content

Commit

Permalink
VFS support (#199)
Browse files Browse the repository at this point in the history
* Add VFS support

* Add some docs

* Fix tests & docs

* Better docs for `Loader::new`

* Address code review comments

* Misc doc improvements

* Rename FilesystemResourceCache
  • Loading branch information
aleokdev authored Mar 22, 2022
1 parent 5e5f708 commit e176b4b
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 174 deletions.
26 changes: 16 additions & 10 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,23 @@ 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.
///
/// # 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(())
/// # }
/// ```
Expand All @@ -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<F, E>(
&mut self,
path: ResourcePathBuf,
Expand All @@ -49,22 +55,22 @@ pub trait ResourceCache {
F: FnOnce() -> Result<Tileset, E>;
}

/// 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<ResourcePathBuf, Arc<Tileset>>,
}

impl FilesystemResourceCache {
/// Creates an empty [`FilesystemResourceCache`].
impl DefaultResourceCache {
/// Creates an empty [`DefaultResourceCache`].
pub fn new() -> Self {
Self {
tilesets: HashMap::new(),
}
}
}

impl ResourceCache for FilesystemResourceCache {
impl ResourceCache for DefaultResourceCache {
fn get_tileset(&self, path: impl AsRef<ResourcePath>) -> Option<Arc<Tileset>> {
self.tilesets.get(path.as_ref()).map(Clone::clone)
}
Expand Down
11 changes: 6 additions & 5 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>,
},
/// There was an invalid tile in the map parsed.
InvalidTileFound,
Expand Down Expand Up @@ -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: {}",
Expand Down Expand Up @@ -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,
}
}
Expand Down
6 changes: 1 addition & 5 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ pub struct Image {
/// use tiled::*;
///
/// # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
/// 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()
Expand Down Expand Up @@ -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,
Expand Down
153 changes: 71 additions & 82 deletions src/loader.rs
Original file line number Diff line number Diff line change
@@ -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<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)
}
}

/// 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<Cache: ResourceCache = FilesystemResourceCache> {
pub struct Loader<
Cache: ResourceCache = DefaultResourceCache,
Reader: ResourceReader = FilesystemResourceReader,
> {
cache: Cache,
reader: Reader,
}

impl Loader<FilesystemResourceCache> {
/// 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<Cache: ResourceCache> Loader<Cache> {
/// Creates a new loader using a specific resource cache.
impl<Cache: ResourceCache, Reader: ResourceReader> Loader<Cache, Reader> {
/// 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;
Expand All @@ -58,7 +104,7 @@ impl<Cache: ResourceCache> Loader<Cache> {
/// }
/// }
///
/// 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")?;
///
Expand All @@ -70,102 +116,45 @@ impl<Cache: ResourceCache> Loader<Cache> {
/// # 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
/// will be loaded relative to the path given.
///
/// 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<Path>) -> Result<Map> {
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<Path>) -> Result<Map> {
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<Path>) -> Result<Tileset> {
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<Path>,
) -> Result<Tileset> {
// 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`].
pub fn cache(&self) -> &Cache {
&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)
}
}
Loading

0 comments on commit e176b4b

Please sign in to comment.